diff options
Diffstat (limited to 'tests/test-term-tmux-history.el')
| -rw-r--r-- | tests/test-term-tmux-history.el | 324 |
1 files changed, 153 insertions, 171 deletions
diff --git a/tests/test-term-tmux-history.el b/tests/test-term-tmux-history.el index 1bb7e73bc..a5f5c93cd 100644 --- a/tests/test-term-tmux-history.el +++ b/tests/test-term-tmux-history.el @@ -1,14 +1,13 @@ -;;; test-term-tmux-history.el --- Tests for term-config tmux history + menu UX -*- lexical-binding: t; -*- +;;; test-term-tmux-history.el --- Tests for the EAT terminal copy-mode + tmux history -*- lexical-binding: t; -*- ;;; Commentary: -;; Exercises the term-config (ghostel) terminal UX: the Emacs-owned tmux -;; history buffer, the copy-mode-dwim engine pick, the tmux pane-id / -;; attached-client predicates, and the C-; x menu bindings. +;; Exercises the terminal UX carried into eat-config for the EAT agent +;; terminals: the Emacs-owned tmux history buffer, the copy-mode-dwim engine +;; pick, the tmux pane-id / attached-client predicates, and the C-; x menu +;; bindings. Agents run EAT over tmux, so copy-mode is tmux's own copy-mode. ;; -;; ghostel is required (which defines `ghostel-mode-map' / -;; `ghostel-keymap-exceptions' and lets term-config's `with-eval-after-load' -;; fire) before term-config. `(require 'ghostel)' does not load the native -;; module; tmux is mocked via `process-file', so nothing spawns. +;; eat is required (so eat-config's `with-eval-after-load' fires for the C-<up> +;; bind) before eat-config; tmux is mocked via `process-file', so nothing spawns. ;;; Code: @@ -21,9 +20,9 @@ (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) (setq load-prefer-newer t) -(require 'ghostel) -(require 'term-config) -(require 'testutil-ghostel-buffers) +(require 'eat) +(require 'eat-config) +(require 'testutil-terminal-buffers) (defmacro test-term-tmux-history--with-tmux-mock (responses &rest body) "Run BODY with `process-file' mocked for tmux RESPONSES. @@ -51,6 +50,8 @@ RESPONSES is an alist of (ARGS EXIT-CODE OUTPUT)." exit-code)))) ,@body))) +;;; tmux helpers + (ert-deftest test-term-tmux-history--pane-id-for-tty-matches-client () "Normal: current terminal pty maps to the active pane for that tmux client." (test-term-tmux-history--with-tmux-mock @@ -66,16 +67,39 @@ RESPONSES is an alist of (ARGS EXIT-CODE OUTPUT)." (should (equal (cj/term--tmux-capture-pane "%8") "first line\nsecond line\n")))) +(ert-deftest test-term-current-tmux-pane-id-rejects-non-eat-buffer () + "Error: pane-id lookup refuses a buffer that is not in `eat-mode'." + (with-temp-buffer + (should-error (cj/term--current-tmux-pane-id) :type 'user-error))) + +(ert-deftest test-term-current-tmux-pane-id-accepts-agent-named-buffer () + "Normal: an agent-named eat buffer resolves by process TTY, not buffer name." + (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]"))) + (unwind-protect + (with-current-buffer agent + (cl-letf (((symbol-function 'get-buffer-process) + (lambda (_buffer) 'fake-process)) + ((symbol-function 'process-tty-name) + (lambda (_process &rest _) "/dev/pts/8"))) + (test-term-tmux-history--with-tmux-mock + '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 + "/dev/pts/1\t%1\n/dev/pts/8\t%8\n")) + (should (equal (cj/term--current-tmux-pane-id) "%8"))))) + (when (buffer-live-p agent) + (kill-buffer agent))))) + +;;; tmux history buffer + (ert-deftest test-term-tmux-history-open-renders-read-only-history-buffer () - "Normal: command renders tmux history in a normal Emacs buffer." - (let ((origin (cj/test--make-fake-ghostel-buffer "*test-term-history-origin*"))) + "Normal: the command renders tmux history in a normal Emacs buffer." + (let ((origin (cj/test--make-fake-eat-buffer "*test-term-history-origin*"))) (unwind-protect (save-window-excursion (switch-to-buffer origin) (cl-letf (((symbol-function 'get-buffer-process) (lambda (_buffer) 'fake-process)) ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8"))) + (lambda (_process &rest _) "/dev/pts/8"))) (test-term-tmux-history--with-tmux-mock '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 "/dev/pts/8\t%8\n") @@ -90,41 +114,8 @@ RESPONSES is an alist of (ARGS EXIT-CODE OUTPUT)." (when (buffer-live-p origin) (kill-buffer origin))))) -(ert-deftest test-term-tmux-history-replaces-origin-buffer-in-same-window () - "Normal: the history view replaces the origin in the selected window. - -`cj/term-tmux-history' uses `switch-to-buffer' so reading scrollback keeps -the terminal's frame slot rather than splitting or popping a new window." - (let ((origin (cj/test--make-fake-ghostel-buffer "*test-term-history-inplace*"))) - (unwind-protect - (save-window-excursion - (delete-other-windows) - (switch-to-buffer origin) - (let ((win (selected-window))) - (should (eq (window-buffer win) origin)) - (should (one-window-p)) - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8"))) - (test-term-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 - "/dev/pts/8\t%8\n") - (("capture-pane" "-p" "-J" "-S" "-" "-E" "-" "-t" "%8") 0 - "scrollback line\n")) - (cj/term-tmux-history))) - (should (one-window-p)) - (should (eq (selected-window) win)) - (should (string-prefix-p - "*terminal tmux history:" - (buffer-name (window-buffer win)))))) - (cj/test--kill-buffers-matching-prefix "*terminal tmux history") - (when (buffer-live-p origin) - (kill-buffer origin))))) - (ert-deftest test-term-tmux-history-quit-returns-to-origin () - "Normal: q / <escape> / C-g (cj/term-tmux-history-quit) kills the history -buffer and restores the origin buffer, window, and point." + "Normal: quit kills the history buffer and restores origin buffer/window/point." (let ((origin (get-buffer-create "*test-term-history-return*"))) (unwind-protect (let ((history (get-buffer-create "*terminal tmux history: test*"))) @@ -149,10 +140,8 @@ buffer and restores the origin buffer, window, and point." (kill-buffer origin))))) (ert-deftest test-term-tmux-history-mode-keymap () - "Normal: in the history buffer M-w copies without quitting; q, <escape>, -and C-g quit back to the terminal; RET is left unbound (no special exit)." - (should (eq (keymap-lookup cj/term-tmux-history-mode-map "M-w") - #'kill-ring-save)) + "Normal: M-w copies; q/<escape>/C-g quit; RET is left unbound." + (should (eq (keymap-lookup cj/term-tmux-history-mode-map "M-w") #'kill-ring-save)) (should (eq (keymap-lookup cj/term-tmux-history-mode-map "q") #'cj/term-tmux-history-quit)) (should (eq (keymap-lookup cj/term-tmux-history-mode-map "<escape>") @@ -161,56 +150,17 @@ and C-g quit back to the terminal; RET is left unbound (no special exit)." #'cj/term-tmux-history-quit)) (should-not (keymap-lookup cj/term-tmux-history-mode-map "RET"))) -(ert-deftest test-term-keymap-includes-history-and-copy-bindings () - "Normal: the personal terminal map owns the high-level UX commands, and C-; -reaches Emacs inside ghostel buffers so the prefix works there." - (should (member "C-;" ghostel-keymap-exceptions)) - (should (eq (keymap-lookup cj/custom-keymap "x h") #'cj/term-tmux-history)) - (should (eq (keymap-lookup cj/custom-keymap "x c") #'cj/term-copy-mode-dwim)) - (should (equal (keymap-lookup ghostel-mode-map "C-;") cj/custom-keymap)) - (should (eq (keymap-lookup ghostel-mode-map "C-; x h") #'cj/term-tmux-history)) - (should (eq (keymap-lookup ghostel-mode-map "C-; x c") #'cj/term-copy-mode-dwim))) - -(ert-deftest test-term-keymap-prompt-navigation () - "Normal: n/p navigate prompts, capital N creates a new terminal buffer." - (should (eq (keymap-lookup cj/custom-keymap "x n") #'ghostel-next-prompt)) - (should (eq (keymap-lookup cj/custom-keymap "x p") #'ghostel-previous-prompt)) - (should (eq (keymap-lookup cj/custom-keymap "x N") #'ghostel))) - -(ert-deftest test-term-current-tmux-pane-id-rejects-non-ghostel-buffer () - "Error: pane-id lookup refuses a buffer that is not in `ghostel-mode'." - (with-temp-buffer - (should-error (cj/term--current-tmux-pane-id) :type 'user-error))) - -(ert-deftest test-term-current-tmux-pane-id-accepts-agent-named-buffer () - "Normal: an agent-named ghostel buffer resolves by process TTY. - -The pane lookup keys off the live process TTY, never the buffer name, so a -buffer named `agent [repo]' (ai-term.el's naming) resolves like any other -ghostel-mode terminal." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))) - (unwind-protect - (with-current-buffer agent - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8"))) - (test-term-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 - "/dev/pts/1\t%1\n/dev/pts/8\t%8\n")) - (should (equal (cj/term--current-tmux-pane-id) "%8"))))) - (when (buffer-live-p agent) - (kill-buffer agent))))) +;;; in-tmux-p predicate (ert-deftest test-term-in-tmux-p-true-when-client-attached () "Normal: predicate returns t when tmux reports a client for our tty." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))) + (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]"))) (unwind-protect (with-current-buffer agent (cl-letf (((symbol-function 'get-buffer-process) (lambda (_buffer) 'fake-process)) ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8"))) + (lambda (_process &rest _) "/dev/pts/8"))) (test-term-tmux-history--with-tmux-mock '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 "/dev/pts/8\t%8\n")) @@ -218,31 +168,24 @@ ghostel-mode terminal." (when (buffer-live-p agent) (kill-buffer agent))))) -(ert-deftest test-term-in-tmux-p-nil-when-no-matching-client () - "Boundary: predicate returns nil when tmux runs but our tty has no client." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))) - (unwind-protect - (with-current-buffer agent - (cl-letf (((symbol-function 'get-buffer-process) - (lambda (_buffer) 'fake-process)) - ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8"))) - (test-term-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 - "/dev/pts/1\t%1\n")) - (should-not (cj/term--in-tmux-p))))) - (when (buffer-live-p agent) - (kill-buffer agent))))) +(ert-deftest test-term-in-tmux-p-nil-when-not-eat-mode () + "Boundary: predicate refuses non-eat buffers without calling tmux." + (with-temp-buffer + (let ((tmux-called nil)) + (cl-letf (((symbol-function 'process-file) + (lambda (&rest _) (setq tmux-called t) 0))) + (should-not (cj/term--in-tmux-p)) + (should-not tmux-called))))) (ert-deftest test-term-in-tmux-p-nil-when-tmux-fails () "Error: predicate swallows tmux failures and returns nil." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]"))) + (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]"))) (unwind-protect (with-current-buffer agent (cl-letf (((symbol-function 'get-buffer-process) (lambda (_buffer) 'fake-process)) ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8"))) + (lambda (_process &rest _) "/dev/pts/8"))) (test-term-tmux-history--with-tmux-mock '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 1 "no server running")) @@ -250,89 +193,128 @@ ghostel-mode terminal." (when (buffer-live-p agent) (kill-buffer agent))))) -(ert-deftest test-term-in-tmux-p-nil-when-not-ghostel-mode () - "Boundary: predicate refuses non-ghostel buffers without calling tmux." - (with-temp-buffer - (let ((tmux-called nil)) - (cl-letf (((symbol-function 'process-file) - (lambda (&rest _) (setq tmux-called t) 0))) - (should-not (cj/term--in-tmux-p)) - (should-not tmux-called))))) +;;; copy-mode (tmux path -- the agent terminal case) (ert-deftest test-term-copy-mode-dwim-sends-tmux-prefix-when-attached () - "Normal: with tmux attached, dwim writes C-b [ into the pty so tmux enters -its own copy-mode against the full pane history." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]")) - (sent nil) - (copy-mode-called nil)) + "Normal: with tmux attached, dwim writes C-b [ then C-a into the pty so tmux +enters copy-mode with the cursor at column 0." + (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]")) + (sent nil)) (unwind-protect (with-current-buffer agent (cl-letf (((symbol-function 'get-buffer-process) (lambda (_buffer) 'fake-process)) ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8")) - ((symbol-function 'ghostel-send-string) - (lambda (s) (push s sent))) - ((symbol-function 'ghostel-copy-mode) - (lambda () (setq copy-mode-called t)))) + (lambda (_process &rest _) "/dev/pts/8")) + ((symbol-function 'cj/--term-send-string) + (lambda (s) (push s sent)))) (test-term-tmux-history--with-tmux-mock '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 "/dev/pts/8\t%8\n")) (cj/term-copy-mode-dwim) - (should (equal sent '("\C-b["))) - (should-not copy-mode-called)))) + (should (equal sent '("\C-b[\C-a")))))) (when (buffer-live-p agent) (kill-buffer agent))))) -(ert-deftest test-term-copy-mode-dwim-falls-back-without-tmux () - "Boundary: without tmux, dwim calls `ghostel-copy-mode' and sends nothing." - (let ((agent (cj/test--make-fake-ghostel-buffer "agent [emacs.d]")) - (sent nil) - (copy-mode-called nil)) +(ert-deftest test-term-copy-mode-up-tmux-enters-then-scrolls-up () + "Normal: from a live (non-copy) tmux pane, C-<up> enters copy-mode then sends +the up-arrow, so one stroke both enters copy-mode and scrolls up." + (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]")) + (sent nil)) (unwind-protect (with-current-buffer agent (cl-letf (((symbol-function 'get-buffer-process) (lambda (_buffer) 'fake-process)) ((symbol-function 'process-tty-name) - (lambda (_process) "/dev/pts/8")) - ((symbol-function 'ghostel-send-string) - (lambda (s) (push s sent))) - ((symbol-function 'ghostel-copy-mode) - (lambda () (setq copy-mode-called t)))) + (lambda (_process &rest _) "/dev/pts/8")) + ((symbol-function 'cj/--term-send-string) + (lambda (s) (push s sent)))) (test-term-tmux-history--with-tmux-mock - '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 1 - "no server running")) - (cj/term-copy-mode-dwim) - (should-not sent) - (should copy-mode-called)))) + '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 + "/dev/pts/8\t%8\n") + (("display-message" "-p" "-t" "%8" "#{pane_in_mode}") 0 "0\n")) + (cj/term-copy-mode-up) + (should (equal (reverse sent) '("\C-b[\C-a" "\e[A")))))) (when (buffer-live-p agent) (kill-buffer agent))))) -(ert-deftest test-term-prefix-and-f12-in-keymap-exceptions () - "Regression: C-; and F12 are in `ghostel-keymap-exceptions' and the rebuilt -semi-char map no longer forwards them to the pty, so the prefix keymap and the -F12 toggle reach Emacs inside ghostel buffers." - (dolist (key '("C-;" "<f12>")) - (should (member key ghostel-keymap-exceptions))) - (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "<f12>") - 'ghostel--send-event))) - -(ert-deftest test-term-window-nav-keys-in-keymap-exceptions () - "Regression: windmove (S-arrows) and buffer-move (C-M-arrows) are in -`ghostel-keymap-exceptions' so they reach Emacs from inside a ghostel buffer -instead of being forwarded to the terminal program." - (dolist (key '("S-<up>" "S-<down>" "S-<left>" "S-<right>" - "C-M-<up>" "C-M-<down>" "C-M-<left>" "C-M-<right>")) - (should (member key ghostel-keymap-exceptions))) - (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "C-M-<left>") - 'ghostel--send-event))) - -(ert-deftest test-term-c-spc-forwarded-not-set-mark () - "Regression: C-SPC is forwarded to the terminal, not bound to the global -`set-mark-command'. ghostel only forwards the `C-@' event, so without this an -Emacs region gets stuck in the ghostel buffer and tmux copy-mode's -begin-selection never starts." - (should (eq (keymap-lookup ghostel-mode-map "C-SPC") #'cj/term-send-C-SPC))) +(ert-deftest test-term-copy-mode-up-tmux-already-in-mode-just-scrolls () + "Normal: when the tmux pane is already in copy-mode, C-<up> only sends the +up-arrow -- it does not re-enter and reset the cursor." + (let ((agent (cj/test--make-fake-eat-buffer "agent [emacs.d]")) + (sent nil)) + (unwind-protect + (with-current-buffer agent + (cl-letf (((symbol-function 'get-buffer-process) + (lambda (_buffer) 'fake-process)) + ((symbol-function 'process-tty-name) + (lambda (_process &rest _) "/dev/pts/8")) + ((symbol-function 'cj/--term-send-string) + (lambda (s) (push s sent)))) + (test-term-tmux-history--with-tmux-mock + '((("list-clients" "-F" "#{client_tty}\t#{pane_id}") 0 + "/dev/pts/8\t%8\n") + (("display-message" "-p" "-t" "%8" "#{pane_in_mode}") 0 "1\n")) + (cj/term-copy-mode-up) + (should (equal (reverse sent) '("\e[A")))))) + (when (buffer-live-p agent) + (kill-buffer agent))))) + +;;; bindings + +(ert-deftest test-term-keymap-history-and-copy-bindings () + "Normal: the C-; x terminal map owns the tmux-history and copy-mode commands." + (should (eq (keymap-lookup cj/custom-keymap "x h") #'cj/term-tmux-history)) + (should (eq (keymap-lookup cj/custom-keymap "x c") #'cj/term-copy-mode-dwim)) + (should (eq (keymap-lookup cj/custom-keymap "x t") #'cj/term-toggle))) + +(ert-deftest test-term-copy-mode-up-bound-in-eat-semi-char-map () + "Normal: C-<up> enters copy-mode + scrolls up from inside an EAT terminal." + (should (eq (keymap-lookup eat-semi-char-mode-map "C-<up>") + #'cj/term-copy-mode-up))) + +(ert-deftest test-term-escape-bound-as-unified-exit () + "Normal: Escape sends ESC in semi-char mode (cancels tmux copy-mode) and +returns to semi-char from EAT's emacs/char mode -- one exit key for both." + (should (eq (keymap-lookup eat-semi-char-mode-map "<escape>") + #'cj/term-send-escape)) + (should (eq (keymap-lookup eat-mode-map "<escape>") #'eat-semi-char-mode))) + +(ert-deftest test-term-send-escape-writes-esc-to-pty () + "Normal: `cj/term-send-escape' sends a bare ESC to the terminal process." + (let ((sent nil)) + (cl-letf (((symbol-function 'cj/--term-send-string) + (lambda (s) (push s sent)))) + (cj/term-send-escape) + (should (equal sent '("\e")))))) + +(ert-deftest test-term-word-motion-arrows-forwarded-not-window-arrows () + "Normal: C-/M-left/right forward to the terminal (word motion in the program's +input) instead of moving Emacs point; windmove's S-arrows still reach Emacs." + (dolist (key '("C-<left>" "C-<right>" "M-<left>" "M-<right>")) + (should (eq (keymap-lookup eat-semi-char-mode-map key) #'eat-self-input))) + (dolist (key '("S-<left>" "S-<right>")) + (should-not (eq (keymap-lookup eat-semi-char-mode-map key) #'eat-self-input)))) + +(ert-deftest test-term-eat-tame-scroll-sets-minimal-scroll () + "Normal: `cj/--eat-tame-scroll' sets buffer-local minimal-scroll behavior so +the EAT window line-scrolls instead of recentering on full-frame redraws." + (with-temp-buffer + (cj/--eat-tame-scroll) + (should (= scroll-conservatively 101)) + (should (= scroll-margin 0)) + (should (null auto-window-vscroll)))) + +(ert-deftest test-term-eat-reset-sgr-at-newline () + "Normal: the SGR-reset advice injects a reset before each newline when enabled +\(containing an unterminated color), and passes output through unchanged when +disabled." + (let ((cj/eat-reset-sgr-at-newline t)) + (should (equal (cj/--eat-reset-sgr-at-newline (list (quote term) "a\nb\n")) + (list (quote term) "a\e[0m\nb\e[0m\n")))) + (let ((cj/eat-reset-sgr-at-newline nil)) + (should (equal (cj/--eat-reset-sgr-at-newline (list (quote term) "a\nb\n")) + (list (quote term) "a\nb\n"))))) (provide 'test-term-tmux-history) ;;; test-term-tmux-history.el ends here |
