aboutsummaryrefslogtreecommitdiff
path: root/tests/test-ai-vterm--close.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-21 20:19:14 -0400
committerCraig Jennings <c@cjennings.net>2026-05-21 20:38:05 -0400
commitc38683f13cf361adc93b72c1e87244a0153b2387 (patch)
treecf9d2d8abf282a7915a804d35aa9e7b5e0226e9b /tests/test-ai-vterm--close.el
parent89477ad8ae5cbfc7f526b80c70b9baa11ee1cd4f (diff)
downloaddotemacs-c38683f13cf361adc93b72c1e87244a0153b2387.tar.gz
dotemacs-c38683f13cf361adc93b72c1e87244a0153b2387.zip
feat(ai-vterm): add graceful agent close on M-f9 / C-S-f9
cj/ai-vterm-close tears an agent down cleanly: it kills the agent's tmux session (stopping the process), removes the vterm window when it isn't the only one in the frame, then kills the buffer. It targets the current agent buffer, the sole live agent, or prompts among several, and confirms before killing since that interrupts work in progress. I also folded the whole F9 family onto ai-vterm. M-f9 used to run cj/toggle-gptel, but gptel is broken right now (the local fork doesn't load, so gptel-make-anthropic is void), and grouping every ai-vterm command under F9 reads better anyway. M-f9 is the primary close binding. C-S-f9 is a second binding that the Wayland/PGTK layer may swallow on some machines. I covered it with 7 tests over the tmux-kill helper, the per-buffer teardown, and target selection, mocking process-file and the prompt at the boundary.
Diffstat (limited to 'tests/test-ai-vterm--close.el')
-rw-r--r--tests/test-ai-vterm--close.el86
1 files changed, 86 insertions, 0 deletions
diff --git a/tests/test-ai-vterm--close.el b/tests/test-ai-vterm--close.el
new file mode 100644
index 00000000..eb89bcc2
--- /dev/null
+++ b/tests/test-ai-vterm--close.el
@@ -0,0 +1,86 @@
+;;; test-ai-vterm--close.el --- Tests for graceful agent close -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; `cj/ai-vterm-close' tears an agent down gracefully: kill its tmux
+;; session (stopping the agent process), kill the vterm buffer, and
+;; remove its window. These tests cover the pure pieces -- the
+;; tmux-kill helper, the per-buffer teardown, and the target selection --
+;; with `process-file' and the prompt mocked at the boundary.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'ai-vterm)
+
+(ert-deftest test-ai-vterm--kill-tmux-session-runs-kill-session ()
+ "Normal: invokes `tmux kill-session -t <session>'."
+ (let (captured)
+ (cl-letf (((symbol-function 'process-file)
+ (lambda (program &rest args)
+ (setq captured (cons program args))
+ 0)))
+ (cj/--ai-vterm-kill-tmux-session "aiv-foo"))
+ (should (equal (car captured) "tmux"))
+ (should (member "kill-session" captured))
+ (should (member "-t" captured))
+ (should (member "aiv-foo" captured))))
+
+(ert-deftest test-ai-vterm--kill-tmux-session-swallows-error ()
+ "Error: returns nil when tmux is unavailable (process-file signals)."
+ (cl-letf (((symbol-function 'process-file)
+ (lambda (&rest _) (error "no tmux"))))
+ (should (null (cj/--ai-vterm-kill-tmux-session "aiv-foo")))))
+
+(ert-deftest test-ai-vterm--close-buffer-kills-session-and-buffer ()
+ "Normal: derives the session from default-directory, kills it and the buffer."
+ (let ((buf (get-buffer-create "agent [foo]"))
+ captured-session)
+ (with-current-buffer buf (setq-local default-directory "/tmp/foo/"))
+ (cl-letf (((symbol-function 'cj/--ai-vterm-kill-tmux-session)
+ (lambda (s) (setq captured-session s) 0)))
+ (cj/--ai-vterm-close-buffer buf))
+ (should (equal captured-session "aiv-foo"))
+ (should-not (buffer-live-p buf))))
+
+(ert-deftest test-ai-vterm--close-buffer-noop-on-non-agent ()
+ "Boundary: does nothing for a buffer that is not an agent buffer."
+ (let ((buf (get-buffer-create "*not-an-agent*"))
+ (called nil))
+ (unwind-protect
+ (progn
+ (cl-letf (((symbol-function 'cj/--ai-vterm-kill-tmux-session)
+ (lambda (_s) (setq called t) 0)))
+ (cj/--ai-vterm-close-buffer buf))
+ (should-not called)
+ (should (buffer-live-p buf)))
+ (when (buffer-live-p buf) (kill-buffer buf)))))
+
+(ert-deftest test-ai-vterm--close-target-current-agent-buffer ()
+ "Normal: returns the current buffer when it is an agent buffer."
+ (let ((buf (get-buffer-create "agent [cur]")))
+ (unwind-protect
+ (with-current-buffer buf
+ (should (eq (cj/--ai-vterm-close-target) buf)))
+ (kill-buffer buf))))
+
+(ert-deftest test-ai-vterm--close-target-sole-agent ()
+ "Normal: returns the only live agent buffer when current isn't an agent."
+ (let ((buf (get-buffer-create "agent [only]")))
+ (unwind-protect
+ (with-temp-buffer
+ (cl-letf (((symbol-function 'cj/--ai-vterm-agent-buffers)
+ (lambda () (list buf))))
+ (should (eq (cj/--ai-vterm-close-target) buf))))
+ (kill-buffer buf))))
+
+(ert-deftest test-ai-vterm--close-target-none-returns-nil ()
+ "Boundary: nil when current buffer isn't an agent and none are alive."
+ (with-temp-buffer
+ (cl-letf (((symbol-function 'cj/--ai-vterm-agent-buffers) (lambda () nil)))
+ (should (null (cj/--ai-vterm-close-target))))))
+
+(provide 'test-ai-vterm--close)
+;;; test-ai-vterm--close.el ends here