aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-02 13:04:37 -0500
committerCraig Jennings <c@cjennings.net>2026-06-02 13:04:37 -0500
commit38dad925528e05f2084474ead5519a6a6ceb70f7 (patch)
tree84f4928e3e9b3fdb88be8d73a4db46728beacc41 /tests
parent76f983eff93e6d37d0710d2854361a8314f63d79 (diff)
downloaddotemacs-38dad925528e05f2084474ead5519a6a6ceb70f7.tar.gz
dotemacs-38dad925528e05f2084474ead5519a6a6ceb70f7.zip
fix(ai-vterm): make F9 a faithful toggle of the agent split
F9 toggle-off used quit-restore-window to dismiss the agent. With several agents alive sharing one slot, switching among them (C-F9) reuses the window via set-window-buffer, which leaves the window's quit-restore parameter pointing at the first agent shown. Once stale, quit-restore-window falls back to switch-to-prev-buffer and surfaces a different agent instead of removing the window, so F9 appeared to "show another agent" rather than hide the split. Toggle-off now collapses the split with delete-window, which is independent of the slot's buffer history, so the working buffer reclaims the frame. Geometry is captured first so the next toggle-on re-splits at the same width. Toggle-on reopens the exact agent that was hidden (new cj/--ai-vterm-last-hidden-buffer), falling back to the most-recent agent only when that buffer has been killed. Hide-then-show is now a faithful round trip, not a jump to whichever agent is most-recent in buffer-list. Sole-window toggle-off returns to the most-recent non-agent buffer instead of other-buffer, which could land on another agent. I updated the two reuse-edge-window tests that asserted the old restore-displaced-into-a-kept-slot behavior to match the new always-collapse behavior.
Diffstat (limited to 'tests')
-rw-r--r--tests/test-ai-vterm--collapse-split.el171
-rw-r--r--tests/test-ai-vterm--reuse-edge-window.el45
2 files changed, 194 insertions, 22 deletions
diff --git a/tests/test-ai-vterm--collapse-split.el b/tests/test-ai-vterm--collapse-split.el
new file mode 100644
index 00000000..ad299e47
--- /dev/null
+++ b/tests/test-ai-vterm--collapse-split.el
@@ -0,0 +1,171 @@
+;;; test-ai-vterm--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-vterm-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-vterm)
+(require 'testutil-vterm-buffers)
+
+;;; cj/--ai-vterm-most-recent-non-agent-buffer
+
+(ert-deftest test-ai-vterm--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-vterm-most-recent-non-agent-buffer)))
+ (should (bufferp result))
+ (should (buffer-live-p result))
+ (should-not (cj/--ai-vterm-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 ()
+ "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-vterm-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-vterm)
+ (should (one-window-p))
+ (should-not (cj/--ai-vterm-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 ()
+ "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-vterm-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-vterm-display-rule-list)))
+ (cj/test--call-as-gui #'cj/ai-vterm))
+ (should (one-window-p))
+ (should-not (cj/--ai-vterm-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 ()
+ "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-vterm-last-hidden-buffer nil))
+ (unwind-protect
+ (cl-letf (((symbol-function 'cj/--ai-vterm-displayed-agent-window)
+ (lambda (&optional _f) nil))
+ ((symbol-function 'cj/--ai-vterm-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)))
+ ;; 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)))
+ ;; 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)
+ (kill-buffer dead))
+ (should (equal (cj/--ai-vterm-dispatch) (cons 'redisplay-recent a1))))
+ (cj/test--kill-agent-buffers))))
+
+(ert-deftest test-ai-vterm--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-vterm-last-was-bury nil)
+ (cj/--ai-vterm-last-direction nil)
+ (cj/--ai-vterm-last-size nil)
+ (cj/--ai-vterm-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-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))
+ 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
diff --git a/tests/test-ai-vterm--reuse-edge-window.el b/tests/test-ai-vterm--reuse-edge-window.el
index 9f621477..eb1b1d75 100644
--- a/tests/test-ai-vterm--reuse-edge-window.el
+++ b/tests/test-ai-vterm--reuse-edge-window.el
@@ -150,10 +150,12 @@ 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-restores-displaced ()
- "Normal: toggle-off after a slot reuse restores the displaced buffer.
-=| 1 | 2 |= + show agent -> =| 1 | A |=; toggle off -> =| 1 | 2 |= again,
-window count stays 2 (the native `quit-restore-window' puts 2 back)."
+(ert-deftest test-ai-vterm--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
+regardless of how the agent window came to be -- it deletes the agent
+window rather than restoring the displaced buffer into a kept slot."
(cj/test--kill-agent-buffers)
(let ((agent-name "agent [edge-restore]")
(left-name "*test-restore-left*")
@@ -174,22 +176,23 @@ window count stays 2 (the native `quit-restore-window' puts 2 back)."
(display-buffer agent-buf)
(should (= (count-windows) 2))
(should (member agent-name (cj/test--displayed-buffer-names)))
- ;; Toggle off -> the displaced buffer (2) returns to the slot.
+ ;; Toggle off -> the agent window is deleted, leaving the
+ ;; working buffer at full frame.
(cj/test--call-as-gui #'cj/ai-vterm)
- (should (= (count-windows) 2))
+ (should (= (count-windows) 1))
(let ((bufs (cj/test--displayed-buffer-names)))
- (should (member right-name bufs))
(should (member left-name bufs))
(should-not (member agent-name bufs)))))))
(when (get-buffer left-name) (kill-buffer left-name))
(when (get-buffer right-name) (kill-buffer right-name))
(cj/test--kill-agent-buffers))))
-(ert-deftest test-ai-vterm--reuse-edge-window-cycle-keeps-count-and-swaps ()
- "Normal: on/off/on cycle keeps the window count at 2 and swaps the slot.
-=| 1 | 2 |= -> on =| 1 | A |= -> off =| 1 | 2 |= -> on =| 1 | A |=, never
-creating or deleting a window, and the agent returns to the same slot at
-the same width."
+(ert-deftest test-ai-vterm--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
+the width captured at toggle-off -- the user's chosen split width is
+preserved across the toggle (respect-split-width)."
(cj/test--kill-agent-buffers)
(let ((agent-name "agent [edge-cycle]")
(left-name "*test-cycle-left*")
@@ -206,26 +209,24 @@ the same width."
slot-width)
(set-window-buffer (selected-window) left-buf)
(let ((rw (split-window (selected-window) nil 'right)))
- (set-window-buffer rw right-buf)
- (setq slot-width (window-body-width rw)))
+ (set-window-buffer rw right-buf))
(let ((display-buffer-alist (cj/--ai-vterm-display-rule-list)))
- ;; on
+ ;; on -- agent takes the existing right slot
(display-buffer agent-buf)
(should (= (count-windows) 2))
- ;; off
+ (setq slot-width
+ (window-body-width (cj/--ai-vterm-displayed-agent-window)))
+ ;; off -- the split collapses to a single window
(cj/test--call-as-gui #'cj/ai-vterm)
- (should (= (count-windows) 2))
+ (should (= (count-windows) 1))
(should-not (cj/--ai-vterm-displayed-agent-window))
- ;; on again
+ ;; on again -- re-split at the captured width
(cj/test--call-as-gui #'cj/ai-vterm)
(should (= (count-windows) 2))
(let ((win (cj/--ai-vterm-displayed-agent-window)))
(should (windowp win))
(should (eq (window-buffer win) agent-buf))
- ;; reused the same slot -> same body width as the
- ;; original right column
- (should (= (window-body-width win) slot-width)))
- (should-not (member right-name (cj/test--displayed-buffer-names)))))))
+ (should (= (window-body-width win) slot-width)))))))
(when (get-buffer left-name) (kill-buffer left-name))
(when (get-buffer right-name) (kill-buffer right-name))
(cj/test--kill-agent-buffers))))