diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-05 05:28:58 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-05 05:28:58 -0500 |
| commit | ebdf9e466b0e1f86e9b7d76650ac32408273e7a7 (patch) | |
| tree | dab9b453f3a93c324b5388b3843502a088c7ed46 /tests/test-ai-term--show-or-create.el | |
| parent | c094b2e4e64530379a9cb273303308a9affcabf6 (diff) | |
| download | dotemacs-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--show-or-create.el')
| -rw-r--r-- | tests/test-ai-term--show-or-create.el | 155 |
1 files changed, 155 insertions, 0 deletions
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 |
