diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-27 21:55:39 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-27 21:55:39 -0500 |
| commit | bfec0eab1132b7713a35500323e06ebea2da17a4 (patch) | |
| tree | 6a41cdb82ec097713d614126262c677eb546c8cd /tests | |
| parent | 2a6ffb0e6a10fdf4fd42fe3e5689f300b25c7cf6 (diff) | |
| download | dotemacs-bfec0eab1132b7713a35500323e06ebea2da17a4.tar.gz dotemacs-bfec0eab1132b7713a35500323e06ebea2da17a4.zip | |
test(signel): cover the JSON-RPC success-result dispatch contract
The fork commit (4740d97 in the signel fork) added a request-callback table, extended signel--send-rpc with an optional success-callback, and routed result responses through signel--dispatch. These tests lock that contract from the consuming project so a future fork change can't silently break the picker that will read listContacts through it.
Five tests, Normal / Boundary / Error categories plus a reconnect-invalidation case:
- result-invokes-callback (Normal): a result response with a registered id fires the callback with the value and removes the handler.
- send-rpc-registers-success-callback (Normal): passing a success callback stores it under the returned id.
- unknown-id-is-noop (Boundary): a result with no registered id is silent — no receive or error handler fires, map stays empty.
- error-cleans-up-handler (Error): an error response removes the handler without firing the callback, so a retry starts clean.
- stop-clears-handler-map: signel-stop empties the map, so a restart can't replay stale callbacks waiting on responses that will never arrive.
The dispatch tests synthesize JSON alists directly. No live process is needed. The send-rpc test stubs get-process and process-send-string so it doesn't need a running signal-cli.
Refactor audit on signel.el surfaced one unrelated pre-existing smell I'm not fixing here: signel--handle-error reads from signel--request-buffer-map but never remhashes, so error responses leak request-id → buffer-name entries. Filed as a separate [#C] follow-up under the Signal parent task; the maps clear on stop/start so the impact is bounded to a single live session.
todo.org: the dispatch task flips to DOING (the fork commit is in, the test contract is locked) and gets the leak follow-up appended.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-signel-rpc-dispatch.el | 94 |
1 files changed, 94 insertions, 0 deletions
diff --git a/tests/test-signel-rpc-dispatch.el b/tests/test-signel-rpc-dispatch.el new file mode 100644 index 00000000..5ae023d6 --- /dev/null +++ b/tests/test-signel-rpc-dispatch.el @@ -0,0 +1,94 @@ +;;; test-signel-rpc-dispatch.el --- Tests for signel JSON-RPC success-result dispatch -*- lexical-binding: t; -*- + +;;; Commentary: +;; signel's JSON-RPC dispatch (signel.el in the fork at ~/code/signel) routes +;; incoming `receive' notifications and errors, but successful +;; `((id . N) (result . VALUE))' responses had no path until this work added a +;; request-callback table. These tests cover the new behavior: a registered +;; callback fires with the result and is then removed; an error response +;; also removes the handler so a retry starts clean; an unregistered id is a +;; silent no-op; passing SUCCESS-CALLBACK to `signel--send-rpc' registers it +;; under the returned id. +;; +;; The dispatch tests exercise `signel--dispatch' directly with synthetic JSON +;; alists; no live process is needed. The send-rpc test stubs `get-process' +;; and `process-send-string' so it doesn't require a running signal-cli. + +;;; Code: + +(require 'ert) +(require 'cl-lib) + +(eval-and-compile + (add-to-list 'load-path (expand-file-name "~/code/signel"))) +(require 'signel) + +(defun test-signel-rpc--reset () + "Reset signel dispatch state to a clean baseline before each test." + (clrhash signel--request-handler-map) + (clrhash signel--request-buffer-map) + (setq signel--rpc-id-counter 0)) + +(ert-deftest test-signel-rpc-dispatch-result-invokes-callback () + "Normal: a result response with a registered id fires the callback with the +result value and removes the handler." + (test-signel-rpc--reset) + (let ((captured nil)) + (puthash 7 (lambda (val) (setq captured val)) signel--request-handler-map) + (signel--dispatch '((jsonrpc . "2.0") (id . 7) + (result . ((contacts . [1 2 3]))))) + (should (equal captured '((contacts . [1 2 3])))) + (should-not (gethash 7 signel--request-handler-map)))) + +(ert-deftest test-signel-rpc-dispatch-unknown-id-is-noop () + "Boundary: a result response with an unregistered id is a silent no-op: +neither receive nor error handler fires, and the handler map stays empty." + (test-signel-rpc--reset) + (let ((called nil)) + (cl-letf (((symbol-function 'signel--handle-error) + (lambda (&rest _) (setq called 'error))) + ((symbol-function 'signel--handle-receive) + (lambda (&rest _) (setq called 'receive)))) + (signel--dispatch '((jsonrpc . "2.0") (id . 99) (result . "anything")))) + (should-not called) + (should (zerop (hash-table-count signel--request-handler-map))))) + +(ert-deftest test-signel-rpc-dispatch-error-cleans-up-handler () + "Error: an error response with a registered id removes the handler without +firing the callback, leaving the map clean for a retry." + (test-signel-rpc--reset) + (let ((fired nil)) + (puthash 11 (lambda (&rest _) (setq fired t)) + signel--request-handler-map) + (cl-letf (((symbol-function 'signel--handle-error) (lambda (&rest _) nil))) + (signel--dispatch '((jsonrpc . "2.0") (id . 11) + (error . ((code . -1) (message . "boom")))))) + (should-not fired) + (should-not (gethash 11 signel--request-handler-map)))) + +(ert-deftest test-signel-rpc-send-rpc-registers-success-callback () + "Normal: passing a SUCCESS-CALLBACK to `signel--send-rpc' stores it under +the returned id so the matching response can route to it." + (test-signel-rpc--reset) + (let ((cb (lambda (_) 'ok)) + (sent nil)) + (cl-letf (((symbol-function 'get-process) (lambda (&rest _) 'fake-proc)) + ((symbol-function 'process-send-string) + (lambda (_ s) (setq sent s))) + ((symbol-function 'signel--log) (lambda (&rest _) nil))) + (let ((id (signel--send-rpc "listContacts" nil nil cb))) + (should (eq cb (gethash id signel--request-handler-map))) + (should (stringp sent)))))) + +(ert-deftest test-signel-rpc-stop-clears-handler-map () + "Normal (reconnect-invalidation): `signel-stop' clears the handler map so a +restart starts with no stale callbacks waiting for responses that will never +arrive." + (test-signel-rpc--reset) + (puthash 13 (lambda (&rest _) nil) signel--request-handler-map) + (cl-letf (((symbol-function 'get-process) (lambda (&rest _) nil))) + (signel-stop)) + (should (zerop (hash-table-count signel--request-handler-map)))) + +(provide 'test-signel-rpc-dispatch) +;;; test-signel-rpc-dispatch.el ends here |
