1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
;;; test-ai-vterm--show-or-create.el --- Tests for cj/--ai-vterm-show-or-create -*- lexical-binding: t; -*-
;;; Commentary:
;; Tests the show-or-create branching:
;;
;; - buffer absent -> vterm called, claude command sent
;; - buffer present, live -> vterm not called, buffer displayed
;; - buffer present, dead -> old buffer killed, vterm recreates
;;
;; vterm functions are stubbed so the test does no process spawning.
;;; Code:
(require 'ert)
(require 'cl-lib)
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(require 'ai-vterm)
;; vterm isn't loaded in batch -- provide stubs so cl-letf has overrides.
(unless (fboundp 'vterm)
(defun vterm (&optional _name) nil))
(unless (fboundp 'vterm-send-string)
(defun vterm-send-string (_s &optional _) nil))
(unless (fboundp 'vterm-send-return)
(defun vterm-send-return () nil))
(defmacro test-ai-vterm--with-mock-vterm (vars &rest body)
"Run BODY with vterm + send-string + send-return mocked.
VARS is a plist of capture variable names: :calls, :strings, :returns,
:default-dir. The test references these names directly inside BODY."
(declare (indent 1) (debug t))
(let ((calls (plist-get vars :calls))
(strings (plist-get vars :strings))
(returns (plist-get vars :returns))
(ddir (plist-get vars :default-dir)))
`(let ((,calls '())
(,strings '())
(,returns 0)
(,ddir nil))
(cl-letf (((symbol-function 'vterm)
(lambda (&optional name)
(push name ,calls)
(setq ,ddir default-directory)
(with-current-buffer (get-buffer-create name)
(current-buffer))))
((symbol-function 'vterm-send-string)
(lambda (s &optional _) (push s ,strings)))
((symbol-function 'vterm-send-return)
(lambda () (cl-incf ,returns))))
,@body))))
(defun test-ai-vterm--cleanup (name)
"Kill buffer NAME if it exists."
(when (get-buffer name)
(kill-buffer name)))
(ert-deftest test-ai-vterm--show-or-create-creates-when-buffer-missing ()
"Normal: no existing buffer -> vterm called once, launch cmd sent."
(let ((name "claude [normal-create-test]"))
(test-ai-vterm--cleanup name)
(unwind-protect
(test-ai-vterm--with-mock-vterm (:calls calls :strings strings
:returns returns :default-dir ddir)
(cj/--ai-vterm-show-or-create "/tmp/some-project" name)
(should (equal calls (list name)))
(should (equal strings
(list (cj/--ai-vterm-launch-command "/tmp/some-project"))))
(should (= returns 1))
(should (equal ddir "/tmp/some-project")))
(test-ai-vterm--cleanup name))))
(ert-deftest test-ai-vterm--show-or-create-displays-existing-when-process-live ()
"Normal: buffer exists with live process -> vterm not called."
(let ((name "claude [reuse-test]"))
(test-ai-vterm--cleanup name)
(unwind-protect
(let ((buf (get-buffer-create name)))
(cl-letf (((symbol-function 'cj/--ai-vterm-process-live-p)
(lambda (b) (and (eq b buf) t))))
(test-ai-vterm--with-mock-vterm (:calls calls :strings strings
:returns returns :default-dir _ddir)
(cj/--ai-vterm-show-or-create "/tmp/reuse" name)
(should (null calls))
(should (null strings))
(should (= returns 0)))))
(test-ai-vterm--cleanup name))))
(ert-deftest test-ai-vterm--show-or-create-recreates-when-process-dead ()
"Boundary: buffer exists with dead process -> killed and recreated."
(let ((name "claude [dead-test]"))
(test-ai-vterm--cleanup name)
(unwind-protect
(let ((stale (get-buffer-create name)))
(cl-letf (((symbol-function 'cj/--ai-vterm-process-live-p)
(lambda (_b) nil)))
(test-ai-vterm--with-mock-vterm (:calls calls :strings strings
:returns returns :default-dir _ddir)
(cj/--ai-vterm-show-or-create "/tmp/dead" name)
(should (equal calls (list name)))
(should (equal strings
(list (cj/--ai-vterm-launch-command "/tmp/dead"))))
(should (= returns 1))
(should-not (buffer-live-p stale)))))
(test-ai-vterm--cleanup name))))
(ert-deftest test-ai-vterm--show-or-create-preserves-selected-window ()
"Regression: vterm's pop-to-buffer-same-window must not bury the dashboard.
Real `vterm' replaces the selected window's buffer as a side-effect of
construction. On a fresh-boot frame (one window showing the dashboard),
that side-effect previously left the original window pointing at the new
claude buffer; the dashboard was buried, the alist-routed split then
created a second window also showing claude. The wrapper must restore
the original window state before `display-buffer' fires so dashboard
stays put and the alist places claude into a fresh right-side split.
This test stubs `vterm' to mimic the pop-to-buffer-same-window side-effect
and asserts the originally-selected window still shows its original buffer
after `cj/--ai-vterm-show-or-create' returns."
(let ((claude-name "claude [preserve-window-test]")
(orig-name "*test-original-buffer*"))
(test-ai-vterm--cleanup claude-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 'vterm)
(lambda (&optional name)
(let ((buf (get-buffer-create name)))
(set-window-buffer (selected-window) buf)
buf)))
((symbol-function 'vterm-send-string)
(lambda (_s &optional _) nil))
((symbol-function 'vterm-send-return)
(lambda () nil)))
(cj/--ai-vterm-show-or-create "/tmp/preserve" claude-name)
(should (eq (window-buffer orig-win) orig-buf)))))
(test-ai-vterm--cleanup claude-name)
(when (get-buffer orig-name) (kill-buffer orig-name)))))
(ert-deftest test-ai-vterm--show-or-create-returns-buffer ()
"Normal: return value is the vterm buffer."
(let ((name "claude [return-test]"))
(test-ai-vterm--cleanup name)
(unwind-protect
(test-ai-vterm--with-mock-vterm (:calls _c :strings _s
:returns _r :default-dir _d)
(let ((result (cj/--ai-vterm-show-or-create "/tmp/return" name)))
(should (bufferp result))
(should (equal (buffer-name result) name))))
(test-ai-vterm--cleanup name))))
(provide 'test-ai-vterm--show-or-create)
;;; test-ai-vterm--show-or-create.el ends here
|