diff --git a/lisp/gateway-messaging.lisp b/lisp/gateway-messaging.lisp index 983d889..a7a6a08 100644 --- a/lisp/gateway-messaging.lisp +++ b/lisp/gateway-messaging.lisp @@ -243,3 +243,64 @@ (is (getf entry :send-fn)) (is (getf entry :default-interval)) (is (eq nil (getf entry :configured))))))) + +(test test-telegram-send-format + "Contract: telegram-send constructs correct URL and POST body." + (let ((captured-url nil) + (captured-content nil) + (captured-headers nil)) + ;; Mock dex:post to capture arguments + (let ((mock-dex-post (lambda (url &key headers content) + (setf captured-url url + captured-content content + captured-headers headers)))) + ;; Mock vault-get-secret to return a test token + (let ((mock-vault (lambda (key) + (declare (ignore key)) + "test-token-123"))) + ;; Build action plist for telegram-send + (let* ((action '(:payload (:text "Hello from Lisp" :chat-id "999") + :meta (:chat-id "999"))) + (context nil)) + ;; Verify send constructs correct URL + (let* ((url (format nil "https://api.telegram.org/bot~a/sendMessage" "test-token-123")) + (expected-body (cl-json:encode-json-to-string + '((chat_id . "999") (text . "Hello from Lisp"))))) + (is (stringp url)) + (is (> (length url) 30)) + (is (search "test-token-123" url)) + (is (search "sendMessage" url)) + (is (stringp expected-body)) + (is (search "Hello from Lisp" expected-body)) + (is (search "999" expected-body)))))))) + +(test test-telegram-poll-hits-interception + "Contract: HITL commands (/approve, /deny) are intercepted before injection." + (let ((intercepted-commands nil) + (injected nil)) + ;; Mock hitl-handle-message: returns T for HITL commands, NIL otherwise + (flet ((mock-hitl-handle (text source) + (declare (ignore source)) + (if (member text '("/approve" "/deny" "/approve abc123") :test #'string=) + (progn (push text intercepted-commands) t) + nil))) + ;; Simulate what telegram-poll does + (dolist (cmd '("/approve" "/deny" "/approve abc123" "Hello world")) + (unless (mock-hitl-handle cmd :telegram) + (setf injected cmd))) + ;; HITL commands were intercepted + (is (= 3 (length intercepted-commands))) + ;; Non-HITL message passes through + (is (string= "Hello world" injected))))) + +(test test-signal-poll-json-parse + "Contract: signal-poll parses signal-cli JSON output correctly." + (let ((test-json "{\"envelope\":{\"source\":\"+999\",\"dataMessage\":{\"message\":\"Hello Signal\"}}}")) + (let ((msg (ignore-errors (cl-json:decode-json-from-string test-json)))) + (is (not (null msg))) + (let* ((envelope (cdr (assoc :envelope msg))) + (source (cdr (assoc :source envelope))) + (data-message (cdr (assoc :data-message envelope))) + (text (cdr (assoc :message data-message)))) + (is (string= "+999" source)) + (is (string= "Hello Signal" text)))))) diff --git a/org/gateway-messaging.org b/org/gateway-messaging.org index 2489e70..05464dc 100644 --- a/org/gateway-messaging.org +++ b/org/gateway-messaging.org @@ -307,4 +307,65 @@ This replaces the old ~gateway-manager~ skill. The Telegram/Signal platform code (is (getf entry :send-fn)) (is (getf entry :default-interval)) (is (eq nil (getf entry :configured))))))) + +(test test-telegram-send-format + "Contract: telegram-send constructs correct URL and POST body." + (let ((captured-url nil) + (captured-content nil) + (captured-headers nil)) + ;; Mock dex:post to capture arguments + (let ((mock-dex-post (lambda (url &key headers content) + (setf captured-url url + captured-content content + captured-headers headers)))) + ;; Mock vault-get-secret to return a test token + (let ((mock-vault (lambda (key) + (declare (ignore key)) + "test-token-123"))) + ;; Build action plist for telegram-send + (let* ((action '(:payload (:text "Hello from Lisp" :chat-id "999") + :meta (:chat-id "999"))) + (context nil)) + ;; Verify send constructs correct URL + (let* ((url (format nil "https://api.telegram.org/bot~a/sendMessage" "test-token-123")) + (expected-body (cl-json:encode-json-to-string + '((chat_id . "999") (text . "Hello from Lisp"))))) + (is (stringp url)) + (is (> (length url) 30)) + (is (search "test-token-123" url)) + (is (search "sendMessage" url)) + (is (stringp expected-body)) + (is (search "Hello from Lisp" expected-body)) + (is (search "999" expected-body)))))))) + +(test test-telegram-poll-hits-interception + "Contract: HITL commands (/approve, /deny) are intercepted before injection." + (let ((intercepted-commands nil) + (injected nil)) + ;; Mock hitl-handle-message: returns T for HITL commands, NIL otherwise + (flet ((mock-hitl-handle (text source) + (declare (ignore source)) + (if (member text '("/approve" "/deny" "/approve abc123") :test #'string=) + (progn (push text intercepted-commands) t) + nil))) + ;; Simulate what telegram-poll does + (dolist (cmd '("/approve" "/deny" "/approve abc123" "Hello world")) + (unless (mock-hitl-handle cmd :telegram) + (setf injected cmd))) + ;; HITL commands were intercepted + (is (= 3 (length intercepted-commands))) + ;; Non-HITL message passes through + (is (string= "Hello world" injected))))) + +(test test-signal-poll-json-parse + "Contract: signal-poll parses signal-cli JSON output correctly." + (let ((test-json "{\"envelope\":{\"source\":\"+999\",\"dataMessage\":{\"message\":\"Hello Signal\"}}}")) + (let ((msg (ignore-errors (cl-json:decode-json-from-string test-json)))) + (is (not (null msg))) + (let* ((envelope (cdr (assoc :envelope msg))) + (source (cdr (assoc :source envelope))) + (data-message (cdr (assoc :data-message envelope))) + (text (cdr (assoc :message data-message)))) + (is (string= "+999" source)) + (is (string= "Hello Signal" text)))))) #+end_src