aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-05 05:28:58 -0500
committerCraig Jennings <c@cjennings.net>2026-06-05 05:28:58 -0500
commitebdf9e466b0e1f86e9b7d76650ac32408273e7a7 (patch)
treedab9b453f3a93c324b5388b3843502a088c7ed46 /tests
parentc094b2e4e64530379a9cb273303308a9affcabf6 (diff)
downloaddotemacs-ebdf9e466b0e1f86e9b7d76650ac32408273e7a7.tar.gz
dotemacs-ebdf9e466b0e1f86e9b7d76650ac32408273e7a7.zip
feat(term): replace vterm with ghostel as the terminal engine
I swapped the terminal engine from vterm to ghostel (libghostty-vt) everywhere. term-config replaces vterm-config (the F12 terminal, the C-; x menu, tmux history capture), and ai-term replaces ai-vterm (the F9 Claude-agent launcher). ghostel renders the agent TUI without vterm's flicker under heavy streaming, and one engine now covers every terminal workflow. Two behavior changes fall out of the swap. F9 launches in a terminal frame now: ghostel renders in TTY frames, so the old GUI-only guard is gone. Terminal windows no longer dim when unfocused: ghostel resolves its palette into the native module per-terminal, so there's no per-window color hook to dim through the way vterm had. auto-dim drops its vterm color-advice path, the dashboard Terminal button launches ghostel, and the vterm and vterm-toggle packages are removed. The tmux pane-history and copy-mode machinery carried over unchanged. It keys on the pty tty, which ghostel exposes.
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