blob: 244d91ee4bcf3d2c8f6ee87faa6719545702e98d (
plain)
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
|
;;; test-ai-term--project-color.el --- Tests for per-project session colors -*- lexical-binding: t; -*-
;;; Commentary:
;; Tests the per-project /color auto-assignment: the project -> color-name
;; mapping (override alist + deterministic hash fallback), the TUI-ready
;; detector, the two-step /color send, the polling scheduler, and the
;; show-or-create wiring (fresh sessions get a color; tmux reattaches are
;; never injected into). eat, tmux discovery, and timers are stubbed.
;;; Code:
(require 'ert)
(require 'cl-lib)
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(require 'ai-term)
(declare-function cj/--ai-term-project-color "ai-term" (dir))
(declare-function cj/--ai-term-color-ready-p "ai-term-backend-eat" (buffer))
(declare-function cj/--ai-term-send-color "ai-term-backend-eat" (buffer color))
(declare-function cj/--ai-term-schedule-color "ai-term-backend-eat" (buffer color))
(declare-function cj/--ai-term-show-or-create "ai-term-backend-eat" (dir name))
(defvar cj/ai-term-project-colors)
(defvar cj/--ai-term-color-names)
(defvar cj/--ai-term-mru)
(defvar eat-buffer-name)
;; eat isn't loaded in batch -- provide a stub so cl-letf has an override.
(unless (fboundp 'eat)
(defun eat (&optional _program _arg) nil))
;;; ------------------------ cj/--ai-term-project-color ------------------------
(ert-deftest test-ai-term-project-color-is-deterministic ()
"Normal: the same project dir always maps to the same color name."
(let ((cj/ai-term-project-colors nil))
(should (equal (cj/--ai-term-project-color "/home/u/code/someproj")
(cj/--ai-term-project-color "/home/u/code/someproj")))))
(ert-deftest test-ai-term-project-color-returns-a-known-name ()
"Normal: the hash fallback lands on one of Claude Code's color names."
(let ((cj/ai-term-project-colors nil))
(should (seq-contains-p cj/--ai-term-color-names
(cj/--ai-term-project-color "/home/u/code/xyz")
#'equal))))
(ert-deftest test-ai-term-project-color-alist-override-wins ()
"Normal: an explicit project entry beats the hash."
(let ((cj/ai-term-project-colors '(("myproj" . "purple"))))
(should (equal (cj/--ai-term-project-color "/home/u/code/myproj") "purple"))))
(ert-deftest test-ai-term-project-color-trailing-slash-insensitive ()
"Boundary: a trailing slash on the dir does not change the color."
(let ((cj/ai-term-project-colors nil))
(should (equal (cj/--ai-term-project-color "/home/u/code/proj")
(cj/--ai-term-project-color "/home/u/code/proj/")))))
;;; ----------------------- cj/--ai-term-color-ready-p -------------------------
(defun test-ai-term-pc--buffer-with (content)
"Make a temp buffer holding CONTENT and return `cj/--ai-term-color-ready-p' on it."
(with-temp-buffer
(insert content)
(cj/--ai-term-color-ready-p (current-buffer))))
(ert-deftest test-ai-term-color-ready-detects-idle-tui ()
"Normal: banner present + untouched prompt line reads as ready."
(should (test-ai-term-pc--buffer-with
"───────\n❯ \n───────\n ⏵⏵ bypass permissions on (shift+tab to cycle)\n")))
(ert-deftest test-ai-term-color-ready-rejects-typed-prompt ()
"Boundary: text already on the prompt line means never inject."
(should-not (test-ai-term-pc--buffer-with
"───────\n❯ fix the bug\n───────\n ⏵⏵ bypass permissions on\n")))
(ert-deftest test-ai-term-color-ready-rejects-plain-shell ()
"Error: a shell without the Claude banner is not ready (fail-safe: no injection)."
(should-not (test-ai-term-pc--buffer-with "cjennings@velox ~ $ \n")))
(ert-deftest test-ai-term-color-ready-rejects-dead-buffer ()
"Error: a killed buffer is never ready."
(let ((buf (generate-new-buffer "pc-dead")))
(kill-buffer buf)
(should-not (cj/--ai-term-color-ready-p buf))))
;;; ------------------------ cj/--ai-term-send-color ---------------------------
(ert-deftest test-ai-term-send-color-two-step ()
"Normal: the command text goes first; the CR follows via a timer (menu race guard)."
(let ((sent nil) (timer-fns nil))
(cl-letf (((symbol-function 'cj/--ai-term-send-string)
(lambda (_buf s) (push s sent)))
((symbol-function 'run-at-time)
(lambda (_time _repeat fn &rest args)
(push (cons fn args) timer-fns)
'fake-timer)))
(with-temp-buffer
(cj/--ai-term-send-color (current-buffer) "purple"))
(should (equal sent '("/color purple")))
(should (= (length timer-fns) 1))
;; Fire the deferred CR.
(apply (caar timer-fns) (cdar timer-fns))
(should (equal sent '("\r" "/color purple"))))))
;;; ---------------------- cj/--ai-term-schedule-color -------------------------
(ert-deftest test-ai-term-schedule-color-sends-when-ready ()
"Normal: the poll sends once the TUI is ready, then cancels its timer."
(let ((poll-fn nil) (cancelled nil) (sent nil))
(cl-letf (((symbol-function 'run-at-time)
(lambda (_time _repeat fn &rest args)
(setq poll-fn (lambda () (apply fn args)))
'fake-timer))
((symbol-function 'cancel-timer)
(lambda (&rest _) (setq cancelled t)))
((symbol-function 'cj/--ai-term-send-color)
(lambda (_buf color) (push color sent))))
(with-temp-buffer
(rename-buffer "pc-sched-ready" t)
(cj/--ai-term-schedule-color (current-buffer) "green")
(should poll-fn)
;; Not ready yet: nothing sent, timer stays.
(funcall poll-fn)
(should-not sent)
(should-not cancelled)
;; Becomes ready: sends and cancels.
(insert "❯ \n⏵⏵ bypass permissions on\n")
(funcall poll-fn)
(should (equal sent '("green")))
(should cancelled)))))
(ert-deftest test-ai-term-schedule-color-gives-up-on-dead-buffer ()
"Error: a killed buffer cancels the poll without sending."
(let ((poll-fn nil) (cancelled nil) (sent nil)
(buf (generate-new-buffer "pc-sched-dead")))
(cl-letf (((symbol-function 'run-at-time)
(lambda (_time _repeat fn &rest args)
(setq poll-fn (lambda () (apply fn args)))
'fake-timer))
((symbol-function 'cancel-timer)
(lambda (&rest _) (setq cancelled t)))
((symbol-function 'cj/--ai-term-send-color)
(lambda (_buf color) (push color sent))))
(cj/--ai-term-schedule-color buf "red")
(kill-buffer buf)
(funcall poll-fn)
(should-not sent)
(should cancelled))))
;;; ------------------------- show-or-create wiring ----------------------------
(defmacro test-ai-term-pc--with-create-mocks (sessions scheduled &rest body)
"Run BODY with the create path mocked; live tmux SESSIONS list injected.
SCHEDULED captures (BUFFER-NAME . COLOR) pairs from the schedule call."
(declare (indent 2) (debug t))
`(cl-letf (((symbol-function 'eat)
(lambda (&optional _program _arg)
(get-buffer-create eat-buffer-name)))
((symbol-function 'cj/--ai-term-send-string)
(lambda (_buf _s) nil))
((symbol-function 'display-buffer)
(lambda (&rest _) nil))
((symbol-function 'cj/--ai-term-apply-accent)
(lambda (_buffer) nil))
((symbol-function 'cj/--ai-term-live-tmux-sessions)
(lambda () ,sessions))
((symbol-function 'cj/--ai-term-schedule-color)
(lambda (buffer color)
(push (cons (buffer-name buffer) color) ,scheduled))))
,@body))
(ert-deftest test-ai-term-show-or-create-schedules-color-on-fresh-session ()
"Normal: a project with no live tmux session gets a scheduled /color."
(let ((name "agent [pc-fresh-test]")
(cj/--ai-term-mru nil)
(scheduled nil))
(when (get-buffer name) (kill-buffer name))
(unwind-protect
(test-ai-term-pc--with-create-mocks nil scheduled
(cj/--ai-term-show-or-create "/tmp/pc-fresh-test" name)
(should (= (length scheduled) 1))
(should (equal (caar scheduled) name))
(should (equal (cdar scheduled)
(cj/--ai-term-project-color "/tmp/pc-fresh-test"))))
(when (get-buffer name) (kill-buffer name)))))
(ert-deftest test-ai-term-show-or-create-skips-color-on-reattach ()
"Boundary: a live tmux session (reattach) is never injected with /color."
(let* ((dir "/tmp/pc-reattach-test")
(name "agent [pc-reattach-test]")
(cj/--ai-term-mru nil)
(scheduled nil)
(live (list (cj/--ai-term-tmux-session-name dir))))
(when (get-buffer name) (kill-buffer name))
(unwind-protect
(test-ai-term-pc--with-create-mocks live scheduled
(cj/--ai-term-show-or-create dir name)
(should-not scheduled))
(when (get-buffer name) (kill-buffer name)))))
(provide 'test-ai-term--project-color)
;;; test-ai-term--project-color.el ends here
|