diff options
Diffstat (limited to 'tests')
45 files changed, 1563 insertions, 1968 deletions
diff --git a/tests/test-ai-vterm--agent-buffers.el b/tests/test-ai-term--agent-buffers.el index 57d01730..20c661c4 100644 --- a/tests/test-ai-vterm--agent-buffers.el +++ b/tests/test-ai-term--agent-buffers.el @@ -1,4 +1,4 @@ -;;; test-ai-vterm--agent-buffers.el --- Tests for cj/--ai-vterm-agent-buffers -*- lexical-binding: t; -*- +;;; test-ai-term--agent-buffers.el --- Tests for cj/--ai-term-agent-buffers -*- lexical-binding: t; -*- ;;; Commentary: ;; The helper returns the list of buffers whose names start with the @@ -13,24 +13,24 @@ (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) -(require 'ai-vterm) -(require 'testutil-vterm-buffers) +(require 'ai-term) +(require 'testutil-ghostel-buffers) -(ert-deftest test-ai-vterm--agent-buffers-empty-when-none-exist () +(ert-deftest test-ai-term--agent-buffers-empty-when-none-exist () "Boundary: no agent-prefixed buffers anywhere -> empty list." (cj/test--kill-agent-buffers) (unwind-protect - (should (null (cj/--ai-vterm-agent-buffers))) + (should (null (cj/--ai-term-agent-buffers))) (cj/test--kill-agent-buffers))) -(ert-deftest test-ai-vterm--agent-buffers-returns-only-agent-buffers () +(ert-deftest test-ai-term--agent-buffers-returns-only-agent-buffers () "Normal: filters to only agent-prefixed buffers, leaves others alone." (cj/test--kill-agent-buffers) (let ((c1 (get-buffer-create "agent [a]")) (c2 (get-buffer-create "agent [b]")) (other (get-buffer-create "regular-buffer"))) (unwind-protect - (let ((result (cj/--ai-vterm-agent-buffers))) + (let ((result (cj/--ai-term-agent-buffers))) (should (memq c1 result)) (should (memq c2 result)) (should-not (memq other result)) @@ -39,21 +39,21 @@ (kill-buffer c2) (kill-buffer other)))) -(ert-deftest test-ai-vterm--agent-buffers-anchors-prefix-not-substring () +(ert-deftest test-ai-term--agent-buffers-anchors-prefix-not-substring () "Boundary: 'foo agent [bar]' is not an agent buffer -- prefix anchored." (cj/test--kill-agent-buffers) (let ((not-agent (get-buffer-create "foo agent [bar]"))) (unwind-protect - (should-not (memq not-agent (cj/--ai-vterm-agent-buffers))) + (should-not (memq not-agent (cj/--ai-term-agent-buffers))) (kill-buffer not-agent)))) -(ert-deftest test-ai-vterm--agent-buffers-bare-agent-not-included () +(ert-deftest test-ai-term--agent-buffers-bare-agent-not-included () "Boundary: 'agent' alone (no bracket) doesn't match the 'agent [' prefix." (cj/test--kill-agent-buffers) (let ((bare (get-buffer-create "agent"))) (unwind-protect - (should-not (memq bare (cj/--ai-vterm-agent-buffers))) + (should-not (memq bare (cj/--ai-term-agent-buffers))) (kill-buffer bare)))) -(provide 'test-ai-vterm--agent-buffers) -;;; test-ai-vterm--agent-buffers.el ends here +(provide 'test-ai-term--agent-buffers) +;;; test-ai-term--agent-buffers.el ends here diff --git a/tests/test-ai-vterm--buffer-name.el b/tests/test-ai-term--buffer-name.el index 2ebe91ee..b241977d 100644 --- a/tests/test-ai-vterm--buffer-name.el +++ b/tests/test-ai-term--buffer-name.el @@ -1,4 +1,4 @@ -;;; test-ai-vterm--buffer-name.el --- Tests for cj/--ai-vterm-buffer-name -*- lexical-binding: t; -*- +;;; test-ai-term--buffer-name.el --- Tests for cj/--ai-term-buffer-name -*- lexical-binding: t; -*- ;;; Commentary: ;; Tests for the buffer-name transform. Given an absolute project @@ -11,32 +11,32 @@ (require 'ert) (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-vterm) +(require 'ai-term) -(ert-deftest test-ai-vterm--buffer-name-normal-project () +(ert-deftest test-ai-term--buffer-name-normal-project () "Normal: a typical project path yields agent [<basename>]." - (should (equal (cj/--ai-vterm-buffer-name "/home/cjennings/projects/foo") + (should (equal (cj/--ai-term-buffer-name "/home/cjennings/projects/foo") "agent [foo]"))) -(ert-deftest test-ai-vterm--buffer-name-trailing-slash () +(ert-deftest test-ai-term--buffer-name-trailing-slash () "Boundary: trailing slash collapses before basename extraction." - (should (equal (cj/--ai-vterm-buffer-name "/home/cjennings/projects/foo/") + (should (equal (cj/--ai-term-buffer-name "/home/cjennings/projects/foo/") "agent [foo]"))) -(ert-deftest test-ai-vterm--buffer-name-dot-prefix-dir () +(ert-deftest test-ai-term--buffer-name-dot-prefix-dir () "Boundary: dot-prefix dirs (.emacs.d) preserve the dot in the basename." - (should (equal (cj/--ai-vterm-buffer-name "/home/cjennings/.emacs.d") + (should (equal (cj/--ai-term-buffer-name "/home/cjennings/.emacs.d") "agent [.emacs.d]"))) -(ert-deftest test-ai-vterm--buffer-name-space-in-basename () +(ert-deftest test-ai-term--buffer-name-space-in-basename () "Boundary: a space in the basename round-trips into the buffer name." - (should (equal (cj/--ai-vterm-buffer-name "/tmp/my work") + (should (equal (cj/--ai-term-buffer-name "/tmp/my work") "agent [my work]"))) -(ert-deftest test-ai-vterm--buffer-name-deeply-nested () +(ert-deftest test-ai-term--buffer-name-deeply-nested () "Normal: only the last path component is used." - (should (equal (cj/--ai-vterm-buffer-name "/a/b/c/d/e/leaf") + (should (equal (cj/--ai-term-buffer-name "/a/b/c/d/e/leaf") "agent [leaf]"))) -(provide 'test-ai-vterm--buffer-name) -;;; test-ai-vterm--buffer-name.el ends here +(provide 'test-ai-term--buffer-name) +;;; test-ai-term--buffer-name.el ends here diff --git a/tests/test-ai-vterm--candidates.el b/tests/test-ai-term--candidates.el index be9041ce..a9a392f3 100644 --- a/tests/test-ai-vterm--candidates.el +++ b/tests/test-ai-term--candidates.el @@ -1,4 +1,4 @@ -;;; test-ai-vterm--candidates.el --- Tests for cj/--ai-vterm-candidates -*- lexical-binding: t; -*- +;;; test-ai-term--candidates.el --- Tests for cj/--ai-term-candidates -*- lexical-binding: t; -*- ;;; Commentary: ;; Tests for the project-candidate walker. Two kinds of search root: @@ -16,124 +16,124 @@ (require 'ert) (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-vterm) +(require 'ai-term) -(defun test-ai-vterm--make-marker (dir) +(defun test-ai-term--make-marker (dir) "Create DIR/.ai/protocols.org so DIR registers as an AI-agent project." (let ((ai-dir (expand-file-name ".ai" dir))) (make-directory ai-dir t) (write-region "" nil (expand-file-name "protocols.org" ai-dir)))) -(defmacro test-ai-vterm--with-fixture (root &rest body) +(defmacro test-ai-term--with-fixture (root &rest body) "Bind ROOT to a fresh temp directory; remove on exit; run BODY." (declare (indent 1) (debug t)) - `(let ((,root (make-temp-file "ai-vterm-test-" t))) + `(let ((,root (make-temp-file "ai-term-test-" t))) (unwind-protect (progn ,@body) (delete-directory ,root t)))) -(ert-deftest test-ai-vterm--candidates-project-root-with-marker () +(ert-deftest test-ai-term--candidates-project-root-with-marker () "Normal: a project root containing .ai/protocols.org is included." - (test-ai-vterm--with-fixture root + (test-ai-term--with-fixture root (let ((proj (expand-file-name "emacs-d-fake" root))) (make-directory proj) - (test-ai-vterm--make-marker proj) - (let ((cj/ai-vterm-project-roots (list proj)) - (cj/ai-vterm-container-roots nil)) - (should (equal (cj/--ai-vterm-candidates) + (test-ai-term--make-marker proj) + (let ((cj/ai-term-project-roots (list proj)) + (cj/ai-term-container-roots nil)) + (should (equal (cj/--ai-term-candidates) (list (expand-file-name proj)))))))) -(ert-deftest test-ai-vterm--candidates-project-root-without-marker () +(ert-deftest test-ai-term--candidates-project-root-without-marker () "Boundary: a project root without .ai/protocols.org is excluded." - (test-ai-vterm--with-fixture root + (test-ai-term--with-fixture root (let ((proj (expand-file-name "no-ai" root))) (make-directory proj) - (let ((cj/ai-vterm-project-roots (list proj)) - (cj/ai-vterm-container-roots nil)) - (should (null (cj/--ai-vterm-candidates))))))) + (let ((cj/ai-term-project-roots (list proj)) + (cj/ai-term-container-roots nil)) + (should (null (cj/--ai-term-candidates))))))) -(ert-deftest test-ai-vterm--candidates-container-includes-children-with-marker () +(ert-deftest test-ai-term--candidates-container-includes-children-with-marker () "Normal: a container's children with .ai/protocols.org are included." - (test-ai-vterm--with-fixture root + (test-ai-term--with-fixture root (let ((container (expand-file-name "code" root)) (foo (expand-file-name "code/foo" root)) (bar (expand-file-name "code/bar" root))) (make-directory container) (make-directory foo) (make-directory bar) - (test-ai-vterm--make-marker foo) - (test-ai-vterm--make-marker bar) - (let* ((cj/ai-vterm-project-roots nil) - (cj/ai-vterm-container-roots (list container)) - (got (sort (cj/--ai-vterm-candidates) #'string<))) + (test-ai-term--make-marker foo) + (test-ai-term--make-marker bar) + (let* ((cj/ai-term-project-roots nil) + (cj/ai-term-container-roots (list container)) + (got (sort (cj/--ai-term-candidates) #'string<))) (should (equal got (sort (list (expand-file-name foo) (expand-file-name bar)) #'string<))))))) -(ert-deftest test-ai-vterm--candidates-container-skips-children-without-marker () +(ert-deftest test-ai-term--candidates-container-skips-children-without-marker () "Boundary: a container's children without .ai/protocols.org are skipped." - (test-ai-vterm--with-fixture root + (test-ai-term--with-fixture root (let ((container (expand-file-name "code" root)) (foo (expand-file-name "code/foo" root)) (bare (expand-file-name "code/bare" root))) (make-directory container) (make-directory foo) (make-directory bare) - (test-ai-vterm--make-marker foo) - (let ((cj/ai-vterm-project-roots nil) - (cj/ai-vterm-container-roots (list container))) - (should (equal (cj/--ai-vterm-candidates) + (test-ai-term--make-marker foo) + (let ((cj/ai-term-project-roots nil) + (cj/ai-term-container-roots (list container))) + (should (equal (cj/--ai-term-candidates) (list (expand-file-name foo)))))))) -(ert-deftest test-ai-vterm--candidates-container-skips-non-directory-entries () +(ert-deftest test-ai-term--candidates-container-skips-non-directory-entries () "Boundary: a container's non-directory entries are ignored." - (test-ai-vterm--with-fixture root + (test-ai-term--with-fixture root (let ((container (expand-file-name "code" root)) (foo (expand-file-name "code/foo" root)) (stray (expand-file-name "code/README.txt" root))) (make-directory container) (make-directory foo) - (test-ai-vterm--make-marker foo) + (test-ai-term--make-marker foo) (write-region "" nil stray) - (let ((cj/ai-vterm-project-roots nil) - (cj/ai-vterm-container-roots (list container))) - (should (equal (cj/--ai-vterm-candidates) + (let ((cj/ai-term-project-roots nil) + (cj/ai-term-container-roots (list container))) + (should (equal (cj/--ai-term-candidates) (list (expand-file-name foo)))))))) -(ert-deftest test-ai-vterm--candidates-nonexistent-root-is-skipped () +(ert-deftest test-ai-term--candidates-nonexistent-root-is-skipped () "Error: a nonexistent search root is skipped silently, no error raised." - (test-ai-vterm--with-fixture root - (let ((cj/ai-vterm-project-roots + (test-ai-term--with-fixture root + (let ((cj/ai-term-project-roots (list (expand-file-name "does-not-exist" root))) - (cj/ai-vterm-container-roots + (cj/ai-term-container-roots (list (expand-file-name "also-missing" root)))) - (should (null (cj/--ai-vterm-candidates)))))) + (should (null (cj/--ai-term-candidates)))))) -(ert-deftest test-ai-vterm--candidates-empty-roots-yield-empty-list () +(ert-deftest test-ai-term--candidates-empty-roots-yield-empty-list () "Boundary: nil roots yield nil." - (let ((cj/ai-vterm-project-roots nil) - (cj/ai-vterm-container-roots nil)) - (should (null (cj/--ai-vterm-candidates))))) + (let ((cj/ai-term-project-roots nil) + (cj/ai-term-container-roots nil)) + (should (null (cj/--ai-term-candidates))))) -(ert-deftest test-ai-vterm--candidates-mixed-roots () +(ert-deftest test-ai-term--candidates-mixed-roots () "Normal: project + container roots combine in one result list." - (test-ai-vterm--with-fixture root + (test-ai-term--with-fixture root (let ((emacs-d (expand-file-name "emacs-d" root)) (container (expand-file-name "code" root)) (foo (expand-file-name "code/foo" root))) (make-directory emacs-d) (make-directory container) (make-directory foo) - (test-ai-vterm--make-marker emacs-d) - (test-ai-vterm--make-marker foo) - (let* ((cj/ai-vterm-project-roots (list emacs-d)) - (cj/ai-vterm-container-roots (list container)) - (got (sort (cj/--ai-vterm-candidates) #'string<))) + (test-ai-term--make-marker emacs-d) + (test-ai-term--make-marker foo) + (let* ((cj/ai-term-project-roots (list emacs-d)) + (cj/ai-term-container-roots (list container)) + (got (sort (cj/--ai-term-candidates) #'string<))) (should (equal got (sort (list (expand-file-name emacs-d) (expand-file-name foo)) #'string<))))))) -(provide 'test-ai-vterm--candidates) -;;; test-ai-vterm--candidates.el ends here +(provide 'test-ai-term--candidates) +;;; test-ai-term--candidates.el ends here diff --git a/tests/test-ai-term--capture-state.el b/tests/test-ai-term--capture-state.el new file mode 100644 index 00000000..543f83ad --- /dev/null +++ b/tests/test-ai-term--capture-state.el @@ -0,0 +1,63 @@ +;;; test-ai-term--capture-state.el --- Tests for cj/--ai-term-capture-state -*- lexical-binding: t; -*- + +;;; Commentary: +;; The capture helper writes WINDOW's direction and size to module- +;; level state vars `cj/--ai-term-last-direction' and +;; `cj/--ai-term-last-size'. Called from `cj/ai-term''s toggle-off +;; branch so the next F9 display can restore the user's chosen +;; orientation and size. No-op on a dead window. + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'ai-term) + +(ert-deftest test-ai-term--capture-state-right-split-sets-direction () + "Normal: right-split window -> direction=right, integer body-cols matching window." + (save-window-excursion + (delete-other-windows) + (let ((right (split-window (selected-window) nil 'right)) + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil)) + (cj/--ai-term-capture-state right) + (should (eq cj/--ai-term-last-direction 'right)) + (should (integerp cj/--ai-term-last-size)) + (should (= cj/--ai-term-last-size (window-body-width right)))))) + +(ert-deftest test-ai-term--capture-state-below-split-sets-direction () + "Normal: below-split window -> direction=below, integer body-lines matching window." + (save-window-excursion + (delete-other-windows) + (let ((below (split-window (selected-window) nil 'below)) + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil)) + (cj/--ai-term-capture-state below) + (should (eq cj/--ai-term-last-direction 'below)) + (should (integerp cj/--ai-term-last-size)) + (should (= cj/--ai-term-last-size (window-body-height below)))))) + +(ert-deftest test-ai-term--capture-state-noop-on-dead-window () + "Boundary: nil window -> state remains unchanged." + (let ((cj/--ai-term-last-direction 'sentinel-dir) + (cj/--ai-term-last-size 0.123)) + (cj/--ai-term-capture-state nil) + (should (eq cj/--ai-term-last-direction 'sentinel-dir)) + (should (= cj/--ai-term-last-size 0.123)))) + +(ert-deftest test-ai-term--capture-state-noop-on-deleted-window () + "Boundary: deleted window -> state remains unchanged." + (let ((cj/--ai-term-last-direction 'sentinel-dir) + (cj/--ai-term-last-size 0.123) + (dead-win (save-window-excursion + (delete-other-windows) + (let ((w (split-window (selected-window) nil 'right))) + (delete-window w) + w)))) + (cj/--ai-term-capture-state dead-win) + (should (eq cj/--ai-term-last-direction 'sentinel-dir)) + (should (= cj/--ai-term-last-size 0.123)))) + +(provide 'test-ai-term--capture-state) +;;; test-ai-term--capture-state.el ends here diff --git a/tests/test-ai-vterm--close.el b/tests/test-ai-term--close.el index eb89bcc2..654e85f0 100644 --- a/tests/test-ai-vterm--close.el +++ b/tests/test-ai-term--close.el @@ -1,8 +1,8 @@ -;;; test-ai-vterm--close.el --- Tests for graceful agent close -*- lexical-binding: t; -*- +;;; test-ai-term--close.el --- Tests for graceful agent close -*- lexical-binding: t; -*- ;;; Commentary: -;; `cj/ai-vterm-close' tears an agent down gracefully: kill its tmux -;; session (stopping the agent process), kill the vterm buffer, and +;; `cj/ai-term-close' tears an agent down gracefully: kill its tmux +;; session (stopping the agent process), kill the ghostel buffer, and ;; remove its window. These tests cover the pure pieces -- the ;; tmux-kill helper, the per-buffer teardown, and the target selection -- ;; with `process-file' and the prompt mocked at the boundary. @@ -13,74 +13,74 @@ (require 'cl-lib) (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-vterm) +(require 'ai-term) -(ert-deftest test-ai-vterm--kill-tmux-session-runs-kill-session () +(ert-deftest test-ai-term--kill-tmux-session-runs-kill-session () "Normal: invokes `tmux kill-session -t <session>'." (let (captured) (cl-letf (((symbol-function 'process-file) (lambda (program &rest args) (setq captured (cons program args)) 0))) - (cj/--ai-vterm-kill-tmux-session "aiv-foo")) + (cj/--ai-term-kill-tmux-session "aiv-foo")) (should (equal (car captured) "tmux")) (should (member "kill-session" captured)) (should (member "-t" captured)) (should (member "aiv-foo" captured)))) -(ert-deftest test-ai-vterm--kill-tmux-session-swallows-error () +(ert-deftest test-ai-term--kill-tmux-session-swallows-error () "Error: returns nil when tmux is unavailable (process-file signals)." (cl-letf (((symbol-function 'process-file) (lambda (&rest _) (error "no tmux")))) - (should (null (cj/--ai-vterm-kill-tmux-session "aiv-foo"))))) + (should (null (cj/--ai-term-kill-tmux-session "aiv-foo"))))) -(ert-deftest test-ai-vterm--close-buffer-kills-session-and-buffer () +(ert-deftest test-ai-term--close-buffer-kills-session-and-buffer () "Normal: derives the session from default-directory, kills it and the buffer." (let ((buf (get-buffer-create "agent [foo]")) captured-session) (with-current-buffer buf (setq-local default-directory "/tmp/foo/")) - (cl-letf (((symbol-function 'cj/--ai-vterm-kill-tmux-session) + (cl-letf (((symbol-function 'cj/--ai-term-kill-tmux-session) (lambda (s) (setq captured-session s) 0))) - (cj/--ai-vterm-close-buffer buf)) + (cj/--ai-term-close-buffer buf)) (should (equal captured-session "aiv-foo")) (should-not (buffer-live-p buf)))) -(ert-deftest test-ai-vterm--close-buffer-noop-on-non-agent () +(ert-deftest test-ai-term--close-buffer-noop-on-non-agent () "Boundary: does nothing for a buffer that is not an agent buffer." (let ((buf (get-buffer-create "*not-an-agent*")) (called nil)) (unwind-protect (progn - (cl-letf (((symbol-function 'cj/--ai-vterm-kill-tmux-session) + (cl-letf (((symbol-function 'cj/--ai-term-kill-tmux-session) (lambda (_s) (setq called t) 0))) - (cj/--ai-vterm-close-buffer buf)) + (cj/--ai-term-close-buffer buf)) (should-not called) (should (buffer-live-p buf))) (when (buffer-live-p buf) (kill-buffer buf))))) -(ert-deftest test-ai-vterm--close-target-current-agent-buffer () +(ert-deftest test-ai-term--close-target-current-agent-buffer () "Normal: returns the current buffer when it is an agent buffer." (let ((buf (get-buffer-create "agent [cur]"))) (unwind-protect (with-current-buffer buf - (should (eq (cj/--ai-vterm-close-target) buf))) + (should (eq (cj/--ai-term-close-target) buf))) (kill-buffer buf)))) -(ert-deftest test-ai-vterm--close-target-sole-agent () +(ert-deftest test-ai-term--close-target-sole-agent () "Normal: returns the only live agent buffer when current isn't an agent." (let ((buf (get-buffer-create "agent [only]"))) (unwind-protect (with-temp-buffer - (cl-letf (((symbol-function 'cj/--ai-vterm-agent-buffers) + (cl-letf (((symbol-function 'cj/--ai-term-agent-buffers) (lambda () (list buf)))) - (should (eq (cj/--ai-vterm-close-target) buf)))) + (should (eq (cj/--ai-term-close-target) buf)))) (kill-buffer buf)))) -(ert-deftest test-ai-vterm--close-target-none-returns-nil () +(ert-deftest test-ai-term--close-target-none-returns-nil () "Boundary: nil when current buffer isn't an agent and none are alive." (with-temp-buffer - (cl-letf (((symbol-function 'cj/--ai-vterm-agent-buffers) (lambda () nil))) - (should (null (cj/--ai-vterm-close-target)))))) + (cl-letf (((symbol-function 'cj/--ai-term-agent-buffers) (lambda () nil))) + (should (null (cj/--ai-term-close-target)))))) -(provide 'test-ai-vterm--close) -;;; test-ai-vterm--close.el ends here +(provide 'test-ai-term--close) +;;; test-ai-term--close.el ends here diff --git a/tests/test-ai-vterm--collapse-split.el b/tests/test-ai-term--collapse-split.el index ad299e47..d7b4ee17 100644 --- a/tests/test-ai-vterm--collapse-split.el +++ b/tests/test-ai-term--collapse-split.el @@ -1,4 +1,4 @@ -;;; test-ai-vterm--collapse-split.el --- F9 collapses the agent split -*- lexical-binding: t; -*- +;;; test-ai-term--collapse-split.el --- F9 collapses the agent split -*- lexical-binding: t; -*- ;;; Commentary: ;; Regression coverage for the F9 toggle-off behavior Craig reported: with @@ -13,7 +13,7 @@ ;; NON-agent buffer (the file being worked on), not another agent -- the prior ;; `other-buffer' call could pick another live agent. ;; -;; Also covers the `cj/--ai-vterm-most-recent-non-agent-buffer' helper. +;; Also covers the `cj/--ai-term-most-recent-non-agent-buffer' helper. ;;; Code: @@ -22,12 +22,12 @@ (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) -(require 'ai-vterm) -(require 'testutil-vterm-buffers) +(require 'ai-term) +(require 'testutil-ghostel-buffers) -;;; cj/--ai-vterm-most-recent-non-agent-buffer +;;; cj/--ai-term-most-recent-non-agent-buffer -(ert-deftest test-ai-vterm--most-recent-non-agent-buffer-skips-agents () +(ert-deftest test-ai-term--most-recent-non-agent-buffer-skips-agents () "Normal: returns a live non-agent buffer even when agents are most-recent." (cj/test--kill-agent-buffers) (let ((work (get-buffer-create "*test-mrna-work*")) @@ -40,16 +40,16 @@ (set-window-buffer (selected-window) work) (set-window-buffer (selected-window) agent-b) (set-window-buffer (selected-window) agent-a) - (let ((result (cj/--ai-vterm-most-recent-non-agent-buffer))) + (let ((result (cj/--ai-term-most-recent-non-agent-buffer))) (should (bufferp result)) (should (buffer-live-p result)) - (should-not (cj/--ai-vterm-buffer-p result)))) + (should-not (cj/--ai-term-buffer-p result)))) (when (get-buffer "*test-mrna-work*") (kill-buffer "*test-mrna-work*")) (cj/test--kill-agent-buffers)))) ;;; Multi-window: F9 collapses the split -(ert-deftest test-ai-vterm--collapse-multi-window-deletes-agent-split () +(ert-deftest test-ai-term--collapse-multi-window-deletes-agent-split () "Normal/Regression: agent in a bottom split with other agents alive; F9 collapses the split so the working buffer reclaims the frame, and no agent is surfaced. Before the fix, `quit-restore-window' could switch the slot to a @@ -59,7 +59,7 @@ different agent (stale quit-restore after slot reuse)." (agent-a (get-buffer-create "agent [collapse-a]")) (agent-b (get-buffer-create "agent [collapse-b]")) (agent-c (get-buffer-create "agent [collapse-c]")) - (cj/--ai-vterm-last-was-bury nil)) + (cj/--ai-term-last-was-bury nil)) (unwind-protect (save-window-excursion (delete-other-windows) @@ -72,16 +72,16 @@ different agent (stale quit-restore after slot reuse)." (set-window-buffer agent-win agent-c) (select-window agent-win) (should-not (one-window-p)) - (cj/test--call-as-gui #'cj/ai-vterm) + (cj/test--call-as-gui #'cj/ai-term) (should (one-window-p)) - (should-not (cj/--ai-vterm-displayed-agent-window)) + (should-not (cj/--ai-term-displayed-agent-window)) (should (eq (window-buffer (selected-window)) work)))) (when (get-buffer "*test-collapse-work*") (kill-buffer "*test-collapse-work*")) (cj/test--kill-agent-buffers)))) ;;; Single-window: F9 returns to a non-agent buffer -(ert-deftest test-ai-vterm--collapse-single-window-returns-non-agent () +(ert-deftest test-ai-term--collapse-single-window-returns-non-agent () "Normal/Regression: agent fills the frame, other agents alive; F9 toggles back to a NON-agent buffer (the working file), never another agent. Before the fix, `other-buffer' could pick another live agent." @@ -89,7 +89,7 @@ to a NON-agent buffer (the working file), never another agent. Before the fix, (let ((work (get-buffer-create "*test-collapse-sw-work*")) (agent-a (get-buffer-create "agent [collapse-sw-a]")) (agent-b (get-buffer-create "agent [collapse-sw-b]")) - (cj/--ai-vterm-last-was-bury nil)) + (cj/--ai-term-last-was-bury nil)) (unwind-protect (save-window-excursion (delete-other-windows) @@ -99,16 +99,16 @@ to a NON-agent buffer (the working file), never another agent. Before the fix, (set-window-buffer (selected-window) agent-b) (set-window-buffer (selected-window) agent-a) (should (one-window-p)) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) - (cj/test--call-as-gui #'cj/ai-vterm)) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) + (cj/test--call-as-gui #'cj/ai-term)) (should (one-window-p)) - (should-not (cj/--ai-vterm-buffer-p (window-buffer (selected-window))))) + (should-not (cj/--ai-term-buffer-p (window-buffer (selected-window))))) (when (get-buffer "*test-collapse-sw-work*") (kill-buffer "*test-collapse-sw-work*")) (cj/test--kill-agent-buffers)))) ;;; Faithful toggle: reopen the SAME agent that was hidden -(ert-deftest test-ai-vterm--dispatch-prefers-last-hidden-agent () +(ert-deftest test-ai-term--dispatch-prefers-last-hidden-agent () "Regression: dispatch reopens the last-hidden agent, not the buffer-list MRU. After F9 hides an agent, the next F9 must reopen the SAME one even when a different agent is ahead of it in `buffer-list'. Falls back to the MRU when @@ -116,25 +116,25 @@ nothing was hidden yet or the remembered buffer was killed." (cj/test--kill-agent-buffers) (let ((a1 (get-buffer-create "agent [disp-mru]")) (a2 (get-buffer-create "agent [disp-shown]")) - (cj/--ai-vterm-last-hidden-buffer nil)) + (cj/--ai-term-last-hidden-buffer nil)) (unwind-protect - (cl-letf (((symbol-function 'cj/--ai-vterm-displayed-agent-window) + (cl-letf (((symbol-function 'cj/--ai-term-displayed-agent-window) (lambda (&optional _f) nil)) - ((symbol-function 'cj/--ai-vterm-agent-buffers) + ((symbol-function 'cj/--ai-term-agent-buffers) (lambda () (list a1 a2)))) ; a1 is the MRU ;; No memory yet -> falls back to MRU (a1). - (should (equal (cj/--ai-vterm-dispatch) (cons 'redisplay-recent a1))) + (should (equal (cj/--ai-term-dispatch) (cons 'redisplay-recent a1))) ;; Remember a2 as last hidden -> dispatch prefers it. - (setq cj/--ai-vterm-last-hidden-buffer a2) - (should (equal (cj/--ai-vterm-dispatch) (cons 'redisplay-recent a2))) + (setq cj/--ai-term-last-hidden-buffer a2) + (should (equal (cj/--ai-term-dispatch) (cons 'redisplay-recent a2))) ;; A killed last-hidden buffer -> falls back to MRU. (let ((dead (get-buffer-create "agent [disp-dead]"))) - (setq cj/--ai-vterm-last-hidden-buffer dead) + (setq cj/--ai-term-last-hidden-buffer dead) (kill-buffer dead)) - (should (equal (cj/--ai-vterm-dispatch) (cons 'redisplay-recent a1)))) + (should (equal (cj/--ai-term-dispatch) (cons 'redisplay-recent a1)))) (cj/test--kill-agent-buffers)))) -(ert-deftest test-ai-vterm--toggle-roundtrip-reopens-same-agent () +(ert-deftest test-ai-term--toggle-roundtrip-reopens-same-agent () "Regression: hide then show brings back the agent that was on screen. With several agents alive and a different one most-recent in `buffer-list', F9 off then F9 on restores the SAME agent that was visible -- not a swap to @@ -143,10 +143,10 @@ another. Reproduces the \"the displayed buffer changes\" report." (let ((work (get-buffer-create "*test-roundtrip-work*")) (a1 (get-buffer-create "agent [rt-1]")) (a2 (get-buffer-create "agent [rt-2]")) - (cj/--ai-vterm-last-was-bury nil) - (cj/--ai-vterm-last-direction nil) - (cj/--ai-vterm-last-size nil) - (cj/--ai-vterm-last-hidden-buffer nil)) + (cj/--ai-term-last-was-bury nil) + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil) + (cj/--ai-term-last-hidden-buffer nil)) (unwind-protect (save-window-excursion (delete-other-windows) @@ -157,15 +157,15 @@ another. Reproduces the \"the displayed buffer changes\" report." (bury-buffer a1) ; a1 stays alive, demoted in MRU (set-window-buffer agent-win a2) (select-window agent-win) - (should (eq (window-buffer (cj/--ai-vterm-displayed-agent-window)) a2)) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) - (cj/test--call-as-gui #'cj/ai-vterm) ; off - (should-not (cj/--ai-vterm-displayed-agent-window)) - (cj/test--call-as-gui #'cj/ai-vterm) ; on -> must be a2 - (should (eq (window-buffer (cj/--ai-vterm-displayed-agent-window)) + (should (eq (window-buffer (cj/--ai-term-displayed-agent-window)) a2)) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) + (cj/test--call-as-gui #'cj/ai-term) ; off + (should-not (cj/--ai-term-displayed-agent-window)) + (cj/test--call-as-gui #'cj/ai-term) ; on -> must be a2 + (should (eq (window-buffer (cj/--ai-term-displayed-agent-window)) a2))))) (when (get-buffer "*test-roundtrip-work*") (kill-buffer "*test-roundtrip-work*")) (cj/test--kill-agent-buffers)))) -(provide 'test-ai-vterm--collapse-split) -;;; test-ai-vterm--collapse-split.el ends here +(provide 'test-ai-term--collapse-split) +;;; test-ai-term--collapse-split.el ends here diff --git a/tests/test-ai-term--default-geometry.el b/tests/test-ai-term--default-geometry.el new file mode 100644 index 00000000..833f2ef4 --- /dev/null +++ b/tests/test-ai-term--default-geometry.el @@ -0,0 +1,56 @@ +;;; test-ai-term--default-geometry.el --- Tests for host-aware display defaults -*- lexical-binding: t; -*- + +;;; Commentary: +;; ai-term's default display geometry is host-aware: a laptop opens the +;; agent from the bottom (75% height), a desktop opens it from the right +;; (50% width). `cj/--ai-term-default-direction' and +;; `cj/--ai-term-default-size' encapsulate the `env-laptop-p' branch; +;; they feed the default fallbacks in `cj/--ai-term-capture-state' and +;; `cj/--ai-term-display-saved'. +;; +;; `env-laptop-p' is stubbed per-test so the assertions are deterministic +;; regardless of the host the suite runs on. + +;;; Code: + +(require 'ert) +(require 'cl-lib) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'ai-term) + +(ert-deftest test-ai-term--default-direction-laptop () + "Normal: on a laptop the default direction is `below'." + (cl-letf (((symbol-function 'env-laptop-p) (lambda () t))) + (should (eq (cj/--ai-term-default-direction) 'below)))) + +(ert-deftest test-ai-term--default-direction-desktop () + "Normal: on a desktop the default direction is `right'." + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) + (should (eq (cj/--ai-term-default-direction) 'right)))) + +(ert-deftest test-ai-term--default-size-laptop () + "Normal: on a laptop the default size is `cj/ai-term-laptop-height'." + (let ((cj/ai-term-laptop-height 0.75) + (cj/ai-term-desktop-width 0.5)) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () t))) + (should (= (cj/--ai-term-default-size) 0.75))))) + +(ert-deftest test-ai-term--default-size-desktop () + "Normal: on a desktop the default size is `cj/ai-term-desktop-width'." + (let ((cj/ai-term-laptop-height 0.75) + (cj/ai-term-desktop-width 0.5)) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) + (should (= (cj/--ai-term-default-size) 0.5))))) + +(ert-deftest test-ai-term--default-size-respects-custom-values () + "Boundary: the helper returns the customized values, not the literals." + (let ((cj/ai-term-laptop-height 0.6) + (cj/ai-term-desktop-width 0.33)) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () t))) + (should (= (cj/--ai-term-default-size) 0.6))) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) + (should (= (cj/--ai-term-default-size) 0.33))))) + +(provide 'test-ai-term--default-geometry) +;;; test-ai-term--default-geometry.el ends here diff --git a/tests/test-ai-vterm--dispatch.el b/tests/test-ai-term--dispatch.el index 94b02123..91b5e1bc 100644 --- a/tests/test-ai-vterm--dispatch.el +++ b/tests/test-ai-term--dispatch.el @@ -1,4 +1,4 @@ -;;; test-ai-vterm--dispatch.el --- Tests for cj/--ai-vterm-dispatch -*- lexical-binding: t; -*- +;;; test-ai-term--dispatch.el --- Tests for cj/--ai-term-dispatch -*- lexical-binding: t; -*- ;;; Commentary: ;; The dispatch helper is a pure decision function used by F9. @@ -15,31 +15,31 @@ (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) -(require 'ai-vterm) -(require 'testutil-vterm-buffers) +(require 'ai-term) +(require 'testutil-ghostel-buffers) -(ert-deftest test-ai-vterm--dispatch-window-displayed-returns-toggle-off () +(ert-deftest test-ai-term--dispatch-window-displayed-returns-toggle-off () "Normal: displayed agent window -> (toggle-off . WIN)." (let ((sentinel-win 'fake-window)) - (cl-letf (((symbol-function 'cj/--ai-vterm-displayed-agent-window) + (cl-letf (((symbol-function 'cj/--ai-term-displayed-agent-window) (lambda (&optional _frame) sentinel-win))) - (should (equal (cj/--ai-vterm-dispatch) + (should (equal (cj/--ai-term-dispatch) (cons 'toggle-off sentinel-win)))))) -(ert-deftest test-ai-vterm--dispatch-no-window-single-buffer-returns-redisplay-recent () +(ert-deftest test-ai-term--dispatch-no-window-single-buffer-returns-redisplay-recent () "Normal: no displayed agent, one alive buffer -> redisplay-recent + buffer." (cj/test--kill-agent-buffers) (let ((b1 (get-buffer-create "agent [single]"))) (unwind-protect - (cl-letf (((symbol-function 'cj/--ai-vterm-displayed-agent-window) + (cl-letf (((symbol-function 'cj/--ai-term-displayed-agent-window) (lambda (&optional _frame) nil)) - ((symbol-function 'cj/--ai-vterm-agent-buffers) + ((symbol-function 'cj/--ai-term-agent-buffers) (lambda () (list b1)))) - (should (equal (cj/--ai-vterm-dispatch) + (should (equal (cj/--ai-term-dispatch) (cons 'redisplay-recent b1)))) (kill-buffer b1)))) -(ert-deftest test-ai-vterm--dispatch-no-window-multiple-buffers-returns-redisplay-recent () +(ert-deftest test-ai-term--dispatch-no-window-multiple-buffers-returns-redisplay-recent () "Normal: no displayed agent, 2+ alive buffers -> redisplay-recent + MRU. F9 redisplays the most-recently-selected agent (head of buffer-list order) rather than opening the project picker, so the user toggles @@ -48,23 +48,23 @@ THE agent they were last using. Other agents are reachable via M-F9." (let ((b1 (get-buffer-create "agent [a]")) (b2 (get-buffer-create "agent [b]"))) (unwind-protect - (cl-letf (((symbol-function 'cj/--ai-vterm-displayed-agent-window) + (cl-letf (((symbol-function 'cj/--ai-term-displayed-agent-window) (lambda (&optional _frame) nil)) - ((symbol-function 'cj/--ai-vterm-agent-buffers) + ((symbol-function 'cj/--ai-term-agent-buffers) (lambda () (list b1 b2)))) - (should (equal (cj/--ai-vterm-dispatch) + (should (equal (cj/--ai-term-dispatch) (cons 'redisplay-recent b1)))) (kill-buffer b1) (kill-buffer b2)))) -(ert-deftest test-ai-vterm--dispatch-no-window-zero-buffers-returns-pick-project () +(ert-deftest test-ai-term--dispatch-no-window-zero-buffers-returns-pick-project () "Boundary: no displayed agent, zero alive buffers -> pick-project." (cj/test--kill-agent-buffers) - (cl-letf (((symbol-function 'cj/--ai-vterm-displayed-agent-window) + (cl-letf (((symbol-function 'cj/--ai-term-displayed-agent-window) (lambda (&optional _frame) nil)) - ((symbol-function 'cj/--ai-vterm-agent-buffers) + ((symbol-function 'cj/--ai-term-agent-buffers) (lambda () nil))) - (should (equal (cj/--ai-vterm-dispatch) '(pick-project))))) + (should (equal (cj/--ai-term-dispatch) '(pick-project))))) -(provide 'test-ai-vterm--dispatch) -;;; test-ai-vterm--dispatch.el ends here +(provide 'test-ai-term--dispatch) +;;; test-ai-term--dispatch.el ends here diff --git a/tests/test-ai-vterm--display-rule.el b/tests/test-ai-term--display-rule.el index 9b70134a..906a4768 100644 --- a/tests/test-ai-vterm--display-rule.el +++ b/tests/test-ai-term--display-rule.el @@ -1,4 +1,4 @@ -;;; test-ai-vterm--display-rule.el --- Tests for the AI-vterm display-buffer rule -*- lexical-binding: t; -*- +;;; test-ai-term--display-rule.el --- Tests for the AI-term display-buffer rule -*- lexical-binding: t; -*- ;;; Commentary: ;; The module installs a `display-buffer-alist' entry routing buffers @@ -12,67 +12,67 @@ (require 'cl-lib) (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-vterm) +(require 'ai-term) -(defun test-ai-vterm--cleanup (name) +(defun test-ai-term--cleanup (name) "Kill buffer NAME if it exists." (when (get-buffer name) (kill-buffer name))) -(defmacro test-ai-vterm--with-clean-frame (&rest body) - "Run BODY in a context with one window and the AI-vterm rule loaded." +(defmacro test-ai-term--with-clean-frame (&rest body) + "Run BODY in a context with one window and the AI-term rule loaded." (declare (indent 0) (debug t)) `(save-window-excursion (delete-other-windows) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) ,@body))) -(ert-deftest test-ai-vterm--display-rule-routes-agent-buffer-to-right () +(ert-deftest test-ai-term--display-rule-routes-agent-buffer-to-right () "Normal: on a desktop, \"agent [foo]\" lands in a window to the right. The desktop default direction is `right' (see -`cj/--ai-vterm-default-direction'), so the rule splits the current +`cj/--ai-term-default-direction'), so the rule splits the current window with `(direction . right)' and the new window's left edge sits at a positive column. `env-laptop-p' is stubbed nil to pin the desktop branch; on a laptop the agent would land below instead." (let ((name "agent [display-rule-test]")) - (test-ai-vterm--cleanup name) + (test-ai-term--cleanup name) (unwind-protect (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) - (test-ai-vterm--with-clean-frame + (test-ai-term--with-clean-frame (let* ((buf (get-buffer-create name)) (win (display-buffer buf))) (should (windowp win)) (should (> (window-left-column win) 0))))) - (test-ai-vterm--cleanup name)))) + (test-ai-term--cleanup name)))) -(ert-deftest test-ai-vterm--display-rule-skips-non-matching-buffer () +(ert-deftest test-ai-term--display-rule-skips-non-matching-buffer () "Boundary: a buffer not named \"agent [...]\" does not match the rule. The rule's regex doesn't fire, so `display-buffer' falls back to the default action -- reuse the current window -- and no rightward split occurs." (let ((name "scratch-buffer-no-match")) - (test-ai-vterm--cleanup name) + (test-ai-term--cleanup name) (unwind-protect - (test-ai-vterm--with-clean-frame + (test-ai-term--with-clean-frame (let* ((buf (get-buffer-create name)) (win (display-buffer buf))) (should (windowp win)) (should (= (window-left-column win) 0)))) - (test-ai-vterm--cleanup name)))) + (test-ai-term--cleanup name)))) -(ert-deftest test-ai-vterm--display-rule-prefix-not-substring () +(ert-deftest test-ai-term--display-rule-prefix-not-substring () "Boundary: \"foo agent [bar]\" does not match -- the rule anchors at start." (let ((name "foo agent [substring-test]")) - (test-ai-vterm--cleanup name) + (test-ai-term--cleanup name) (unwind-protect - (test-ai-vterm--with-clean-frame + (test-ai-term--with-clean-frame (let* ((buf (get-buffer-create name)) (win (display-buffer buf))) (should (windowp win)) (should (= (window-left-column win) 0)))) - (test-ai-vterm--cleanup name)))) + (test-ai-term--cleanup name)))) -(provide 'test-ai-vterm--display-rule) -;;; test-ai-vterm--display-rule.el ends here +(provide 'test-ai-term--display-rule) +;;; test-ai-term--display-rule.el ends here diff --git a/tests/test-ai-vterm--display-saved.el b/tests/test-ai-term--display-saved.el index 0cf59a29..8b689aa6 100644 --- a/tests/test-ai-vterm--display-saved.el +++ b/tests/test-ai-term--display-saved.el @@ -1,10 +1,10 @@ -;;; test-ai-vterm--display-saved.el --- Tests for the display-saved action -*- lexical-binding: t; -*- +;;; test-ai-term--display-saved.el --- Tests for the display-saved action -*- lexical-binding: t; -*- ;;; Commentary: -;; `cj/--ai-vterm-display-saved' is the split path of the F9 display +;; `cj/--ai-term-display-saved' is the split path of the F9 display ;; chain -- it runs only when no agent window and no reusable edge slot ;; exist (a single-window frame, or a layout split on the other axis). -;; It reads `cj/--ai-vterm-last-direction' + `cj/--ai-vterm-last-size' +;; It reads `cj/--ai-term-last-direction' + `cj/--ai-term-last-size' ;; (with default fallbacks), builds an alist with direction + the ;; matching size key, strips any conflicting entries that came in via the ;; rule, and delegates to `display-buffer-in-direction'. @@ -13,7 +13,7 @@ ;; would have reached it. ;; ;; Multi-window toggle round-trips no longer resplit -- they reuse the -;; existing half (see test-ai-vterm--reuse-edge-window.el), so the former +;; existing half (see test-ai-term--reuse-edge-window.el), so the former ;; resplit/body-width-preservation round-trip tests were retired with the ;; swap-the-slot model. The buffer-move teardown test stays here because ;; it exercises the split-window delete path on toggle-off. @@ -25,79 +25,79 @@ (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) -(require 'ai-vterm) -(require 'testutil-vterm-buffers) +(require 'ai-term) +(require 'testutil-ghostel-buffers) -(ert-deftest test-ai-vterm--display-saved-uses-desktop-defaults-when-state-nil () - "Normal: nil state on a desktop -> rightmost, size=cj/ai-vterm-desktop-width. +(ert-deftest test-ai-term--display-saved-uses-desktop-defaults-when-state-nil () + "Normal: nil state on a desktop -> rightmost, size=cj/ai-term-desktop-width. The cardinal `right' default maps to the frame-edge variant `rightmost' so agent lands at the frame's right edge regardless of which window is selected. `env-laptop-p' is stubbed nil to pin the desktop branch." (let (received-buf received-alist - (cj/--ai-vterm-last-direction nil) - (cj/--ai-vterm-last-size nil) - (cj/ai-vterm-desktop-width 0.5)) + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil) + (cj/ai-term-desktop-width 0.5)) (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil)) ((symbol-function 'display-buffer-in-direction) (lambda (b a) (setq received-buf b received-alist a) 'fake-window))) - (cj/--ai-vterm-display-saved 'fake-buf '((inhibit-same-window . t)))) + (cj/--ai-term-display-saved 'fake-buf '((inhibit-same-window . t)))) (should (eq received-buf 'fake-buf)) (should (eq (cdr (assq 'direction received-alist)) 'rightmost)) (should (= (cdr (assq 'window-width received-alist)) 0.5)) (should (eq (cdr (assq 'inhibit-same-window received-alist)) t)))) -(ert-deftest test-ai-vterm--display-saved-uses-laptop-defaults-when-state-nil () - "Normal: nil state on a laptop -> bottom, size=cj/ai-vterm-laptop-height. +(ert-deftest test-ai-term--display-saved-uses-laptop-defaults-when-state-nil () + "Normal: nil state on a laptop -> bottom, size=cj/ai-term-laptop-height. The cardinal `below' default maps to the frame-edge variant `bottom' and the size lands on the `window-height' axis. `env-laptop-p' is stubbed t to pin the laptop branch." (let (received-alist - (cj/--ai-vterm-last-direction nil) - (cj/--ai-vterm-last-size nil) - (cj/ai-vterm-laptop-height 0.75)) + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil) + (cj/ai-term-laptop-height 0.75)) (cl-letf (((symbol-function 'env-laptop-p) (lambda () t)) ((symbol-function 'display-buffer-in-direction) (lambda (_b a) (setq received-alist a) 'fake-window))) - (cj/--ai-vterm-display-saved 'fake-buf '((inhibit-same-window . t)))) + (cj/--ai-term-display-saved 'fake-buf '((inhibit-same-window . t)))) (should (eq (cdr (assq 'direction received-alist)) 'bottom)) (should (= (cdr (assq 'window-height received-alist)) 0.75)) (should-not (assq 'window-width received-alist)))) -(ert-deftest test-ai-vterm--display-saved-uses-saved-direction-and-size-below () +(ert-deftest test-ai-term--display-saved-uses-saved-direction-and-size-below () "Normal: saved direction=below maps to bottom edge; size=0.4 passes through." (let (received-alist - (cj/--ai-vterm-last-direction 'below) - (cj/--ai-vterm-last-size 0.4)) + (cj/--ai-term-last-direction 'below) + (cj/--ai-term-last-size 0.4)) (cl-letf (((symbol-function 'display-buffer-in-direction) (lambda (_b a) (setq received-alist a) 'fake-window))) - (cj/--ai-vterm-display-saved 'fake-buf nil)) + (cj/--ai-term-display-saved 'fake-buf nil)) (should (eq (cdr (assq 'direction received-alist)) 'bottom)) (should (= (cdr (assq 'window-height received-alist)) 0.4)) (should-not (assq 'window-width received-alist)))) -(ert-deftest test-ai-vterm--display-saved-uses-saved-direction-and-size-right () +(ert-deftest test-ai-term--display-saved-uses-saved-direction-and-size-right () "Normal: saved direction=right maps to rightmost edge; size=0.7 passes through." (let (received-alist - (cj/--ai-vterm-last-direction 'right) - (cj/--ai-vterm-last-size 0.7)) + (cj/--ai-term-last-direction 'right) + (cj/--ai-term-last-size 0.7)) (cl-letf (((symbol-function 'display-buffer-in-direction) (lambda (_b a) (setq received-alist a) 'fake-window))) - (cj/--ai-vterm-display-saved 'fake-buf nil)) + (cj/--ai-term-display-saved 'fake-buf nil)) (should (eq (cdr (assq 'direction received-alist)) 'rightmost)) (should (= (cdr (assq 'window-width received-alist)) 0.7)) (should-not (assq 'window-height received-alist)))) -(ert-deftest test-ai-vterm--display-saved-strips-conflicting-alist-entries () +(ert-deftest test-ai-term--display-saved-strips-conflicting-alist-entries () "Boundary: caller-supplied direction/size are stripped, saved values win." (let (received-alist - (cj/--ai-vterm-last-direction 'right) - (cj/--ai-vterm-last-size 0.7)) + (cj/--ai-term-last-direction 'right) + (cj/--ai-term-last-size 0.7)) (cl-letf (((symbol-function 'display-buffer-in-direction) (lambda (_b a) (setq received-alist a) 'fake-window))) - (cj/--ai-vterm-display-saved + (cj/--ai-term-display-saved 'fake-buf '((direction . below) (window-width . 0.2) @@ -113,17 +113,17 @@ stubbed t to pin the laptop branch." received-alist))) (should (null wh-cells))))) -(ert-deftest test-ai-vterm--display-saved-passes-buffer-through () +(ert-deftest test-ai-term--display-saved-passes-buffer-through () "Normal: BUFFER argument reaches display-buffer-in-direction unchanged." (let (received-buf - (cj/--ai-vterm-last-direction 'right) - (cj/--ai-vterm-last-size 0.5)) + (cj/--ai-term-last-direction 'right) + (cj/--ai-term-last-size 0.5)) (cl-letf (((symbol-function 'display-buffer-in-direction) (lambda (b _a) (setq received-buf b) 'fake-window))) - (cj/--ai-vterm-display-saved 'sentinel-buffer nil)) + (cj/--ai-term-display-saved 'sentinel-buffer nil)) (should (eq received-buf 'sentinel-buffer)))) -(ert-deftest test-ai-vterm--toggle-after-buffer-move-no-extra-window () +(ert-deftest test-ai-term--toggle-after-buffer-move-no-extra-window () "Regression: toggle-off must not leak a window even when buffer-move has cleared the agent window's `quit-restore' parameter. @@ -152,11 +152,11 @@ once and no spurious extra window leaks." ;; Mimic buffer-move's effect: agent lives in this ;; window but quit-restore says nothing about it. (set-window-parameter agent-win 'quit-restore nil) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list)) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list)) (window-count-before (count-windows))) (select-window agent-win) - (cj/test--call-as-gui #'cj/ai-vterm) ; off - (cj/test--call-as-gui #'cj/ai-vterm) ; on + (cj/test--call-as-gui #'cj/ai-term) ; off + (cj/test--call-as-gui #'cj/ai-term) ; on (should (<= (count-windows) window-count-before)) ;; Agent must be displayed exactly once. (let ((agent-windows @@ -169,5 +169,5 @@ once and no spurious extra window leaks." (when (get-buffer right-name) (kill-buffer right-name)) (cj/test--kill-agent-buffers)))) -(provide 'test-ai-vterm--display-saved) -;;; test-ai-vterm--display-saved.el ends here +(provide 'test-ai-term--display-saved) +;;; test-ai-term--display-saved.el ends here diff --git a/tests/test-ai-vterm--displayed-agent-window.el b/tests/test-ai-term--displayed-agent-window.el index f36ca9f5..eeb40ed3 100644 --- a/tests/test-ai-vterm--displayed-agent-window.el +++ b/tests/test-ai-term--displayed-agent-window.el @@ -1,8 +1,8 @@ -;;; test-ai-vterm--displayed-agent-window.el --- Tests for the displayed-window helper -*- lexical-binding: t; -*- +;;; test-ai-term--displayed-agent-window.el --- Tests for the displayed-window helper -*- lexical-binding: t; -*- ;;; Commentary: ;; The helper returns a window in the selected frame whose buffer -;; satisfies `cj/--ai-vterm-buffer-p', or nil when no such window +;; satisfies `cj/--ai-term-buffer-p', or nil when no such window ;; exists. Used by F9 dispatch and M-F9 in-place replacement. ;;; Code: @@ -11,27 +11,27 @@ (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) -(require 'ai-vterm) -(require 'testutil-vterm-buffers) +(require 'ai-term) +(require 'testutil-ghostel-buffers) -(ert-deftest test-ai-vterm--displayed-agent-window-no-buffers-returns-nil () +(ert-deftest test-ai-term--displayed-agent-window-no-buffers-returns-nil () "Boundary: no agent buffers anywhere -> nil." (cj/test--kill-agent-buffers) (save-window-excursion (delete-other-windows) - (should-not (cj/--ai-vterm-displayed-agent-window)))) + (should-not (cj/--ai-term-displayed-agent-window)))) -(ert-deftest test-ai-vterm--displayed-agent-window-not-displayed-returns-nil () +(ert-deftest test-ai-term--displayed-agent-window-not-displayed-returns-nil () "Boundary: agent buffer exists but not in any window -> nil." (cj/test--kill-agent-buffers) (let ((b1 (get-buffer-create "agent [hidden]"))) (unwind-protect (save-window-excursion (delete-other-windows) - (should-not (cj/--ai-vterm-displayed-agent-window))) + (should-not (cj/--ai-term-displayed-agent-window))) (kill-buffer b1)))) -(ert-deftest test-ai-vterm--displayed-agent-window-returns-window-when-displayed () +(ert-deftest test-ai-term--displayed-agent-window-returns-window-when-displayed () "Normal: agent buffer in a window -> returns that window." (cj/test--kill-agent-buffers) (let ((b1 (get-buffer-create "agent [shown]"))) @@ -40,12 +40,12 @@ (delete-other-windows) (let ((win (split-window-right))) (set-window-buffer win b1) - (let ((result (cj/--ai-vterm-displayed-agent-window))) + (let ((result (cj/--ai-term-displayed-agent-window))) (should (windowp result)) (should (eq (window-buffer result) b1))))) (kill-buffer b1)))) -(ert-deftest test-ai-vterm--displayed-agent-window-ignores-non-agent-windows () +(ert-deftest test-ai-term--displayed-agent-window-ignores-non-agent-windows () "Boundary: only a non-agent buffer is displayed -> nil." (cj/test--kill-agent-buffers) (let ((other (get-buffer-create "regular-displayed-buffer"))) @@ -53,8 +53,8 @@ (save-window-excursion (delete-other-windows) (set-window-buffer (selected-window) other) - (should-not (cj/--ai-vterm-displayed-agent-window))) + (should-not (cj/--ai-term-displayed-agent-window))) (kill-buffer other)))) -(provide 'test-ai-vterm--displayed-agent-window) -;;; test-ai-vterm--displayed-agent-window.el ends here +(provide 'test-ai-term--displayed-agent-window) +;;; test-ai-term--displayed-agent-window.el ends here diff --git a/tests/test-ai-term--f9-in-term.el b/tests/test-ai-term--f9-in-term.el new file mode 100644 index 00000000..53e1c4e7 --- /dev/null +++ b/tests/test-ai-term--f9-in-term.el @@ -0,0 +1,45 @@ +;;; test-ai-term--f9-in-term.el --- F9 reaches Emacs from inside an agent buffer -*- lexical-binding: t; -*- + +;;; Commentary: +;; ghostel's semi-char mode forwards keys not in `ghostel-keymap-exceptions' to +;; the terminal program, so a plain <f9> typed while point is in an agent +;; buffer would be sent to the program instead of toggling the agent -- exactly +;; the case when the agent buffer fills the frame. `ai-term.el' re-binds the F9 +;; family in `ghostel-mode-map'. These tests require ghostel (which defines +;; `ghostel-mode-map' and lets ai-term's `with-eval-after-load' fire) BEFORE +;; ai-term, then confirm the bindings landed (and the global ones are intact). +;; `(require 'ghostel)' does not load the native module, so this stays light. + +;;; Code: + +(require 'ert) +(require 'package) + +(setq package-user-dir (expand-file-name "elpa" user-emacs-directory)) +(package-initialize) +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'ghostel) +(require 'ai-term) + +(ert-deftest test-ai-term-f9-bound-in-ghostel-mode-map () + "Normal: <f9> in `ghostel-mode-map' runs the agent toggle." + (should (eq (keymap-lookup ghostel-mode-map "<f9>") #'cj/ai-term))) + +(ert-deftest test-ai-term-f9-family-bound-in-ghostel-mode-map () + "Normal: the C-/M-/C-S- F9 variants are bound in `ghostel-mode-map' too. +`M-<f9>' and `C-S-<f9>' both close an agent via `cj/ai-term-close'." + (should (eq (keymap-lookup ghostel-mode-map "C-<f9>") #'cj/ai-term-pick-project)) + (should (eq (keymap-lookup ghostel-mode-map "M-<f9>") #'cj/ai-term-close)) + (should (eq (keymap-lookup ghostel-mode-map "C-S-<f9>") #'cj/ai-term-close))) + +(ert-deftest test-ai-term-f9-still-bound-globally () + "Normal: the global F9 family bindings are intact. +`<f9>' toggles the ai-term agent window; `C-<f9>' picks a project +agent; `M-<f9>' and `C-S-<f9>' close an agent via `cj/ai-term-close'." + (should (eq (lookup-key (current-global-map) (kbd "<f9>")) #'cj/ai-term)) + (should (eq (lookup-key (current-global-map) (kbd "C-<f9>")) #'cj/ai-term-pick-project)) + (should (eq (lookup-key (current-global-map) (kbd "M-<f9>")) #'cj/ai-term-close)) + (should (eq (lookup-key (current-global-map) (kbd "C-S-<f9>")) #'cj/ai-term-close))) + +(provide 'test-ai-term--f9-in-term) +;;; test-ai-term--f9-in-term.el ends here diff --git a/tests/test-ai-term--launch-command.el b/tests/test-ai-term--launch-command.el new file mode 100644 index 00000000..246e70a3 --- /dev/null +++ b/tests/test-ai-term--launch-command.el @@ -0,0 +1,94 @@ +;;; test-ai-term--launch-command.el --- Tests for cj/--ai-term-launch-command -*- lexical-binding: t; -*- + +;;; Commentary: +;; The launch command is what gets typed into a fresh ghostel shell to bring +;; up the agent inside a per-project tmux session. The session is named +;; `cj/ai-term-tmux-session-prefix' + the project basename, so a second +;; F9 on the same project reattaches to the running agent rather than +;; spawning a new one, and `tmux ls' output can be filtered to AI-term's +;; own sessions. The trailing `exec bash' keeps the tmux window alive if +;; the agent exits, leaving the session intact for recovery. + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'ai-term) + +(ert-deftest test-ai-term--launch-command-uses-new-session-attach () + "Normal: starts with `tmux new-session -A' so existing sessions reattach." + (let ((cj/ai-term-agent-command "agent")) + (should (string-prefix-p + "tmux new-session -A " + (cj/--ai-term-launch-command "/code/foo"))))) + +(ert-deftest test-ai-term--launch-command-includes-prefixed-session-name () + "Normal: the session name is the prefixed form from the name helper." + (let ((cj/ai-term-agent-command "agent") + (cj/ai-term-tmux-session-prefix "aiv-")) + (should (string-match-p + " -s aiv-foo " + (cj/--ai-term-launch-command "/code/foo"))))) + +(ert-deftest test-ai-term--launch-command-names-window () + "Normal: `-n <window-name>' so the agent window is named distinctly." + (let ((cj/ai-term-agent-command "agent") + (cj/ai-term-tmux-window-name "ai")) + (should (string-match-p + " -n ai " + (cj/--ai-term-launch-command "/code/foo"))))) + +(ert-deftest test-ai-term--launch-command-honors-custom-window-name () + "Boundary: a non-default window name is what `-n' gets." + (let ((cj/ai-term-agent-command "agent") + (cj/ai-term-tmux-window-name "agent")) + (should (string-match-p + " -n agent " + (cj/--ai-term-launch-command "/code/foo"))))) + +(ert-deftest test-ai-term--launch-command-includes-start-directory () + "Normal: `-c <dir>' so the new session's first window starts in DIR." + (let ((cj/ai-term-agent-command "agent")) + (should (string-match-p + " -c /code/foo " + (cj/--ai-term-launch-command "/code/foo"))))) + +(ert-deftest test-ai-term--launch-command-includes-agent-command () + "Normal: the configured agent command is in the launched shell command. +The inner command is passed through `shell-quote-argument', so spaces +are escaped (`\\\\ ') -- the regex below accepts either form." + (let ((cj/ai-term-agent-command "agent --some-flag")) + (should (string-match-p + "agent\\(\\\\\\)? --some-flag" + (cj/--ai-term-launch-command "/code/foo"))))) + +(ert-deftest test-ai-term--launch-command-tails-with-exec-bash () + "Boundary: `exec bash' tails so the tmux window survives the agent exiting. +Accepts the post-`shell-quote-argument' shape (`exec\\\\ bash')." + (let ((cj/ai-term-agent-command "agent")) + (should (string-match-p + "exec\\(\\\\\\)? bash" + (cj/--ai-term-launch-command "/code/foo"))))) + +(ert-deftest test-ai-term--launch-command-survives-single-quote-in-agent () + "Normal: a user-customized agent command containing a single quote +shouldn't break the shell parse. `shell-quote-argument' produces a +valid shell token regardless of the embedded quote shape -- the +escaping is implementation-detail, so we assert the literal words +\"hi\" and \"there\" both appear (the space between them may be +escaped as \\\\ )." + (let ((cj/ai-term-agent-command "agent --say 'hi there'")) + (let ((cmd (cj/--ai-term-launch-command "/code/foo"))) + (should (string-match-p "hi\\(\\\\\\)? there" cmd))))) + +(ert-deftest test-ai-term--launch-command-handles-spaces-in-basename () + "Boundary: a basename with whitespace becomes hyphenated before quoting." + (let ((cj/ai-term-agent-command "agent") + (cj/ai-term-tmux-session-prefix "aiv-")) + (should (string-match-p + " -s aiv-my-work " + (cj/--ai-term-launch-command "/code/my work"))))) + +(provide 'test-ai-term--launch-command) +;;; test-ai-term--launch-command.el ends here diff --git a/tests/test-ai-vterm--live-tmux-sessions.el b/tests/test-ai-term--live-tmux-sessions.el index e00b0018..1952caed 100644 --- a/tests/test-ai-vterm--live-tmux-sessions.el +++ b/tests/test-ai-term--live-tmux-sessions.el @@ -1,7 +1,7 @@ -;;; test-ai-vterm--live-tmux-sessions.el --- Tests for cj/--ai-vterm-live-tmux-sessions -*- lexical-binding: t; -*- +;;; test-ai-term--live-tmux-sessions.el --- Tests for cj/--ai-term-live-tmux-sessions -*- lexical-binding: t; -*- ;;; Commentary: -;; Lists the live tmux sessions that carry the AI-vterm prefix so the +;; Lists the live tmux sessions that carry the AI-term prefix so the ;; project picker can surface projects whose agent session survived an ;; Emacs crash. tmux being absent or no server running is a normal ;; "nothing to match" outcome, not an error -- the lister returns nil. @@ -12,9 +12,9 @@ (require 'cl-lib) (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-vterm) +(require 'ai-term) -(defmacro test-ai-vterm--with-tmux-list (exit-code output &rest body) +(defmacro test-ai-term--with-tmux-list (exit-code output &rest body) "Run BODY with `process-file' mocked to a tmux list-sessions response. EXIT-CODE is what `process-file' returns (or the symbol `error' to @@ -35,37 +35,37 @@ make it signal). OUTPUT is written to the stdout destination buffer." ,exit-code))) ,@body)) -(ert-deftest test-ai-vterm--live-tmux-sessions-filters-to-prefix () - "Normal: only sessions starting with the AI-vterm prefix come back." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (test-ai-vterm--with-tmux-list 0 "aiv-foo\nrandom-session\naiv-bar\n" - (should (equal (cj/--ai-vterm-live-tmux-sessions) +(ert-deftest test-ai-term--live-tmux-sessions-filters-to-prefix () + "Normal: only sessions starting with the AI-term prefix come back." + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (test-ai-term--with-tmux-list 0 "aiv-foo\nrandom-session\naiv-bar\n" + (should (equal (cj/--ai-term-live-tmux-sessions) '("aiv-foo" "aiv-bar")))))) -(ert-deftest test-ai-vterm--live-tmux-sessions-honors-custom-prefix () +(ert-deftest test-ai-term--live-tmux-sessions-honors-custom-prefix () "Normal: a non-default prefix is what gets matched." - (let ((cj/ai-vterm-tmux-session-prefix "em-")) - (test-ai-vterm--with-tmux-list 0 "em-foo\naiv-bar\nem-baz\n" - (should (equal (cj/--ai-vterm-live-tmux-sessions) + (let ((cj/ai-term-tmux-session-prefix "em-")) + (test-ai-term--with-tmux-list 0 "em-foo\naiv-bar\nem-baz\n" + (should (equal (cj/--ai-term-live-tmux-sessions) '("em-foo" "em-baz")))))) -(ert-deftest test-ai-vterm--live-tmux-sessions-empty-output-yields-nil () +(ert-deftest test-ai-term--live-tmux-sessions-empty-output-yields-nil () "Boundary: a running server with no matching sessions yields nil." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (test-ai-vterm--with-tmux-list 0 "other-a\nother-b\n" - (should (null (cj/--ai-vterm-live-tmux-sessions)))))) + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (test-ai-term--with-tmux-list 0 "other-a\nother-b\n" + (should (null (cj/--ai-term-live-tmux-sessions)))))) -(ert-deftest test-ai-vterm--live-tmux-sessions-no-server-yields-nil () +(ert-deftest test-ai-term--live-tmux-sessions-no-server-yields-nil () "Error: tmux exits non-zero (no server running) -> nil, not a signal." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (test-ai-vterm--with-tmux-list 1 "no server running on /tmp/tmux-1000/default\n" - (should (null (cj/--ai-vterm-live-tmux-sessions)))))) + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (test-ai-term--with-tmux-list 1 "no server running on /tmp/tmux-1000/default\n" + (should (null (cj/--ai-term-live-tmux-sessions)))))) -(ert-deftest test-ai-vterm--live-tmux-sessions-tmux-missing-yields-nil () +(ert-deftest test-ai-term--live-tmux-sessions-tmux-missing-yields-nil () "Error: tmux not installed -> `process-file' signals; lister returns nil." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (test-ai-vterm--with-tmux-list 'error "" - (should (null (cj/--ai-vterm-live-tmux-sessions)))))) + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (test-ai-term--with-tmux-list 'error "" + (should (null (cj/--ai-term-live-tmux-sessions)))))) -(provide 'test-ai-vterm--live-tmux-sessions) -;;; test-ai-vterm--live-tmux-sessions.el ends here +(provide 'test-ai-term--live-tmux-sessions) +;;; test-ai-term--live-tmux-sessions.el ends here diff --git a/tests/test-ai-vterm--pick-project.el b/tests/test-ai-term--pick-project.el index f332589a..e6d2f25b 100644 --- a/tests/test-ai-vterm--pick-project.el +++ b/tests/test-ai-term--pick-project.el @@ -1,4 +1,4 @@ -;;; test-ai-vterm--pick-project.el --- Tests for cj/--ai-vterm-pick-project -*- lexical-binding: t; -*- +;;; test-ai-term--pick-project.el --- Tests for cj/--ai-term-pick-project -*- lexical-binding: t; -*- ;;; Commentary: ;; The picker presents abbreviated paths to `completing-read' (projects @@ -14,104 +14,104 @@ (require 'cl-lib) (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-vterm) +(require 'ai-term) -(defun test-ai-vterm--collection-strings (collection) +(defun test-ai-term--collection-strings (collection) "Return the candidate display strings from a completing-read COLLECTION. Works whether COLLECTION is an alist or a completion-table function." (all-completions "" collection)) -(ert-deftest test-ai-vterm--pick-project-returns-absolute-path-of-choice () +(ert-deftest test-ai-term--pick-project-returns-absolute-path-of-choice () "Normal: user picks a candidate, picker returns its absolute path." - (cl-letf (((symbol-function 'cj/--ai-vterm-candidates) + (cl-letf (((symbol-function 'cj/--ai-term-candidates) (lambda () '("/home/u/code/foo" "/home/u/code/bar"))) - ((symbol-function 'cj/--ai-vterm-live-tmux-sessions) + ((symbol-function 'cj/--ai-term-live-tmux-sessions) (lambda () nil)) ((symbol-function 'completing-read) (lambda (_p collection &rest _) (seq-find (lambda (s) (string-match-p "bar" s)) - (test-ai-vterm--collection-strings collection))))) - (should (equal (cj/--ai-vterm-pick-project) "/home/u/code/bar")))) + (test-ai-term--collection-strings collection))))) + (should (equal (cj/--ai-term-pick-project) "/home/u/code/bar")))) -(ert-deftest test-ai-vterm--pick-project-empty-candidates-raises-user-error () +(ert-deftest test-ai-term--pick-project-empty-candidates-raises-user-error () "Error: no candidates -> user-error rather than empty prompt." - (cl-letf (((symbol-function 'cj/--ai-vterm-candidates) (lambda () nil))) - (should-error (cj/--ai-vterm-pick-project) :type 'user-error))) + (cl-letf (((symbol-function 'cj/--ai-term-candidates) (lambda () nil))) + (should-error (cj/--ai-term-pick-project) :type 'user-error))) -(ert-deftest test-ai-vterm--pick-project-presents-abbreviated-paths () +(ert-deftest test-ai-term--pick-project-presents-abbreviated-paths () "Normal: the completing-read collection holds abbreviated display forms." (let (received-strings) - (cl-letf (((symbol-function 'cj/--ai-vterm-candidates) + (cl-letf (((symbol-function 'cj/--ai-term-candidates) (lambda () (list (expand-file-name "~/code/foo")))) - ((symbol-function 'cj/--ai-vterm-live-tmux-sessions) + ((symbol-function 'cj/--ai-term-live-tmux-sessions) (lambda () nil)) ((symbol-function 'completing-read) (lambda (_p collection &rest _) - (setq received-strings (test-ai-vterm--collection-strings collection)) + (setq received-strings (test-ai-term--collection-strings collection)) (car received-strings)))) - (cj/--ai-vterm-pick-project) + (cj/--ai-term-pick-project) (should (equal (car received-strings) "~/code/foo"))))) -(ert-deftest test-ai-vterm--pick-project-active-sessions-sort-first () +(ert-deftest test-ai-term--pick-project-active-sessions-sort-first () "Normal: a project with a live tmux session leads; it carries [detached]." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-") + (let ((cj/ai-term-tmux-session-prefix "aiv-") received-strings) - (cl-letf (((symbol-function 'cj/--ai-vterm-candidates) + (cl-letf (((symbol-function 'cj/--ai-term-candidates) (lambda () '("/c/foo" "/c/bar" "/c/baz"))) - ((symbol-function 'cj/--ai-vterm-live-tmux-sessions) + ((symbol-function 'cj/--ai-term-live-tmux-sessions) (lambda () '("aiv-baz"))) ((symbol-function 'completing-read) (lambda (_p collection &rest _) - (setq received-strings (test-ai-vterm--collection-strings collection)) + (setq received-strings (test-ai-term--collection-strings collection)) (car received-strings)))) - (cj/--ai-vterm-pick-project) + (cj/--ai-term-pick-project) (should (equal received-strings '("/c/baz [detached]" "/c/bar" "/c/foo")))))) -(ert-deftest test-ai-vterm--format-candidate-flags-running-project () +(ert-deftest test-ai-term--format-candidate-flags-running-project () "Normal: a path whose agent buffer has a live process gets a [running] suffix." (let* ((path (expand-file-name "~/code/already-running")) - (buffer-name (cj/--ai-vterm-buffer-name path)) + (buffer-name (cj/--ai-term-buffer-name path)) (buf (get-buffer-create buffer-name))) (unwind-protect - (cl-letf (((symbol-function 'cj/--ai-vterm-process-live-p) + (cl-letf (((symbol-function 'cj/--ai-term-process-live-p) (lambda (b) (eq b buf)))) - (should (equal (cj/--ai-vterm-format-candidate path) + (should (equal (cj/--ai-term-format-candidate path) (format "%s [running]" (abbreviate-file-name path))))) (kill-buffer buf)))) -(ert-deftest test-ai-vterm--format-candidate-flags-detached-session () +(ert-deftest test-ai-term--format-candidate-flags-detached-session () "Normal: no buffer but a matching tmux session -> [detached] suffix." - (let* ((cj/ai-vterm-tmux-session-prefix "aiv-") + (let* ((cj/ai-term-tmux-session-prefix "aiv-") (path (expand-file-name "~/code/has-session")) - (bn (cj/--ai-vterm-buffer-name path))) + (bn (cj/--ai-term-buffer-name path))) (when (get-buffer bn) (kill-buffer bn)) - (should (equal (cj/--ai-vterm-format-candidate - path (list (cj/--ai-vterm-tmux-session-name path))) + (should (equal (cj/--ai-term-format-candidate + path (list (cj/--ai-term-tmux-session-name path))) (format "%s [detached]" (abbreviate-file-name path)))))) -(ert-deftest test-ai-vterm--format-candidate-running-beats-detached () +(ert-deftest test-ai-term--format-candidate-running-beats-detached () "Boundary: a live buffer wins over a matching session -> [running], not [detached]." - (let* ((cj/ai-vterm-tmux-session-prefix "aiv-") + (let* ((cj/ai-term-tmux-session-prefix "aiv-") (path (expand-file-name "~/code/both")) - (bn (cj/--ai-vterm-buffer-name path)) + (bn (cj/--ai-term-buffer-name path)) (buf (get-buffer-create bn))) (unwind-protect - (cl-letf (((symbol-function 'cj/--ai-vterm-process-live-p) + (cl-letf (((symbol-function 'cj/--ai-term-process-live-p) (lambda (b) (eq b buf)))) - (should (equal (cj/--ai-vterm-format-candidate - path (list (cj/--ai-vterm-tmux-session-name path))) + (should (equal (cj/--ai-term-format-candidate + path (list (cj/--ai-term-tmux-session-name path))) (format "%s [running]" (abbreviate-file-name path))))) (kill-buffer buf)))) -(ert-deftest test-ai-vterm--format-candidate-omits-flag-when-not-running () +(ert-deftest test-ai-term--format-candidate-omits-flag-when-not-running () "Boundary: a path with no buffer or no live process -> plain abbreviated path." (let ((path (expand-file-name "~/code/not-running"))) ;; Make sure no agent buffer exists for this path. - (let ((bn (cj/--ai-vterm-buffer-name path))) + (let ((bn (cj/--ai-term-buffer-name path))) (when (get-buffer bn) (kill-buffer bn))) - (should (equal (cj/--ai-vterm-format-candidate path) + (should (equal (cj/--ai-term-format-candidate path) (abbreviate-file-name path))))) -(provide 'test-ai-vterm--pick-project) -;;; test-ai-vterm--pick-project.el ends here +(provide 'test-ai-term--pick-project) +;;; test-ai-term--pick-project.el ends here diff --git a/tests/test-ai-term--record-mru.el b/tests/test-ai-term--record-mru.el new file mode 100644 index 00000000..e00f6814 --- /dev/null +++ b/tests/test-ai-term--record-mru.el @@ -0,0 +1,48 @@ +;;; test-ai-term--record-mru.el --- Tests for the AI-term project MRU list -*- lexical-binding: t; -*- + +;;; Commentary: +;; `cj/--ai-term-record-mru' tracks which project dirs have been opened via +;; the launcher this session, most-recently-opened first, so the picker can +;; surface recently-used projects at the top of the active-sessions group. +;; `cj/--ai-term-mru-rank' reports a dir's position in that list (or nil). + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'ai-term) + +(ert-deftest test-ai-term--record-mru-pushes-to-front () + "Normal: a freshly recorded dir leads the list, newest first." + (let ((cj/--ai-term-mru nil)) + (cj/--ai-term-record-mru "/c/alpha") + (cj/--ai-term-record-mru "/c/beta") + (should (equal cj/--ai-term-mru '("/c/beta" "/c/alpha"))))) + +(ert-deftest test-ai-term--record-mru-dedups-and-moves-to-front () + "Normal: re-recording a dir moves it to the front with no duplicate." + (let ((cj/--ai-term-mru nil)) + (cj/--ai-term-record-mru "/c/alpha") + (cj/--ai-term-record-mru "/c/beta") + (cj/--ai-term-record-mru "/c/alpha") + (should (equal cj/--ai-term-mru '("/c/alpha" "/c/beta"))))) + +(ert-deftest test-ai-term--record-mru-normalizes-trailing-slash () + "Boundary: `/c/foo' and `/c/foo/' are the same MRU entry." + (let ((cj/--ai-term-mru nil)) + (cj/--ai-term-record-mru "/c/foo/") + (cj/--ai-term-record-mru "/c/foo") + (should (equal cj/--ai-term-mru '("/c/foo"))))) + +(ert-deftest test-ai-term--mru-rank-returns-index-or-nil () + "Normal/Boundary: rank is the list position; nil when the dir isn't there; +the lookup normalizes a trailing slash the same way `record-mru' does." + (let ((cj/--ai-term-mru '("/c/beta" "/c/alpha"))) + (should (= 0 (cj/--ai-term-mru-rank "/c/beta"))) + (should (= 1 (cj/--ai-term-mru-rank "/c/alpha"))) + (should (= 0 (cj/--ai-term-mru-rank "/c/beta/"))) + (should-not (cj/--ai-term-mru-rank "/c/gamma")))) + +(provide 'test-ai-term--record-mru) +;;; test-ai-term--record-mru.el ends here diff --git a/tests/test-ai-vterm--reuse-edge-window.el b/tests/test-ai-term--reuse-edge-window.el index eb1b1d75..c41aab73 100644 --- a/tests/test-ai-vterm--reuse-edge-window.el +++ b/tests/test-ai-term--reuse-edge-window.el @@ -1,11 +1,11 @@ -;;; test-ai-vterm--reuse-edge-window.el --- Tests for edge-window reuse -*- lexical-binding: t; -*- +;;; test-ai-term--reuse-edge-window.el --- Tests for edge-window reuse -*- lexical-binding: t; -*- ;;; Commentary: -;; `cj/--ai-vterm-reuse-edge-window' is the display-buffer action that +;; `cj/--ai-term-reuse-edge-window' is the display-buffer action that ;; reuses the window already forming the half the agent would occupy ;; (the right column on a desktop, the bottom row on a laptop) instead ;; of splitting a third window in. It runs between -;; `cj/--ai-vterm-reuse-existing-agent' and `cj/--ai-vterm-display-saved' +;; `cj/--ai-term-reuse-existing-agent' and `cj/--ai-term-display-saved' ;; in the rule chain. ;; ;; Regression target (Craig, 2026-05-24): a frame already split into two @@ -14,8 +14,8 @@ ;; stays put -- the dimension the older display-saved tests never checked. ;; ;; Tests build real windows (split-window) and route a fresh agent buffer -;; through the actual `cj/--ai-vterm-display-rule-list', the same pattern -;; as test-ai-vterm--display-saved.el. +;; through the actual `cj/--ai-term-display-rule-list', the same pattern +;; as test-ai-term--display-saved.el. ;;; Code: @@ -24,15 +24,15 @@ (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) -(require 'ai-vterm) -(require 'testutil-vterm-buffers) +(require 'ai-term) +(require 'testutil-ghostel-buffers) (defun cj/test--displayed-buffer-names () "Return the buffer names shown in the selected frame, left/top to right/bottom." (mapcar (lambda (w) (buffer-name (window-buffer w))) (window-list nil 'never))) -(ert-deftest test-ai-vterm--reuse-edge-window-2col-desktop-no-third-window () +(ert-deftest test-ai-term--reuse-edge-window-2col-desktop-no-third-window () "Normal: F9 in a 2-column split reuses the right column, no third window. Desktop default direction is `right', so the agent takes the existing right half: the frame stays at two windows [left | agent]." @@ -40,8 +40,8 @@ right half: the frame stays at two windows [left | agent]." (let ((agent-name "agent [edge-2col]") (left-name "*test-edge-left*") (right-name "*test-edge-right*") - (cj/--ai-vterm-last-direction nil) - (cj/--ai-vterm-last-size nil)) + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil)) (unwind-protect (save-window-excursion (delete-other-windows) @@ -52,7 +52,7 @@ right half: the frame stays at two windows [left | agent]." (set-window-buffer (selected-window) left-buf) (let ((rw (split-window (selected-window) nil 'right))) (set-window-buffer rw right-buf)) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) (display-buffer agent-buf)) (should (= (count-windows) 2)) (let ((bufs (cj/test--displayed-buffer-names))) @@ -64,7 +64,7 @@ right half: the frame stays at two windows [left | agent]." (when (get-buffer right-name) (kill-buffer right-name)) (cj/test--kill-agent-buffers)))) -(ert-deftest test-ai-vterm--reuse-edge-window-2row-laptop-no-third-window () +(ert-deftest test-ai-term--reuse-edge-window-2row-laptop-no-third-window () "Normal: F9 in a 2-row split on a laptop reuses the bottom row. Laptop default direction is `below', so the agent takes the existing bottom half: the frame stays at two windows." @@ -72,8 +72,8 @@ bottom half: the frame stays at two windows." (let ((agent-name "agent [edge-2row]") (top-name "*test-edge-top*") (bottom-name "*test-edge-bottom*") - (cj/--ai-vterm-last-direction nil) - (cj/--ai-vterm-last-size nil)) + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil)) (unwind-protect (save-window-excursion (delete-other-windows) @@ -84,7 +84,7 @@ bottom half: the frame stays at two windows." (set-window-buffer (selected-window) top-buf) (let ((bw (split-window (selected-window) nil 'below))) (set-window-buffer bw bottom-buf)) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) (display-buffer agent-buf)) (should (= (count-windows) 2)) (let ((bufs (cj/test--displayed-buffer-names))) @@ -95,15 +95,15 @@ bottom half: the frame stays at two windows." (when (get-buffer bottom-name) (kill-buffer bottom-name)) (cj/test--kill-agent-buffers)))) -(ert-deftest test-ai-vterm--reuse-edge-window-single-window-splits () +(ert-deftest test-ai-term--reuse-edge-window-single-window-splits () "Boundary: a single-window frame still splits to create the half. No existing edge window to reuse, so the display-saved path runs and the frame goes from one window to two with the agent present." (cj/test--kill-agent-buffers) (let ((agent-name "agent [edge-single]") (sole-name "*test-edge-sole*") - (cj/--ai-vterm-last-direction nil) - (cj/--ai-vterm-last-size nil)) + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil)) (unwind-protect (save-window-excursion (delete-other-windows) @@ -111,14 +111,14 @@ the frame goes from one window to two with the agent present." (let ((sole-buf (get-buffer-create sole-name)) (agent-buf (get-buffer-create agent-name))) (set-window-buffer (selected-window) sole-buf) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) (display-buffer agent-buf)) (should (= (count-windows) 2)) (should (member agent-name (cj/test--displayed-buffer-names)))))) (when (get-buffer sole-name) (kill-buffer sole-name)) (cj/test--kill-agent-buffers)))) -(ert-deftest test-ai-vterm--reuse-edge-window-axis-mismatch-falls-through () +(ert-deftest test-ai-term--reuse-edge-window-axis-mismatch-falls-through () "Error/Boundary: a top/bottom split on a desktop has no right half. Desktop direction is `right' but the frame is split horizontally, so no single full-height right column exists to reuse. The chain falls @@ -128,8 +128,8 @@ ends up displayed." (let ((agent-name "agent [edge-mismatch]") (top-name "*test-edge-mm-top*") (bottom-name "*test-edge-mm-bottom*") - (cj/--ai-vterm-last-direction nil) - (cj/--ai-vterm-last-size nil)) + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil)) (unwind-protect (save-window-excursion (delete-other-windows) @@ -140,7 +140,7 @@ ends up displayed." (set-window-buffer (selected-window) top-buf) (let ((bw (split-window (selected-window) nil 'below))) (set-window-buffer bw bottom-buf)) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) (display-buffer agent-buf)) ;; No half to reuse, so a fresh column is split: three windows. (should (member agent-name (cj/test--displayed-buffer-names))) @@ -150,7 +150,7 @@ ends up displayed." (when (get-buffer bottom-name) (kill-buffer bottom-name)) (cj/test--kill-agent-buffers)))) -(ert-deftest test-ai-vterm--reuse-edge-window-toggle-off-collapses-split () +(ert-deftest test-ai-term--reuse-edge-window-toggle-off-collapses-split () "Normal: toggle-off after a slot reuse collapses the agent split. =| 1 | 2 |= + show agent -> =| 1 | A |=; toggle off -> =| 1 |= (one window). F9 always collapses the agent split back to the working layout @@ -160,8 +160,8 @@ window rather than restoring the displaced buffer into a kept slot." (let ((agent-name "agent [edge-restore]") (left-name "*test-restore-left*") (right-name "*test-restore-right*") - (cj/--ai-vterm-last-direction nil) - (cj/--ai-vterm-last-size nil)) + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil)) (unwind-protect (save-window-excursion (delete-other-windows) @@ -172,13 +172,13 @@ window rather than restoring the displaced buffer into a kept slot." (set-window-buffer (selected-window) left-buf) (let ((rw (split-window (selected-window) nil 'right))) (set-window-buffer rw right-buf)) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) (display-buffer agent-buf) (should (= (count-windows) 2)) (should (member agent-name (cj/test--displayed-buffer-names))) ;; Toggle off -> the agent window is deleted, leaving the ;; working buffer at full frame. - (cj/test--call-as-gui #'cj/ai-vterm) + (cj/test--call-as-gui #'cj/ai-term) (should (= (count-windows) 1)) (let ((bufs (cj/test--displayed-buffer-names))) (should (member left-name bufs)) @@ -187,7 +187,7 @@ window rather than restoring the displaced buffer into a kept slot." (when (get-buffer right-name) (kill-buffer right-name)) (cj/test--kill-agent-buffers)))) -(ert-deftest test-ai-vterm--reuse-edge-window-cycle-collapses-then-resplits () +(ert-deftest test-ai-term--reuse-edge-window-cycle-collapses-then-resplits () "Normal: on/off/on cycle collapses on off and re-splits at the same width. =| 1 | 2 |= -> on =| 1 | A |= (2 windows) -> off =| 1 |= (1 window, collapsed) -> on =| 1 | A |= (2 windows again), with the agent re-split at @@ -197,8 +197,8 @@ preserved across the toggle (respect-split-width)." (let ((agent-name "agent [edge-cycle]") (left-name "*test-cycle-left*") (right-name "*test-cycle-right*") - (cj/--ai-vterm-last-direction nil) - (cj/--ai-vterm-last-size nil)) + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil)) (unwind-protect (save-window-excursion (delete-other-windows) @@ -210,20 +210,20 @@ preserved across the toggle (respect-split-width)." (set-window-buffer (selected-window) left-buf) (let ((rw (split-window (selected-window) nil 'right))) (set-window-buffer rw right-buf)) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) ;; on -- agent takes the existing right slot (display-buffer agent-buf) (should (= (count-windows) 2)) (setq slot-width - (window-body-width (cj/--ai-vterm-displayed-agent-window))) + (window-body-width (cj/--ai-term-displayed-agent-window))) ;; off -- the split collapses to a single window - (cj/test--call-as-gui #'cj/ai-vterm) + (cj/test--call-as-gui #'cj/ai-term) (should (= (count-windows) 1)) - (should-not (cj/--ai-vterm-displayed-agent-window)) + (should-not (cj/--ai-term-displayed-agent-window)) ;; on again -- re-split at the captured width - (cj/test--call-as-gui #'cj/ai-vterm) + (cj/test--call-as-gui #'cj/ai-term) (should (= (count-windows) 2)) - (let ((win (cj/--ai-vterm-displayed-agent-window))) + (let ((win (cj/--ai-term-displayed-agent-window))) (should (windowp win)) (should (eq (window-buffer win) agent-buf)) (should (= (window-body-width win) slot-width))))))) @@ -231,18 +231,18 @@ preserved across the toggle (respect-split-width)." (when (get-buffer right-name) (kill-buffer right-name)) (cj/test--kill-agent-buffers)))) -(ert-deftest test-ai-vterm--reuse-edge-window-toggle-keeps-same-agent-with-multiple () +(ert-deftest test-ai-term--reuse-edge-window-toggle-keeps-same-agent-with-multiple () "Regression: with two agents alive, toggle-off then on restores the SAME agent, not a different one. Toggle-off must not bury the agent to the end -of the buffer list -- if it does, `cj/--ai-vterm-dispatch' re-shows the +of the buffer list -- if it does, `cj/--ai-term-dispatch' re-shows the most-recent agent, which would now be the other one." (cj/test--kill-agent-buffers) (let ((a1-name "agent [multi-1]") (a2-name "agent [multi-2]") (left-name "*test-multi-left*") (right-name "*test-multi-right*") - (cj/--ai-vterm-last-direction nil) - (cj/--ai-vterm-last-size nil)) + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil)) (unwind-protect (save-window-excursion (delete-other-windows) @@ -256,18 +256,18 @@ most-recent agent, which would now be the other one." (set-window-buffer (selected-window) left-buf) (let ((rw (split-window (selected-window) nil 'right))) (set-window-buffer rw right-buf)) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) (display-buffer a2) ; | left | A2 | - (should (eq (window-buffer (cj/--ai-vterm-displayed-agent-window)) + (should (eq (window-buffer (cj/--ai-term-displayed-agent-window)) a2)) - (cj/test--call-as-gui #'cj/ai-vterm) ; off -> | left | right | - (should-not (cj/--ai-vterm-displayed-agent-window)) - (cj/test--call-as-gui #'cj/ai-vterm) ; on -> must bring A2 back - (should (eq (window-buffer (cj/--ai-vterm-displayed-agent-window)) + (cj/test--call-as-gui #'cj/ai-term) ; off -> | left | right | + (should-not (cj/--ai-term-displayed-agent-window)) + (cj/test--call-as-gui #'cj/ai-term) ; on -> must bring A2 back + (should (eq (window-buffer (cj/--ai-term-displayed-agent-window)) a2)))))) (when (get-buffer left-name) (kill-buffer left-name)) (when (get-buffer right-name) (kill-buffer right-name)) (cj/test--kill-agent-buffers)))) -(provide 'test-ai-vterm--reuse-edge-window) -;;; test-ai-vterm--reuse-edge-window.el ends here +(provide 'test-ai-term--reuse-edge-window) +;;; test-ai-term--reuse-edge-window.el ends here diff --git a/tests/test-ai-vterm--reuse-existing-agent.el b/tests/test-ai-term--reuse-existing-agent.el index e6848014..3f0c6449 100644 --- a/tests/test-ai-vterm--reuse-existing-agent.el +++ b/tests/test-ai-term--reuse-existing-agent.el @@ -1,8 +1,8 @@ -;;; test-ai-vterm--reuse-existing-agent.el --- Tests for reuse-existing-agent action -*- lexical-binding: t; -*- +;;; test-ai-term--reuse-existing-agent.el --- Tests for reuse-existing-agent action -*- lexical-binding: t; -*- ;;; Commentary: ;; The action looks for any window in the selected frame whose buffer -;; satisfies `cj/--ai-vterm-buffer-p'. When found, swaps that +;; satisfies `cj/--ai-term-buffer-p'. When found, swaps that ;; window's buffer for the one being displayed and returns the ;; window. When not found, returns nil so the next action in the ;; chain runs. @@ -16,10 +16,10 @@ (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) -(require 'ai-vterm) -(require 'testutil-vterm-buffers) +(require 'ai-term) +(require 'testutil-ghostel-buffers) -(ert-deftest test-ai-vterm--reuse-existing-agent-swaps-buffer-when-window-exists () +(ert-deftest test-ai-term--reuse-existing-agent-swaps-buffer-when-window-exists () "Normal: an agent window exists -> swap its buffer, return the window." (cj/test--kill-agent-buffers) (save-window-excursion @@ -30,23 +30,23 @@ (unwind-protect (progn (set-window-buffer split existing) - (let ((result (cj/--ai-vterm-reuse-existing-agent new-buf nil))) + (let ((result (cj/--ai-term-reuse-existing-agent new-buf nil))) (should (eq result split)) (should (eq (window-buffer split) new-buf)))) (kill-buffer existing) (kill-buffer new-buf))))) -(ert-deftest test-ai-vterm--reuse-existing-agent-returns-nil-when-no-agent-window () +(ert-deftest test-ai-term--reuse-existing-agent-returns-nil-when-no-agent-window () "Boundary: no agent window in frame -> nil (chain continues to next action)." (cj/test--kill-agent-buffers) (save-window-excursion (delete-other-windows) (let ((new-buf (get-buffer-create "agent [no-existing]"))) (unwind-protect - (should (null (cj/--ai-vterm-reuse-existing-agent new-buf nil))) + (should (null (cj/--ai-term-reuse-existing-agent new-buf nil))) (kill-buffer new-buf))))) -(ert-deftest test-ai-vterm--reuse-existing-agent-leaves-non-agent-windows-alone () +(ert-deftest test-ai-term--reuse-existing-agent-leaves-non-agent-windows-alone () "Boundary: only non-agent windows in frame -> nil; other windows untouched." (cj/test--kill-agent-buffers) (save-window-excursion @@ -58,7 +58,7 @@ (progn (set-window-buffer (selected-window) code-buf) (set-window-buffer other-win code-buf) - (let ((result (cj/--ai-vterm-reuse-existing-agent + (let ((result (cj/--ai-term-reuse-existing-agent new-agent nil))) (should (null result)) (should (eq (window-buffer (selected-window)) code-buf)) @@ -66,7 +66,7 @@ (kill-buffer code-buf) (kill-buffer new-agent))))) -(ert-deftest test-ai-vterm--reuse-existing-agent-preserves-non-agent-window-when-swapping () +(ert-deftest test-ai-term--reuse-existing-agent-preserves-non-agent-window-when-swapping () "Normal: swap agent window only; the other window keeps its buffer. This is the C-F9-from-agent regression: with agent at the bottom @@ -86,7 +86,7 @@ buffer, not the top window's." (set-window-buffer bottom-win agent-a) ;; Focus the agent window -- this is the regression scenario. (select-window bottom-win) - (let ((result (cj/--ai-vterm-reuse-existing-agent + (let ((result (cj/--ai-term-reuse-existing-agent agent-b nil))) (should (eq result bottom-win)) (should (eq (window-buffer bottom-win) agent-b)) @@ -95,5 +95,5 @@ buffer, not the top window's." (kill-buffer agent-a) (kill-buffer agent-b))))) -(provide 'test-ai-vterm--reuse-existing-agent) -;;; test-ai-vterm--reuse-existing-agent.el ends here +(provide 'test-ai-term--reuse-existing-agent) +;;; test-ai-term--reuse-existing-agent.el ends here diff --git a/tests/test-ai-vterm--server-display.el b/tests/test-ai-term--server-display.el index 1d0d1001..b3d32dc8 100644 --- a/tests/test-ai-vterm--server-display.el +++ b/tests/test-ai-term--server-display.el @@ -1,12 +1,12 @@ -;;; test-ai-vterm--server-display.el --- Tests for emacsclient window routing -*- lexical-binding: t; -*- +;;; test-ai-term--server-display.el --- Tests for emacsclient window routing -*- lexical-binding: t; -*- ;;; Commentary: -;; `cj/--ai-vterm-server-display' is wired as `server-window' so a file +;; `cj/--ai-term-server-display' is wired as `server-window' so a file ;; opened via `emacsclient -n' (e.g. when Craig tells the agent to open -;; something) doesn't land on top of the agent vterm. When the selected +;; something) doesn't land on top of the agent terminal. When the selected ;; window shows an `agent [...]' buffer, the file goes to a non-agent ;; window instead -- splitting one off the agent if it is the only window. -;; `cj/--ai-vterm-non-agent-window' picks that window. +;; `cj/--ai-term-non-agent-window' picks that window. ;;; Code: @@ -14,11 +14,11 @@ (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) -(require 'ai-vterm) +(require 'ai-term) (require 'server) -(require 'testutil-vterm-buffers) +(require 'testutil-ghostel-buffers) -(ert-deftest test-ai-vterm--non-agent-window-finds-code-window () +(ert-deftest test-ai-term--non-agent-window-finds-code-window () "Normal: agent on the right, code on the left -> returns the code window." (cj/test--kill-agent-buffers) (let ((agent (get-buffer-create "agent [proj]")) @@ -29,13 +29,13 @@ (set-window-buffer (selected-window) code) (let ((right (split-window-right))) (set-window-buffer right agent) - (let ((found (cj/--ai-vterm-non-agent-window right))) + (let ((found (cj/--ai-term-non-agent-window right))) (should (windowp found)) (should (eq (window-buffer found) code))))) (kill-buffer agent) (kill-buffer code)))) -(ert-deftest test-ai-vterm--non-agent-window-none-when-only-agent () +(ert-deftest test-ai-term--non-agent-window-none-when-only-agent () "Boundary: the agent window is the only one -> nil." (cj/test--kill-agent-buffers) (let ((agent (get-buffer-create "agent [solo]"))) @@ -43,10 +43,10 @@ (save-window-excursion (delete-other-windows) (set-window-buffer (selected-window) agent) - (should-not (cj/--ai-vterm-non-agent-window (selected-window)))) + (should-not (cj/--ai-term-non-agent-window (selected-window)))) (kill-buffer agent)))) -(ert-deftest test-ai-vterm--non-agent-window-skips-dedicated () +(ert-deftest test-ai-term--non-agent-window-skips-dedicated () "Boundary: a dedicated non-agent window is not a valid target." (cj/test--kill-agent-buffers) (let ((agent (get-buffer-create "agent [proj]")) @@ -59,12 +59,12 @@ (set-window-buffer w side) (set-window-dedicated-p w t) (unwind-protect - (should-not (cj/--ai-vterm-non-agent-window (selected-window))) + (should-not (cj/--ai-term-non-agent-window (selected-window))) (set-window-dedicated-p w nil)))) (kill-buffer agent) (kill-buffer side)))) -(ert-deftest test-ai-vterm--server-display-routes-around-agent () +(ert-deftest test-ai-term--server-display-routes-around-agent () "Normal: selected window is the agent -> the file lands in the other window and the agent window keeps the agent buffer." (cj/test--kill-agent-buffers) @@ -78,7 +78,7 @@ window and the agent window keeps the agent buffer." (let ((agent-win (split-window-right))) (set-window-buffer agent-win agent) (select-window agent-win) - (cj/--ai-vterm-server-display file) + (cj/--ai-term-server-display file) (should (eq (window-buffer agent-win) agent)) (should (get-buffer-window file)) (should-not (eq (get-buffer-window file) agent-win)))) @@ -86,7 +86,7 @@ window and the agent window keeps the agent buffer." (kill-buffer code) (kill-buffer file)))) -(ert-deftest test-ai-vterm--server-display-splits-when-agent-is-only-window () +(ert-deftest test-ai-term--server-display-splits-when-agent-is-only-window () "Boundary: the agent is the only window -> a window is split off for the file; the agent window keeps the agent buffer." (cj/test--kill-agent-buffers) @@ -97,14 +97,14 @@ file; the agent window keeps the agent buffer." (delete-other-windows) (set-window-buffer (selected-window) agent) (let ((agent-win (selected-window))) - (cj/--ai-vterm-server-display file) + (cj/--ai-term-server-display file) (should (= 2 (length (window-list (selected-frame) 'never)))) (should (eq (window-buffer agent-win) agent)) (should (eq (window-buffer (get-buffer-window file)) file)))) (kill-buffer agent) (kill-buffer file)))) -(ert-deftest test-ai-vterm--server-display-passthrough-when-not-agent () +(ert-deftest test-ai-term--server-display-passthrough-when-not-agent () "Normal: selected window is a regular buffer -> the file is displayed normally and nothing special happens (no agent window to protect)." (cj/test--kill-agent-buffers) @@ -114,14 +114,14 @@ normally and nothing special happens (no agent window to protect)." (save-window-excursion (delete-other-windows) (set-window-buffer (selected-window) code) - (cj/--ai-vterm-server-display file) + (cj/--ai-term-server-display file) (should (get-buffer-window file))) (kill-buffer code) (kill-buffer file)))) -(ert-deftest test-ai-vterm--server-window-wired-to-helper () +(ert-deftest test-ai-term--server-window-wired-to-helper () "Normal: the module sets `server-window' to its display function." - (should (eq server-window #'cj/--ai-vterm-server-display))) + (should (eq server-window #'cj/--ai-term-server-display))) -(provide 'test-ai-vterm--server-display) -;;; test-ai-vterm--server-display.el ends here +(provide 'test-ai-term--server-display) +;;; test-ai-term--server-display.el ends here diff --git a/tests/test-ai-term--show-or-create.el b/tests/test-ai-term--show-or-create.el new file mode 100644 index 00000000..c6653dcd --- /dev/null +++ b/tests/test-ai-term--show-or-create.el @@ -0,0 +1,155 @@ +;;; test-ai-term--show-or-create.el --- Tests for cj/--ai-term-show-or-create -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests the show-or-create branching: +;; +;; - buffer absent -> ghostel called, agent command + newline sent +;; - buffer present, live -> ghostel not called, buffer displayed +;; - buffer present, dead -> old buffer killed, ghostel recreates +;; +;; ghostel functions are stubbed so the test does no process spawning and +;; never loads the native module. Production calls (ghostel) with no name and +;; relies on the dynamically bound `ghostel-buffer-name'; the mock honors that. + +;;; Code: + +(require 'ert) +(require 'cl-lib) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'ai-term) + +;; ghostel isn't loaded in batch -- provide stubs so cl-letf has overrides. +(unless (fboundp 'ghostel) + (defun ghostel (&optional _arg) nil)) +(unless (fboundp 'ghostel-send-string) + (defun ghostel-send-string (_s) nil)) + +(defmacro test-ai-term--with-mock-ghostel (vars &rest body) + "Run BODY with ghostel + ghostel-send-string mocked. + +VARS is a plist of capture variable names: :calls (buffer names ghostel +was asked to create), :strings (sent strings), :default-dir. The mocked +`ghostel' creates and returns a buffer named after the dynamically bound +`ghostel-buffer-name', mirroring the real entry point." + (declare (indent 1) (debug t)) + (let ((calls (plist-get vars :calls)) + (strings (plist-get vars :strings)) + (ddir (plist-get vars :default-dir))) + `(let ((,calls '()) + (,strings '()) + (,ddir nil)) + (cl-letf (((symbol-function 'ghostel) + (lambda (&optional _arg) + (setq ,ddir default-directory) + (let ((b (get-buffer-create ghostel-buffer-name))) + (push (buffer-name b) ,calls) + b))) + ((symbol-function 'ghostel-send-string) + (lambda (s) (push s ,strings)))) + ,@body)))) + +(defun test-ai-term--cleanup (name) + "Kill buffer NAME if it exists." + (when (get-buffer name) + (kill-buffer name))) + +(ert-deftest test-ai-term--show-or-create-creates-when-buffer-missing () + "Normal: no existing buffer -> ghostel called once, launch cmd + newline +sent, the project recorded at the front of the MRU list." + (let ((name "agent [normal-create-test]") + (cj/--ai-term-mru nil)) + (test-ai-term--cleanup name) + (unwind-protect + (test-ai-term--with-mock-ghostel (:calls calls :strings strings + :default-dir ddir) + (cj/--ai-term-show-or-create "/tmp/some-project" name) + (should (equal calls (list name))) + (should (equal (reverse strings) + (list (cj/--ai-term-launch-command "/tmp/some-project") + "\n"))) + (should (equal ddir "/tmp/some-project")) + (should (equal (car cj/--ai-term-mru) "/tmp/some-project"))) + (test-ai-term--cleanup name)))) + +(ert-deftest test-ai-term--show-or-create-displays-existing-when-process-live () + "Normal: buffer exists with live process -> ghostel not called." + (let ((name "agent [reuse-test]")) + (test-ai-term--cleanup name) + (unwind-protect + (let ((buf (get-buffer-create name))) + (cl-letf (((symbol-function 'cj/--ai-term-process-live-p) + (lambda (b) (and (eq b buf) t)))) + (test-ai-term--with-mock-ghostel (:calls calls :strings strings + :default-dir _ddir) + (cj/--ai-term-show-or-create "/tmp/reuse" name) + (should (null calls)) + (should (null strings))))) + (test-ai-term--cleanup name)))) + +(ert-deftest test-ai-term--show-or-create-recreates-when-process-dead () + "Boundary: buffer exists with dead process -> killed and recreated." + (let ((name "agent [dead-test]")) + (test-ai-term--cleanup name) + (unwind-protect + (let ((stale (get-buffer-create name))) + (cl-letf (((symbol-function 'cj/--ai-term-process-live-p) + (lambda (_b) nil))) + (test-ai-term--with-mock-ghostel (:calls calls :strings strings + :default-dir _ddir) + (cj/--ai-term-show-or-create "/tmp/dead" name) + (should (equal calls (list name))) + (should (equal (reverse strings) + (list (cj/--ai-term-launch-command "/tmp/dead") + "\n"))) + (should-not (buffer-live-p stale))))) + (test-ai-term--cleanup name)))) + +(ert-deftest test-ai-term--show-or-create-preserves-selected-window () + "Regression: ghostel's same-window switch must not bury the dashboard. + +Real `ghostel' switches the selected window to its buffer as a side-effect of +construction. On a fresh-boot frame (one window showing the dashboard), that +side-effect would otherwise leave the original window pointing at the new +agent buffer. The wrapper runs `(ghostel)' inside `save-window-excursion' so +the original window state is restored before `display-buffer' fires, leaving +the dashboard put and letting the alist place agent into a fresh split. + +This test stubs `ghostel' to mimic the same-window side-effect and asserts the +originally-selected window still shows its original buffer afterward." + (let ((agent-name "agent [preserve-window-test]") + (orig-name "*test-original-buffer*")) + (test-ai-term--cleanup agent-name) + (when (get-buffer orig-name) (kill-buffer orig-name)) + (unwind-protect + (save-window-excursion + (delete-other-windows) + (let ((orig-buf (get-buffer-create orig-name)) + (orig-win (selected-window))) + (set-window-buffer orig-win orig-buf) + (cl-letf + (((symbol-function 'ghostel) + (lambda (&optional _arg) + (let ((buf (get-buffer-create ghostel-buffer-name))) + (set-window-buffer (selected-window) buf) + buf))) + ((symbol-function 'ghostel-send-string) + (lambda (_s) nil))) + (cj/--ai-term-show-or-create "/tmp/preserve" agent-name) + (should (eq (window-buffer orig-win) orig-buf))))) + (test-ai-term--cleanup agent-name) + (when (get-buffer orig-name) (kill-buffer orig-name))))) + +(ert-deftest test-ai-term--show-or-create-returns-buffer () + "Normal: return value is the ghostel buffer named after the project." + (let ((name "agent [return-test]")) + (test-ai-term--cleanup name) + (unwind-protect + (test-ai-term--with-mock-ghostel (:calls _c :strings _s :default-dir _d) + (let ((result (cj/--ai-term-show-or-create "/tmp/return" name))) + (should (bufferp result)) + (should (equal (buffer-name result) name)))) + (test-ai-term--cleanup name)))) + +(provide 'test-ai-term--show-or-create) +;;; test-ai-term--show-or-create.el ends here diff --git a/tests/test-ai-vterm--single-window-toggle.el b/tests/test-ai-term--single-window-toggle.el index 928656f2..aa507f03 100644 --- a/tests/test-ai-vterm--single-window-toggle.el +++ b/tests/test-ai-term--single-window-toggle.el @@ -1,11 +1,11 @@ -;;; test-ai-vterm--single-window-toggle.el --- F9 toggle round-trip when agent is the only window -*- lexical-binding: t; -*- +;;; test-ai-term--single-window-toggle.el --- F9 toggle round-trip when agent is the only window -*- lexical-binding: t; -*- ;;; Commentary: ;; Regression coverage for the bug where toggling off a single-window ;; agent (bury) then toggling on again redisplays the agent in a side ;; split instead of restoring the full-frame layout. ;; -;; The fix introduces a `cj/--ai-vterm-last-was-bury' flag set at +;; The fix introduces a `cj/--ai-term-last-was-bury' flag set at ;; toggle-off when `one-window-p' was true. At toggle-on the display ;; action consumes the flag and, if the frame is still single-window, ;; replaces the current window's buffer in place rather than calling @@ -18,12 +18,12 @@ (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) -(require 'ai-vterm) -(require 'testutil-vterm-buffers) +(require 'ai-term) +(require 'testutil-ghostel-buffers) ;;; Normal Cases -(ert-deftest test-ai-vterm--single-window-toggle-normal-roundtrip-preserves-fullscreen () +(ert-deftest test-ai-term--single-window-toggle-normal-roundtrip-preserves-fullscreen () "Normal: agent in the only window, F9 (off), F9 (on) -> still single window with agent. Reproduces Craig's report. Before the original fix the toggle-on path @@ -36,33 +36,33 @@ agent buffer after bury so the toggle-off is observable in real and batch use both." (cj/test--kill-agent-buffers) (let ((agent-name "agent [single-window-roundtrip]") - (cj/--ai-vterm-last-was-bury nil) - (cj/--ai-vterm-last-direction nil) - (cj/--ai-vterm-last-size nil)) + (cj/--ai-term-last-was-bury nil) + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil)) (unwind-protect (save-window-excursion (delete-other-windows) (let ((agent-buf (get-buffer-create agent-name))) (set-window-buffer (selected-window) agent-buf) (should (one-window-p)) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) ;; Toggle off -- the dispatcher's force-swap should put the ;; window on a non-agent buffer. - (cj/test--call-as-gui #'cj/ai-vterm) + (cj/test--call-as-gui #'cj/ai-term) (should (one-window-p)) - (should-not (cj/--ai-vterm-displayed-agent-window)) - (should (eq cj/--ai-vterm-last-was-bury t)) + (should-not (cj/--ai-term-displayed-agent-window)) + (should (eq cj/--ai-term-last-was-bury t)) ;; Toggle on -- should restore agent in the same lone window. - (cj/test--call-as-gui #'cj/ai-vterm) + (cj/test--call-as-gui #'cj/ai-term) (should (one-window-p)) - (let ((win (cj/--ai-vterm-displayed-agent-window))) + (let ((win (cj/--ai-term-displayed-agent-window))) (should (windowp win)) (should (eq (window-buffer win) agent-buf))) ;; Flag consumed by the display-saved action. - (should-not cj/--ai-vterm-last-was-bury)))) + (should-not cj/--ai-term-last-was-bury)))) (cj/test--kill-agent-buffers)))) -(ert-deftest test-ai-vterm--single-window-toggle-off-swaps-window-buffer () +(ert-deftest test-ai-term--single-window-toggle-off-swaps-window-buffer () "Normal: toggle-off in single-window state forces the window onto a non- agent buffer when `bury-buffer' itself didn't swap. @@ -72,35 +72,35 @@ ensures the displayed buffer changes -- so the next F9 sees an empty agent-window state and can route through the display-saved path." (cj/test--kill-agent-buffers) (let ((agent-name "agent [bury-swap-observable]") - (cj/--ai-vterm-last-was-bury nil)) + (cj/--ai-term-last-was-bury nil)) (unwind-protect (save-window-excursion (delete-other-windows) (let* ((agent-buf (get-buffer-create agent-name)) (win (selected-window))) (set-window-buffer win agent-buf) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) - (cj/test--call-as-gui #'cj/ai-vterm)) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) + (cj/test--call-as-gui #'cj/ai-term)) (should (window-live-p win)) - (should-not (cj/--ai-vterm-buffer-p (window-buffer win))))) + (should-not (cj/--ai-term-buffer-p (window-buffer win))))) (cj/test--kill-agent-buffers)))) -(ert-deftest test-ai-vterm--single-window-toggle-normal-flag-set-on-bury () +(ert-deftest test-ai-term--single-window-toggle-normal-flag-set-on-bury () "Normal: single-window toggle-off sets the bury flag." (cj/test--kill-agent-buffers) (let ((agent-name "agent [bury-flag-set]") - (cj/--ai-vterm-last-was-bury nil)) + (cj/--ai-term-last-was-bury nil)) (unwind-protect (save-window-excursion (delete-other-windows) (let ((agent-buf (get-buffer-create agent-name))) (set-window-buffer (selected-window) agent-buf) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) - (cj/test--call-as-gui #'cj/ai-vterm) - (should (eq cj/--ai-vterm-last-was-bury t))))) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) + (cj/test--call-as-gui #'cj/ai-term) + (should (eq cj/--ai-term-last-was-bury t))))) (cj/test--kill-agent-buffers)))) -(ert-deftest test-ai-vterm--single-window-toggle-normal-flag-cleared-on-multi-window-off () +(ert-deftest test-ai-term--single-window-toggle-normal-flag-cleared-on-multi-window-off () "Normal: multi-window toggle-off clears the bury flag. Mirrors the existing `delete-window' branch of the dispatcher -- the flag should not carry over a prior bury into a delete-window @@ -108,7 +108,7 @@ toggle-off." (cj/test--kill-agent-buffers) (let ((agent-name "agent [bury-flag-clear]") (left-name "*test-sw-left*") - (cj/--ai-vterm-last-was-bury t)) ; stale t from prior bury + (cj/--ai-term-last-was-bury t)) ; stale t from prior bury (unwind-protect (save-window-excursion (delete-other-windows) @@ -116,25 +116,25 @@ toggle-off." (left-buf (get-buffer-create left-name))) (set-window-buffer (selected-window) left-buf) (let* ((agent-win (split-window (selected-window) nil 'right)) - (display-buffer-alist (cj/--ai-vterm-display-rule-list))) + (display-buffer-alist (cj/--ai-term-display-rule-list))) (set-window-buffer agent-win agent-buf) (select-window agent-win) - (cj/test--call-as-gui #'cj/ai-vterm) - (should-not cj/--ai-vterm-last-was-bury)))) + (cj/test--call-as-gui #'cj/ai-term) + (should-not cj/--ai-term-last-was-bury)))) (when (get-buffer left-name) (kill-buffer left-name)) (cj/test--kill-agent-buffers)))) ;;; Boundary Cases -(ert-deftest test-ai-vterm--single-window-toggle-boundary-flag-respected-only-when-still-one-window () +(ert-deftest test-ai-term--single-window-toggle-boundary-flag-respected-only-when-still-one-window () "Boundary: if the frame got split between toggle-off and toggle-on, the saved-direction split applies as usual. The flag is a fast-path for the genuine single-window case, not an override for every redisplay." (cj/test--kill-agent-buffers) (let ((agent-name "agent [flag-fallback]") - (cj/--ai-vterm-last-was-bury t) ; flag pretends prior bury - (cj/--ai-vterm-last-direction 'right) - (cj/--ai-vterm-last-size 40)) + (cj/--ai-term-last-was-bury t) ; flag pretends prior bury + (cj/--ai-term-last-direction 'right) + (cj/--ai-term-last-size 40)) (unwind-protect (save-window-excursion (delete-other-windows) @@ -146,26 +146,26 @@ genuine single-window case, not an override for every redisplay." (split-window-right) (should-not (one-window-p)) (let (received-buf - (display-buffer-alist (cj/--ai-vterm-display-rule-list))) + (display-buffer-alist (cj/--ai-term-display-rule-list))) (cl-letf (((symbol-function 'display-buffer-in-direction) (lambda (b _a) (setq received-buf b) (selected-window)))) - (cj/--ai-vterm-display-saved agent-buf nil)) + (cj/--ai-term-display-saved agent-buf nil)) ;; The saved-direction split path ran (display-buffer-in-direction ;; was called) rather than the in-place fast path. (should (eq received-buf agent-buf)) ;; And the flag is cleared either way. - (should-not cj/--ai-vterm-last-was-bury)))) + (should-not cj/--ai-term-last-was-bury)))) (when (get-buffer "*test-sw-other*") (kill-buffer "*test-sw-other*")) (cj/test--kill-agent-buffers)))) -(ert-deftest test-ai-vterm--single-window-toggle-boundary-flag-not-set-when-bury-not-used () +(ert-deftest test-ai-term--single-window-toggle-boundary-flag-not-set-when-bury-not-used () "Boundary: a fresh dispatcher run with the agent displayed multi-window leaves the flag nil (no spurious set)." (cj/test--kill-agent-buffers) (let ((agent-name "agent [bury-flag-untouched]") - (cj/--ai-vterm-last-was-bury nil)) + (cj/--ai-term-last-was-bury nil)) (unwind-protect (save-window-excursion (delete-other-windows) @@ -173,14 +173,14 @@ the flag nil (no spurious set)." (left-buf (get-buffer-create "*test-sw-untouched-left*"))) (set-window-buffer (selected-window) left-buf) (let* ((agent-win (split-window (selected-window) nil 'right)) - (display-buffer-alist (cj/--ai-vterm-display-rule-list))) + (display-buffer-alist (cj/--ai-term-display-rule-list))) (set-window-buffer agent-win agent-buf) (select-window agent-win) - (cj/test--call-as-gui #'cj/ai-vterm) - (should-not cj/--ai-vterm-last-was-bury)))) + (cj/test--call-as-gui #'cj/ai-term) + (should-not cj/--ai-term-last-was-bury)))) (when (get-buffer "*test-sw-untouched-left*") (kill-buffer "*test-sw-untouched-left*")) (cj/test--kill-agent-buffers)))) -(provide 'test-ai-vterm--single-window-toggle) -;;; test-ai-vterm--single-window-toggle.el ends here +(provide 'test-ai-term--single-window-toggle) +;;; test-ai-term--single-window-toggle.el ends here diff --git a/tests/test-ai-vterm--sort-candidates.el b/tests/test-ai-term--sort-candidates.el index 26953604..f1f6155f 100644 --- a/tests/test-ai-vterm--sort-candidates.el +++ b/tests/test-ai-term--sort-candidates.el @@ -1,10 +1,10 @@ -;;; test-ai-vterm--sort-candidates.el --- Tests for cj/--ai-vterm-sort-candidates -*- lexical-binding: t; -*- +;;; test-ai-term--sort-candidates.el --- Tests for cj/--ai-term-sort-candidates -*- lexical-binding: t; -*- ;;; Commentary: ;; The project picker lists candidates with a live tmux session first ;; (so an agent that survived an Emacs crash is easy to get back to), ;; then everything else. Within the active group, projects opened this -;; session (`cj/--ai-vterm-mru') lead, most-recent first; the rest of the +;; session (`cj/--ai-term-mru') lead, most-recent first; the rest of the ;; active group, and the whole no-session group, sort alphabetically by ;; abbreviated path. With an empty MRU it's just active-first-then-alpha. @@ -13,62 +13,62 @@ (require 'ert) (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-vterm) +(require 'ai-term) -(ert-deftest test-ai-vterm--sort-candidates-active-first-then-alpha () +(ert-deftest test-ai-term--sort-candidates-active-first-then-alpha () "Normal: the one project with a live session leads; the rest go alpha." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (should (equal (cj/--ai-vterm-sort-candidates + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (should (equal (cj/--ai-term-sort-candidates '("/c/foo" "/c/bar" "/c/baz") '("aiv-bar")) '("/c/bar" "/c/baz" "/c/foo"))))) -(ert-deftest test-ai-vterm--sort-candidates-multiple-active-each-group-alpha () +(ert-deftest test-ai-term--sort-candidates-multiple-active-each-group-alpha () "Normal: both groups sort alphabetically internally." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (should (equal (cj/--ai-vterm-sort-candidates + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (should (equal (cj/--ai-term-sort-candidates '("/c/foo" "/c/bar" "/c/baz") '("aiv-foo" "aiv-bar")) '("/c/bar" "/c/foo" "/c/baz"))))) -(ert-deftest test-ai-vterm--sort-candidates-no-sessions-is-plain-alpha () +(ert-deftest test-ai-term--sort-candidates-no-sessions-is-plain-alpha () "Boundary: nil session set -> a plain alphabetical list." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (should (equal (cj/--ai-vterm-sort-candidates + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (should (equal (cj/--ai-term-sort-candidates '("/c/foo" "/c/bar") nil) '("/c/bar" "/c/foo"))))) -(ert-deftest test-ai-vterm--sort-candidates-empty-dirs-yields-nil () +(ert-deftest test-ai-term--sort-candidates-empty-dirs-yields-nil () "Boundary: no candidates -> nil." - (should (null (cj/--ai-vterm-sort-candidates nil '("aiv-foo"))))) + (should (null (cj/--ai-term-sort-candidates nil '("aiv-foo"))))) -(ert-deftest test-ai-vterm--sort-candidates-active-group-mru-first () +(ert-deftest test-ai-term--sort-candidates-active-group-mru-first () "Normal: within the active group, recently-opened projects lead in MRU order; active dirs not opened this session fall after them alpha; the no-session group trails, alpha." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-") - (cj/--ai-vterm-mru '("/c/baz" "/c/foo"))) - (should (equal (cj/--ai-vterm-sort-candidates + (let ((cj/ai-term-tmux-session-prefix "aiv-") + (cj/--ai-term-mru '("/c/baz" "/c/foo"))) + (should (equal (cj/--ai-term-sort-candidates '("/c/foo" "/c/bar" "/c/baz" "/c/qux") '("aiv-foo" "aiv-bar" "aiv-baz")) '("/c/baz" "/c/foo" "/c/bar" "/c/qux"))))) -(ert-deftest test-ai-vterm--sort-candidates-mru-does-not-bump-inactive () +(ert-deftest test-ai-term--sort-candidates-mru-does-not-bump-inactive () "Boundary: an MRU dir whose tmux session has died sorts alpha in the no-session group, not at the top." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-") - (cj/--ai-vterm-mru '("/c/zed"))) - (should (equal (cj/--ai-vterm-sort-candidates + (let ((cj/ai-term-tmux-session-prefix "aiv-") + (cj/--ai-term-mru '("/c/zed"))) + (should (equal (cj/--ai-term-sort-candidates '("/c/foo" "/c/zed" "/c/bar") '("aiv-foo")) '("/c/foo" "/c/bar" "/c/zed"))))) -(ert-deftest test-ai-vterm--session-active-p-matches-by-derived-name () +(ert-deftest test-ai-term--session-active-p-matches-by-derived-name () "Normal: a dir is active when its derived session name is in the set." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (should (cj/--ai-vterm-session-active-p "/c/foo" '("aiv-bar" "aiv-foo"))) - (should-not (cj/--ai-vterm-session-active-p "/c/qux" '("aiv-bar" "aiv-foo"))) - (should-not (cj/--ai-vterm-session-active-p "/c/foo" nil)))) + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (should (cj/--ai-term-session-active-p "/c/foo" '("aiv-bar" "aiv-foo"))) + (should-not (cj/--ai-term-session-active-p "/c/qux" '("aiv-bar" "aiv-foo"))) + (should-not (cj/--ai-term-session-active-p "/c/foo" nil)))) -(provide 'test-ai-vterm--sort-candidates) -;;; test-ai-vterm--sort-candidates.el ends here +(provide 'test-ai-term--sort-candidates) +;;; test-ai-term--sort-candidates.el ends here diff --git a/tests/test-ai-term--tmux-session-name.el b/tests/test-ai-term--tmux-session-name.el new file mode 100644 index 00000000..db8e836f --- /dev/null +++ b/tests/test-ai-term--tmux-session-name.el @@ -0,0 +1,65 @@ +;;; test-ai-term--tmux-session-name.el --- Tests for cj/--ai-term-tmux-session-name -*- lexical-binding: t; -*- + +;;; Commentary: +;; The tmux session name is `cj/ai-term-tmux-session-prefix' followed by +;; the project's basename, so reopening the agent on the same project (e.g. +;; after an Emacs crash) reattaches to the same tmux session rather than +;; spawning a new one -- and the prefix lets `tmux ls' output be filtered +;; down to AI-term's own sessions. The basename is sanitized to a form +;; tmux won't re-mangle: runs of whitespace become hyphens, and `.' / `:' +;; (which tmux disallows in session names and silently rewrites to `_') +;; become `_' up front so the computed name matches the real session. + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'ai-term) + +(ert-deftest test-ai-term--tmux-session-name-normal-project () + "Normal: basename gets the configured prefix." + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (should (equal (cj/--ai-term-tmux-session-name "/home/cjennings/projects/foo") + "aiv-foo")))) + +(ert-deftest test-ai-term--tmux-session-name-trailing-slash () + "Boundary: trailing slash collapses before basename extraction." + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (should (equal (cj/--ai-term-tmux-session-name "/home/cjennings/projects/foo/") + "aiv-foo")))) + +(ert-deftest test-ai-term--tmux-session-name-dots-become-underscores () + "Boundary: tmux disallows `.' in session names and rewrites it to `_', +so the basename's dots are sanitized to `_' up front -- `.emacs.d' must +yield `aiv-_emacs_d', matching the session tmux actually creates." + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (should (equal (cj/--ai-term-tmux-session-name "/home/cjennings/.emacs.d") + "aiv-_emacs_d")))) + +(ert-deftest test-ai-term--tmux-session-name-colon-becomes-underscore () + "Boundary: `:' is also disallowed by tmux in session names -> `_'." + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (should (equal (cj/--ai-term-tmux-session-name "/tmp/a:b") + "aiv-a_b")))) + +(ert-deftest test-ai-term--tmux-session-name-space-becomes-hyphen () + "Boundary: a space in the basename is replaced with a hyphen." + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (should (equal (cj/--ai-term-tmux-session-name "/tmp/my work") + "aiv-my-work")))) + +(ert-deftest test-ai-term--tmux-session-name-multiple-spaces-collapse () + "Boundary: a run of whitespace collapses to a single hyphen." + (let ((cj/ai-term-tmux-session-prefix "aiv-")) + (should (equal (cj/--ai-term-tmux-session-name "/tmp/a b\tc") + "aiv-a-b-c")))) + +(ert-deftest test-ai-term--tmux-session-name-honors-custom-prefix () + "Normal: a non-default prefix is what gets prepended." + (let ((cj/ai-term-tmux-session-prefix "em-")) + (should (equal (cj/--ai-term-tmux-session-name "/home/cjennings/projects/foo") + "em-foo")))) + +(provide 'test-ai-term--tmux-session-name) +;;; test-ai-term--tmux-session-name.el ends here diff --git a/tests/test-ai-vterm--capture-state.el b/tests/test-ai-vterm--capture-state.el deleted file mode 100644 index 88a7784b..00000000 --- a/tests/test-ai-vterm--capture-state.el +++ /dev/null @@ -1,63 +0,0 @@ -;;; test-ai-vterm--capture-state.el --- Tests for cj/--ai-vterm-capture-state -*- lexical-binding: t; -*- - -;;; Commentary: -;; The capture helper writes WINDOW's direction and size to module- -;; level state vars `cj/--ai-vterm-last-direction' and -;; `cj/--ai-vterm-last-size'. Called from `cj/ai-vterm''s toggle-off -;; branch so the next F9 display can restore the user's chosen -;; orientation and size. No-op on a dead window. - -;;; Code: - -(require 'ert) - -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-vterm) - -(ert-deftest test-ai-vterm--capture-state-right-split-sets-direction () - "Normal: right-split window -> direction=right, integer body-cols matching window." - (save-window-excursion - (delete-other-windows) - (let ((right (split-window (selected-window) nil 'right)) - (cj/--ai-vterm-last-direction nil) - (cj/--ai-vterm-last-size nil)) - (cj/--ai-vterm-capture-state right) - (should (eq cj/--ai-vterm-last-direction 'right)) - (should (integerp cj/--ai-vterm-last-size)) - (should (= cj/--ai-vterm-last-size (window-body-width right)))))) - -(ert-deftest test-ai-vterm--capture-state-below-split-sets-direction () - "Normal: below-split window -> direction=below, integer body-lines matching window." - (save-window-excursion - (delete-other-windows) - (let ((below (split-window (selected-window) nil 'below)) - (cj/--ai-vterm-last-direction nil) - (cj/--ai-vterm-last-size nil)) - (cj/--ai-vterm-capture-state below) - (should (eq cj/--ai-vterm-last-direction 'below)) - (should (integerp cj/--ai-vterm-last-size)) - (should (= cj/--ai-vterm-last-size (window-body-height below)))))) - -(ert-deftest test-ai-vterm--capture-state-noop-on-dead-window () - "Boundary: nil window -> state remains unchanged." - (let ((cj/--ai-vterm-last-direction 'sentinel-dir) - (cj/--ai-vterm-last-size 0.123)) - (cj/--ai-vterm-capture-state nil) - (should (eq cj/--ai-vterm-last-direction 'sentinel-dir)) - (should (= cj/--ai-vterm-last-size 0.123)))) - -(ert-deftest test-ai-vterm--capture-state-noop-on-deleted-window () - "Boundary: deleted window -> state remains unchanged." - (let ((cj/--ai-vterm-last-direction 'sentinel-dir) - (cj/--ai-vterm-last-size 0.123) - (dead-win (save-window-excursion - (delete-other-windows) - (let ((w (split-window (selected-window) nil 'right))) - (delete-window w) - w)))) - (cj/--ai-vterm-capture-state dead-win) - (should (eq cj/--ai-vterm-last-direction 'sentinel-dir)) - (should (= cj/--ai-vterm-last-size 0.123)))) - -(provide 'test-ai-vterm--capture-state) -;;; test-ai-vterm--capture-state.el ends here diff --git a/tests/test-ai-vterm--default-geometry.el b/tests/test-ai-vterm--default-geometry.el deleted file mode 100644 index f8ec08c9..00000000 --- a/tests/test-ai-vterm--default-geometry.el +++ /dev/null @@ -1,56 +0,0 @@ -;;; test-ai-vterm--default-geometry.el --- Tests for host-aware display defaults -*- lexical-binding: t; -*- - -;;; Commentary: -;; ai-vterm's default display geometry is host-aware: a laptop opens the -;; agent from the bottom (75% height), a desktop opens it from the right -;; (50% width). `cj/--ai-vterm-default-direction' and -;; `cj/--ai-vterm-default-size' encapsulate the `env-laptop-p' branch; -;; they feed the default fallbacks in `cj/--ai-vterm-capture-state' and -;; `cj/--ai-vterm-display-saved'. -;; -;; `env-laptop-p' is stubbed per-test so the assertions are deterministic -;; regardless of the host the suite runs on. - -;;; Code: - -(require 'ert) -(require 'cl-lib) - -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-vterm) - -(ert-deftest test-ai-vterm--default-direction-laptop () - "Normal: on a laptop the default direction is `below'." - (cl-letf (((symbol-function 'env-laptop-p) (lambda () t))) - (should (eq (cj/--ai-vterm-default-direction) 'below)))) - -(ert-deftest test-ai-vterm--default-direction-desktop () - "Normal: on a desktop the default direction is `right'." - (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) - (should (eq (cj/--ai-vterm-default-direction) 'right)))) - -(ert-deftest test-ai-vterm--default-size-laptop () - "Normal: on a laptop the default size is `cj/ai-vterm-laptop-height'." - (let ((cj/ai-vterm-laptop-height 0.75) - (cj/ai-vterm-desktop-width 0.5)) - (cl-letf (((symbol-function 'env-laptop-p) (lambda () t))) - (should (= (cj/--ai-vterm-default-size) 0.75))))) - -(ert-deftest test-ai-vterm--default-size-desktop () - "Normal: on a desktop the default size is `cj/ai-vterm-desktop-width'." - (let ((cj/ai-vterm-laptop-height 0.75) - (cj/ai-vterm-desktop-width 0.5)) - (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) - (should (= (cj/--ai-vterm-default-size) 0.5))))) - -(ert-deftest test-ai-vterm--default-size-respects-custom-values () - "Boundary: the helper returns the customized values, not the literals." - (let ((cj/ai-vterm-laptop-height 0.6) - (cj/ai-vterm-desktop-width 0.33)) - (cl-letf (((symbol-function 'env-laptop-p) (lambda () t))) - (should (= (cj/--ai-vterm-default-size) 0.6))) - (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) - (should (= (cj/--ai-vterm-default-size) 0.33))))) - -(provide 'test-ai-vterm--default-geometry) -;;; test-ai-vterm--default-geometry.el ends here diff --git a/tests/test-ai-vterm--f9-in-vterm.el b/tests/test-ai-vterm--f9-in-vterm.el deleted file mode 100644 index ec67ac9b..00000000 --- a/tests/test-ai-vterm--f9-in-vterm.el +++ /dev/null @@ -1,47 +0,0 @@ -;;; test-ai-vterm--f9-in-vterm.el --- F9 reaches Emacs from inside an agent buffer -*- lexical-binding: t; -*- - -;;; Commentary: -;; vterm binds <f1>..<f12> to `vterm--self-insert', so a plain <f9> typed -;; while point is in an agent buffer is sent to the terminal program instead -;; of toggling the agent -- which is exactly the case when the agent buffer -;; fills the frame. `ai-vterm.el' re-binds the F9 family in `vterm-mode-map'. -;; These tests load real vterm so `vterm-mode-map' exists, then confirm the -;; bindings landed (and the global ones are still there). - -;;; Code: - -(require 'ert) -(require 'package) - -(setq package-user-dir (expand-file-name "elpa" user-emacs-directory)) -(package-initialize) -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'vterm) -(require 'ai-vterm) - -(ert-deftest test-ai-vterm-f9-bound-in-vterm-mode-map () - "Normal: <f9> in `vterm-mode-map' runs the agent toggle, not `vterm--self-insert'." - (should (eq (keymap-lookup vterm-mode-map "<f9>") #'cj/ai-vterm))) - -(ert-deftest test-ai-vterm-f9-family-bound-in-vterm-mode-map () - "Normal: the C-/M-/C-S- F9 variants are bound in `vterm-mode-map' too. -`M-<f9>' and `C-S-<f9>' both close an agent via `cj/ai-vterm-close'." - (should (eq (keymap-lookup vterm-mode-map "C-<f9>") #'cj/ai-vterm-pick-project)) - (should (eq (keymap-lookup vterm-mode-map "M-<f9>") #'cj/ai-vterm-close)) - (should (eq (keymap-lookup vterm-mode-map "C-S-<f9>") #'cj/ai-vterm-close))) - -(ert-deftest test-ai-vterm-f9-not-self-insert-in-vterm () - "Boundary: vterm's default <f9> -> `vterm--self-insert' was overridden." - (should-not (eq (keymap-lookup vterm-mode-map "<f9>") 'vterm--self-insert))) - -(ert-deftest test-ai-vterm-f9-still-bound-globally () - "Normal: the global F9 family bindings are intact. -`<f9>' toggles the ai-vterm agent window; `C-<f9>' picks a project -agent; `M-<f9>' and `C-S-<f9>' close an agent via `cj/ai-vterm-close'." - (should (eq (lookup-key (current-global-map) (kbd "<f9>")) #'cj/ai-vterm)) - (should (eq (lookup-key (current-global-map) (kbd "C-<f9>")) #'cj/ai-vterm-pick-project)) - (should (eq (lookup-key (current-global-map) (kbd "M-<f9>")) #'cj/ai-vterm-close)) - (should (eq (lookup-key (current-global-map) (kbd "C-S-<f9>")) #'cj/ai-vterm-close))) - -(provide 'test-ai-vterm--f9-in-vterm) -;;; test-ai-vterm--f9-in-vterm.el ends here diff --git a/tests/test-ai-vterm--launch-command.el b/tests/test-ai-vterm--launch-command.el deleted file mode 100644 index bac36d4e..00000000 --- a/tests/test-ai-vterm--launch-command.el +++ /dev/null @@ -1,94 +0,0 @@ -;;; test-ai-vterm--launch-command.el --- Tests for cj/--ai-vterm-launch-command -*- lexical-binding: t; -*- - -;;; Commentary: -;; The launch command is what gets typed into a fresh vterm shell to bring -;; up the agent inside a per-project tmux session. The session is named -;; `cj/ai-vterm-tmux-session-prefix' + the project basename, so a second -;; F9 on the same project reattaches to the running agent rather than -;; spawning a new one, and `tmux ls' output can be filtered to AI-vterm's -;; own sessions. The trailing `exec bash' keeps the tmux window alive if -;; the agent exits, leaving the session intact for recovery. - -;;; Code: - -(require 'ert) - -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-vterm) - -(ert-deftest test-ai-vterm--launch-command-uses-new-session-attach () - "Normal: starts with `tmux new-session -A' so existing sessions reattach." - (let ((cj/ai-vterm-agent-command "agent")) - (should (string-prefix-p - "tmux new-session -A " - (cj/--ai-vterm-launch-command "/code/foo"))))) - -(ert-deftest test-ai-vterm--launch-command-includes-prefixed-session-name () - "Normal: the session name is the prefixed form from the name helper." - (let ((cj/ai-vterm-agent-command "agent") - (cj/ai-vterm-tmux-session-prefix "aiv-")) - (should (string-match-p - " -s aiv-foo " - (cj/--ai-vterm-launch-command "/code/foo"))))) - -(ert-deftest test-ai-vterm--launch-command-names-window () - "Normal: `-n <window-name>' so the agent window is named distinctly." - (let ((cj/ai-vterm-agent-command "agent") - (cj/ai-vterm-tmux-window-name "ai")) - (should (string-match-p - " -n ai " - (cj/--ai-vterm-launch-command "/code/foo"))))) - -(ert-deftest test-ai-vterm--launch-command-honors-custom-window-name () - "Boundary: a non-default window name is what `-n' gets." - (let ((cj/ai-vterm-agent-command "agent") - (cj/ai-vterm-tmux-window-name "agent")) - (should (string-match-p - " -n agent " - (cj/--ai-vterm-launch-command "/code/foo"))))) - -(ert-deftest test-ai-vterm--launch-command-includes-start-directory () - "Normal: `-c <dir>' so the new session's first window starts in DIR." - (let ((cj/ai-vterm-agent-command "agent")) - (should (string-match-p - " -c /code/foo " - (cj/--ai-vterm-launch-command "/code/foo"))))) - -(ert-deftest test-ai-vterm--launch-command-includes-agent-command () - "Normal: the configured agent command is in the launched shell command. -The inner command is passed through `shell-quote-argument', so spaces -are escaped (`\\\\ ') -- the regex below accepts either form." - (let ((cj/ai-vterm-agent-command "agent --some-flag")) - (should (string-match-p - "agent\\(\\\\\\)? --some-flag" - (cj/--ai-vterm-launch-command "/code/foo"))))) - -(ert-deftest test-ai-vterm--launch-command-tails-with-exec-bash () - "Boundary: `exec bash' tails so the tmux window survives the agent exiting. -Accepts the post-`shell-quote-argument' shape (`exec\\\\ bash')." - (let ((cj/ai-vterm-agent-command "agent")) - (should (string-match-p - "exec\\(\\\\\\)? bash" - (cj/--ai-vterm-launch-command "/code/foo"))))) - -(ert-deftest test-ai-vterm--launch-command-survives-single-quote-in-agent () - "Normal: a user-customized agent command containing a single quote -shouldn't break the shell parse. `shell-quote-argument' produces a -valid shell token regardless of the embedded quote shape -- the -escaping is implementation-detail, so we assert the literal words -\"hi\" and \"there\" both appear (the space between them may be -escaped as \\\\ )." - (let ((cj/ai-vterm-agent-command "agent --say 'hi there'")) - (let ((cmd (cj/--ai-vterm-launch-command "/code/foo"))) - (should (string-match-p "hi\\(\\\\\\)? there" cmd))))) - -(ert-deftest test-ai-vterm--launch-command-handles-spaces-in-basename () - "Boundary: a basename with whitespace becomes hyphenated before quoting." - (let ((cj/ai-vterm-agent-command "agent") - (cj/ai-vterm-tmux-session-prefix "aiv-")) - (should (string-match-p - " -s aiv-my-work " - (cj/--ai-vterm-launch-command "/code/my work"))))) - -(provide 'test-ai-vterm--launch-command) -;;; test-ai-vterm--launch-command.el ends here diff --git a/tests/test-ai-vterm--record-mru.el b/tests/test-ai-vterm--record-mru.el deleted file mode 100644 index 16db4eea..00000000 --- a/tests/test-ai-vterm--record-mru.el +++ /dev/null @@ -1,48 +0,0 @@ -;;; test-ai-vterm--record-mru.el --- Tests for the AI-vterm project MRU list -*- lexical-binding: t; -*- - -;;; Commentary: -;; `cj/--ai-vterm-record-mru' tracks which project dirs have been opened via -;; the launcher this session, most-recently-opened first, so the picker can -;; surface recently-used projects at the top of the active-sessions group. -;; `cj/--ai-vterm-mru-rank' reports a dir's position in that list (or nil). - -;;; Code: - -(require 'ert) - -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-vterm) - -(ert-deftest test-ai-vterm--record-mru-pushes-to-front () - "Normal: a freshly recorded dir leads the list, newest first." - (let ((cj/--ai-vterm-mru nil)) - (cj/--ai-vterm-record-mru "/c/alpha") - (cj/--ai-vterm-record-mru "/c/beta") - (should (equal cj/--ai-vterm-mru '("/c/beta" "/c/alpha"))))) - -(ert-deftest test-ai-vterm--record-mru-dedups-and-moves-to-front () - "Normal: re-recording a dir moves it to the front with no duplicate." - (let ((cj/--ai-vterm-mru nil)) - (cj/--ai-vterm-record-mru "/c/alpha") - (cj/--ai-vterm-record-mru "/c/beta") - (cj/--ai-vterm-record-mru "/c/alpha") - (should (equal cj/--ai-vterm-mru '("/c/alpha" "/c/beta"))))) - -(ert-deftest test-ai-vterm--record-mru-normalizes-trailing-slash () - "Boundary: `/c/foo' and `/c/foo/' are the same MRU entry." - (let ((cj/--ai-vterm-mru nil)) - (cj/--ai-vterm-record-mru "/c/foo/") - (cj/--ai-vterm-record-mru "/c/foo") - (should (equal cj/--ai-vterm-mru '("/c/foo"))))) - -(ert-deftest test-ai-vterm--mru-rank-returns-index-or-nil () - "Normal/Boundary: rank is the list position; nil when the dir isn't there; -the lookup normalizes a trailing slash the same way `record-mru' does." - (let ((cj/--ai-vterm-mru '("/c/beta" "/c/alpha"))) - (should (= 0 (cj/--ai-vterm-mru-rank "/c/beta"))) - (should (= 1 (cj/--ai-vterm-mru-rank "/c/alpha"))) - (should (= 0 (cj/--ai-vterm-mru-rank "/c/beta/"))) - (should-not (cj/--ai-vterm-mru-rank "/c/gamma")))) - -(provide 'test-ai-vterm--record-mru) -;;; test-ai-vterm--record-mru.el ends here diff --git a/tests/test-ai-vterm--show-or-create.el b/tests/test-ai-vterm--show-or-create.el deleted file mode 100644 index 01083f84..00000000 --- a/tests/test-ai-vterm--show-or-create.el +++ /dev/null @@ -1,163 +0,0 @@ -;;; test-ai-vterm--show-or-create.el --- Tests for cj/--ai-vterm-show-or-create -*- lexical-binding: t; -*- - -;;; Commentary: -;; Tests the show-or-create branching: -;; -;; - buffer absent -> vterm called, agent command sent -;; - buffer present, live -> vterm not called, buffer displayed -;; - buffer present, dead -> old buffer killed, vterm recreates -;; -;; vterm functions are stubbed so the test does no process spawning. - -;;; Code: - -(require 'ert) -(require 'cl-lib) - -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-vterm) - -;; vterm isn't loaded in batch -- provide stubs so cl-letf has overrides. -(unless (fboundp 'vterm) - (defun vterm (&optional _name) nil)) -(unless (fboundp 'vterm-send-string) - (defun vterm-send-string (_s &optional _) nil)) -(unless (fboundp 'vterm-send-return) - (defun vterm-send-return () nil)) - -(defmacro test-ai-vterm--with-mock-vterm (vars &rest body) - "Run BODY with vterm + send-string + send-return mocked. - -VARS is a plist of capture variable names: :calls, :strings, :returns, -:default-dir. The test references these names directly inside BODY." - (declare (indent 1) (debug t)) - (let ((calls (plist-get vars :calls)) - (strings (plist-get vars :strings)) - (returns (plist-get vars :returns)) - (ddir (plist-get vars :default-dir))) - `(let ((,calls '()) - (,strings '()) - (,returns 0) - (,ddir nil)) - (cl-letf (((symbol-function 'vterm) - (lambda (&optional name) - (push name ,calls) - (setq ,ddir default-directory) - (with-current-buffer (get-buffer-create name) - (current-buffer)))) - ((symbol-function 'vterm-send-string) - (lambda (s &optional _) (push s ,strings))) - ((symbol-function 'vterm-send-return) - (lambda () (cl-incf ,returns)))) - ,@body)))) - -(defun test-ai-vterm--cleanup (name) - "Kill buffer NAME if it exists." - (when (get-buffer name) - (kill-buffer name))) - -(ert-deftest test-ai-vterm--show-or-create-creates-when-buffer-missing () - "Normal: no existing buffer -> vterm called once, launch cmd sent, the -project recorded at the front of the MRU list." - (let ((name "agent [normal-create-test]") - (cj/--ai-vterm-mru nil)) - (test-ai-vterm--cleanup name) - (unwind-protect - (test-ai-vterm--with-mock-vterm (:calls calls :strings strings - :returns returns :default-dir ddir) - (cj/--ai-vterm-show-or-create "/tmp/some-project" name) - (should (equal calls (list name))) - (should (equal strings - (list (cj/--ai-vterm-launch-command "/tmp/some-project")))) - (should (= returns 1)) - (should (equal ddir "/tmp/some-project")) - (should (equal (car cj/--ai-vterm-mru) "/tmp/some-project"))) - (test-ai-vterm--cleanup name)))) - -(ert-deftest test-ai-vterm--show-or-create-displays-existing-when-process-live () - "Normal: buffer exists with live process -> vterm not called." - (let ((name "agent [reuse-test]")) - (test-ai-vterm--cleanup name) - (unwind-protect - (let ((buf (get-buffer-create name))) - (cl-letf (((symbol-function 'cj/--ai-vterm-process-live-p) - (lambda (b) (and (eq b buf) t)))) - (test-ai-vterm--with-mock-vterm (:calls calls :strings strings - :returns returns :default-dir _ddir) - (cj/--ai-vterm-show-or-create "/tmp/reuse" name) - (should (null calls)) - (should (null strings)) - (should (= returns 0))))) - (test-ai-vterm--cleanup name)))) - -(ert-deftest test-ai-vterm--show-or-create-recreates-when-process-dead () - "Boundary: buffer exists with dead process -> killed and recreated." - (let ((name "agent [dead-test]")) - (test-ai-vterm--cleanup name) - (unwind-protect - (let ((stale (get-buffer-create name))) - (cl-letf (((symbol-function 'cj/--ai-vterm-process-live-p) - (lambda (_b) nil))) - (test-ai-vterm--with-mock-vterm (:calls calls :strings strings - :returns returns :default-dir _ddir) - (cj/--ai-vterm-show-or-create "/tmp/dead" name) - (should (equal calls (list name))) - (should (equal strings - (list (cj/--ai-vterm-launch-command "/tmp/dead")))) - (should (= returns 1)) - (should-not (buffer-live-p stale))))) - (test-ai-vterm--cleanup name)))) - -(ert-deftest test-ai-vterm--show-or-create-preserves-selected-window () - "Regression: vterm's pop-to-buffer-same-window must not bury the dashboard. - -Real `vterm' replaces the selected window's buffer as a side-effect of -construction. On a fresh-boot frame (one window showing the dashboard), -that side-effect previously left the original window pointing at the new -agent buffer; the dashboard was buried, the alist-routed split then -created a second window also showing agent. The wrapper must restore -the original window state before `display-buffer' fires so dashboard -stays put and the alist places agent into a fresh right-side split. - -This test stubs `vterm' to mimic the pop-to-buffer-same-window side-effect -and asserts the originally-selected window still shows its original buffer -after `cj/--ai-vterm-show-or-create' returns." - (let ((agent-name "agent [preserve-window-test]") - (orig-name "*test-original-buffer*")) - (test-ai-vterm--cleanup agent-name) - (when (get-buffer orig-name) (kill-buffer orig-name)) - (unwind-protect - (save-window-excursion - (delete-other-windows) - (let ((orig-buf (get-buffer-create orig-name)) - (orig-win (selected-window))) - (set-window-buffer orig-win orig-buf) - (cl-letf - (((symbol-function 'vterm) - (lambda (&optional name) - (let ((buf (get-buffer-create name))) - (set-window-buffer (selected-window) buf) - buf))) - ((symbol-function 'vterm-send-string) - (lambda (_s &optional _) nil)) - ((symbol-function 'vterm-send-return) - (lambda () nil))) - (cj/--ai-vterm-show-or-create "/tmp/preserve" agent-name) - (should (eq (window-buffer orig-win) orig-buf))))) - (test-ai-vterm--cleanup agent-name) - (when (get-buffer orig-name) (kill-buffer orig-name))))) - -(ert-deftest test-ai-vterm--show-or-create-returns-buffer () - "Normal: return value is the vterm buffer." - (let ((name "agent [return-test]")) - (test-ai-vterm--cleanup name) - (unwind-protect - (test-ai-vterm--with-mock-vterm (:calls _c :strings _s - :returns _r :default-dir _d) - (let ((result (cj/--ai-vterm-show-or-create "/tmp/return" name))) - (should (bufferp result)) - (should (equal (buffer-name result) name)))) - (test-ai-vterm--cleanup name)))) - -(provide 'test-ai-vterm--show-or-create) -;;; test-ai-vterm--show-or-create.el ends here diff --git a/tests/test-ai-vterm--terminal-guard.el b/tests/test-ai-vterm--terminal-guard.el deleted file mode 100644 index 5a7971bf..00000000 --- a/tests/test-ai-vterm--terminal-guard.el +++ /dev/null @@ -1,78 +0,0 @@ -;;; test-ai-vterm--terminal-guard.el --- Tests for the terminal-frame guard -*- lexical-binding: t; -*- - -;;; Commentary: -;; AI-vterm launches a graphical vterm side window, so it is GUI-only. -;; `cj/--ai-vterm-refuse-in-terminal' signals a `user-error' when the -;; current frame is a terminal frame; each interactive entry point -;; (`cj/ai-vterm', `cj/ai-vterm-pick-project', `cj/ai-vterm-close') -;; calls it first so F9 and friends decline -- with a message -- in a -;; terminal frame instead of launching a vterm. The check is per-frame -;; at command time, not at load, so a daemon serving both GUI and -;; terminal frames keeps the launcher working in its GUI frames. -;; -;; `env-terminal-p' is mocked so the tests are deterministic regardless -;; of whether the run itself is graphical (batch runs are terminal). - -;;; Code: - -(require 'ert) -(require 'cl-lib) - -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) -(require 'ai-vterm) - -;; ---------------------------- the guard helper ---------------------------- - -(ert-deftest test-ai-vterm--refuse-in-terminal-errors-in-terminal-frame () - "Error: terminal frame -> `user-error', so the command declines." - (cl-letf (((symbol-function 'env-terminal-p) (lambda () t))) - (should-error (cj/--ai-vterm-refuse-in-terminal) :type 'user-error))) - -(ert-deftest test-ai-vterm--refuse-in-terminal-passes-in-gui-frame () - "Normal: GUI frame -> returns nil, no error, command proceeds." - (cl-letf (((symbol-function 'env-terminal-p) (lambda () nil))) - (should-not (cj/--ai-vterm-refuse-in-terminal)))) - -;; ------------------- the three interactive entry points ------------------- - -(ert-deftest test-ai-vterm-f9-declines-in-terminal-without-dispatching () - "Error: F9 in a terminal frame errors and never reaches dispatch." - (let ((dispatched nil)) - (cl-letf (((symbol-function 'env-terminal-p) (lambda () t)) - ((symbol-function 'cj/--ai-vterm-dispatch) - (lambda () (setq dispatched t) '(pick-project)))) - (should-error (cj/ai-vterm) :type 'user-error) - (should-not dispatched)))) - -(ert-deftest test-ai-vterm-pick-project-declines-in-terminal-without-prompting () - "Error: C-F9 in a terminal frame errors and never reaches the picker." - (let ((prompted nil)) - (cl-letf (((symbol-function 'env-terminal-p) (lambda () t)) - ((symbol-function 'cj/--ai-vterm-pick-project) - (lambda () (setq prompted t) "/tmp"))) - (should-error (cj/ai-vterm-pick-project) :type 'user-error) - (should-not prompted)))) - -(ert-deftest test-ai-vterm-close-declines-in-terminal-without-targeting () - "Error: M-F9 in a terminal frame errors and never reaches close-target." - (let ((targeted nil)) - (cl-letf (((symbol-function 'env-terminal-p) (lambda () t)) - ((symbol-function 'cj/--ai-vterm-close-target) - (lambda () (setq targeted t) nil))) - (should-error (cj/ai-vterm-close) :type 'user-error) - (should-not targeted)))) - -(ert-deftest test-ai-vterm-f9-passes-guard-in-gui-frame () - "Normal: F9 in a GUI frame passes the guard and reaches dispatch." - (let ((dispatched nil)) - (cl-letf (((symbol-function 'env-terminal-p) (lambda () nil)) - ((symbol-function 'cj/--ai-vterm-dispatch) - (lambda () (setq dispatched t) '(pick-project))) - ((symbol-function 'cj/ai-vterm-pick-project) - (lambda (&optional _arg) nil))) - (cj/ai-vterm) - (should dispatched)))) - -(provide 'test-ai-vterm--terminal-guard) -;;; test-ai-vterm--terminal-guard.el ends here diff --git a/tests/test-ai-vterm--tmux-session-name.el b/tests/test-ai-vterm--tmux-session-name.el deleted file mode 100644 index 073dc312..00000000 --- a/tests/test-ai-vterm--tmux-session-name.el +++ /dev/null @@ -1,65 +0,0 @@ -;;; test-ai-vterm--tmux-session-name.el --- Tests for cj/--ai-vterm-tmux-session-name -*- lexical-binding: t; -*- - -;;; Commentary: -;; The tmux session name is `cj/ai-vterm-tmux-session-prefix' followed by -;; the project's basename, so reopening the agent on the same project (e.g. -;; after an Emacs crash) reattaches to the same tmux session rather than -;; spawning a new one -- and the prefix lets `tmux ls' output be filtered -;; down to AI-vterm's own sessions. The basename is sanitized to a form -;; tmux won't re-mangle: runs of whitespace become hyphens, and `.' / `:' -;; (which tmux disallows in session names and silently rewrites to `_') -;; become `_' up front so the computed name matches the real session. - -;;; Code: - -(require 'ert) - -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-vterm) - -(ert-deftest test-ai-vterm--tmux-session-name-normal-project () - "Normal: basename gets the configured prefix." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (should (equal (cj/--ai-vterm-tmux-session-name "/home/cjennings/projects/foo") - "aiv-foo")))) - -(ert-deftest test-ai-vterm--tmux-session-name-trailing-slash () - "Boundary: trailing slash collapses before basename extraction." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (should (equal (cj/--ai-vterm-tmux-session-name "/home/cjennings/projects/foo/") - "aiv-foo")))) - -(ert-deftest test-ai-vterm--tmux-session-name-dots-become-underscores () - "Boundary: tmux disallows `.' in session names and rewrites it to `_', -so the basename's dots are sanitized to `_' up front -- `.emacs.d' must -yield `aiv-_emacs_d', matching the session tmux actually creates." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (should (equal (cj/--ai-vterm-tmux-session-name "/home/cjennings/.emacs.d") - "aiv-_emacs_d")))) - -(ert-deftest test-ai-vterm--tmux-session-name-colon-becomes-underscore () - "Boundary: `:' is also disallowed by tmux in session names -> `_'." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (should (equal (cj/--ai-vterm-tmux-session-name "/tmp/a:b") - "aiv-a_b")))) - -(ert-deftest test-ai-vterm--tmux-session-name-space-becomes-hyphen () - "Boundary: a space in the basename is replaced with a hyphen." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (should (equal (cj/--ai-vterm-tmux-session-name "/tmp/my work") - "aiv-my-work")))) - -(ert-deftest test-ai-vterm--tmux-session-name-multiple-spaces-collapse () - "Boundary: a run of whitespace collapses to a single hyphen." - (let ((cj/ai-vterm-tmux-session-prefix "aiv-")) - (should (equal (cj/--ai-vterm-tmux-session-name "/tmp/a b\tc") - "aiv-a-b-c")))) - -(ert-deftest test-ai-vterm--tmux-session-name-honors-custom-prefix () - "Normal: a non-default prefix is what gets prepended." - (let ((cj/ai-vterm-tmux-session-prefix "em-")) - (should (equal (cj/--ai-vterm-tmux-session-name "/home/cjennings/projects/foo") - "em-foo")))) - -(provide 'test-ai-vterm--tmux-session-name) -;;; test-ai-vterm--tmux-session-name.el ends here diff --git a/tests/test-auto-dim-config.el b/tests/test-auto-dim-config.el index 4648f062..532e7dfa 100644 --- a/tests/test-auto-dim-config.el +++ b/tests/test-auto-dim-config.el @@ -6,6 +6,10 @@ ;; fringe from the dimmed faces to avoid flicker on this non-pgtk build, and ;; enable the global mode. Guarded with `skip-unless' because the fork lives ;; in ~/code and may be absent on a clean checkout. +;; +;; The vterm dim-integration tests were removed when the terminal engine moved +;; to ghostel: ghostel bakes its palette per-terminal (no per-window color +;; hook), so terminal buffers no longer participate in window dimming. ;;; Code: @@ -30,108 +34,6 @@ (when (fboundp 'auto-dim-other-buffers-mode) (auto-dim-other-buffers-mode -1)))) -(ert-deftest test-auto-dim-config-vterm-dimmed-p-all-windows-dimmed () - "Normal: a vterm buffer is dimmed when all displayed windows are dimmed." - (skip-unless (file-directory-p test-auto-dim--fork)) - (require 'auto-dim-config) - (let ((major-mode 'vterm-mode)) - (cl-letf (((symbol-function 'get-buffer-window-list) - (lambda (&rest _) '(left right))) - ((symbol-function 'window-parameter) - (lambda (window parameter) - (and (eq parameter 'adob--dim) - (memq window '(left right)))))) - (should (cj/auto-dim--vterm-buffer-dimmed-p))))) - -(ert-deftest test-auto-dim-config-vterm-dimmed-p-undimmed-window-keeps-buffer-bright () - "Normal: a selected/undimmed vterm window keeps the buffer bright." - (skip-unless (file-directory-p test-auto-dim--fork)) - (require 'auto-dim-config) - (let ((major-mode 'vterm-mode)) - (cl-letf (((symbol-function 'get-buffer-window-list) - (lambda (&rest _) '(left right))) - ((symbol-function 'window-parameter) - (lambda (window parameter) - (and (eq parameter 'adob--dim) - (eq window 'right))))) - (should-not (cj/auto-dim--vterm-buffer-dimmed-p))))) - -(ert-deftest test-auto-dim-config-vterm-get-color-dims-only-dimmed-vterm-buffers () - "Normal: vterm color advice dims only buffers marked dimmed." - (skip-unless (file-directory-p test-auto-dim--fork)) - (require 'auto-dim-config) - (let ((major-mode 'vterm-mode) - (cj/auto-dim-vterm-foreground-blend 1.0)) - (cl-letf (((symbol-function 'cj/auto-dim--vterm-buffer-dimmed-p) - (lambda () t)) - ((symbol-function 'cj/auto-dim--face-color) - (lambda (&rest _) "#555555"))) - (should (equal "#555555" - (cj/auto-dim--vterm-get-color - (lambda (&rest _) "#ffffff") 7 :foreground)))) - (cl-letf (((symbol-function 'cj/auto-dim--vterm-buffer-dimmed-p) - (lambda () nil))) - (should (equal "#ffffff" - (cj/auto-dim--vterm-get-color - (lambda (&rest _) "#ffffff") 7 :foreground)))))) - -(ert-deftest test-auto-dim-config-vterm-post-command-schedules-refresh-on-window-change () - "Normal: post-command vterm refresh schedules only after selection changes." - (skip-unless (file-directory-p test-auto-dim--fork)) - (require 'auto-dim-config) - (let ((cj/auto-dim--last-selected-window 'old) - (calls 0)) - (cl-letf (((symbol-function 'selected-window) - (lambda () 'new)) - ((symbol-function 'cj/auto-dim--schedule-vterm-refresh) - (lambda (&optional _) (setq calls (1+ calls))))) - (cj/auto-dim--refresh-vterm-on-command) - (cj/auto-dim--refresh-vterm-on-command)) - (should (eq cj/auto-dim--last-selected-window 'new)) - (should (= calls 1)))) - -(ert-deftest test-auto-dim-config-vterm-refresh-runs-auto-dim-before-invalidate () - "Normal: deferred vterm refresh updates auto-dim before invalidating vterm." - (skip-unless (file-directory-p test-auto-dim--fork)) - (require 'auto-dim-config) - (let (events) - (cl-letf (((symbol-function 'adob--update) - (lambda () (push 'adob events))) - ((symbol-function 'cj/auto-dim--refresh-vterm-windows) - (lambda (&optional _) (push 'vterm events)))) - (cj/auto-dim--refresh-vterm-after-auto-dim)) - (should (equal events '(vterm adob))))) - -(ert-deftest test-auto-dim-config-vterm-refresh-nudges-size-for-full-redraw () - "Normal: vterm refresh nudges size to force full-grid redraw." - (skip-unless (file-directory-p test-auto-dim--fork)) - (require 'auto-dim-config) - (let ((calls nil) - (vterm-min-window-width 80)) - (with-temp-buffer - (setq major-mode 'vterm-mode) - (setq-local vterm--term 'term) - (let ((buffer (current-buffer))) - (cl-letf (((symbol-function 'window-list) - (lambda (&rest _) '(vterm-window))) - ((symbol-function 'window-buffer) - (lambda (_) buffer)) - ((symbol-function 'window-live-p) - (lambda (_) t)) - ((symbol-function 'window-body-height) - (lambda (_) 24)) - ((symbol-function 'window-body-width) - (lambda (_) 100)) - ((symbol-function 'vterm--get-margin-width) - (lambda () 3)) - ((symbol-function 'vterm--set-size) - (lambda (term height width) - (push (list term height width) calls)))) - (cj/auto-dim--refresh-vterm-windows)))) - (should (equal (nreverse calls) - '((term 25 97) - (term 24 97)))))) - (ert-deftest test-auto-dim-config-never-dim-dashboard-exempts-dashboard () "Normal: the *dashboard* buffer is exempt from dimming." (skip-unless (file-directory-p test-auto-dim--fork)) diff --git a/tests/test-dashboard-config-launchers.el b/tests/test-dashboard-config-launchers.el index cb925075..0ac37f87 100644 --- a/tests/test-dashboard-config-launchers.el +++ b/tests/test-dashboard-config-launchers.el @@ -83,7 +83,7 @@ Slack and Linear sharing the last row." (let ((map (make-sparse-keymap)) (calls nil)) (cl-letf (((symbol-function 'projectile-switch-project) (lambda (&rest _) (push 'code calls))) ((symbol-function 'dirvish) (lambda (&rest _) (push 'files calls))) - ((symbol-function 'vterm) (lambda (&rest _) (push 'term calls))) + ((symbol-function 'ghostel) (lambda (&rest _) (push 'term calls))) ((symbol-function 'cj/main-agenda-display) (lambda (&rest _) (push 'agenda calls))) ((symbol-function 'cj/elfeed-open) (lambda (&rest _) (push 'feeds calls))) ((symbol-function 'calibredb) (lambda (&rest _) (push 'books calls))) diff --git a/tests/test-init-module-headers.el b/tests/test-init-module-headers.el index ef5a7132..2680a19c 100644 --- a/tests/test-init-module-headers.el +++ b/tests/test-init-module-headers.el @@ -56,7 +56,6 @@ "selection-framework" "modeline-config" "mousetrap-mode" - "popper-config" "dashboard-config" "nerd-icons-config" ;; Batch 5 — Dev entry-points, diff, help, lint, VC (Layer 2) @@ -96,7 +95,7 @@ "hugo-config" ;; Batch 8 — Domain / integration / optional modules (Layer 2-4) "ai-config" - "ai-vterm" + "ai-term" "browser-config" "calendar-sync" "calibredb-epub-config" @@ -130,7 +129,7 @@ "tramp-config" "transcription-config" "video-audio-recording" - "vterm-config" + "term-config" "weather-config" "wrap-up") "Modules annotated with the load-graph header contract. diff --git a/tests/test-term-tmux-history.el b/tests/test-term-tmux-history.el new file mode 100644 index 00000000..2c9c38f8 --- /dev/null +++ b/tests/test-term-tmux-history.el @@ -0,0 +1,312 @@ +;;; test-term-tmux-history.el --- Tests for term-config tmux history + menu UX -*- lexical-binding: t; -*- + +;;; Commentary: +;; Exercises the term-config (ghostel) terminal UX: the Emacs-owned tmux +;; history buffer, the copy-mode-dwim engine pick, the tmux pane-id / +;; attached-client predicates, and the C-; x menu bindings. +;; +;; ghostel is required (which defines `ghostel-mode-map' / +;; `ghostel-keymap-exceptions' and lets term-config's `with-eval-after-load' +;; fire) before term-config. `(require 'ghostel)' does not load the native +;; module; tmux is mocked via `process-file', so nothing spawns. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'package) + +(setq package-user-dir (expand-file-name "elpa" user-emacs-directory)) +(package-initialize) +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) +(setq load-prefer-newer t) +(require 'ghostel) +(require 'term-config) +(require 'testutil-ghostel-buffers) + +(defmacro test-term-tmux-history--with-tmux-mock (responses &rest body) + "Run BODY with `process-file' mocked for tmux RESPONSES. + +RESPONSES is an alist of (ARGS EXIT-CODE OUTPUT)." + (declare (indent 1)) + `(let ((calls nil)) + (cl-letf (((symbol-function 'process-file) + (lambda (program _infile destination _display &rest args) + (push (cons program args) calls) + (let* ((entry (seq-find + (lambda (candidate) + (equal (car candidate) args)) + ,responses)) + (exit-code (or (cadr entry) 1)) + (output (or (caddr entry) ""))) + (when destination + (let ((buffer (cond + ((eq destination t) (current-buffer)) + ((bufferp destination) destination) + ((consp destination) (car destination))))) + (when (bufferp buffer) + (with-current-buffer buffer + (insert output))))) + exit-code)))) + ,@body))) + +(ert-deftest test-term-tmux-history--pane-id-for-tty-matches-client () + "Normal: current terminal pty maps to the active pane for that tmux client." + (test-term-tmux-history--with-tmux-mock + '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 + "/dev/pts/1\t%1\n/dev/pts/8\t%8\n")) + (should (equal (cj/term--tmux-pane-id-for-tty "/dev/pts/8") "%8")))) + +(ert-deftest test-term-tmux-history--capture-pane-uses-full-history () + "Normal: capture asks tmux for joined full pane history." + (test-term-tmux-history--with-tmux-mock + '((("capture-pane" "-p" "-J" "-S" "-" "-E" "-" "-t" "%8") 0 + "first line\nsecond line\n")) + (should (equal (cj/term--tmux-capture-pane "%8") + "first line\nsecond line\n")))) + +(ert-deftest test-term-tmux-history-open-renders-read-only-history-buffer () + "Normal: command renders tmux history in a normal Emacs buffer." + (let ((origin (cj/test--make-fake-ghostel-buffer "*test-term-history-origin*"))) + (unwind-protect + (save-window-excursion + (switch-to-buffer origin) + (cl-letf (((symbol-function 'get-buffer-process) + (lambda (_buffer) 'fake-process)) + ((symbol-function 'process-tty-name) + (lambda (_process) "/dev/pts/8"))) + (test-term-tmux-history--with-tmux-mock + '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 + "/dev/pts/8\t%8\n") + (("capture-pane" "-p" "-J" "-S" "-" "-E" "-" "-t" "%8") 0 + "history http://example.com\n")) + (cj/term-tmux-history) + (should (eq major-mode 'cj/term-tmux-history-mode)) + (should buffer-read-only) + (should (string-match-p "history http://example.com" + (buffer-string)))))) + (cj/test--kill-buffers-matching-prefix "*terminal tmux history") + (when (buffer-live-p origin) + (kill-buffer origin))))) + +(ert-deftest test-term-tmux-history-replaces-origin-buffer-in-same-window () + "Normal: the history view replaces the origin in the selected window. + +`cj/term-tmux-history' uses `switch-to-buffer' so reading scrollback keeps +the terminal's frame slot rather than splitting or popping a new window." + (let ((origin (cj/test--make-fake-ghostel-buffer "*test-term-history-inplace*"))) + (unwind-protect + (save-window-excursion + (delete-other-windows) + (switch-to-buffer origin) + (let ((win (selected-window))) + (should (eq (window-buffer win) origin)) + (should (one-window-p)) + (cl-letf (((symbol-function 'get-buffer-process) + (lambda (_buffer) 'fake-process)) + ((symbol-function 'process-tty-name) + (lambda (_process) "/dev/pts/8"))) + (test-term-tmux-history--with-tmux-mock + '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 + "/dev/pts/8\t%8\n") + (("capture-pane" "-p" "-J" "-S" "-" "-E" "-" "-t" "%8") 0 + "scrollback line\n")) + (cj/term-tmux-history))) + (should (one-window-p)) + (should (eq (selected-window) win)) + (should (string-prefix-p + "*terminal tmux history:" + (buffer-name (window-buffer win)))))) + (cj/test--kill-buffers-matching-prefix "*terminal tmux history") + (when (buffer-live-p origin) + (kill-buffer origin))))) + +(ert-deftest test-term-tmux-history-quit-returns-to-origin () + "Normal: q / <escape> / C-g (cj/term-tmux-history-quit) kills the history +buffer and restores the origin buffer, window, and point." + (let ((origin (get-buffer-create "*test-term-history-return*"))) + (unwind-protect + (let ((history (get-buffer-create "*terminal tmux history: test*"))) + (with-current-buffer origin + (erase-buffer) + (insert "origin") + (goto-char (point-min))) + (switch-to-buffer origin) + (let ((origin-window (selected-window))) + (with-current-buffer history + (cj/term-tmux-history-mode) + (let ((inhibit-read-only t)) + (insert "alpha\nbeta\ngamma\n")) + (setq-local cj/term-tmux-history--origin-buffer origin) + (setq-local cj/term-tmux-history--origin-window origin-window) + (setq-local cj/term-tmux-history--origin-point (point-min)) + (cj/term-tmux-history-quit)) + (should-not (buffer-live-p history)) + (should (eq (current-buffer) origin)) + (should (= (point) (point-min))))) + (when (buffer-live-p origin) + (kill-buffer origin))))) + +(ert-deftest test-term-tmux-history-mode-keymap () + "Normal: in the history buffer M-w copies without quitting; q, <escape>, +and C-g quit back to the terminal; RET is left unbound (no special exit)." + (should (eq (keymap-lookup cj/term-tmux-history-mode-map "M-w") + #'kill-ring-save)) + (should (eq (keymap-lookup cj/term-tmux-history-mode-map "q") + #'cj/term-tmux-history-quit)) + (should (eq (keymap-lookup cj/term-tmux-history-mode-map "<escape>") + #'cj/term-tmux-history-quit)) + (should (eq (keymap-lookup cj/term-tmux-history-mode-map "C-g") + #'cj/term-tmux-history-quit)) + (should-not (keymap-lookup cj/term-tmux-history-mode-map "RET"))) + +(ert-deftest test-term-keymap-includes-history-and-copy-bindings () + "Normal: the personal terminal map owns the high-level UX commands, and C-; +reaches Emacs inside ghostel buffers so the prefix works there." + (should (member "C-;" ghostel-keymap-exceptions)) + (should (eq (keymap-lookup cj/custom-keymap "x h") #'cj/term-tmux-history)) + (should (eq (keymap-lookup cj/custom-keymap "x c") #'cj/term-copy-mode-dwim)) + (should (equal (keymap-lookup ghostel-mode-map "C-;") cj/custom-keymap)) + (should (eq (keymap-lookup ghostel-mode-map "C-; x h") #'cj/term-tmux-history)) + (should (eq (keymap-lookup ghostel-mode-map "C-; x c") #'cj/term-copy-mode-dwim))) + +(ert-deftest test-term-keymap-prompt-navigation () + "Normal: n/p navigate prompts, capital N creates a new terminal buffer." + (should (eq (keymap-lookup cj/custom-keymap "x n") #'ghostel-next-prompt)) + (should (eq (keymap-lookup cj/custom-keymap "x p") #'ghostel-previous-prompt)) + (should (eq (keymap-lookup cj/custom-keymap "x N") #'ghostel))) + +(ert-deftest test-term-current-tmux-pane-id-rejects-non-ghostel-buffer () + "Error: pane-id lookup refuses a buffer that is not in `ghostel-mode'." + (with-temp-buffer + (should-error (cj/term--current-tmux-pane-id) :type 'user-error))) + +(ert-deftest test-term-current-tmux-pane-id-accepts-agent-named-buffer () + "Normal: an agent-named ghostel buffer resolves by process TTY. + +The pane lookup keys off the live process TTY, never the buffer name, so a +buffer named `agent [repo]' (ai-term.el's naming) resolves like any other +ghostel-mode terminal." + (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))) + (unwind-protect + (with-current-buffer agent + (cl-letf (((symbol-function 'get-buffer-process) + (lambda (_buffer) 'fake-process)) + ((symbol-function 'process-tty-name) + (lambda (_process) "/dev/pts/8"))) + (test-term-tmux-history--with-tmux-mock + '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 + "/dev/pts/1\t%1\n/dev/pts/8\t%8\n")) + (should (equal (cj/term--current-tmux-pane-id) "%8"))))) + (when (buffer-live-p agent) + (kill-buffer agent))))) + +(ert-deftest test-term-in-tmux-p-true-when-client-attached () + "Normal: predicate returns t when tmux reports a client for our tty." + (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))) + (unwind-protect + (with-current-buffer agent + (cl-letf (((symbol-function 'get-buffer-process) + (lambda (_buffer) 'fake-process)) + ((symbol-function 'process-tty-name) + (lambda (_process) "/dev/pts/8"))) + (test-term-tmux-history--with-tmux-mock + '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 + "/dev/pts/8\t%8\n")) + (should (cj/term--in-tmux-p))))) + (when (buffer-live-p agent) + (kill-buffer agent))))) + +(ert-deftest test-term-in-tmux-p-nil-when-no-matching-client () + "Boundary: predicate returns nil when tmux runs but our tty has no client." + (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))) + (unwind-protect + (with-current-buffer agent + (cl-letf (((symbol-function 'get-buffer-process) + (lambda (_buffer) 'fake-process)) + ((symbol-function 'process-tty-name) + (lambda (_process) "/dev/pts/8"))) + (test-term-tmux-history--with-tmux-mock + '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 + "/dev/pts/1\t%1\n")) + (should-not (cj/term--in-tmux-p))))) + (when (buffer-live-p agent) + (kill-buffer agent))))) + +(ert-deftest test-term-in-tmux-p-nil-when-tmux-fails () + "Error: predicate swallows tmux failures and returns nil." + (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))) + (unwind-protect + (with-current-buffer agent + (cl-letf (((symbol-function 'get-buffer-process) + (lambda (_buffer) 'fake-process)) + ((symbol-function 'process-tty-name) + (lambda (_process) "/dev/pts/8"))) + (test-term-tmux-history--with-tmux-mock + '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 1 + "no server running")) + (should-not (cj/term--in-tmux-p))))) + (when (buffer-live-p agent) + (kill-buffer agent))))) + +(ert-deftest test-term-in-tmux-p-nil-when-not-ghostel-mode () + "Boundary: predicate refuses non-ghostel buffers without calling tmux." + (with-temp-buffer + (let ((tmux-called nil)) + (cl-letf (((symbol-function 'process-file) + (lambda (&rest _) (setq tmux-called t) 0))) + (should-not (cj/term--in-tmux-p)) + (should-not tmux-called))))) + +(ert-deftest test-term-copy-mode-dwim-sends-tmux-prefix-when-attached () + "Normal: with tmux attached, dwim writes C-b [ into the pty so tmux enters +its own copy-mode against the full pane history." + (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]")) + (sent nil) + (copy-mode-called nil)) + (unwind-protect + (with-current-buffer agent + (cl-letf (((symbol-function 'get-buffer-process) + (lambda (_buffer) 'fake-process)) + ((symbol-function 'process-tty-name) + (lambda (_process) "/dev/pts/8")) + ((symbol-function 'ghostel-send-string) + (lambda (s) (push s sent))) + ((symbol-function 'ghostel-copy-mode) + (lambda () (setq copy-mode-called t)))) + (test-term-tmux-history--with-tmux-mock + '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 + "/dev/pts/8\t%8\n")) + (cj/term-copy-mode-dwim) + (should (equal sent '("\C-b["))) + (should-not copy-mode-called)))) + (when (buffer-live-p agent) + (kill-buffer agent))))) + +(ert-deftest test-term-copy-mode-dwim-falls-back-without-tmux () + "Boundary: without tmux, dwim calls `ghostel-copy-mode' and sends nothing." + (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]")) + (sent nil) + (copy-mode-called nil)) + (unwind-protect + (with-current-buffer agent + (cl-letf (((symbol-function 'get-buffer-process) + (lambda (_buffer) 'fake-process)) + ((symbol-function 'process-tty-name) + (lambda (_process) "/dev/pts/8")) + ((symbol-function 'ghostel-send-string) + (lambda (s) (push s sent))) + ((symbol-function 'ghostel-copy-mode) + (lambda () (setq copy-mode-called t)))) + (test-term-tmux-history--with-tmux-mock + '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 1 + "no server running")) + (cj/term-copy-mode-dwim) + (should-not sent) + (should copy-mode-called)))) + (when (buffer-live-p agent) + (kill-buffer agent))))) + +(provide 'test-term-tmux-history) +;;; test-term-tmux-history.el ends here diff --git a/tests/test-term-toggle--buffer-filter.el b/tests/test-term-toggle--buffer-filter.el new file mode 100644 index 00000000..2c96ecb3 --- /dev/null +++ b/tests/test-term-toggle--buffer-filter.el @@ -0,0 +1,94 @@ +;;; test-term-toggle--buffer-filter.el --- Tests for F12's buffer filter -*- lexical-binding: t; -*- + +;;; Commentary: +;; Three closely-related helpers determine which terminal buffers F12 +;; manages: the predicate `cj/--term-toggle-buffer-p', the MRU list +;; `cj/--term-toggle-buffers', and the per-frame window finder +;; `cj/--term-toggle-displayed-window'. All three exclude agent- +;; prefixed buffers so agent has its own F9 surface. + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) +(require 'term-config) +(require 'testutil-ghostel-buffers) + +(defun test-term-toggle--cleanup () + "Kill leftover agent- and *test-term- prefixed buffers." + (cj/test--kill-agent-buffers) + (cj/test--kill-test-term-buffers)) + +(ert-deftest test-term-toggle--buffer-p-accepts-ghostel-mode () + "Normal: a ghostel-mode buffer with non-agent name qualifies." + (test-term-toggle--cleanup) + (let ((buf (cj/test--make-fake-ghostel-buffer "*test-term-1*"))) + (unwind-protect + (should (cj/--term-toggle-buffer-p buf)) + (kill-buffer buf)))) + +(ert-deftest test-term-toggle--buffer-p-rejects-agent () + "Boundary: agent-prefixed terminal buffers are excluded from F12's set." + (test-term-toggle--cleanup) + (let ((buf (cj/test--make-fake-ghostel-buffer "agent [project-a]"))) + (unwind-protect + (should-not (cj/--term-toggle-buffer-p buf)) + (kill-buffer buf)))) + +(ert-deftest test-term-toggle--buffer-p-rejects-non-terminal () + "Boundary: a regular buffer (not ghostel-mode, no terminal name prefix) -> nil." + (test-term-toggle--cleanup) + (let ((buf (get-buffer-create "*test-term-regular*"))) + (unwind-protect + (should-not (cj/--term-toggle-buffer-p buf)) + (kill-buffer buf)))) + +(ert-deftest test-term-toggle--buffer-p-rejects-dead-buffer () + "Boundary: nil and dead buffers -> nil." + (should-not (cj/--term-toggle-buffer-p nil)) + (let ((buf (cj/test--make-fake-ghostel-buffer "*test-term-dead*"))) + (kill-buffer buf) + (should-not (cj/--term-toggle-buffer-p buf)))) + +(ert-deftest test-term-toggle--buffers-filters-agent () + "Normal: returns terminal buffers but excludes agent-prefixed ones." + (test-term-toggle--cleanup) + (let ((normal (cj/test--make-fake-ghostel-buffer "*test-term-normal*")) + (agent (cj/test--make-fake-ghostel-buffer "agent [for-test]"))) + (unwind-protect + (let ((result (cj/--term-toggle-buffers))) + (should (memq normal result)) + (should-not (memq agent result))) + (kill-buffer normal) + (kill-buffer agent)))) + +(ert-deftest test-term-toggle--displayed-window-finds-terminal () + "Normal: terminal in a window -> returns that window." + (test-term-toggle--cleanup) + (let ((vt (cj/test--make-fake-ghostel-buffer "*test-term-shown*"))) + (unwind-protect + (save-window-excursion + (delete-other-windows) + (let ((win (split-window-right))) + (set-window-buffer win vt) + (let ((result (cj/--term-toggle-displayed-window))) + (should (windowp result)) + (should (eq (window-buffer result) vt))))) + (kill-buffer vt)))) + +(ert-deftest test-term-toggle--displayed-window-skips-agent () + "Boundary: only an agent terminal is displayed -> nil (agent not F12-managed)." + (test-term-toggle--cleanup) + (let ((agent (cj/test--make-fake-ghostel-buffer "agent [skip-test]"))) + (unwind-protect + (save-window-excursion + (delete-other-windows) + (let ((win (split-window-right))) + (set-window-buffer win agent) + (should-not (cj/--term-toggle-displayed-window)))) + (kill-buffer agent)))) + +(provide 'test-term-toggle--buffer-filter) +;;; test-term-toggle--buffer-filter.el ends here diff --git a/tests/test-term-toggle--dispatch.el b/tests/test-term-toggle--dispatch.el new file mode 100644 index 00000000..f13c2840 --- /dev/null +++ b/tests/test-term-toggle--dispatch.el @@ -0,0 +1,53 @@ +;;; test-term-toggle--dispatch.el --- Tests for cj/--term-toggle-dispatch -*- lexical-binding: t; -*- + +;;; Commentary: +;; Pure decision helper for F12. Returns one of (toggle-off . WIN), +;; (show-recent . BUFFER), or (create-new) based on whether a terminal +;; window is currently displayed and whether any terminal buffers are +;; alive. Mocking the underlying helpers keeps the dispatch logic +;; exercisable without touching real windows. + +;;; Code: + +(require 'ert) +(require 'cl-lib) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) +(require 'term-config) +(require 'testutil-ghostel-buffers) + +(ert-deftest test-term-toggle--dispatch-window-displayed-returns-toggle-off () + "Normal: displayed terminal window -> (toggle-off . WIN)." + (let ((sentinel-win 'fake-window)) + (cl-letf (((symbol-function 'cj/--term-toggle-displayed-window) + (lambda (&optional _frame) sentinel-win))) + (should (equal (cj/--term-toggle-dispatch) + (cons 'toggle-off sentinel-win)))))) + +(ert-deftest test-term-toggle--dispatch-no-window-buffer-alive-returns-show-recent () + "Normal: no displayed terminal, at least one alive -> show-recent + first." + (cj/test--kill-test-term-buffers) + (let ((b1 (get-buffer-create "*test-term-mru-1*")) + (b2 (get-buffer-create "*test-term-mru-2*"))) + (unwind-protect + (cl-letf (((symbol-function 'cj/--term-toggle-displayed-window) + (lambda (&optional _frame) nil)) + ((symbol-function 'cj/--term-toggle-buffers) + (lambda () (list b1 b2)))) + (should (equal (cj/--term-toggle-dispatch) + (cons 'show-recent b1)))) + (kill-buffer b1) + (kill-buffer b2)))) + +(ert-deftest test-term-toggle--dispatch-no-window-no-buffer-returns-create-new () + "Boundary: nothing displayed, no alive terminals -> create-new." + (cj/test--kill-test-term-buffers) + (cl-letf (((symbol-function 'cj/--term-toggle-displayed-window) + (lambda (&optional _frame) nil)) + ((symbol-function 'cj/--term-toggle-buffers) + (lambda () nil))) + (should (equal (cj/--term-toggle-dispatch) '(create-new))))) + +(provide 'test-term-toggle--dispatch) +;;; test-term-toggle--dispatch.el ends here diff --git a/tests/test-vterm-toggle--display.el b/tests/test-term-toggle--display.el index 69bf2360..0943a488 100644 --- a/tests/test-vterm-toggle--display.el +++ b/tests/test-term-toggle--display.el @@ -1,7 +1,7 @@ -;;; test-vterm-toggle--display.el --- Tests for the F12 display-saved action -*- lexical-binding: t; -*- +;;; test-term-toggle--display.el --- Tests for the F12 display-saved action -*- lexical-binding: t; -*- ;;; Commentary: -;; Covers the F12-side equivalents of the ai-vterm display tests: +;; Covers the F12-side equivalents of the ai-term display tests: ;; geometry capture (window-direction, window-size with 'below ;; default), capture-state writing module-level vars, and the custom ;; display action mapping cardinal -> edge directions. Tests stub @@ -14,62 +14,62 @@ (require 'cl-lib) (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'vterm-config) +(require 'term-config) -(ert-deftest test-vterm-toggle--capture-state-records-direction-and-size () +(ert-deftest test-term-toggle--capture-state-records-direction-and-size () "Normal: capture-state writes direction and integer body size." (save-window-excursion (delete-other-windows) (let ((below (split-window (selected-window) nil 'below)) - (cj/--vterm-toggle-last-direction nil) - (cj/--vterm-toggle-last-size nil)) - (cj/--vterm-toggle-capture-state below) - (should (eq cj/--vterm-toggle-last-direction 'below)) - (should (integerp cj/--vterm-toggle-last-size)) - (should (= cj/--vterm-toggle-last-size (window-body-height below)))))) + (cj/--term-toggle-last-direction nil) + (cj/--term-toggle-last-size nil)) + (cj/--term-toggle-capture-state below) + (should (eq cj/--term-toggle-last-direction 'below)) + (should (integerp cj/--term-toggle-last-size)) + (should (= cj/--term-toggle-last-size (window-body-height below)))))) -(ert-deftest test-vterm-toggle--capture-state-noop-on-dead-window () +(ert-deftest test-term-toggle--capture-state-noop-on-dead-window () "Boundary: nil window -> state remains unchanged." - (let ((cj/--vterm-toggle-last-direction 'sentinel) - (cj/--vterm-toggle-last-size 0.123)) - (cj/--vterm-toggle-capture-state nil) - (should (eq cj/--vterm-toggle-last-direction 'sentinel)) - (should (= cj/--vterm-toggle-last-size 0.123)))) + (let ((cj/--term-toggle-last-direction 'sentinel) + (cj/--term-toggle-last-size 0.123)) + (cj/--term-toggle-capture-state nil) + (should (eq cj/--term-toggle-last-direction 'sentinel)) + (should (= cj/--term-toggle-last-size 0.123)))) -(ert-deftest test-vterm-toggle--display-saved-defaults-when-state-nil () - "Normal: nil state -> direction=bottom, size=cj/vterm-toggle-window-height." +(ert-deftest test-term-toggle--display-saved-defaults-when-state-nil () + "Normal: nil state -> direction=bottom, size=cj/term-toggle-window-height." (let (received-alist - (cj/--vterm-toggle-last-direction nil) - (cj/--vterm-toggle-last-size nil) - (cj/vterm-toggle-window-height 0.7)) + (cj/--term-toggle-last-direction nil) + (cj/--term-toggle-last-size nil) + (cj/term-toggle-window-height 0.7)) (cl-letf (((symbol-function 'display-buffer-in-direction) (lambda (_b a) (setq received-alist a) 'fake-window))) - (cj/--vterm-toggle-display-saved 'fake-buf '((inhibit-same-window . t)))) + (cj/--term-toggle-display-saved 'fake-buf '((inhibit-same-window . t)))) (should (eq (cdr (assq 'direction received-alist)) 'bottom)) (should (= (cdr (assq 'window-height received-alist)) 0.7)) (should (eq (cdr (assq 'inhibit-same-window received-alist)) t)))) -(ert-deftest test-vterm-toggle--display-saved-maps-cardinal-to-edge () +(ert-deftest test-term-toggle--display-saved-maps-cardinal-to-edge () "Normal: saved 'below maps to bottom edge; integer size wraps in body-lines." (let (received-alist - (cj/--vterm-toggle-last-direction 'below) - (cj/--vterm-toggle-last-size 12)) + (cj/--term-toggle-last-direction 'below) + (cj/--term-toggle-last-size 12)) (cl-letf (((symbol-function 'display-buffer-in-direction) (lambda (_b a) (setq received-alist a) 'fake-window))) - (cj/--vterm-toggle-display-saved 'fake-buf nil)) + (cj/--term-toggle-display-saved 'fake-buf nil)) (should (eq (cdr (assq 'direction received-alist)) 'bottom)) (should (equal (cdr (assq 'window-height received-alist)) '(body-lines . 12))) (should-not (assq 'window-width received-alist)))) -(ert-deftest test-vterm-toggle--display-saved-strips-conflicting-alist-entries () +(ert-deftest test-term-toggle--display-saved-strips-conflicting-alist-entries () "Boundary: caller-supplied direction/size are stripped, saved values win." (let (received-alist - (cj/--vterm-toggle-last-direction 'right) - (cj/--vterm-toggle-last-size 30)) + (cj/--term-toggle-last-direction 'right) + (cj/--term-toggle-last-size 30)) (cl-letf (((symbol-function 'display-buffer-in-direction) (lambda (_b a) (setq received-alist a) 'fake-window))) - (cj/--vterm-toggle-display-saved + (cj/--term-toggle-display-saved 'fake-buf '((direction . above) (window-width . 0.2) @@ -83,5 +83,5 @@ received-alist))) (should (null wh-cells))))) -(provide 'test-vterm-toggle--display) -;;; test-vterm-toggle--display.el ends here +(provide 'test-term-toggle--display) +;;; test-term-toggle--display.el ends here diff --git a/tests/test-ui-config--buffer-cursor-state.el b/tests/test-ui-config--buffer-cursor-state.el index ead05741..85286586 100644 --- a/tests/test-ui-config--buffer-cursor-state.el +++ b/tests/test-ui-config--buffer-cursor-state.el @@ -3,12 +3,12 @@ ;;; Commentary: ;; `cj/--buffer-cursor-state' picks the buffer-state symbol that ;; `cj/set-cursor-color-according-to-mode' maps to a cursor color via -;; `cj/buffer-status-colors'. The subtle case: a live vterm buffer is -;; technically `buffer-read-only' (the `vterm-mode' body sets it) but the -;; user can type into it -- keystrokes go to the terminal process -- so it -;; must report a writeable state, not `read-only'. `vterm-copy-mode' is -;; the exception: there the buffer really is a read-only Emacs buffer the -;; user navigates, so `read-only' (the orange cursor) is correct and kept. +;; `cj/buffer-status-colors'. The subtle case: a live ghostel terminal is +;; technically `buffer-read-only' but the user types into it -- keystrokes go +;; to the terminal process -- so it must report a writeable state, not +;; `read-only'. ghostel's `copy' / `emacs' input modes are the exception: +;; there the buffer really is a read-only Emacs buffer the user navigates, so +;; `read-only' (the orange cursor) is correct and kept. ;;; Code: @@ -18,9 +18,9 @@ (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) (setq load-prefer-newer t) -(defvar vterm-copy-mode nil) +(defvar ghostel--input-mode nil) (require 'ui-config) -(require 'testutil-vterm-buffers) +(require 'testutil-ghostel-buffers) (ert-deftest test-ui-config-buffer-cursor-state-readwrite-unmodified () "Normal: a clean writeable buffer reports `unmodified'." @@ -47,40 +47,40 @@ (overwrite-mode 1) (should (eq (cj/--buffer-cursor-state) 'overwrite)))) -(ert-deftest test-ui-config-buffer-cursor-state-live-vterm-is-writeable () - "Boundary: a live vterm buffer is `buffer-read-only' but reports a +(ert-deftest test-ui-config-buffer-cursor-state-live-ghostel-is-writeable () + "Boundary: a live ghostel buffer is `buffer-read-only' but reports a writeable state -- the user types into the terminal process there, so the read-only (orange) cursor would be misleading." - (let ((buf (cj/test--make-fake-vterm-buffer "*test-vterm-cursor-state*"))) + (let ((buf (cj/test--make-fake-ghostel-buffer "*test-ghostel-cursor-state*"))) (unwind-protect (with-current-buffer buf - (setq buffer-read-only t) ; `vterm-mode' does this - (setq-local vterm-copy-mode nil) + (setq buffer-read-only t) ; ghostel keeps the buffer read-only + (setq-local ghostel--input-mode 'semi-char) (should-not (eq (cj/--buffer-cursor-state) 'read-only))) (when (buffer-live-p buf) (kill-buffer buf))))) -(ert-deftest test-ui-config-buffer-cursor-state-vterm-copy-mode-is-read-only () - "Boundary: in `vterm-copy-mode' the vterm buffer is a read-only Emacs -buffer the user navigates, so `read-only' (orange) is kept." - (let ((buf (cj/test--make-fake-vterm-buffer "*test-vterm-cursor-state-copy*"))) +(ert-deftest test-ui-config-buffer-cursor-state-ghostel-copy-mode-is-read-only () + "Boundary: in ghostel `copy' mode the buffer is a read-only Emacs buffer +the user navigates, so `read-only' (orange) is kept." + (let ((buf (cj/test--make-fake-ghostel-buffer "*test-ghostel-cursor-state-copy*"))) (unwind-protect (with-current-buffer buf (setq buffer-read-only t) - (setq-local vterm-copy-mode t) + (setq-local ghostel--input-mode 'copy) (should (eq (cj/--buffer-cursor-state) 'read-only))) (when (buffer-live-p buf) (kill-buffer buf))))) -(ert-deftest test-ui-config-set-cursor-color-live-vterm-not-orange () - "Normal: in a live vterm the cursor-color hook picks a writeable color, -not the read-only orange -- even though the vterm buffer is read-only. -`display-graphic-p' is stubbed t so the function reaches its work body -in batch mode (the live function no-ops on TTY frames by design)." - (let ((buf (cj/test--make-fake-vterm-buffer "*test-vterm-cursor-color*")) +(ert-deftest test-ui-config-set-cursor-color-live-ghostel-not-orange () + "Normal: in a live ghostel terminal the cursor-color hook picks a writeable +color, not the read-only orange -- even though the buffer is read-only. +`display-graphic-p' is stubbed t so the function reaches its work body in +batch mode (the live function no-ops on TTY frames by design)." + (let ((buf (cj/test--make-fake-ghostel-buffer "*test-ghostel-cursor-color*")) (applied 'unset)) (unwind-protect (with-current-buffer buf (setq buffer-read-only t) - (setq-local vterm-copy-mode nil) + (setq-local ghostel--input-mode 'semi-char) (let ((cj/-cursor-last-color nil) (cj/-cursor-last-buffer nil)) (cl-letf (((symbol-function 'display-graphic-p) (lambda () t)) diff --git a/tests/test-vterm-copy-mode-cursor.el b/tests/test-vterm-copy-mode-cursor.el deleted file mode 100644 index c549a44f..00000000 --- a/tests/test-vterm-copy-mode-cursor.el +++ /dev/null @@ -1,145 +0,0 @@ -;;; test-vterm-copy-mode-cursor.el --- Tests for cursor visibility in vterm-copy-mode -*- lexical-binding: t; -*- - -;;; Commentary: -;; vterm's C module sets `cursor-type' to nil when the underlying TUI -;; sends DECTCEM (`\e[?25l'). Most full-screen TUIs (Claude Code, htop, -;; etc.) hide the cursor on startup. In `vterm-copy-mode' the user is -;; navigating the buffer, not watching the TUI, so the cursor must be -;; forced visible -- the hook in `vterm-config.el' handles that. On -;; exit, the buffer-local override is killed so the live terminal goes -;; back to the TUI's chosen cursor state. - -;;; Code: - -(require 'ert) -(require 'cl-lib) -(require 'package) - -(setq package-user-dir (expand-file-name "elpa" user-emacs-directory)) -(package-initialize) -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(defvar vterm-copy-mode nil) -(require 'vterm-config) -(require 'vterm) - -(defmacro test-vterm-copy-mode-cursor--in-fake-vterm-buffer (&rest body) - "Run BODY in a temp buffer pretending to be a live vterm. -Stubs `vterm--enter-copy-mode' and `vterm--exit-copy-mode' so toggling -`vterm-copy-mode' doesn't try to talk to a real vterm process." - (declare (indent 0)) - `(cl-letf (((symbol-function 'vterm--enter-copy-mode) #'ignore) - ((symbol-function 'vterm--exit-copy-mode) #'ignore)) - (with-temp-buffer - (setq-local major-mode 'vterm-mode) - ,@body))) - -(ert-deftest test-vterm-copy-mode-cursor-restored-on-enter () - "Normal: entering copy-mode with cursor-type nil sets a visible cursor." - (with-temp-buffer - (setq-local cursor-type nil) - (let ((vterm-copy-mode t)) - (cj/--vterm-copy-mode-restore-cursor)) - (should (equal cursor-type 'box)))) - -(ert-deftest test-vterm-copy-mode-cursor-restored-when-prior-was-hbar () - "Boundary: entering copy-mode overrides any prior cursor-type with the block." - (with-temp-buffer - (setq-local cursor-type 'hbar) - (let ((vterm-copy-mode t)) - (cj/--vterm-copy-mode-restore-cursor)) - (should (equal cursor-type 'box)))) - -(ert-deftest test-vterm-copy-mode-cursor-override-killed-on-exit () - "Normal: exiting copy-mode kills the buffer-local cursor-type override." - (with-temp-buffer - (setq-local cursor-type 'box) - (should (local-variable-p 'cursor-type)) - (let ((vterm-copy-mode nil)) - (cj/--vterm-copy-mode-restore-cursor)) - (should-not (local-variable-p 'cursor-type)))) - -(ert-deftest test-vterm-copy-mode-cursor-hook-installed () - "Normal: the cursor-restoration hook is registered on vterm-copy-mode-hook." - (should (memq #'cj/--vterm-copy-mode-restore-cursor - vterm-copy-mode-hook))) - -(ert-deftest test-vterm-copy-mode-cursor-end-to-end-via-mode-toggle () - "Normal: toggling `vterm-copy-mode' on then off via the real minor mode -command produces the visible cursor on entry and removes the override on -exit. This exercises the full path -- mode body, hook registration, our -restore function -- not just the helper in isolation." - (test-vterm-copy-mode-cursor--in-fake-vterm-buffer - (setq-local cursor-type nil) - ;; Enter copy-mode through the actual minor-mode command, not by - ;; let-binding the variable. This fires `vterm-copy-mode-hook'. - (vterm-copy-mode 1) - (should (eq vterm-copy-mode t)) - (should (equal cursor-type 'box)) - ;; Exit through the same path. - (vterm-copy-mode -1) - (should (eq vterm-copy-mode nil)) - (should-not (local-variable-p 'cursor-type)))) - -(ert-deftest test-vterm-copy-mode-cursor-end-to-end-via-copy-done () - "Normal: `vterm-copy-mode-done' toggles copy-mode off and triggers cursor -restoration. No key is bound to it in this config (M-w copies and stays; -RET is unbound), but it stays reachable via \\[execute-extended-command] -and its exit path must still restore the cursor." - (test-vterm-copy-mode-cursor--in-fake-vterm-buffer - (setq-local cursor-type nil) - (vterm-copy-mode 1) - (should (eq vterm-copy-mode t)) - (should (equal cursor-type 'box)) - (insert "selectable text on this line") - (set-mark (point-min)) - (goto-char (point-max)) - (vterm-copy-mode-done nil) - (should (eq vterm-copy-mode nil)) - (should-not (local-variable-p 'cursor-type)))) - -(ert-deftest test-vterm-copy-mode-cursor-end-to-end-via-cancel () - "Normal: `cj/vterm-copy-mode-cancel' (C-g / <escape> binding) toggles -copy-mode off and triggers cursor restoration even when no region was -selected -- the cancel path skips the kill-ring step entirely." - (test-vterm-copy-mode-cursor--in-fake-vterm-buffer - (setq-local cursor-type nil) - (vterm-copy-mode 1) - (should (equal cursor-type 'box)) - (cj/vterm-copy-mode-cancel) - (should (eq vterm-copy-mode nil)) - (should-not (local-variable-p 'cursor-type)))) - -(ert-deftest test-vterm-copy-mode-cursor-end-to-end-via-copy-done-no-region () - "Boundary: `vterm-copy-mode-done' called with no active region falls -into its line-selection branch. The branch calls vterm-internal -helpers that aren't safe in a fake buffer, so stub them to point-min / -point-max. The exit-and-fire-hook chain at the function's tail must -still run; cursor restoration must still happen." - (cl-letf (((symbol-function 'vterm--get-beginning-of-line) - (lambda (&rest _) (point-min))) - ((symbol-function 'vterm--get-end-of-line) - (lambda (&rest _) (point-max)))) - (test-vterm-copy-mode-cursor--in-fake-vterm-buffer - (insert "line content") - (setq-local cursor-type nil) - (vterm-copy-mode 1) - (should (equal cursor-type 'box)) - (deactivate-mark) - (should-not (use-region-p)) - (vterm-copy-mode-done nil) - (should (eq vterm-copy-mode nil)) - (should-not (local-variable-p 'cursor-type))))) - -(ert-deftest test-vterm-copy-mode-cursor-survives-multiple-cycles () - "Boundary: enter/exit/enter/exit cycles don't accumulate buffer-local -state. The cursor goes back and forth cleanly." - (test-vterm-copy-mode-cursor--in-fake-vterm-buffer - (setq-local cursor-type nil) - (dotimes (_ 3) - (vterm-copy-mode 1) - (should (equal cursor-type 'box)) - (vterm-copy-mode -1) - (should-not (local-variable-p 'cursor-type))))) - -(provide 'test-vterm-copy-mode-cursor) -;;; test-vterm-copy-mode-cursor.el ends here diff --git a/tests/test-vterm-tmux-history.el b/tests/test-vterm-tmux-history.el deleted file mode 100644 index 88bd5593..00000000 --- a/tests/test-vterm-tmux-history.el +++ /dev/null @@ -1,383 +0,0 @@ -;;; test-vterm-tmux-history.el --- Tests for tmux history capture UX -*- lexical-binding: t; -*- - -;;; Commentary: -;; Exercises the Emacs-owned history buffer used to copy text from the -;; current tmux pane without entering tmux copy-mode. - -;;; Code: - -(require 'ert) -(require 'cl-lib) - -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) -(setq load-prefer-newer t) -(defvar vterm-mode-map (make-sparse-keymap)) -(defvar vterm-copy-mode-map (make-sparse-keymap)) -(keymap-set vterm-mode-map "C-c C-t" #'ignore) -(require 'vterm-config) -(require 'testutil-vterm-buffers) - -(defmacro test-vterm-tmux-history--with-tmux-mock (responses &rest body) - "Run BODY with `process-file' mocked for tmux RESPONSES. - -RESPONSES is an alist of (ARGS EXIT-CODE OUTPUT)." - (declare (indent 1)) - `(let ((calls nil)) - (cl-letf (((symbol-function 'process-file) - (lambda (program _infile destination _display &rest args) - (push (cons program args) calls) - (let* ((entry (seq-find - (lambda (candidate) - (equal (car candidate) args)) - ,responses)) - (exit-code (or (cadr entry) 1)) - (output (or (caddr entry) ""))) - (when destination - (let ((buffer (cond - ((eq destination t) (current-buffer)) - ((bufferp destination) destination) - ((consp destination) (car destination))))) - (when (bufferp buffer) - (with-current-buffer buffer - (insert output))))) - exit-code)))) - ,@body))) - -(ert-deftest test-vterm-tmux-history--pane-id-for-tty-matches-client () - "Normal: current vterm pty maps to the active pane for that tmux client." - (test-vterm-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 - "/dev/pts/1\t%1\n/dev/pts/8\t%8\n")) - (should (equal (cj/vterm--tmux-pane-id-for-tty "/dev/pts/8") "%8")))) - -(ert-deftest test-vterm-tmux-history--capture-pane-uses-full-history () - "Normal: capture asks tmux for joined full pane history." - (test-vterm-tmux-history--with-tmux-mock - '((("capture-pane" "-p" "-J" "-S" "-" "-E" "-" "-t" "%8") 0 - "first line\nsecond line\n")) - (should (equal (cj/vterm--tmux-capture-pane "%8") - "first line\nsecond line\n")))) - -(ert-deftest test-vterm-tmux-history-open-renders-read-only-history-buffer () - "Normal: command renders tmux history in a normal Emacs buffer." - (let ((origin (cj/test--make-fake-vterm-buffer "*test-vterm-history-origin*"))) - (unwind-protect - (save-window-excursion - (switch-to-buffer origin) - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8"))) - (test-vterm-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 - "/dev/pts/8\t%8\n") - (("capture-pane" "-p" "-J" "-S" "-" "-E" "-" "-t" "%8") 0 - "history http://example.com\n")) - (cj/vterm-tmux-history) - (should (eq major-mode 'cj/vterm-tmux-history-mode)) - (should buffer-read-only) - (should (string-match-p "history http://example.com" - (buffer-string)))))) - (cj/test--kill-buffers-matching-prefix "*vterm tmux history") - (when (buffer-live-p origin) - (kill-buffer origin))))) - -(ert-deftest test-vterm-tmux-history-replaces-origin-buffer-in-same-window () - "Normal: the history view replaces the origin in the selected window. - -Before the in-place change, `cj/vterm-tmux-history' used `pop-to-buffer' -which could split or hand the buffer to a different window. The fix -uses `switch-to-buffer' so reading scrollback keeps the agent's frame -slot." - (let ((origin (cj/test--make-fake-vterm-buffer "*test-vterm-history-inplace*"))) - (unwind-protect - (save-window-excursion - (delete-other-windows) - (switch-to-buffer origin) - (let ((win (selected-window))) - (should (eq (window-buffer win) origin)) - (should (one-window-p)) - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8"))) - (test-vterm-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 - "/dev/pts/8\t%8\n") - (("capture-pane" "-p" "-J" "-S" "-" "-E" "-" "-t" "%8") 0 - "scrollback line\n")) - (cj/vterm-tmux-history))) - ;; Same window, no split, history buffer now in the slot. - (should (one-window-p)) - (should (eq (selected-window) win)) - (should (string-prefix-p - "*vterm tmux history:" - (buffer-name (window-buffer win)))))) - (cj/test--kill-buffers-matching-prefix "*vterm tmux history") - (when (buffer-live-p origin) - (kill-buffer origin))))) - -(ert-deftest test-vterm-tmux-history-quit-returns-to-origin () - "Normal: q / <escape> / C-g (cj/vterm-tmux-history-quit) kills the history -buffer and restores the origin buffer, window, and point." - (let ((origin (get-buffer-create "*test-vterm-history-return*"))) - (unwind-protect - (let ((history (get-buffer-create "*vterm tmux history: test*"))) - (with-current-buffer origin - (erase-buffer) - (insert "origin") - (goto-char (point-min))) - (switch-to-buffer origin) - (let ((origin-window (selected-window))) - (with-current-buffer history - (cj/vterm-tmux-history-mode) - (let ((inhibit-read-only t)) - (insert "alpha\nbeta\ngamma\n")) - (setq-local cj/vterm-tmux-history--origin-buffer origin) - (setq-local cj/vterm-tmux-history--origin-window origin-window) - (setq-local cj/vterm-tmux-history--origin-point (point-min)) - (cj/vterm-tmux-history-quit)) - (should-not (buffer-live-p history)) - (should (eq (current-buffer) origin)) - (should (= (point) (point-min))))) - (when (buffer-live-p origin) - (kill-buffer origin))))) - -(ert-deftest test-vterm-tmux-history-mode-keymap () - "Normal: in the history buffer M-w copies without quitting; q, <escape>, -and C-g quit back to the vterm; RET is left unbound (no special exit)." - (should (eq (keymap-lookup cj/vterm-tmux-history-mode-map "M-w") - #'kill-ring-save)) - (should (eq (keymap-lookup cj/vterm-tmux-history-mode-map "q") - #'cj/vterm-tmux-history-quit)) - (should (eq (keymap-lookup cj/vterm-tmux-history-mode-map "<escape>") - #'cj/vterm-tmux-history-quit)) - (should (eq (keymap-lookup cj/vterm-tmux-history-mode-map "C-g") - #'cj/vterm-tmux-history-quit)) - (should-not (keymap-lookup cj/vterm-tmux-history-mode-map "RET"))) - -(ert-deftest test-vterm-keymap-includes-history-and-copy-bindings () - "Normal: personal vterm map owns the high-level vterm UX commands. -`C-; x c' resolves to `cj/vterm-copy-mode-dwim' so the binding can pick -the right copy-mode engine (tmux when attached, vterm otherwise)." - (should (member "C-;" vterm-keymap-exceptions)) - (should-not (eq (keymap-lookup cj/custom-keymap "X c") #'vterm-copy-mode)) - (should (eq (keymap-lookup cj/custom-keymap "x h") #'cj/vterm-tmux-history)) - (should (eq (keymap-lookup cj/custom-keymap "x c") #'cj/vterm-copy-mode-dwim)) - (should (equal (keymap-lookup vterm-mode-map "C-;") cj/custom-keymap)) - (should (eq (keymap-lookup vterm-mode-map "C-; x h") #'cj/vterm-tmux-history)) - (should (eq (keymap-lookup vterm-mode-map "C-; x c") #'cj/vterm-copy-mode-dwim)) - (should-not (keymap-lookup vterm-mode-map "C-c C-t"))) - -(ert-deftest test-vterm-keymap-prompt-navigation () - "Normal: n/p navigate prompts, capital N creates a new vterm buffer." - (should (eq (keymap-lookup cj/custom-keymap "x n") #'vterm-next-prompt)) - (should (eq (keymap-lookup cj/custom-keymap "x p") #'vterm-previous-prompt)) - (should (eq (keymap-lookup cj/custom-keymap "x N") #'vterm))) - -(ert-deftest test-vterm-pause-not-bound-to-copy-mode () - "Normal: <pause> is no longer wired as a vterm-copy-mode entry point. -The personal `C-; x c' binding is the canonical entry; <pause> is rare on -modern keyboards and was redundant." - (let ((binding (keymap-lookup vterm-mode-map "<pause>"))) - (should-not (eq binding #'vterm-copy-mode)))) - -(ert-deftest test-vterm-copy-mode-keys () - "Normal: copy mode mirrors the history buffer -- M-w copies without -leaving; C-g, <escape>, and q leave without copying; RET is unbound." - (should (eq (keymap-lookup vterm-copy-mode-map "M-w") - #'kill-ring-save)) - (should (eq (keymap-lookup vterm-copy-mode-map "C-g") - #'cj/vterm-copy-mode-cancel)) - (should (eq (keymap-lookup vterm-copy-mode-map "<escape>") - #'cj/vterm-copy-mode-cancel)) - (should (eq (keymap-lookup vterm-copy-mode-map "q") - #'cj/vterm-copy-mode-cancel)) - (should-not (keymap-lookup vterm-copy-mode-map "RET")) - (should-not (keymap-lookup vterm-copy-mode-map "<return>"))) - -(ert-deftest test-vterm-copy-mode-cancel-errors-outside-copy-mode () - "Error: `cj/vterm-copy-mode-cancel' refuses to run when not in copy mode." - (with-temp-buffer - (should-error (cj/vterm-copy-mode-cancel) :type 'user-error))) - -(ert-deftest test-vterm-current-tmux-pane-id-rejects-non-vterm-buffer () - "Error: pane-id lookup refuses a buffer that is not in `vterm-mode'." - (with-temp-buffer - (should-error (cj/vterm--current-tmux-pane-id) :type 'user-error))) - -(ert-deftest test-vterm-current-tmux-pane-id-accepts-ai-vterm-named-buffer () - "Normal: an AI-vterm-named buffer still resolves by process TTY. - -The copy path belongs to `vterm-mode', not to `*vterm*'-named buffers. -A buffer named like `agent [repo]' (ai-vterm.el's naming) is a -`vterm-mode' buffer and must inherit tmux history copy. The pane lookup -keys off the live process TTY, never the buffer name -- so the -AI-vterm name neither helps nor blocks resolution." - (let ((agent (cj/test--make-fake-vterm-buffer "agent [emacs.d]"))) - (unwind-protect - (with-current-buffer agent - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8"))) - (test-vterm-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 - "/dev/pts/1\t%1\n/dev/pts/8\t%8\n")) - (should (equal (cj/vterm--current-tmux-pane-id) "%8"))))) - (when (buffer-live-p agent) - (kill-buffer agent))))) - -(ert-deftest test-vterm-in-tmux-p-true-when-client-attached () - "Normal: predicate returns t when tmux reports a client for our tty." - (let ((agent (cj/test--make-fake-vterm-buffer "agent [emacs.d]"))) - (unwind-protect - (with-current-buffer agent - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8"))) - (test-vterm-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 - "/dev/pts/8\t%8\n")) - (should (cj/vterm--in-tmux-p))))) - (when (buffer-live-p agent) - (kill-buffer agent))))) - -(ert-deftest test-vterm-in-tmux-p-nil-when-no-matching-client () - "Boundary: predicate returns nil when tmux runs but our tty has no client." - (let ((agent (cj/test--make-fake-vterm-buffer "agent [emacs.d]"))) - (unwind-protect - (with-current-buffer agent - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8"))) - (test-vterm-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 - "/dev/pts/1\t%1\n")) - (should-not (cj/vterm--in-tmux-p))))) - (when (buffer-live-p agent) - (kill-buffer agent))))) - -(ert-deftest test-vterm-in-tmux-p-nil-when-tmux-fails () - "Error: predicate swallows tmux failures and returns nil." - (let ((agent (cj/test--make-fake-vterm-buffer "agent [emacs.d]"))) - (unwind-protect - (with-current-buffer agent - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8"))) - (test-vterm-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 1 - "no server running")) - (should-not (cj/vterm--in-tmux-p))))) - (when (buffer-live-p agent) - (kill-buffer agent))))) - -(ert-deftest test-vterm-in-tmux-p-nil-when-not-vterm-mode () - "Boundary: predicate refuses non-vterm buffers without calling tmux." - (with-temp-buffer - (let ((tmux-called nil)) - (cl-letf (((symbol-function 'process-file) - (lambda (&rest _) (setq tmux-called t) 0))) - (should-not (cj/vterm--in-tmux-p)) - (should-not tmux-called))))) - -(ert-deftest test-vterm-copy-mode-dwim-sends-tmux-prefix-when-attached () - "Normal: with tmux attached, dwim writes C-b [ into the pty. -The literal control-B + open-bracket bytes reach tmux which then enters -its own copy-mode against the full pane history." - (let ((agent (cj/test--make-fake-vterm-buffer "agent [emacs.d]")) - (sent nil) - (copy-mode-called nil)) - (unwind-protect - (with-current-buffer agent - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8")) - ((symbol-function 'vterm-send-string) - (lambda (s &optional _paste-p) (push s sent))) - ((symbol-function 'vterm-copy-mode) - (lambda (&optional _arg) (setq copy-mode-called t)))) - (test-vterm-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 - "/dev/pts/8\t%8\n")) - (cj/vterm-copy-mode-dwim) - (should (equal sent '("\C-b["))) - (should-not copy-mode-called)))) - (when (buffer-live-p agent) - (kill-buffer agent))))) - -(ert-deftest test-vterm-copy-mode-dwim-falls-back-without-tmux () - "Boundary: without tmux, dwim calls `vterm-copy-mode' and sends nothing." - (let ((agent (cj/test--make-fake-vterm-buffer "agent [emacs.d]")) - (sent nil) - (copy-mode-called nil)) - (unwind-protect - (with-current-buffer agent - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8")) - ((symbol-function 'vterm-send-string) - (lambda (s &optional _paste-p) (push s sent))) - ((symbol-function 'vterm-copy-mode) - (lambda (&optional _arg) (setq copy-mode-called t)))) - (test-vterm-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 1 - "no server running")) - (cj/vterm-copy-mode-dwim) - (should-not sent) - (should copy-mode-called)))) - (when (buffer-live-p agent) - (kill-buffer agent))))) - -(ert-deftest test-vterm-mouse-wheel-up-sends-sgr-button-64 () - "Normal: wheel-up emits the SGR mouse-wheel-up sequence (button 64)." - (let ((sent nil)) - (cl-letf (((symbol-function 'vterm-send-string) - (lambda (s &optional _paste-p) (push s sent)))) - (cj/vterm-mouse-wheel-up) - (should (equal sent '("\e[<64;1;1M")))))) - -(ert-deftest test-vterm-mouse-wheel-down-sends-sgr-button-65 () - "Normal: wheel-down emits the SGR mouse-wheel-down sequence (button 65)." - (let ((sent nil)) - (cl-letf (((symbol-function 'vterm-send-string) - (lambda (s &optional _paste-p) (push s sent)))) - (cj/vterm-mouse-wheel-down) - (should (equal sent '("\e[<65;1;1M")))))) - -(ert-deftest test-vterm-send-escape-writes-esc-byte () - "Normal: `cj/vterm-send-escape' forwards a literal ESC byte to the pty so -tmux copy-mode, vi-mode exits, etc., can see the key past Emacs's global -`<escape>' → `keyboard-escape-quit' binding." - (let ((sent nil)) - (cl-letf (((symbol-function 'vterm-send-string) - (lambda (s &optional _paste-p) (push s sent)))) - (cj/vterm-send-escape) - (should (equal sent '("\e")))))) - -(ert-deftest test-vterm-escape-binding-installed-on-vterm-mode-map () - "Normal: `<escape>' in `vterm-mode-map' routes through `cj/vterm-send-escape'." - (should (eq (keymap-lookup vterm-mode-map "<escape>") - #'cj/vterm-send-escape))) - -(ert-deftest test-vterm-wheel-bindings-installed-on-vterm-mode-map () - "Normal: wheel-up / wheel-down (and X11 mouse-4 / mouse-5) route to the -forwarding commands so tmux can see them via `set -g mouse on'." - (should (eq (keymap-lookup vterm-mode-map "<wheel-up>") - #'cj/vterm-mouse-wheel-up)) - (should (eq (keymap-lookup vterm-mode-map "<wheel-down>") - #'cj/vterm-mouse-wheel-down)) - (should (eq (keymap-lookup vterm-mode-map "<mouse-4>") - #'cj/vterm-mouse-wheel-up)) - (should (eq (keymap-lookup vterm-mode-map "<mouse-5>") - #'cj/vterm-mouse-wheel-down))) - -(provide 'test-vterm-tmux-history) -;;; test-vterm-tmux-history.el ends here diff --git a/tests/test-vterm-toggle--buffer-filter.el b/tests/test-vterm-toggle--buffer-filter.el deleted file mode 100644 index d6fd2c8c..00000000 --- a/tests/test-vterm-toggle--buffer-filter.el +++ /dev/null @@ -1,94 +0,0 @@ -;;; test-vterm-toggle--buffer-filter.el --- Tests for F12's buffer filter -*- lexical-binding: t; -*- - -;;; Commentary: -;; Three closely-related helpers determine which vterm buffers F12 -;; manages: the predicate `cj/--vterm-toggle-buffer-p', the MRU list -;; `cj/--vterm-toggle-buffers', and the per-frame window finder -;; `cj/--vterm-toggle-displayed-window'. All three exclude agent- -;; prefixed buffers so agent has its own F9 surface. - -;;; Code: - -(require 'ert) - -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) -(require 'vterm-config) -(require 'testutil-vterm-buffers) - -(defun test-vterm-toggle--cleanup () - "Kill leftover agent- and *test-vterm- prefixed buffers." - (cj/test--kill-agent-buffers) - (cj/test--kill-test-vterm-buffers)) - -(ert-deftest test-vterm-toggle--buffer-p-accepts-vterm-mode () - "Normal: a vterm-mode buffer with non-agent name qualifies." - (test-vterm-toggle--cleanup) - (let ((buf (cj/test--make-fake-vterm-buffer "*test-vterm-1*"))) - (unwind-protect - (should (cj/--vterm-toggle-buffer-p buf)) - (kill-buffer buf)))) - -(ert-deftest test-vterm-toggle--buffer-p-rejects-agent () - "Boundary: agent-prefixed vterm buffers are excluded from F12's set." - (test-vterm-toggle--cleanup) - (let ((buf (cj/test--make-fake-vterm-buffer "agent [project-a]"))) - (unwind-protect - (should-not (cj/--vterm-toggle-buffer-p buf)) - (kill-buffer buf)))) - -(ert-deftest test-vterm-toggle--buffer-p-rejects-non-vterm () - "Boundary: a regular buffer (not vterm-mode, no vterm name prefix) -> nil." - (test-vterm-toggle--cleanup) - (let ((buf (get-buffer-create "*test-vterm-regular*"))) - (unwind-protect - (should-not (cj/--vterm-toggle-buffer-p buf)) - (kill-buffer buf)))) - -(ert-deftest test-vterm-toggle--buffer-p-rejects-dead-buffer () - "Boundary: nil and dead buffers -> nil." - (should-not (cj/--vterm-toggle-buffer-p nil)) - (let ((buf (cj/test--make-fake-vterm-buffer "*test-vterm-dead*"))) - (kill-buffer buf) - (should-not (cj/--vterm-toggle-buffer-p buf)))) - -(ert-deftest test-vterm-toggle--buffers-filters-agent () - "Normal: returns vterm buffers but excludes agent-prefixed ones." - (test-vterm-toggle--cleanup) - (let ((normal (cj/test--make-fake-vterm-buffer "*test-vterm-normal*")) - (agent (cj/test--make-fake-vterm-buffer "agent [for-test]"))) - (unwind-protect - (let ((result (cj/--vterm-toggle-buffers))) - (should (memq normal result)) - (should-not (memq agent result))) - (kill-buffer normal) - (kill-buffer agent)))) - -(ert-deftest test-vterm-toggle--displayed-window-finds-vterm () - "Normal: vterm in a window -> returns that window." - (test-vterm-toggle--cleanup) - (let ((vt (cj/test--make-fake-vterm-buffer "*test-vterm-shown*"))) - (unwind-protect - (save-window-excursion - (delete-other-windows) - (let ((win (split-window-right))) - (set-window-buffer win vt) - (let ((result (cj/--vterm-toggle-displayed-window))) - (should (windowp result)) - (should (eq (window-buffer result) vt))))) - (kill-buffer vt)))) - -(ert-deftest test-vterm-toggle--displayed-window-skips-agent () - "Boundary: only an agent vterm is displayed -> nil (agent not F12-managed)." - (test-vterm-toggle--cleanup) - (let ((agent (cj/test--make-fake-vterm-buffer "agent [skip-test]"))) - (unwind-protect - (save-window-excursion - (delete-other-windows) - (let ((win (split-window-right))) - (set-window-buffer win agent) - (should-not (cj/--vterm-toggle-displayed-window)))) - (kill-buffer agent)))) - -(provide 'test-vterm-toggle--buffer-filter) -;;; test-vterm-toggle--buffer-filter.el ends here diff --git a/tests/test-vterm-toggle--dispatch.el b/tests/test-vterm-toggle--dispatch.el deleted file mode 100644 index 7e87f2b1..00000000 --- a/tests/test-vterm-toggle--dispatch.el +++ /dev/null @@ -1,53 +0,0 @@ -;;; test-vterm-toggle--dispatch.el --- Tests for cj/--vterm-toggle-dispatch -*- lexical-binding: t; -*- - -;;; Commentary: -;; Pure decision helper for F12. Returns one of (toggle-off . WIN), -;; (show-recent . BUFFER), or (create-new) based on whether a vterm -;; window is currently displayed and whether any vterm buffers are -;; alive. Mocking the underlying helpers keeps the dispatch logic -;; exercisable without touching real windows. - -;;; Code: - -(require 'ert) -(require 'cl-lib) - -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) -(require 'vterm-config) -(require 'testutil-vterm-buffers) - -(ert-deftest test-vterm-toggle--dispatch-window-displayed-returns-toggle-off () - "Normal: displayed vterm window -> (toggle-off . WIN)." - (let ((sentinel-win 'fake-window)) - (cl-letf (((symbol-function 'cj/--vterm-toggle-displayed-window) - (lambda (&optional _frame) sentinel-win))) - (should (equal (cj/--vterm-toggle-dispatch) - (cons 'toggle-off sentinel-win)))))) - -(ert-deftest test-vterm-toggle--dispatch-no-window-buffer-alive-returns-show-recent () - "Normal: no displayed vterm, at least one alive -> show-recent + first." - (cj/test--kill-test-vterm-buffers) - (let ((b1 (get-buffer-create "*test-vterm-mru-1*")) - (b2 (get-buffer-create "*test-vterm-mru-2*"))) - (unwind-protect - (cl-letf (((symbol-function 'cj/--vterm-toggle-displayed-window) - (lambda (&optional _frame) nil)) - ((symbol-function 'cj/--vterm-toggle-buffers) - (lambda () (list b1 b2)))) - (should (equal (cj/--vterm-toggle-dispatch) - (cons 'show-recent b1)))) - (kill-buffer b1) - (kill-buffer b2)))) - -(ert-deftest test-vterm-toggle--dispatch-no-window-no-buffer-returns-create-new () - "Boundary: nothing displayed, no alive vterms -> create-new." - (cj/test--kill-test-vterm-buffers) - (cl-letf (((symbol-function 'cj/--vterm-toggle-displayed-window) - (lambda (&optional _frame) nil)) - ((symbol-function 'cj/--vterm-toggle-buffers) - (lambda () nil))) - (should (equal (cj/--vterm-toggle-dispatch) '(create-new))))) - -(provide 'test-vterm-toggle--dispatch) -;;; test-vterm-toggle--dispatch.el ends here diff --git a/tests/testutil-ghostel-buffers.el b/tests/testutil-ghostel-buffers.el new file mode 100644 index 00000000..52fb27e0 --- /dev/null +++ b/tests/testutil-ghostel-buffers.el @@ -0,0 +1,49 @@ +;;; testutil-ghostel-buffers.el --- Shared helpers for ghostel/agent buffer tests -*- lexical-binding: t; -*- + +;;; Commentary: +;; Cleanup helpers and a fake-ghostel constructor used across the +;; ai-term and term-toggle test files. Replaces the older +;; testutil-vterm-buffers helpers when the terminal engine moved from +;; vterm to ghostel. + +;;; Code: + +(require 'cl-lib) + +(defun cj/test--call-as-gui (fn) + "Call FN, stubbing `env-terminal-p' to return nil (a GUI frame). + +The terminal refuse-guard was dropped when ghostel replaced vterm (ghostel +renders in TTY frames too), so this no longer gates behavior; it is kept as a +thin passthrough so window-behavior tests written against the old guard keep +working unchanged." + (cl-letf (((symbol-function 'env-terminal-p) (lambda () nil))) + (funcall fn))) + +(defun cj/test--kill-buffers-matching-prefix (prefix) + "Kill all live buffers whose name starts with PREFIX." + (dolist (b (buffer-list)) + (when (string-prefix-p prefix (buffer-name b)) + (kill-buffer b)))) + +(defun cj/test--kill-agent-buffers () + "Kill all live buffers whose name matches the AI-term prefix \"agent [\"." + (cj/test--kill-buffers-matching-prefix "agent [")) + +(defun cj/test--kill-test-term-buffers () + "Kill all live buffers whose name starts with \"*test-term\"." + (cj/test--kill-buffers-matching-prefix "*test-term")) + +(defun cj/test--make-fake-ghostel-buffer (name) + "Return a buffer named NAME with `major-mode' set to `ghostel-mode'. + +Avoids actually launching a ghostel process by setting the mode +buffer-locally. Used by tests that need a buffer satisfying the +ghostel-mode predicate without the side-effects of `(ghostel)'." + (let ((buf (get-buffer-create name))) + (with-current-buffer buf + (setq-local major-mode 'ghostel-mode)) + buf)) + +(provide 'testutil-ghostel-buffers) +;;; testutil-ghostel-buffers.el ends here diff --git a/tests/testutil-vterm-buffers.el b/tests/testutil-vterm-buffers.el deleted file mode 100644 index 17f0a69a..00000000 --- a/tests/testutil-vterm-buffers.el +++ /dev/null @@ -1,51 +0,0 @@ -;;; testutil-vterm-buffers.el --- Shared helpers for vterm/agent buffer tests -*- lexical-binding: t; -*- - -;;; Commentary: -;; Cleanup helpers and a fake-vterm constructor used across the -;; ai-vterm and vterm-toggle test files. Before this module, each -;; test file re-implemented the same `(dolist (b (buffer-list)) -;; (when (string-prefix-p ...) (kill-buffer b)))' loop with a -;; different prefix. - -;;; Code: - -(require 'cl-lib) - -(defun cj/test--call-as-gui (fn) - "Call FN with `env-terminal-p' stubbed to return nil (a GUI frame). - -The AI-vterm interactive commands refuse to run in a terminal frame -via `cj/--ai-vterm-refuse-in-terminal'. A batch test run is itself a -terminal frame, so tests that exercise the GUI-frame window behavior -of those commands call them through this helper to present a GUI -context." - (cl-letf (((symbol-function 'env-terminal-p) (lambda () nil))) - (funcall fn))) - -(defun cj/test--kill-buffers-matching-prefix (prefix) - "Kill all live buffers whose name starts with PREFIX." - (dolist (b (buffer-list)) - (when (string-prefix-p prefix (buffer-name b)) - (kill-buffer b)))) - -(defun cj/test--kill-agent-buffers () - "Kill all live buffers whose name matches the AI-vterm prefix \"agent [\"." - (cj/test--kill-buffers-matching-prefix "agent [")) - -(defun cj/test--kill-test-vterm-buffers () - "Kill all live buffers whose name starts with \"*test-vterm\"." - (cj/test--kill-buffers-matching-prefix "*test-vterm")) - -(defun cj/test--make-fake-vterm-buffer (name) - "Return a buffer named NAME with `major-mode' set to `vterm-mode'. - -Avoids actually launching a vterm process by setting the mode -buffer-locally. Used by tests that need a buffer satisfying the -vterm-mode predicate without the side-effects of `(vterm)'." - (let ((buf (get-buffer-create name))) - (with-current-buffer buf - (setq-local major-mode 'vterm-mode)) - buf)) - -(provide 'testutil-vterm-buffers) -;;; testutil-vterm-buffers.el ends here |
