aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-ai-term--agent-buffers.el (renamed from tests/test-ai-vterm--agent-buffers.el)26
-rw-r--r--tests/test-ai-term--buffer-name.el (renamed from tests/test-ai-vterm--buffer-name.el)28
-rw-r--r--tests/test-ai-term--candidates.el (renamed from tests/test-ai-vterm--candidates.el)106
-rw-r--r--tests/test-ai-term--capture-state.el63
-rw-r--r--tests/test-ai-term--close.el (renamed from tests/test-ai-vterm--close.el)48
-rw-r--r--tests/test-ai-term--collapse-split.el (renamed from tests/test-ai-vterm--collapse-split.el)78
-rw-r--r--tests/test-ai-term--default-geometry.el56
-rw-r--r--tests/test-ai-term--dispatch.el (renamed from tests/test-ai-vterm--dispatch.el)40
-rw-r--r--tests/test-ai-term--display-rule.el (renamed from tests/test-ai-vterm--display-rule.el)42
-rw-r--r--tests/test-ai-term--display-saved.el (renamed from tests/test-ai-vterm--display-saved.el)80
-rw-r--r--tests/test-ai-term--displayed-agent-window.el (renamed from tests/test-ai-vterm--displayed-agent-window.el)28
-rw-r--r--tests/test-ai-term--f9-in-term.el45
-rw-r--r--tests/test-ai-term--launch-command.el94
-rw-r--r--tests/test-ai-term--live-tmux-sessions.el (renamed from tests/test-ai-vterm--live-tmux-sessions.el)54
-rw-r--r--tests/test-ai-term--pick-project.el (renamed from tests/test-ai-vterm--pick-project.el)84
-rw-r--r--tests/test-ai-term--record-mru.el48
-rw-r--r--tests/test-ai-term--reuse-edge-window.el (renamed from tests/test-ai-vterm--reuse-edge-window.el)98
-rw-r--r--tests/test-ai-term--reuse-existing-agent.el (renamed from tests/test-ai-vterm--reuse-existing-agent.el)28
-rw-r--r--tests/test-ai-term--server-display.el (renamed from tests/test-ai-vterm--server-display.el)44
-rw-r--r--tests/test-ai-term--show-or-create.el155
-rw-r--r--tests/test-ai-term--single-window-toggle.el (renamed from tests/test-ai-vterm--single-window-toggle.el)88
-rw-r--r--tests/test-ai-term--sort-candidates.el (renamed from tests/test-ai-vterm--sort-candidates.el)58
-rw-r--r--tests/test-ai-term--tmux-session-name.el65
-rw-r--r--tests/test-ai-vterm--capture-state.el63
-rw-r--r--tests/test-ai-vterm--default-geometry.el56
-rw-r--r--tests/test-ai-vterm--f9-in-vterm.el47
-rw-r--r--tests/test-ai-vterm--launch-command.el94
-rw-r--r--tests/test-ai-vterm--record-mru.el48
-rw-r--r--tests/test-ai-vterm--show-or-create.el163
-rw-r--r--tests/test-ai-vterm--terminal-guard.el78
-rw-r--r--tests/test-ai-vterm--tmux-session-name.el65
-rw-r--r--tests/test-auto-dim-config.el106
-rw-r--r--tests/test-dashboard-config-launchers.el2
-rw-r--r--tests/test-init-module-headers.el5
-rw-r--r--tests/test-term-tmux-history.el312
-rw-r--r--tests/test-term-toggle--buffer-filter.el94
-rw-r--r--tests/test-term-toggle--dispatch.el53
-rw-r--r--tests/test-term-toggle--display.el (renamed from tests/test-vterm-toggle--display.el)64
-rw-r--r--tests/test-ui-config--buffer-cursor-state.el50
-rw-r--r--tests/test-vterm-copy-mode-cursor.el145
-rw-r--r--tests/test-vterm-tmux-history.el383
-rw-r--r--tests/test-vterm-toggle--buffer-filter.el94
-rw-r--r--tests/test-vterm-toggle--dispatch.el53
-rw-r--r--tests/testutil-ghostel-buffers.el49
-rw-r--r--tests/testutil-vterm-buffers.el51
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