diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-25 08:51:56 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-25 08:51:56 -0400 |
| commit | 79cbccb59da539d6cce0f7f67b8efe1844ae4cbd (patch) | |
| tree | 397b5d86ddf3e84e80fac5f1a665555c0fdb734b /tests | |
| parent | 8c23d030942a9f50ce64b55cf12ecb6a14337c51 (diff) | |
| download | dotemacs-79cbccb59da539d6cce0f7f67b8efe1844ae4cbd.tar.gz dotemacs-79cbccb59da539d6cce0f7f67b8efe1844ae4cbd.zip | |
feat(ai-term): step into detached sessions too, attaching them
The next-agent step (C-; a n / M-SPC) cycled only live agent buffers, so a detached session (alive in tmux, no Emacs buffer) was reachable only through the picker. Now the queue is every active agent, live buffer or live session, keyed on the project dir and ordered by buffer name. Stepping onto a detached one attaches it: show-or-create recreates the terminal, which reattaches the tmux session. The live-buffer swap path is unchanged. I replaced the buffer-rotation helper with a dir-based one and added an active-agent enumerator, with 10 tests.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-ai-term--active-agent-dirs.el | 50 | ||||
| -rw-r--r-- | tests/test-ai-term--next-agent-buffer.el | 73 | ||||
| -rw-r--r-- | tests/test-ai-term--next-agent-dir.el | 48 | ||||
| -rw-r--r-- | tests/test-ai-term--next-no-agents.el | 4 |
4 files changed, 100 insertions, 75 deletions
diff --git a/tests/test-ai-term--active-agent-dirs.el b/tests/test-ai-term--active-agent-dirs.el new file mode 100644 index 000000000..86e557b42 --- /dev/null +++ b/tests/test-ai-term--active-agent-dirs.el @@ -0,0 +1,50 @@ +;;; test-ai-term--active-agent-dirs.el --- Tests for cj/--ai-term-active-agent-dirs -*- lexical-binding: t; -*- + +;;; Commentary: +;; The queue `cj/ai-term-next' steps through: project dirs with an active +;; agent, which is either a live agent buffer (attached) or a live tmux session +;; with no Emacs buffer (detached). Folding detached sessions in is what lets +;; the step key reach and attach a session that isn't currently on screen. +;; Candidates / buffers / sessions are mocked so the enumeration logic is +;; exercised without a real tmux server. + +;;; Code: + +(require 'ert) +(require 'cl-lib) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'ai-term) + +(ert-deftest test-ai-term--active-agent-dirs-includes-attached-and-detached () + "Normal: dirs with a live buffer OR a live session are active and sorted by +name; dirs with neither are excluded." + (let ((buf (get-buffer-create (cj/--ai-term-buffer-name "/p/alpha")))) + (unwind-protect + (cl-letf (((symbol-function 'cj/--ai-term-candidates) + (lambda (&rest _) '("/p/alpha" "/p/beta" "/p/gamma" "/p/delta"))) + ((symbol-function 'cj/--ai-term-agent-buffers) + (lambda (&rest _) (list buf))) + ((symbol-function 'cj/--ai-term-live-tmux-sessions) + (lambda (&rest _) (list (cj/--ai-term-tmux-session-name "/p/gamma"))))) + ;; alpha attached (buffer), gamma detached (session); beta/delta neither. + (should (equal '("/p/alpha" "/p/gamma") (cj/--ai-term-active-agent-dirs)))) + (kill-buffer buf)))) + +(ert-deftest test-ai-term--active-agent-dirs-detached-only () + "Normal: a dir with only a live session (no buffer) is included -- the detached case." + (cl-letf (((symbol-function 'cj/--ai-term-candidates) (lambda (&rest _) '("/p/solo"))) + ((symbol-function 'cj/--ai-term-agent-buffers) (lambda (&rest _) nil)) + ((symbol-function 'cj/--ai-term-live-tmux-sessions) + (lambda (&rest _) (list (cj/--ai-term-tmux-session-name "/p/solo"))))) + (should (equal '("/p/solo") (cj/--ai-term-active-agent-dirs))))) + +(ert-deftest test-ai-term--active-agent-dirs-empty-when-none-active () + "Boundary: no live buffers and no sessions -> an empty queue." + (cl-letf (((symbol-function 'cj/--ai-term-candidates) (lambda (&rest _) '("/p/a" "/p/b"))) + ((symbol-function 'cj/--ai-term-agent-buffers) (lambda (&rest _) nil)) + ((symbol-function 'cj/--ai-term-live-tmux-sessions) (lambda (&rest _) nil))) + (should (null (cj/--ai-term-active-agent-dirs))))) + +(provide 'test-ai-term--active-agent-dirs) +;;; test-ai-term--active-agent-dirs.el ends here diff --git a/tests/test-ai-term--next-agent-buffer.el b/tests/test-ai-term--next-agent-buffer.el deleted file mode 100644 index 330714a92..000000000 --- a/tests/test-ai-term--next-agent-buffer.el +++ /dev/null @@ -1,73 +0,0 @@ -;;; test-ai-term--next-agent-buffer.el --- Tests for cj/--ai-term-next-agent-buffer -*- lexical-binding: t; -*- - -;;; Commentary: -;; The pure decision helper behind `cj/ai-term-next' (s-F9). Given the -;; current agent buffer and the ordered list of live agent buffers, it -;; returns the next buffer in the queue, wrapping after the last. A nil -;; or non-member CURRENT returns the first; an empty list returns nil. -;; No buffer or window side effects -- list logic only. - -;;; Code: - -(require 'ert) - -(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) -(require 'ai-term) - -(ert-deftest test-ai-term--next-agent-buffer-advances-from-first () - "Normal: current is the first element -> returns the second." - (let ((a (get-buffer-create "agent [a]")) - (b (get-buffer-create "agent [b]")) - (c (get-buffer-create "agent [c]"))) - (unwind-protect - (should (eq b (cj/--ai-term-next-agent-buffer a (list a b c)))) - (mapc #'kill-buffer (list a b c))))) - -(ert-deftest test-ai-term--next-agent-buffer-advances-from-middle () - "Normal: current in the middle -> returns the following element." - (let ((a (get-buffer-create "agent [a]")) - (b (get-buffer-create "agent [b]")) - (c (get-buffer-create "agent [c]"))) - (unwind-protect - (should (eq c (cj/--ai-term-next-agent-buffer b (list a b c)))) - (mapc #'kill-buffer (list a b c))))) - -(ert-deftest test-ai-term--next-agent-buffer-wraps-after-last () - "Boundary: current is the last element -> wraps to the first." - (let ((a (get-buffer-create "agent [a]")) - (b (get-buffer-create "agent [b]")) - (c (get-buffer-create "agent [c]"))) - (unwind-protect - (should (eq a (cj/--ai-term-next-agent-buffer c (list a b c)))) - (mapc #'kill-buffer (list a b c))))) - -(ert-deftest test-ai-term--next-agent-buffer-single-element-returns-itself () - "Boundary: a one-agent queue wraps current back to itself." - (let ((a (get-buffer-create "agent [a]"))) - (unwind-protect - (should (eq a (cj/--ai-term-next-agent-buffer a (list a)))) - (kill-buffer a)))) - -(ert-deftest test-ai-term--next-agent-buffer-nil-current-returns-first () - "Boundary: nil current (no agent displayed) -> returns the first." - (let ((a (get-buffer-create "agent [a]")) - (b (get-buffer-create "agent [b]"))) - (unwind-protect - (should (eq a (cj/--ai-term-next-agent-buffer nil (list a b)))) - (mapc #'kill-buffer (list a b))))) - -(ert-deftest test-ai-term--next-agent-buffer-non-member-current-returns-first () - "Error: current not in the queue -> returns the first rather than nil." - (let ((a (get-buffer-create "agent [a]")) - (b (get-buffer-create "agent [b]")) - (stray (get-buffer-create "agent [stray]"))) - (unwind-protect - (should (eq a (cj/--ai-term-next-agent-buffer stray (list a b)))) - (mapc #'kill-buffer (list a b stray))))) - -(ert-deftest test-ai-term--next-agent-buffer-empty-queue-returns-nil () - "Boundary: an empty queue returns nil (nothing to switch to)." - (should (null (cj/--ai-term-next-agent-buffer nil '())))) - -(provide 'test-ai-term--next-agent-buffer) -;;; test-ai-term--next-agent-buffer.el ends here diff --git a/tests/test-ai-term--next-agent-dir.el b/tests/test-ai-term--next-agent-dir.el new file mode 100644 index 000000000..b5cf1cdf5 --- /dev/null +++ b/tests/test-ai-term--next-agent-dir.el @@ -0,0 +1,48 @@ +;;; test-ai-term--next-agent-dir.el --- Tests for cj/--ai-term-next-agent-dir -*- lexical-binding: t; -*- + +;;; Commentary: +;; The pure decision helper behind `cj/ai-term-next'. Given the current +;; active-agent project dir and the ordered list of active-agent dirs, it +;; returns the next dir in the queue, wrapping after the last. A nil or +;; non-member CURRENT returns the first; an empty list returns nil. Dirs are +;; matched with `member' (string equality). No side effects -- list logic only. + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'ai-term) + +(defconst test-ai-term--dirs '("/p/a" "/p/b" "/p/c")) + +(ert-deftest test-ai-term--next-agent-dir-advances-from-first () + "Normal: current is the first element -> returns the second." + (should (equal "/p/b" (cj/--ai-term-next-agent-dir "/p/a" test-ai-term--dirs)))) + +(ert-deftest test-ai-term--next-agent-dir-advances-from-middle () + "Normal: current in the middle -> returns the following element." + (should (equal "/p/c" (cj/--ai-term-next-agent-dir "/p/b" test-ai-term--dirs)))) + +(ert-deftest test-ai-term--next-agent-dir-wraps-after-last () + "Boundary: current is the last element -> wraps to the first." + (should (equal "/p/a" (cj/--ai-term-next-agent-dir "/p/c" test-ai-term--dirs)))) + +(ert-deftest test-ai-term--next-agent-dir-single-element-returns-itself () + "Boundary: a one-agent queue wraps current back to itself." + (should (equal "/p/a" (cj/--ai-term-next-agent-dir "/p/a" '("/p/a"))))) + +(ert-deftest test-ai-term--next-agent-dir-nil-current-returns-first () + "Boundary: nil current (no agent displayed) -> returns the first." + (should (equal "/p/a" (cj/--ai-term-next-agent-dir nil '("/p/a" "/p/b"))))) + +(ert-deftest test-ai-term--next-agent-dir-non-member-current-returns-first () + "Error: current not in the queue -> returns the first rather than nil." + (should (equal "/p/a" (cj/--ai-term-next-agent-dir "/p/stray" '("/p/a" "/p/b"))))) + +(ert-deftest test-ai-term--next-agent-dir-empty-queue-returns-nil () + "Boundary: an empty queue returns nil (nothing to switch to)." + (should (null (cj/--ai-term-next-agent-dir nil '())))) + +(provide 'test-ai-term--next-agent-dir) +;;; test-ai-term--next-agent-dir.el ends here diff --git a/tests/test-ai-term--next-no-agents.el b/tests/test-ai-term--next-no-agents.el index ef87d71ee..59132df8e 100644 --- a/tests/test-ai-term--next-no-agents.el +++ b/tests/test-ai-term--next-no-agents.el @@ -17,7 +17,7 @@ (ert-deftest test-ai-term-next-no-agents-launches-picker () "Error: no agents open -> launches the picker instead of erroring." (let ((picked 0)) - (cl-letf (((symbol-function 'cj/--ai-term-agent-buffers) (lambda (&rest _) nil)) + (cl-letf (((symbol-function 'cj/--ai-term-active-agent-dirs) (lambda (&rest _) nil)) ((symbol-function 'cj/--ai-term-displayed-agent-window) (lambda (&rest _) nil)) ((symbol-function 'cj/ai-term-pick-project) (lambda (&rest _) (setq picked (1+ picked))))) (cj/ai-term-next) @@ -25,7 +25,7 @@ (ert-deftest test-ai-term-next-no-agents-does-not-signal () "Error: no agents open -> returns normally, no user-error raised." - (cl-letf (((symbol-function 'cj/--ai-term-agent-buffers) (lambda (&rest _) nil)) + (cl-letf (((symbol-function 'cj/--ai-term-active-agent-dirs) (lambda (&rest _) nil)) ((symbol-function 'cj/--ai-term-displayed-agent-window) (lambda (&rest _) nil)) ((symbol-function 'cj/ai-term-pick-project) (lambda (&rest _) nil))) (should (progn (cj/ai-term-next) t)))) |
