From aeeeada9f5d8bb63464e42cb41605efbedca5787 Mon Sep 17 00:00:00 2001 From: Henry Park Date: Thu, 12 Mar 2026 16:38:17 -0700 Subject: [PATCH] test: add event-trigger routine e2e coverage --- tests/e2e_advanced_traces.rs | 138 ++++++++++++++++++ .../advanced/routine_event_any_channel.json | 54 +++++++ .../advanced/routine_event_telegram.json | 55 +++++++ 3 files changed, 247 insertions(+) create mode 100644 tests/fixtures/llm_traces/advanced/routine_event_any_channel.json create mode 100644 tests/fixtures/llm_traces/advanced/routine_event_telegram.json diff --git a/tests/e2e_advanced_traces.rs b/tests/e2e_advanced_traces.rs index 7b114d289c..0182d99950 100644 --- a/tests/e2e_advanced_traces.rs +++ b/tests/e2e_advanced_traces.rs @@ -9,6 +9,10 @@ mod support; mod advanced { use std::time::Duration; + use ironclaw::agent::routine::Trigger; + use ironclaw::channels::IncomingMessage; + use ironclaw::db::Database; + use crate::support::cleanup::CleanupGuard; use crate::support::test_rig::TestRigBuilder; use crate::support::trace_llm::LlmTrace; @@ -19,6 +23,28 @@ mod advanced { ); const TIMEOUT: Duration = Duration::from_secs(30); + async fn wait_for_routine_run( + db: &std::sync::Arc, + routine_id: uuid::Uuid, + timeout: Duration, + ) -> Vec { + let deadline = tokio::time::Instant::now() + timeout; + loop { + let runs = db + .list_routine_runs(routine_id, 10) + .await + .expect("list_routine_runs"); + if !runs.is_empty() { + return runs; + } + assert!( + tokio::time::Instant::now() < deadline, + "timed out waiting for routine run" + ); + tokio::time::sleep(Duration::from_millis(100)).await; + } + } + // ----------------------------------------------------------------------- // 1. Multi-turn memory coherence // ----------------------------------------------------------------------- @@ -380,6 +406,118 @@ mod advanced { rig.shutdown(); } + // ----------------------------------------------------------------------- + // 6b. Event routine: Telegram-scoped trigger fires on matching message + // ----------------------------------------------------------------------- + + #[tokio::test] + async fn routine_event_trigger_telegram_channel_fires() { + let trace = LlmTrace::from_file(format!("{FIXTURES}/routine_event_telegram.json")).unwrap(); + let rig = TestRigBuilder::new() + .with_trace(trace.clone()) + .with_routines() + .with_auto_approve_tools(true) + .build() + .await; + + rig.send_message( + "Create a routine that watches Telegram messages starting with 'bug:' and alerts me.", + ) + .await; + let create_responses = rig.wait_for_responses(1, TIMEOUT).await; + rig.verify_trace_expects(&trace, &create_responses); + + let routine = rig + .database() + .get_routine_by_name("test-user", "telegram-bug-watcher") + .await + .expect("get_routine_by_name") + .expect("telegram-bug-watcher should exist"); + + match &routine.trigger { + Trigger::Event { channel, pattern } => { + assert_eq!(channel.as_deref(), Some("telegram")); + assert_eq!(pattern, "^bug\\b"); + } + other => panic!("expected event trigger, got {other:?}"), + } + + rig.send_incoming(IncomingMessage::new( + "telegram", + "test-user", + "bug: home button broken", + )) + .await; + + let runs = wait_for_routine_run(rig.database(), routine.id, TIMEOUT).await; + assert_eq!(runs[0].trigger_type, "event"); + + let responses = rig.wait_for_responses(3, TIMEOUT).await; + assert!( + responses.iter().any(|response| { + response + .metadata + .get("source") + .and_then(|value| value.as_str()) + == Some("routine") + && response.content.contains("telegram-bug-watcher") + && response.content.contains("Bug report detected") + }), + "expected routine notification in responses: {responses:?}" + ); + + rig.shutdown(); + } + + // ----------------------------------------------------------------------- + // 6c. Event routine without channel filter still fires on Telegram + // ----------------------------------------------------------------------- + + #[tokio::test] + async fn routine_event_trigger_without_channel_filter_still_fires() { + let trace = + LlmTrace::from_file(format!("{FIXTURES}/routine_event_any_channel.json")).unwrap(); + let rig = TestRigBuilder::new() + .with_trace(trace) + .with_routines() + .with_auto_approve_tools(true) + .build() + .await; + + rig.send_message( + "Create a routine that watches messages starting with 'bug:' and alerts me.", + ) + .await; + let _ = rig.wait_for_responses(1, TIMEOUT).await; + + let routine = rig + .database() + .get_routine_by_name("test-user", "any-channel-bug-watcher") + .await + .expect("get_routine_by_name") + .expect("any-channel-bug-watcher should exist"); + + match &routine.trigger { + Trigger::Event { channel, pattern } => { + assert_eq!(channel, &None); + assert_eq!(pattern, "^bug\\b"); + } + other => panic!("expected event trigger, got {other:?}"), + } + + rig.send_incoming(IncomingMessage::new( + "telegram", + "test-user", + "bug: login button broken", + )) + .await; + + let runs = wait_for_routine_run(rig.database(), routine.id, TIMEOUT).await; + assert_eq!(runs[0].trigger_type, "event"); + + rig.shutdown(); + } + // ----------------------------------------------------------------------- // 7. Prompt injection resilience // ----------------------------------------------------------------------- diff --git a/tests/fixtures/llm_traces/advanced/routine_event_any_channel.json b/tests/fixtures/llm_traces/advanced/routine_event_any_channel.json new file mode 100644 index 0000000000..6ff2ec54dd --- /dev/null +++ b/tests/fixtures/llm_traces/advanced/routine_event_any_channel.json @@ -0,0 +1,54 @@ +{ + "model_name": "advanced-routine-event-any-channel", + "expects": { + "tools_used": ["routine_create"], + "all_tools_succeeded": true, + "min_responses": 1 + }, + "steps": [ + { + "response": { + "type": "tool_calls", + "tool_calls": [ + { + "id": "call_routine_create_event_any_channel", + "name": "routine_create", + "arguments": { + "name": "any-channel-bug-watcher", + "description": "Watch bug reports from any incoming channel.", + "trigger_type": "event", + "event_pattern": "^bug\\b", + "prompt": "Summarize the bug report in one line." + } + } + ], + "input_tokens": 130, + "output_tokens": 38 + } + }, + { + "response": { + "type": "text", + "content": "Created the any-channel-bug-watcher routine for bug messages.", + "input_tokens": 170, + "output_tokens": 18 + } + }, + { + "response": { + "type": "text", + "content": "I saw the Telegram message.", + "input_tokens": 90, + "output_tokens": 12 + } + }, + { + "response": { + "type": "text", + "content": "Bug report detected: login button broken.", + "input_tokens": 120, + "output_tokens": 14 + } + } + ] +} diff --git a/tests/fixtures/llm_traces/advanced/routine_event_telegram.json b/tests/fixtures/llm_traces/advanced/routine_event_telegram.json new file mode 100644 index 0000000000..afa38062f2 --- /dev/null +++ b/tests/fixtures/llm_traces/advanced/routine_event_telegram.json @@ -0,0 +1,55 @@ +{ + "model_name": "advanced-routine-event-telegram", + "expects": { + "tools_used": ["routine_create"], + "all_tools_succeeded": true, + "min_responses": 1 + }, + "steps": [ + { + "response": { + "type": "tool_calls", + "tool_calls": [ + { + "id": "call_routine_create_event_telegram", + "name": "routine_create", + "arguments": { + "name": "telegram-bug-watcher", + "description": "Watch Telegram bug reports and alert on them.", + "trigger_type": "event", + "event_channel": "telegram", + "event_pattern": "^bug\\b", + "prompt": "Summarize the bug report in one line." + } + } + ], + "input_tokens": 140, + "output_tokens": 40 + } + }, + { + "response": { + "type": "text", + "content": "Created the telegram-bug-watcher routine for Telegram bug messages.", + "input_tokens": 180, + "output_tokens": 20 + } + }, + { + "response": { + "type": "text", + "content": "I saw the Telegram message.", + "input_tokens": 90, + "output_tokens": 12 + } + }, + { + "response": { + "type": "text", + "content": "Bug report detected: home button broken.", + "input_tokens": 120, + "output_tokens": 14 + } + } + ] +}