aboutsummaryrefslogtreecommitdiff
path: root/tests/test-ai-term--show-or-create.el
blob: c6653dcddde44232e49f1d9814ec1d14643ac0de (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
;;; 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