aboutsummaryrefslogtreecommitdiff
path: root/tests/test-ai-term--display-saved.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--display-saved.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--display-saved.el')
-rw-r--r--tests/test-ai-term--display-saved.el173
1 files changed, 173 insertions, 0 deletions
diff --git a/tests/test-ai-term--display-saved.el b/tests/test-ai-term--display-saved.el
new file mode 100644
index 00000000..8b689aa6
--- /dev/null
+++ b/tests/test-ai-term--display-saved.el
@@ -0,0 +1,173 @@
+;;; test-ai-term--display-saved.el --- Tests for the display-saved action -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; `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-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'.
+;;
+;; 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-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.
+
+;;; 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)
+
+(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-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-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-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-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-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-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-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-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-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-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-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-term--display-saved-strips-conflicting-alist-entries ()
+ "Boundary: caller-supplied direction/size are stripped, saved values win."
+ (let (received-alist
+ (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-term-display-saved
+ 'fake-buf
+ '((direction . below)
+ (window-width . 0.2)
+ (window-height . 0.3)
+ (inhibit-same-window . t))))
+ (should (eq (cdr (assq 'direction received-alist)) 'rightmost))
+ (should (= (cdr (assq 'window-width received-alist)) 0.7))
+ (should (eq (cdr (assq 'inhibit-same-window received-alist)) t))
+ ;; window-height should not be in the alist when direction is right
+ ;; -- the action picks the matching size key based on direction.
+ (let ((wh-cells (cl-remove-if-not
+ (lambda (cell) (eq (car-safe cell) 'window-height))
+ received-alist)))
+ (should (null wh-cells)))))
+
+(ert-deftest test-ai-term--display-saved-passes-buffer-through ()
+ "Normal: BUFFER argument reaches display-buffer-in-direction unchanged."
+ (let (received-buf
+ (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-term-display-saved 'sentinel-buffer nil))
+ (should (eq received-buf 'sentinel-buffer))))
+
+(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.
+
+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.
+
+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*")
+ (right-name "*test-bm-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)
+ ;; 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-term-display-rule-list))
+ (window-count-before (count-windows)))
+ (select-window agent-win)
+ (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
+ (seq-filter
+ (lambda (w)
+ (eq (window-buffer w) agent-buf))
+ (window-list))))
+ (should (= (length agent-windows) 1)))))))
+ (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-term--display-saved)
+;;; test-ai-term--display-saved.el ends here