aboutsummaryrefslogtreecommitdiff
path: root/tests/test-ai-term--collapse-split.el
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/test-ai-term--collapse-split.el
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/test-ai-term--collapse-split.el')
-rw-r--r--tests/test-ai-term--collapse-split.el171
1 files changed, 171 insertions, 0 deletions
diff --git a/tests/test-ai-term--collapse-split.el b/tests/test-ai-term--collapse-split.el
new file mode 100644
index 00000000..d7b4ee17
--- /dev/null
+++ b/tests/test-ai-term--collapse-split.el
@@ -0,0 +1,171 @@
+;;; 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
+;; several agents alive, F9 should HIDE the agent split (collapse it back to the
+;; working layout) rather than surfacing a different agent. Two cases:
+;;
+;; - Multi-window: the agent occupies a split. F9 deletes that window so the
+;; working buffer reclaims the frame -- never swaps in another agent. The
+;; prior `quit-restore-window' path went stale after the slot was reused
+;; across agents (C-F9 switching), so it surfaced a different agent.
+;; - Single-window: the agent fills the frame. F9 returns to the most-recent
+;; 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-term-most-recent-non-agent-buffer' helper.
+
+;;; 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-term)
+(require 'testutil-ghostel-buffers)
+
+;;; cj/--ai-term-most-recent-non-agent-buffer
+
+(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*"))
+ (agent-a (get-buffer-create "agent [mrna-a]"))
+ (agent-b (get-buffer-create "agent [mrna-b]")))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ ;; Make the agents most-recent in this window's history.
+ (set-window-buffer (selected-window) work)
+ (set-window-buffer (selected-window) agent-b)
+ (set-window-buffer (selected-window) agent-a)
+ (let ((result (cj/--ai-term-most-recent-non-agent-buffer)))
+ (should (bufferp result))
+ (should (buffer-live-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-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
+different agent (stale quit-restore after slot reuse)."
+ (cj/test--kill-agent-buffers)
+ (let ((work (get-buffer-create "*test-collapse-work*"))
+ (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-term-last-was-bury nil))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ (set-window-buffer (selected-window) work)
+ (let ((agent-win (split-window (selected-window) nil 'below)))
+ ;; Reuse the slot across agents (as C-F9 switching does) so the
+ ;; window's prev-buffer history holds another agent.
+ (set-window-buffer agent-win agent-a)
+ (set-window-buffer agent-win agent-b)
+ (set-window-buffer agent-win agent-c)
+ (select-window agent-win)
+ (should-not (one-window-p))
+ (cj/test--call-as-gui #'cj/ai-term)
+ (should (one-window-p))
+ (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-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."
+ (cj/test--kill-agent-buffers)
+ (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-term-last-was-bury nil))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ ;; MRU: work, then agent-b, then agent-a (current). `other-buffer'
+ ;; would pick agent-b; the fix must skip it for a non-agent.
+ (set-window-buffer (selected-window) work)
+ (set-window-buffer (selected-window) agent-b)
+ (set-window-buffer (selected-window) agent-a)
+ (should (one-window-p))
+ (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-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-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
+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-term-last-hidden-buffer nil))
+ (unwind-protect
+ (cl-letf (((symbol-function 'cj/--ai-term-displayed-agent-window)
+ (lambda (&optional _f) nil))
+ ((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-term-dispatch) (cons 'redisplay-recent a1)))
+ ;; Remember a2 as last hidden -> dispatch prefers it.
+ (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-term-last-hidden-buffer dead)
+ (kill-buffer dead))
+ (should (equal (cj/--ai-term-dispatch) (cons 'redisplay-recent a1))))
+ (cj/test--kill-agent-buffers))))
+
+(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
+another. Reproduces the \"the displayed buffer changes\" report."
+ (cj/test--kill-agent-buffers)
+ (let ((work (get-buffer-create "*test-roundtrip-work*"))
+ (a1 (get-buffer-create "agent [rt-1]"))
+ (a2 (get-buffer-create "agent [rt-2]"))
+ (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)
+ (set-window-buffer (selected-window) work)
+ (let ((agent-win (split-window (selected-window) nil 'below)))
+ ;; a2 is the visible agent; a1 sits ahead of it in buffer-list.
+ (set-window-buffer agent-win a1)
+ (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-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-term--collapse-split)
+;;; test-ai-term--collapse-split.el ends here