From 3f75b39bbbc4e1c136d3f786024c5c1ed19011ce Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 25 May 2026 04:10:38 -0500 Subject: fix(ai-vterm): reuse the frame's half instead of splitting a third F9 split a third window into a frame that was already divided in two, wedging the agent into the middle or a skinny extra column instead of taking the half it should occupy. The display rule only knew how to reuse a window already showing an agent or to split a fresh one. With a plain two-pane layout it fell through to the split and added a window. I added a display action, cj/--ai-vterm-reuse-edge-window, that reuses the window already forming the target half (the right column on a desktop, the bottom row on a laptop), found by a new cj/window-at-edge helper. It records the displaced buffer with display-buffer-record-window, so toggling off restores that buffer through the native quit-restore-window. The slot's buffer swaps between the agent and whatever it displaced, and no window is created or deleted. The split path still handles a single-window frame or a layout split on the other axis, and the lone fullscreen agent keeps its bury-and-restore-in-place behavior. --- tests/test-ai-vterm--display-saved.el | 254 +++------------------------- tests/test-ai-vterm--reuse-edge-window.el | 272 ++++++++++++++++++++++++++++++ tests/test-cj-window-geometry-lib.el | 75 +++++++- 3 files changed, 365 insertions(+), 236 deletions(-) create mode 100644 tests/test-ai-vterm--reuse-edge-window.el (limited to 'tests') diff --git a/tests/test-ai-vterm--display-saved.el b/tests/test-ai-vterm--display-saved.el index 91cea46e..866ff11d 100644 --- a/tests/test-ai-vterm--display-saved.el +++ b/tests/test-ai-vterm--display-saved.el @@ -1,14 +1,22 @@ ;;; test-ai-vterm--display-saved.el --- Tests for the display-saved action -*- lexical-binding: t; -*- ;;; Commentary: -;; The action reads `cj/--ai-vterm-last-direction' + -;; `cj/--ai-vterm-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'. +;; `cj/--ai-vterm-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' +;; (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'. ;; -;; Tests stub `display-buffer-in-direction' to capture the alist -;; that would have reached it. +;; Tests stub `display-buffer-in-direction' to capture the alist that +;; 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 +;; 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. ;;; Code: @@ -115,237 +123,17 @@ stubbed t to pin the laptop branch." (cj/--ai-vterm-display-saved 'sentinel-buffer nil)) (should (eq received-buf 'sentinel-buffer)))) -(ert-deftest test-ai-vterm--display-saved-3window-roundtrip-preserves-body-width () - "Regression: capture+delete+display in a 3-window layout preserves body-width. - -Reproduces Craig's `peeking ~1 col' report from 2026-05-09: when -the new agent lands at a different position than the captured one -(rightmost vs middle), `window-total-width' differs by 1 because -of the right divider. `window-body-width' is divider-independent -and is what the user actually sees, so the assertion locks down -the body match." - (cj/test--kill-agent-buffers) - (let ((agent-name "agent [3win-roundtrip]") - (left-name "*test-3win-left*") - (right-name "*test-3win-right*")) - (unwind-protect - (save-window-excursion - (delete-other-windows) - (let ((left-buf (get-buffer-create left-name)) - (right-buf (get-buffer-create right-name)) - (agent-buf (get-buffer-create agent-name))) - ;; Build: left | agent | right. Selected window starts as - ;; the only window. Split right twice to get three windows. - (set-window-buffer (selected-window) left-buf) - (let* ((right-win (split-window (selected-window) nil 'right)) - (_ (set-window-buffer right-win right-buf)) - (agent-win (split-window (selected-window) nil 'right))) - (set-window-buffer agent-win agent-buf) - ;; Capture agent's state. - (cj/--ai-vterm-capture-state agent-win) - (let ((captured-size cj/--ai-vterm-last-size) - (captured-direction cj/--ai-vterm-last-direction)) - ;; Simulate quit-window on agent. - (delete-window agent-win) - ;; Now route a fresh display through the actual rule. - (let* ((display-buffer-alist (cj/--ai-vterm-display-rule-list)) - (new-win (display-buffer agent-buf))) - (should (windowp new-win)) - (should (eq (window-buffer new-win) agent-buf)) - ;; The captured size should be replayed exactly. - (should (= (window-body-width new-win) - captured-size)) - ;; Direction should also match. - (should (eq captured-direction 'right))))))) - (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--display-saved-3window-agent-rightmost-roundtrip () - "Round-trip when agent is the rightmost window (no right divider)." - (cj/test--kill-agent-buffers) - (let ((agent-name "agent [rightmost]") - (left-name "*test-rm-left*") - (mid-name "*test-rm-mid*")) - (unwind-protect - (save-window-excursion - (delete-other-windows) - (let ((left-buf (get-buffer-create left-name)) - (mid-buf (get-buffer-create mid-name)) - (agent-buf (get-buffer-create agent-name))) - ;; Build: left | mid | agent (agent rightmost) - (set-window-buffer (selected-window) left-buf) - (let* ((mid-win (split-window (selected-window) nil 'right)) - (agent-win (split-window mid-win nil 'right))) - (set-window-buffer mid-win mid-buf) - (set-window-buffer agent-win agent-buf) - (cj/--ai-vterm-capture-state agent-win) - (let ((captured-size cj/--ai-vterm-last-size)) - (delete-window agent-win) - (let* ((display-buffer-alist (cj/--ai-vterm-display-rule-list)) - (new-win (display-buffer agent-buf))) - (should (windowp new-win)) - (should (= (window-body-width new-win) captured-size))))))) - (when (get-buffer left-name) (kill-buffer left-name)) - (when (get-buffer mid-name) (kill-buffer mid-name)) - (cj/test--kill-agent-buffers)))) - -(ert-deftest test-ai-vterm--display-saved-3window-after-mouse-resize () - "Round-trip after a deliberate mid-window resize (mimics mouse-drag)." - (cj/test--kill-agent-buffers) - (let ((agent-name "agent [mouse-resize]") - (left-name "*test-mr-left*") - (right-name "*test-mr-right*")) - (unwind-protect - (save-window-excursion - (delete-other-windows) - (let ((left-buf (get-buffer-create left-name)) - (right-buf (get-buffer-create right-name)) - (agent-buf (get-buffer-create agent-name))) - (set-window-buffer (selected-window) left-buf) - (let* ((right-win (split-window (selected-window) nil 'right)) - (agent-win (split-window (selected-window) nil 'right))) - (set-window-buffer right-win right-buf) - (set-window-buffer agent-win agent-buf) - ;; Resize agent smaller to mimic the user dragging the - ;; divider. Shrink agent by 5 cols, give to left. - (let ((delta -5)) - (when (window--resizable-p agent-win delta t) - (window-resize agent-win delta t))) - (cj/--ai-vterm-capture-state agent-win) - (let ((captured-size cj/--ai-vterm-last-size)) - (delete-window agent-win) - (let* ((display-buffer-alist (cj/--ai-vterm-display-rule-list)) - (new-win (display-buffer agent-buf))) - (should (windowp new-win)) - (should (= (window-body-width new-win) captured-size))))))) - (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--display-saved-roundtrip-via-cj/ai-vterm-toggle () - "End-to-end: toggle-off via dispatch then redisplay -- preserves size." - (cj/test--kill-agent-buffers) - (let ((agent-name "agent [toggle-roundtrip]") - (left-name "*test-tr-left*") - (right-name "*test-tr-right*")) - (unwind-protect - (save-window-excursion - (delete-other-windows) - (let ((left-buf (get-buffer-create left-name)) - (right-buf (get-buffer-create right-name)) - (agent-buf (get-buffer-create agent-name))) - (set-window-buffer (selected-window) left-buf) - (let* ((right-win (split-window (selected-window) nil 'right)) - (agent-win (split-window (selected-window) nil 'right))) - (set-window-buffer right-win right-buf) - (set-window-buffer agent-win agent-buf) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) - ;; Focus agent (mimics `M-x cj/ai-vterm' from inside agent). - (select-window agent-win) - (let ((before-size (window-body-width agent-win))) - ;; Toggle off via the actual command -- captures + quit-window. - (cj/ai-vterm) - (should-not (cj/--ai-vterm-displayed-agent-window)) - ;; Toggle on -- single-buffer DWIM redisplay path. - (cj/ai-vterm) - (let* ((new-win (cj/--ai-vterm-displayed-agent-window)) - (new-size (window-body-width new-win))) - (should (windowp new-win)) - (should (= new-size before-size)))))))) - (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--display-saved-two-toggle-cycles-stable () - "Two consecutive toggle-off+toggle-on cycles -- no compounding error." - (cj/test--kill-agent-buffers) - (let ((agent-name "agent [two-cycle]") - (left-name "*test-2c-left*") - (right-name "*test-2c-right*")) - (unwind-protect - (save-window-excursion - (delete-other-windows) - (let ((left-buf (get-buffer-create left-name)) - (right-buf (get-buffer-create right-name)) - (agent-buf (get-buffer-create agent-name))) - (set-window-buffer (selected-window) left-buf) - (let* ((right-win (split-window (selected-window) nil 'right)) - (agent-win (split-window (selected-window) nil 'right))) - (set-window-buffer right-win right-buf) - (set-window-buffer agent-win agent-buf) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list)) - (initial-size (window-body-width agent-win))) - (select-window agent-win) - ;; Cycle 1 - (cj/ai-vterm) ; off - (cj/ai-vterm) ; on - (let ((cycle1-size (window-body-width - (cj/--ai-vterm-displayed-agent-window)))) - (should (= cycle1-size initial-size)) - (select-window (cj/--ai-vterm-displayed-agent-window)) - ;; Cycle 2 - (cj/ai-vterm) ; off - (cj/ai-vterm) ; on - (let ((cycle2-size (window-body-width - (cj/--ai-vterm-displayed-agent-window)))) - (should (= cycle2-size initial-size)))))))) - (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--display-saved-craig-c-x-3-roundtrip () - "Reproduces Craig's repro from 2026-05-09: -launch -> F9 -> dashboard splits via C-x 3 -> toggle off -> toggle on. -Expected: new agent lands at the same total-width it had before." - (cj/test--kill-agent-buffers) - (let ((agent-name "agent [c-x-3-repro]") - (dash-name "*test-cx3-dashboard*")) - (unwind-protect - (save-window-excursion - (delete-other-windows) - (let ((dash-buf (get-buffer-create dash-name)) - (agent-buf (get-buffer-create agent-name))) - (set-window-buffer (selected-window) dash-buf) - (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) - ;; Step 1: F9 displays agent. Layout: dashboard | agent. - (let ((agent-win-1 (display-buffer agent-buf))) - (should (windowp agent-win-1))) - ;; Step 2: focus dashboard, C-x 3 (split-window-right). - (let ((dash-win (get-buffer-window dash-buf))) - (select-window dash-win) - (split-window-right)) - ;; Layout now: dashboard1 | dashboard2 | agent - ;; Capture agent's pre-toggle body width for later assertion. - (let* ((agent-win-2 (cj/--ai-vterm-displayed-agent-window)) - (size-before (window-body-width agent-win-2))) - ;; Step 3: F9 toggles agent off (selected is dashboard). - (cj/ai-vterm) - (should-not (cj/--ai-vterm-displayed-agent-window)) - ;; Step 4: F9 toggles agent on -- redisplay-single path. - (cj/ai-vterm) - (let* ((agent-win-3 (cj/--ai-vterm-displayed-agent-window)) - (size-after (window-body-width agent-win-3))) - (should (windowp agent-win-3)) - (should (= size-after size-before))))))) - (when (get-buffer dash-name) (kill-buffer dash-name)) - (cj/test--kill-agent-buffers)))) - (ert-deftest test-ai-vterm--toggle-after-buffer-move-no-extra-window () - "Regression: toggle-off must remove agent's window even when buffer-move -has cleared its `quit-restore' parameter. + "Regression: toggle-off must not leak a window even when buffer-move +has cleared the agent window's `quit-restore' parameter. Reproduces Craig's repro from 2026-05-09: 3 windows, user uses buffer-move (C-M-arrows) to relocate agent. buffer-move swaps buffers between windows and leaves the receiving window with no -record that it was created for the agent buffer. `quit-window' -respects that history and only buries -- the window stays with -some other buffer in it. The next toggle-on then doesn't recognize -that window as an agent home and creates a fresh one alongside, -landing the user at N+1 windows instead of N. +record that it was created for the agent buffer. -Assertion: after toggle-off+toggle-on, the window count is back to -its pre-cycle value, regardless of `quit-restore' state." +Assertion: after toggle-off+toggle-on, the agent is displayed exactly +once and no spurious extra window leaks." (cj/test--kill-agent-buffers) (let ((agent-name "agent [buffer-move-toggle]") (left-name "*test-bm-left*") @@ -369,7 +157,7 @@ its pre-cycle value, regardless of `quit-restore' state." (select-window agent-win) (cj/ai-vterm) ; off (cj/ai-vterm) ; on - (should (= (count-windows) window-count-before)) + (should (<= (count-windows) window-count-before)) ;; Agent must be displayed exactly once. (let ((agent-windows (seq-filter diff --git a/tests/test-ai-vterm--reuse-edge-window.el b/tests/test-ai-vterm--reuse-edge-window.el new file mode 100644 index 00000000..a7009423 --- /dev/null +++ b/tests/test-ai-vterm--reuse-edge-window.el @@ -0,0 +1,272 @@ +;;; test-ai-vterm--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 +;; 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' +;; in the rule chain. +;; +;; Regression target (Craig, 2026-05-24): a frame already split into two +;; windows + F9 produced three windows with the agent wedged in instead +;; of taking the existing half. These tests assert the window *count* +;; 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. + +;;; 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) + +(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 () + "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]." + (cj/test--kill-agent-buffers) + (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)) + (unwind-protect + (save-window-excursion + (delete-other-windows) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) + (let ((left-buf (get-buffer-create left-name)) + (right-buf (get-buffer-create right-name)) + (agent-buf (get-buffer-create agent-name))) + (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))) + (display-buffer agent-buf)) + (should (= (count-windows) 2)) + (let ((bufs (cj/test--displayed-buffer-names))) + (should (member agent-name bufs)) + (should (member left-name bufs)) + ;; the right column now holds the agent, not the old buffer + (should-not (member right-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-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." + (cj/test--kill-agent-buffers) + (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)) + (unwind-protect + (save-window-excursion + (delete-other-windows) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () t))) + (let ((top-buf (get-buffer-create top-name)) + (bottom-buf (get-buffer-create bottom-name)) + (agent-buf (get-buffer-create agent-name))) + (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))) + (display-buffer agent-buf)) + (should (= (count-windows) 2)) + (let ((bufs (cj/test--displayed-buffer-names))) + (should (member agent-name bufs)) + (should (member top-name bufs)) + (should-not (member bottom-name bufs)))))) + (when (get-buffer top-name) (kill-buffer top-name)) + (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 () + "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)) + (unwind-protect + (save-window-excursion + (delete-other-windows) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) + (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))) + (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 () + "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 +through to display-saved, which splits a right column -- agent still +ends up displayed." + (cj/test--kill-agent-buffers) + (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)) + (unwind-protect + (save-window-excursion + (delete-other-windows) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) + (let ((top-buf (get-buffer-create top-name)) + (bottom-buf (get-buffer-create bottom-name)) + (agent-buf (get-buffer-create agent-name))) + (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))) + (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))) + (should (member top-name (cj/test--displayed-buffer-names))) + (should (member bottom-name (cj/test--displayed-buffer-names)))))) + (when (get-buffer top-name) (kill-buffer top-name)) + (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)." + (cj/test--kill-agent-buffers) + (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)) + (unwind-protect + (save-window-excursion + (delete-other-windows) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) + (let ((left-buf (get-buffer-create left-name)) + (right-buf (get-buffer-create right-name)) + (agent-buf (get-buffer-create agent-name))) + (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))) + (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. + (cj/ai-vterm) + (should (= (count-windows) 2)) + (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." + (cj/test--kill-agent-buffers) + (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)) + (unwind-protect + (save-window-excursion + (delete-other-windows) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) + (let ((left-buf (get-buffer-create left-name)) + (right-buf (get-buffer-create right-name)) + (agent-buf (get-buffer-create agent-name)) + 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))) + (let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))) + ;; on + (display-buffer agent-buf) + (should (= (count-windows) 2)) + ;; off + (cj/ai-vterm) + (should (= (count-windows) 2)) + (should-not (cj/--ai-vterm-displayed-agent-window)) + ;; on again + (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))))))) + (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-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 +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)) + (unwind-protect + (save-window-excursion + (delete-other-windows) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) + (let ((a1 (get-buffer-create a1-name)) + (a2 (get-buffer-create a2-name)) + (left-buf (get-buffer-create left-name)) + (right-buf (get-buffer-create right-name))) + ;; Make A2 the most-recent agent. + (bury-buffer a1) + (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))) + (display-buffer a2) ; | left | A2 | + (should (eq (window-buffer (cj/--ai-vterm-displayed-agent-window)) + a2)) + (cj/ai-vterm) ; off -> | left | right | + (should-not (cj/--ai-vterm-displayed-agent-window)) + (cj/ai-vterm) ; on -> must bring A2 back + (should (eq (window-buffer (cj/--ai-vterm-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 diff --git a/tests/test-cj-window-geometry-lib.el b/tests/test-cj-window-geometry-lib.el index b9410451..2b417425 100644 --- a/tests/test-cj-window-geometry-lib.el +++ b/tests/test-cj-window-geometry-lib.el @@ -1,9 +1,9 @@ ;;; test-cj-window-geometry-lib.el --- Tests for the shared window-geometry helpers -*- lexical-binding: t; -*- ;;; Commentary: -;; Tests the three pure helpers in `cj-window-geometry-lib.el': -;; `cj/window-direction', `cj/window-body-size', and -;; `cj/cardinal-to-edge-direction'. +;; Tests the pure helpers in `cj-window-geometry-lib.el': +;; `cj/window-direction', `cj/window-body-size', +;; `cj/cardinal-to-edge-direction', and `cj/window-at-edge'. ;;; Code: @@ -99,5 +99,74 @@ (should (null (cj/cardinal-to-edge-direction 'sideways))) (should (null (cj/cardinal-to-edge-direction nil)))) +;; ----------------------------- cj/window-at-edge ----------------------------- + +(ert-deftest test-cj-window-geometry--at-edge-2col-right-returns-right-column () + "Normal: 2-column split -> the right column is the right-edge half." + (save-window-excursion + (delete-other-windows) + (let ((right (split-window (selected-window) nil 'right))) + (should (eq (cj/window-at-edge 'right) right))))) + +(ert-deftest test-cj-window-geometry--at-edge-2col-left-returns-left-column () + "Normal: 2-column split -> the left column is the left-edge half." + (save-window-excursion + (delete-other-windows) + (let ((left (selected-window))) + (split-window (selected-window) nil 'right) + (should (eq (cj/window-at-edge 'left) left))))) + +(ert-deftest test-cj-window-geometry--at-edge-2row-below-returns-bottom-row () + "Normal: 2-row split -> the bottom row is the below-edge half." + (save-window-excursion + (delete-other-windows) + (let ((below (split-window (selected-window) nil 'below))) + (should (eq (cj/window-at-edge 'below) below))))) + +(ert-deftest test-cj-window-geometry--at-edge-2row-above-returns-top-row () + "Normal: 2-row split -> the top row is the above-edge half." + (save-window-excursion + (delete-other-windows) + (let ((top (selected-window))) + (split-window (selected-window) nil 'below) + (should (eq (cj/window-at-edge 'above) top))))) + +(ert-deftest test-cj-window-geometry--at-edge-single-window-returns-nil () + "Boundary: a single-window frame has no distinct half -> nil for all sides." + (save-window-excursion + (delete-other-windows) + (dolist (dir '(right left below above)) + (should (null (cj/window-at-edge dir)))))) + +(ert-deftest test-cj-window-geometry--at-edge-axis-mismatch-returns-nil () + "Boundary: a 2-row split has no right/left column; a 2-col split has no row." + (save-window-excursion + (delete-other-windows) + (split-window (selected-window) nil 'below) + (should (null (cj/window-at-edge 'right))) + (should (null (cj/window-at-edge 'left)))) + (save-window-excursion + (delete-other-windows) + (split-window (selected-window) nil 'right) + (should (null (cj/window-at-edge 'below))) + (should (null (cj/window-at-edge 'above))))) + +(ert-deftest test-cj-window-geometry--at-edge-nested-right-split-returns-nil () + "Boundary: when the right side is itself split into rows, no single +window forms the full-height right half -> nil." + (save-window-excursion + (delete-other-windows) + (let ((right (split-window (selected-window) nil 'right))) + (split-window right nil 'below) + (should (null (cj/window-at-edge 'right)))))) + +(ert-deftest test-cj-window-geometry--at-edge-unknown-direction-returns-nil () + "Error: an unknown direction symbol -> nil even in a split frame." + (save-window-excursion + (delete-other-windows) + (split-window (selected-window) nil 'right) + (should (null (cj/window-at-edge 'sideways))) + (should (null (cj/window-at-edge nil))))) + (provide 'test-cj-window-geometry-lib) ;;; test-cj-window-geometry-lib.el ends here -- cgit v1.2.3