diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-27 22:11:24 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-27 22:11:24 -0500 |
| commit | 4daf2328756fa29b087870a2b1f672bc01aa6b51 (patch) | |
| tree | 69e51b9156cbdda8946b4b24c795da2df2685c33 /tests | |
| parent | bfec0eab1132b7713a35500323e06ebea2da17a4 (diff) | |
| download | dotemacs-4daf2328756fa29b087870a2b1f672bc01aa6b51.tar.gz dotemacs-4daf2328756fa29b087870a2b1f672bc01aa6b51.zip | |
feat(signal): initiate-message workflow (picker, guard, cache, keymap)
I built pieces 2-7 of the initiate-message workflow from docs/design/signal-client.org and added tests covering the fork's clobber-fix (commit 5ec56c0 over there). The picker is the user-facing change: a single key opens a name-based completing-read for any contact, with "Note to Self" pinned first.
The picker stack from the bottom up:
cj/signel--ensure-started is the daemon guard. With a live process it's a no-op. With signel-account set but no process it calls signel-start and pre-warms the contact cache. With signel-account nil it user-errors naming the remedy. Pre-warming on start means the picker feels instant on first use.
cj/signel--fetch-contacts issues a listContacts RPC through the new request-callback contract (signel--send-rpc with a success-callback). The callback runs the result through the verified cj/signal--parse-contacts and stores the (LABEL . RECIPIENT) alist in cj/signel--contact-cache, a cj-owned variable kept separate from signel's receive-time contact-map. An empty result populates the cache as nil, distinct from an RPC failure (which never invokes the callback so the prior cache survives). cj/signel-refresh-contacts is the user-facing command that clears and refetches.
cj/signel-message is the picker. Warm cache opens completing-read immediately. Cold cache kicks off a fetch and accept-process-outputs up to cj/signel-fetch-timeout seconds (3s default), then user-errors if the daemon hasn't responded so a wedged process can't hang Emacs. The candidate list pins "Note to Self" first (resolves to signel-account) with a display-sort metadata function that preserves the given order rather than alphabetizing.
cj/signel-message-self skips the picker and goes straight to signel-account. cj/signel-connect is the friendly verb on the prefix key.
cj/signel-prefix-map binds m / s / d / q / SPC and attaches under C-; M via with-eval-after-load keybindings so the binding survives load-order. l stays unbound for the future link command.
15 new ERT tests cover the ensure-started branches, the fetch + cache contract (issued, populated, empty), refresh-contacts, the picker's four scenarios (warm-cache contact, warm-cache Note to Self, cold-cache resolves in time, cold-cache timeout), message-self, and the keymap bindings. Plus 4 new tests in tests/test-signel-input-preservation.el for the fork's clobber fix: pending-input captures typed text and returns nil when empty; both signel--insert-msg and signel--insert-system-msg redraw the prompt without clobbering "halfwritten".
todo.org closes three tasks as dated event-log entries: the contact picker, the input clobber, and the use-package wiring.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-signal-config.el | 208 | ||||
| -rw-r--r-- | tests/test-signel-input-preservation.el | 68 |
2 files changed, 276 insertions, 0 deletions
diff --git a/tests/test-signal-config.el b/tests/test-signal-config.el index 40879342..6ddc7917 100644 --- a/tests/test-signal-config.el +++ b/tests/test-signal-config.el @@ -8,7 +8,16 @@ ;;; Code: (require 'ert) +(require 'cl-lib) (require 'json) + +;; signel is the fork at ~/code/signel; signal-config wires it via +;; use-package but the connection-guard/fetch tests need the symbols +;; available directly. +(eval-and-compile + (add-to-list 'load-path (expand-file-name "~/code/signel"))) +(require 'signel) + (require 'signal-config) ;;; cj/signal--jstr @@ -137,5 +146,204 @@ against a live linked account on 2026-05-26; the values here are fake.") "Error: a nil viewing-buffer name does not suppress." (should-not (cj/signal--suppress-notify-p "+15551112222" nil t))) +;;; cj/signel--ensure-started + +(ert-deftest test-signal-config-ensure-started-live-process-noop () + "Normal: with a live signel process, ensure-started returns without +calling `signel-start' or the pre-warm fetch." + (let ((start-called nil) + (fetch-called nil)) + (cl-letf (((symbol-function 'process-live-p) (lambda (_) t)) + ((symbol-function 'get-process) (lambda (_) 'fake-proc)) + ((symbol-function 'signel-start) + (lambda () (setq start-called t))) + ((symbol-function 'cj/signel--fetch-contacts) + (lambda (&rest _) (setq fetch-called t)))) + (cj/signel--ensure-started) + (should-not start-called) + (should-not fetch-called)))) + +(ert-deftest test-signal-config-ensure-started-starts-when-account-set () + "Normal: with `signel-account' set and no live process, ensure-started +calls `signel-start' to bring the daemon up." + (let ((start-called nil) + (signel-account "+15555550100")) + (cl-letf (((symbol-function 'process-live-p) (lambda (_) nil)) + ((symbol-function 'get-process) (lambda (_) nil)) + ((symbol-function 'signel-start) + (lambda () (setq start-called t))) + ((symbol-function 'cj/signel--fetch-contacts) + (lambda (&rest _) nil))) + (cj/signel--ensure-started) + (should start-called)))) + +(ert-deftest test-signal-config-ensure-started-prewarms-on-start () + "Normal: when ensure-started actually starts the daemon, it triggers a +pre-warm fetch so the picker cache is warm on first invocation." + (let ((fetch-called nil) + (signel-account "+15555550100")) + (cl-letf (((symbol-function 'process-live-p) (lambda (_) nil)) + ((symbol-function 'get-process) (lambda (_) nil)) + ((symbol-function 'signel-start) (lambda () nil)) + ((symbol-function 'cj/signel--fetch-contacts) + (lambda (&rest _) (setq fetch-called t)))) + (cj/signel--ensure-started) + (should fetch-called)))) + +(ert-deftest test-signal-config-ensure-started-errors-when-no-account () + "Error: with `signel-account' nil, ensure-started signals a user-error +naming the remedy (set the account in the private config) instead of +starting an account-less daemon." + (let ((signel-account nil)) + (cl-letf (((symbol-function 'process-live-p) (lambda (_) nil)) + ((symbol-function 'get-process) (lambda (_) nil))) + (should-error (cj/signel--ensure-started) :type 'user-error)))) + +;;; cj/signel--fetch-contacts + cj/signel--contact-cache + +(ert-deftest test-signal-config-fetch-contacts-issues-list-contacts-rpc () + "Normal: fetch-contacts sends a `listContacts' RPC and registers a +success callback so the response routes back." + (let (sent-method sent-callback) + (cl-letf (((symbol-function 'signel--send-rpc) + (lambda (method _params _target callback) + (setq sent-method method + sent-callback callback) + 1))) + (cj/signel--fetch-contacts)) + (should (equal sent-method "listContacts")) + (should (functionp sent-callback)))) + +(ert-deftest test-signal-config-fetch-contacts-callback-populates-cache () + "Normal: on a successful result, the callback parses the contact list +and stores the (LABEL . RECIPIENT) alist in `cj/signel--contact-cache'." + (let (sent-callback) + (cl-letf (((symbol-function 'signel--send-rpc) + (lambda (_method _params _target callback) + (setq sent-callback callback) 1))) + (setq cj/signel--contact-cache nil) + (cj/signel--fetch-contacts) + (funcall sent-callback + [((number . "+15555550100") (givenName . "Alice"))])) + (should (equal cj/signel--contact-cache + '(("Alice (+15555550100)" . "+15555550100")))))) + +(ert-deftest test-signal-config-fetch-contacts-empty-result-clears-cache () + "Boundary: an empty listContacts result populates the cache as nil, +distinct from a failure path (which never invokes the success callback)." + (let (sent-callback) + (cl-letf (((symbol-function 'signel--send-rpc) + (lambda (_method _params _target callback) + (setq sent-callback callback) 1))) + (setq cj/signel--contact-cache '(("stale" . "+10000000000"))) + (cj/signel--fetch-contacts) + (funcall sent-callback [])) + (should-not cj/signel--contact-cache))) + +;;; cj/signel-refresh-contacts + +(ert-deftest test-signal-config-refresh-contacts-clears-and-refetches () + "Normal: `cj/signel-refresh-contacts' clears the cache and triggers a +fresh fetch so a stale entry can't survive a user-driven refresh." + (let ((fetch-called nil)) + (setq cj/signel--contact-cache '(("stale" . "+10000000000"))) + (cl-letf (((symbol-function 'cj/signel--fetch-contacts) + (lambda (&rest _) (setq fetch-called t)))) + (cj/signel-refresh-contacts)) + (should-not cj/signel--contact-cache) + (should fetch-called))) + +;;; cj/signel-message picker + +(ert-deftest test-signal-config-message-warm-cache-picks-contact () + "Normal: with a warm cache, picking a contact label opens that +recipient's chat buffer." + (let ((chosen-recipient nil) + (signel-account "+15555550100")) + (setq cj/signel--contact-cache + '(("Alice (+15555550200)" . "+15555550200"))) + (cl-letf (((symbol-function 'cj/signel--ensure-started) (lambda () nil)) + ((symbol-function 'completing-read) + (lambda (&rest _) "Alice (+15555550200)")) + ((symbol-function 'signel-chat) + (lambda (r) (setq chosen-recipient r)))) + (cj/signel-message)) + (should (equal chosen-recipient "+15555550200")))) + +(ert-deftest test-signal-config-message-warm-cache-picks-note-to-self () + "Normal: the pinned `Note to Self' entry resolves to `signel-account' +so a self-message lands in the Signal Note-to-Self thread." + (let ((chosen-recipient nil) + (signel-account "+15555550100")) + (setq cj/signel--contact-cache + '(("Alice (+15555550200)" . "+15555550200"))) + (cl-letf (((symbol-function 'cj/signel--ensure-started) (lambda () nil)) + ((symbol-function 'completing-read) + (lambda (&rest _) "Note to Self")) + ((symbol-function 'signel-chat) + (lambda (r) (setq chosen-recipient r)))) + (cj/signel-message)) + (should (equal chosen-recipient "+15555550100")))) + +(ert-deftest test-signal-config-message-cold-cache-fetch-resolves-in-time () + "Normal: cold cache, fetch's after-callback fires inside the bounded +wait, picker proceeds with the now-warm cache." + (let ((chosen-recipient nil) + (signel-account "+15555550100") + (cj/signel-fetch-timeout 1.0)) + (setq cj/signel--contact-cache nil) + (cl-letf (((symbol-function 'cj/signel--ensure-started) (lambda () nil)) + ((symbol-function 'cj/signel--fetch-contacts) + (lambda (&optional after-cb) + (setq cj/signel--contact-cache + '(("Bob (+15555550300)" . "+15555550300"))) + (when after-cb (funcall after-cb)))) + ((symbol-function 'completing-read) + (lambda (&rest _) "Bob (+15555550300)")) + ((symbol-function 'signel-chat) + (lambda (r) (setq chosen-recipient r)))) + (cj/signel-message)) + (should (equal chosen-recipient "+15555550300")))) + +(ert-deftest test-signal-config-message-cold-cache-timeout-errors () + "Error: cold cache, fetch never resolves, picker user-errors before +the bounded wait would let Emacs hang on a dead daemon." + (let ((signel-account "+15555550100") + (cj/signel-fetch-timeout 0.1)) + (setq cj/signel--contact-cache nil) + (cl-letf (((symbol-function 'cj/signel--ensure-started) (lambda () nil)) + ((symbol-function 'cj/signel--fetch-contacts) + (lambda (&rest _) nil))) + (should-error (cj/signel-message) :type 'user-error)))) + +;;; cj/signel-message-self + +(ert-deftest test-signal-config-message-self-calls-signel-chat-with-account () + "Normal: the direct self-message command opens a chat buffer addressed +to `signel-account', skipping the picker entirely." + (let ((chosen-recipient nil) + (signel-account "+15555550100")) + (cl-letf (((symbol-function 'cj/signel--ensure-started) (lambda () nil)) + ((symbol-function 'signel-chat) + (lambda (r) (setq chosen-recipient r)))) + (cj/signel-message-self)) + (should (equal chosen-recipient "+15555550100")))) + +;;; cj/signel-prefix-map (C-; M) + +(ert-deftest test-signal-config-prefix-map-has-expected-bindings () + "Normal: the signel C-; M prefix map binds m / s / d / q / SPC to the +commands the workflow spec names." + (should (eq (keymap-lookup cj/signel-prefix-map "m") + #'cj/signel-message)) + (should (eq (keymap-lookup cj/signel-prefix-map "s") + #'cj/signel-message-self)) + (should (eq (keymap-lookup cj/signel-prefix-map "d") + #'signel-dashboard)) + (should (eq (keymap-lookup cj/signel-prefix-map "q") + #'signel-stop)) + (should (eq (keymap-lookup cj/signel-prefix-map "SPC") + #'cj/signel-connect))) + (provide 'test-signal-config) ;;; test-signal-config.el ends here diff --git a/tests/test-signel-input-preservation.el b/tests/test-signel-input-preservation.el new file mode 100644 index 00000000..e8ce4ddb --- /dev/null +++ b/tests/test-signel-input-preservation.el @@ -0,0 +1,68 @@ +;;; test-signel-input-preservation.el --- Regression for signel #2 input clobber -*- lexical-binding: t; -*- + +;;; Commentary: +;; signel-chat-mode buffers have an editable prompt area starting at +;; `signel--input-marker'. Before this fix, both `signel--insert-msg' (the +;; receive path) and `signel--insert-system-msg' (the RPC-error path) +;; called `(delete-region (point) (point-max))' to clear the old prompt +;; before redrawing it, which destroyed any text the user was mid-typing. +;; +;; These tests lock the preservation contract: a small `signel--pending-input' +;; helper captures the in-progress input from the marker to `point-max', and +;; both inserters restore it after the freshly drawn prompt. The chat-mode +;; buffer is constructed in a temp buffer; `signel--insert-msg' is steered +;; to it via a stub on `signel--get-buffer'. + +;;; Code: + +(require 'ert) +(require 'cl-lib) + +(eval-and-compile + (add-to-list 'load-path (expand-file-name "~/code/signel"))) +(require 'signel) + +(defmacro test-signel--with-chat-buffer (&rest body) + "Set up a temp signel-chat-mode buffer with prompt drawn and run BODY." + (declare (indent 0)) + `(with-temp-buffer + (signel-chat-mode) + (setq signel--chat-id "+15555550100") + (signel--draw-prompt) + ,@body)) + +(ert-deftest test-signel-input-pending-returns-typed-text () + "Normal: with text after the prompt marker, `signel--pending-input' +returns the captured text." + (test-signel--with-chat-buffer + (insert "halfwritten") + (should (equal (signel--pending-input) "halfwritten")))) + +(ert-deftest test-signel-input-pending-returns-nil-when-empty () + "Boundary: an empty input area returns nil so callers don't restore an +empty string after the prompt." + (test-signel--with-chat-buffer + (should-not (signel--pending-input)))) + +(ert-deftest test-signel-input-system-msg-preserves-pending-input () + "Regression for #2: `signel--insert-system-msg' redraws the prompt +without clobbering text the user was mid-typing." + (test-signel--with-chat-buffer + (insert "halfwritten") + (signel--insert-system-msg "An error happened" 'signel-error-face) + (should (string-match-p "An error happened" (buffer-string))) + (should (equal (signel--pending-input) "halfwritten")))) + +(ert-deftest test-signel-input-msg-preserves-pending-input () + "Regression for #2: `signel--insert-msg' (the receive path) redraws the +prompt without clobbering the user's in-progress input." + (test-signel--with-chat-buffer + (insert "halfwritten") + (cl-letf (((symbol-function 'signel--get-buffer) + (lambda (_) (current-buffer)))) + (signel--insert-msg "+15555550100" "Alice" "Hi there" nil nil nil)) + (should (string-match-p "Hi there" (buffer-string))) + (should (equal (signel--pending-input) "halfwritten")))) + +(provide 'test-signel-input-preservation) +;;; test-signel-input-preservation.el ends here |
