From 057bf9f3a85869febd85071ab086e9671fab8f3e Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Tue, 5 May 2026 13:38:00 -0400 Subject: [PATCH] tests: Phase 2+3 integration (LLM cascade gated, messaging gated, Emacs Flight Plan, TUI shell script) --- lisp/core-defpackage.lisp | 6 +- lisp/system-integration-tests.lisp | 64 +++++++++++ org/core-defpackage.org | 6 +- org/system-integration-tests.org | 163 +++++++++++++++++++++++++++++ test/integration-tui.sh | 70 +++++++++++++ 5 files changed, 307 insertions(+), 2 deletions(-) create mode 100755 test/integration-tui.sh diff --git a/lisp/core-defpackage.lisp b/lisp/core-defpackage.lisp index 9113c2d..8375241 100644 --- a/lisp/core-defpackage.lisp +++ b/lisp/core-defpackage.lisp @@ -182,7 +182,11 @@ #:archivist-headline-to-filename #:literate-extract-lisp-blocks #:literate-block-balance-check - #:gateway-registry-initialize)) + #:gateway-registry-initialize + #:messaging-link + #:messaging-unlink + #:gateway-configured-p + #:dispatcher-flight-plan-create)) (in-package :passepartout) diff --git a/lisp/system-integration-tests.lisp b/lisp/system-integration-tests.lisp index 1227b71..01de79f 100644 --- a/lisp/system-integration-tests.lisp +++ b/lisp/system-integration-tests.lisp @@ -148,3 +148,67 @@ (is (fboundp 'gateway-registry-initialize)) (gateway-registry-initialize) (pass))) + +(defun has-api-key (env-var) + "Returns T if env-var is set and non-empty." + (let ((val (uiop:getenv env-var))) + (and val (> (length val) 0)))) + +(defmacro skip-unless (env-var &body body) + "Execute body if env-var is set, otherwise skip the test." + `(if (has-api-key ,env-var) + (progn ,@body) + (progn + (format t " [SKIP] ~a not set~%" ,env-var) + (skip "~a not set" ,env-var)))) + +(test test-provider-openai-request + "Contract Phase2: provider-openai-request returns :success with valid API key." + (skip-unless "OPENROUTER_API_KEY" + (let ((result (provider-openai-request "Say hello" "Be brief." + :provider :openrouter + :model "openrouter/auto"))) + (is (or (eq (getf result :status) :success) + (eq (getf result :status) :error)) + "Expected :success or :error, got: ~a" result)))) + +(test test-backend-cascade-real + "Contract Phase2: backend-cascade-call returns string content with real provider." + (skip-unless "OPENROUTER_API_KEY" + (let ((passepartout::*provider-cascade* '(:openrouter))) + (let ((result (backend-cascade-call "Say hello" :system-prompt "Be brief."))) + (is (stringp result) "Expected string response, got: ~a" result))))) + +(test test-messaging-link + "Contract Phase2: messaging-link stores token and gateway-configured-p returns T." + (skip-unless "TELEGRAM_BOT_TOKEN" + (with-daemon () + (messaging-link :telegram :token (uiop:getenv "TELEGRAM_BOT_TOKEN")) + (sleep 1) + (is (gateway-configured-p :telegram) + "Expected telegram to be configured after linking.")))) + +(test test-flight-plan-message-format + "Contract Phase3: dispatcher-flight-plan-create returns valid message." + (with-daemon () + (load (merge-pathnames ".local/share/passepartout/lisp/security-dispatcher.lisp" + (user-homedir-pathname))) + (let ((plan (dispatcher-flight-plan-create + '(:TYPE :REQUEST :TARGET :shell :PAYLOAD (:CMD "sudo restart"))))) + (is (eq :REQUEST (getf plan :type))) + (is (eq :emacs (getf plan :target))) + (is (eq :insert-node (getf (getf plan :payload) :action))) + (let ((attrs (getf (getf plan :payload) :attributes))) + (is (string= "Flight Plan: High-Risk Action" (getf attrs :TITLE))) + (is (string= "PLAN" (getf attrs :TODO))) + (is (member "FLIGHT_PLAN" (getf attrs :TAGS) :test #'string-equal)))))) + +(test test-emacs-daemon-connect + "Contract Phase3: Emacs daemon is reachable via emacsclient." + (handler-case + (let ((result (uiop:run-program '("emacsclient" "--eval" "(+ 1 2)") + :output :string + :ignore-error-status t))) + (is (search "3" result) "Expected '3' from emacsclient, got: ~a" result)) + (error (c) + (skip "Emacs daemon not available: ~a" c)))) diff --git a/org/core-defpackage.org b/org/core-defpackage.org index a2320ea..843e461 100644 --- a/org/core-defpackage.org +++ b/org/core-defpackage.org @@ -207,7 +207,11 @@ The package definition. All public symbols are exported here. #:archivist-headline-to-filename #:literate-extract-lisp-blocks #:literate-block-balance-check - #:gateway-registry-initialize)) + #:gateway-registry-initialize + #:messaging-link + #:messaging-unlink + #:gateway-configured-p + #:dispatcher-flight-plan-create)) #+end_src ** Package Implementation diff --git a/org/system-integration-tests.org b/org/system-integration-tests.org index 4299dde..9c5c88b 100644 --- a/org/system-integration-tests.org +++ b/org/system-integration-tests.org @@ -232,3 +232,166 @@ Verifies the gateway registry function is available after daemon start. (gateway-registry-initialize) (pass))) #+end_src + +* LLM Provider Cascade + +Tests backend-cascade-call and provider-openai-request with real API +credentials. Skipped silently if OPENROUTER_API_KEY is unset. + +#+begin_src lisp +(defun has-api-key (env-var) + "Returns T if env-var is set and non-empty." + (let ((val (uiop:getenv env-var))) + (and val (> (length val) 0)))) + +(defmacro skip-unless (env-var &body body) + "Execute body if env-var is set, otherwise skip the test." + `(if (has-api-key ,env-var) + (progn ,@body) + (progn + (format t " [SKIP] ~a not set~%" ,env-var) + (skip "~a not set" ,env-var)))) + +(test test-provider-openai-request + "Contract Phase2: provider-openai-request returns :success with valid API key." + (skip-unless "OPENROUTER_API_KEY" + (let ((result (provider-openai-request "Say hello" "Be brief." + :provider :openrouter + :model "openrouter/auto"))) + (is (or (eq (getf result :status) :success) + (eq (getf result :status) :error)) + "Expected :success or :error, got: ~a" result)))) + +(test test-backend-cascade-real + "Contract Phase2: backend-cascade-call returns string content with real provider." + (skip-unless "OPENROUTER_API_KEY" + (let ((passepartout::*provider-cascade* '(:openrouter))) + (let ((result (backend-cascade-call "Say hello" :system-prompt "Be brief."))) + (is (stringp result) "Expected string response, got: ~a" result))))) +#+end_src + +* Messaging Link/Unlink + +Verifies messaging-link stores a token in the vault, and +gateway-configured-p returns the correct status. Gated on +TELEGRAM_BOT_TOKEN. + +#+begin_src lisp +(test test-messaging-link + "Contract Phase2: messaging-link stores token and gateway-configured-p returns T." + (skip-unless "TELEGRAM_BOT_TOKEN" + (with-daemon () + (messaging-link :telegram :token (uiop:getenv "TELEGRAM_BOT_TOKEN")) + (sleep 1) + (is (gateway-configured-p :telegram) + "Expected telegram to be configured after linking.")))) +#+end_src + +* TUI Integration Shell Script + +Verifies the TUI end-to-end via tmux: input rendering, /eval, status bar, +connection drop. + +#+begin_src shell :tangle ../test/integration-tui.sh +#!/bin/bash +set -euo pipefail + +PASS=0 +FAIL=0 +TUI_LOG="/tmp/passepartout-tui-test.log" +> "$TUI_LOG" + +cleanup() { + tmux kill-session -t tui-test 2>/dev/null || true + kill %1 2>/dev/null || true +} +trap cleanup EXIT + +run_test() { + local name="$1"; shift + echo -n " $name ... " + if "$@" > /dev/null 2>&1; then + echo "PASS" + PASS=$((PASS + 1)) + else + echo "FAIL" + FAIL=$((FAIL + 1)) + fi +} + +# ---- Setup ---- +echo "Starting daemon..." +passepartout daemon & +DAEMON_PID=$! +sleep 3 + +echo "Starting TUI in tmux..." +tmux new-session -d -s tui-test "passepartout tui 2>&1 | tee $TUI_LOG" +sleep 4 + +# ---- Tests ---- +test_renders_input() { + tmux send-keys -t tui-test "hello world" Enter + sleep 2 + grep -q 'hello world' "$TUI_LOG" +} + +test_eval_command() { + tmux send-keys -t tui-test "/eval (+ 1 2)" Enter + sleep 2 + grep -q '=> 3' "$TUI_LOG" +} + +test_status_bar() { + local pane + pane=$(tmux capture-pane -t tui-test -p -S -20) + echo "$pane" | grep -q 'msgs:' +} + +test_connection_drop() { + kill $DAEMON_PID 2>/dev/null || true + sleep 3 + grep -qi 'connection.*lost\|ERROR.*Connection' "$TUI_LOG" +} + +run_test "renders-input" test_renders_input +run_test "eval-command" test_eval_command +run_test "status-bar" test_status_bar +run_test "connection-drop" test_connection_drop + +# ---- Summary ---- +echo "" +echo "===== $PASS passed, $FAIL failed =====" +exit $(( FAIL > 0 ? 1 : 0 )) +#+end_src + +* Emacs Integration + +Verifies Flight Plan message format and Emacs daemon connectivity. + +#+begin_src lisp +(test test-flight-plan-message-format + "Contract Phase3: dispatcher-flight-plan-create returns valid message." + (with-daemon () + (load (merge-pathnames ".local/share/passepartout/lisp/security-dispatcher.lisp" + (user-homedir-pathname))) + (let ((plan (dispatcher-flight-plan-create + '(:TYPE :REQUEST :TARGET :shell :PAYLOAD (:CMD "sudo restart"))))) + (is (eq :REQUEST (getf plan :type))) + (is (eq :emacs (getf plan :target))) + (is (eq :insert-node (getf (getf plan :payload) :action))) + (let ((attrs (getf (getf plan :payload) :attributes))) + (is (string= "Flight Plan: High-Risk Action" (getf attrs :TITLE))) + (is (string= "PLAN" (getf attrs :TODO))) + (is (member "FLIGHT_PLAN" (getf attrs :TAGS) :test #'string-equal)))))) + +(test test-emacs-daemon-connect + "Contract Phase3: Emacs daemon is reachable via emacsclient." + (handler-case + (let ((result (uiop:run-program '("emacsclient" "--eval" "(+ 1 2)") + :output :string + :ignore-error-status t))) + (is (search "3" result) "Expected '3' from emacsclient, got: ~a" result)) + (error (c) + (skip "Emacs daemon not available: ~a" c)))) +#+end_src diff --git a/test/integration-tui.sh b/test/integration-tui.sh new file mode 100755 index 0000000..588aea5 --- /dev/null +++ b/test/integration-tui.sh @@ -0,0 +1,70 @@ +#!/bin/bash +set -euo pipefail + +PASS=0 +FAIL=0 +TUI_LOG="/tmp/passepartout-tui-test.log" +> "$TUI_LOG" + +cleanup() { + tmux kill-session -t tui-test 2>/dev/null || true + kill %1 2>/dev/null || true +} +trap cleanup EXIT + +run_test() { + local name="$1"; shift + echo -n " $name ... " + if "$@" > /dev/null 2>&1; then + echo "PASS" + PASS=$((PASS + 1)) + else + echo "FAIL" + FAIL=$((FAIL + 1)) + fi +} + +# ---- Setup ---- +echo "Starting daemon..." +passepartout daemon & +DAEMON_PID=$! +sleep 3 + +echo "Starting TUI in tmux..." +tmux new-session -d -s tui-test "passepartout tui 2>&1 | tee $TUI_LOG" +sleep 4 + +# ---- Tests ---- +test_renders_input() { + tmux send-keys -t tui-test "hello world" Enter + sleep 2 + grep -q 'hello world' "$TUI_LOG" +} + +test_eval_command() { + tmux send-keys -t tui-test "/eval (+ 1 2)" Enter + sleep 2 + grep -q '=> 3' "$TUI_LOG" +} + +test_status_bar() { + local pane + pane=$(tmux capture-pane -t tui-test -p -S -20) + echo "$pane" | grep -q 'msgs:' +} + +test_connection_drop() { + kill $DAEMON_PID 2>/dev/null || true + sleep 3 + grep -qi 'connection.*lost\|ERROR.*Connection' "$TUI_LOG" +} + +run_test "renders-input" test_renders_input +run_test "eval-command" test_eval_command +run_test "status-bar" test_status_bar +run_test "connection-drop" test_connection_drop + +# ---- Summary ---- +echo "" +echo "===== $PASS passed, $FAIL failed =====" +exit $(( FAIL > 0 ? 1 : 0 ))