diff options
| -rw-r--r-- | tests/test-duet-backend.el | 39 | ||||
| -rw-r--r-- | tests/test-duet-safety.el | 8 | ||||
| -rw-r--r-- | tests/test-duet-smoke.el | 4 | ||||
| -rw-r--r-- | tests/test-duet-transfer.el | 23 |
4 files changed, 74 insertions, 0 deletions
diff --git a/tests/test-duet-backend.el b/tests/test-duet-backend.el index f0a999c..629d2eb 100644 --- a/tests/test-duet-backend.el +++ b/tests/test-duet-backend.el @@ -188,6 +188,45 @@ value for a duplicated keyword." (n (duet--normalize-failure backend '(:exit 1 :stderr "API rate exceeded")))) (should (eq 'rate-limited (plist-get n :class))))) +(ert-deftest test-duet-normalize-failure-minimal-branches () + "The minimal normalizer maps each generic failure context to its class." + (let ((b (test-duet-backend--fake 'x 1))) + (should (eq 'missing-executable + (plist-get (duet--normalize-failure b '(:executable-missing t)) :class))) + (should (eq 'stalled + (plist-get (duet--normalize-failure b '(:timeout t)) :class))) + (should (eq 'cancelled + (plist-get (duet--normalize-failure b '(:signal 9)) :class))) + (should (eq 'backend-unknown-failure + (plist-get (duet--normalize-failure b '(:stderr "no exit code")) :class))))) + +(ert-deftest test-duet-failure-patterns-predicate-match () + "A :match predicate (not a regexp) is called with the whole context." + (let* ((norm (duet-define-cli-failure-patterns + (list (list :match (lambda (ctx) (eq 99 (plist-get ctx :exit))) + :class 'special :cause "x" :next-actions '(retry))))) + (n (funcall norm '(:exit 99 :stderr "")))) + (should (eq 'special (plist-get n :class))))) + +(ert-deftest test-duet-redact-whole-match-without-group () + "A pattern with no capture group redacts the whole match." + (should (equal "<redacted>" (duet--redact "secretvalue" '("secret[a-z]+"))))) + +(ert-deftest test-duet-backend-check-capability-flags-undeclared () + "A capability asserted but absent from `capabilities' is flagged." + (let ((b (test-duet-backend--fake + 'cap 10 :cleanup :none + :normalizer (duet-define-cli-failure-patterns nil)))) + (should (cl-some (lambda (s) (string-match-p "resume" s)) + (duet-backend-check-capability b :resume))))) + +(ert-deftest test-duet-backend-check-capability-passes-declared () + "A declared capability on an otherwise-publishable backend passes." + (let ((b (test-duet-backend--fake + 'cap 10 :cleanup :none :capabilities '(:resume t) + :normalizer (duet-define-cli-failure-patterns nil)))) + (should (null (duet-backend-check-capability b :resume))))) + ;;; Contract checks — tiered (ert-deftest test-duet-backend-check-minimum-passes-clean-backend () diff --git a/tests/test-duet-safety.el b/tests/test-duet-safety.el index 450a8ea..3f07b6f 100644 --- a/tests/test-duet-safety.el +++ b/tests/test-duet-safety.el @@ -101,6 +101,14 @@ (should (null (duet--plan-safety "/a/f" "/b/f" (list :file-type (lambda (_) 'file)))))) +(ert-deftest test-duet-safety-plan-detects-case-collision-via-caps () + "The composite invokes the injected existing-names and case-fold predicates." + (let ((caps (list :file-type (lambda (_) 'file) + :existing-names (lambda (_) '("foo.txt")) + :case-insensitive (lambda (_) t)))) + (should (cl-some (lambda (p) (eq 'case-collision (plist-get p :class))) + (duet--plan-safety "/a/Foo.txt" "/b/Foo.txt" caps))))) + (ert-deftest test-duet-safety-plan-collects-multiple-problems () "A pair tripping several checks reports each problem." (let* ((caps (list :file-type (lambda (_) 'fifo) diff --git a/tests/test-duet-smoke.el b/tests/test-duet-smoke.el index a101dd2..4f88a0e 100644 --- a/tests/test-duet-smoke.el +++ b/tests/test-duet-smoke.el @@ -32,5 +32,9 @@ (should (featurep 'duet)) (should (commandp 'duet))) +(ert-deftest test-duet-smoke-entry-not-yet-implemented () + "The entry command errors until the pane layout lands (Phase 4)." + (should-error (duet))) + (provide 'test-duet-smoke) ;;; test-duet-smoke.el ends here diff --git a/tests/test-duet-transfer.el b/tests/test-duet-transfer.el index 84cf678..ebef0f9 100644 --- a/tests/test-duet-transfer.el +++ b/tests/test-duet-transfer.el @@ -180,6 +180,29 @@ (should (null (plist-get spec :argv))) (should (eq 'rsync-remote-to-remote (plist-get spec :exec-mode)))))) +(ert-deftest test-duet-transfer-spec-async-explicit-nil () + "An explicit :async nil in opts is honored rather than defaulting to t." + (test-duet-transfer--with-builtins + (let ((spec (duet--transfer-spec '("/tmp/a") "/tmp/b" '(:async nil)))) + (should (null (plist-get spec :async)))))) + +(ert-deftest test-duet-rsync-normalizer-classifies-known-stderr () + "The rsync backend's normalizer maps known stderr signatures to classes." + (test-duet-transfer--with-builtins + (let ((rsync (duet-backend-by-name 'rsync))) + (should (eq 'permission-denied + (plist-get (duet--normalize-failure + rsync '(:exit 23 :stderr "rsync: Permission denied (13)")) + :class))) + (should (eq 'destination-full + (plist-get (duet--normalize-failure + rsync '(:exit 11 :stderr "No space left on device")) + :class))) + (should (eq 'rsync-protocol-mismatch + (plist-get (duet--normalize-failure + rsync '(:exit 12 :stderr "protocol version mismatch")) + :class)))))) + ;;; Conflict planning — pure, prompt-free (ert-deftest test-duet-plan-conflicts-no-collisions-all-copy () |
