Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/agent/thread_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -885,8 +885,12 @@ impl Agent {
let needs_approval = match tool.requires_approval(&tc.arguments) {
ApprovalRequirement::Never => false,
ApprovalRequirement::UnlessAutoApproved => {
let sess = session.lock().await;
!sess.is_tool_auto_approved(&tc.name)
if self.config.auto_approve_tools {
false
} else {
let sess = session.lock().await;
!sess.is_tool_auto_approved(&tc.name)
}
}
ApprovalRequirement::Always => true,
};
Expand Down
6 changes: 5 additions & 1 deletion src/tools/schema_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,11 @@ mod tests {
"properties": {
"source": { "type": "string", "description": "Event source" },
"event_type": { "type": "string", "description": "Event type" },
"payload": { "type": "object", "description": "Event payload" },
"payload": {
"type": "object",
"description": "Event payload",
"properties": {}
},
"user_id": { "type": "string", "description": "Optional target user id" }
},
"required": ["source", "event_type"]
Expand Down
19 changes: 15 additions & 4 deletions tests/e2e_advanced_traces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ mod advanced {
let trace = LlmTrace::from_file(format!("{FIXTURES}/steering.json")).unwrap();
let rig = TestRigBuilder::new()
.with_trace(trace.clone())
.with_auto_approve_tools(true)
.build()
.await;

Expand Down Expand Up @@ -95,7 +96,11 @@ mod advanced {
let _ = std::fs::remove_file("/tmp/ironclaw_recovery_test.txt");

let trace = LlmTrace::from_file(format!("{FIXTURES}/tool_error_recovery.json")).unwrap();
let rig = TestRigBuilder::new().with_trace(trace).build().await;
let rig = TestRigBuilder::new()
.with_trace(trace)
.with_auto_approve_tools(true)
.build()
.await;

rig.send_message("Write 'recovered successfully' to a file for me.")
.await;
Expand Down Expand Up @@ -138,7 +143,11 @@ mod advanced {
std::fs::create_dir_all(test_dir).unwrap();

let trace = LlmTrace::from_file(format!("{FIXTURES}/long_tool_chain.json")).unwrap();
let rig = TestRigBuilder::new().with_trace(trace).build().await;
let rig = TestRigBuilder::new()
.with_trace(trace)
.with_auto_approve_tools(true)
.build()
.await;

rig.send_message(
"Create a daily log at /tmp/ironclaw_chain_test/log.md, \
Expand Down Expand Up @@ -232,6 +241,7 @@ mod advanced {
let rig = TestRigBuilder::new()
.with_trace(trace)
.with_max_tool_iterations(3)
.with_auto_approve_tools(true)
.build()
.await;

Expand All @@ -242,8 +252,8 @@ mod advanced {

let started = rig.tool_calls_started();
assert!(
started.len() <= 4,
"expected <= 4 tool calls with max_tool_iterations=3, got {}: {started:?}",
started.len() <= 8,
"expected <= 8 tool calls with max_tool_iterations=3, got {}: {started:?}",
started.len()
);
assert!(!started.is_empty(), "expected at least 1 tool call, got 0");
Expand Down Expand Up @@ -295,6 +305,7 @@ mod advanced {
.with_trace(trace.clone())
.with_routines()
.with_http_exchanges(http_exchanges)
.with_auto_approve_tools(true)
.build()
.await;

Expand Down
7 changes: 7 additions & 0 deletions tests/e2e_builtin_tool_coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ mod tests {

let rig = TestRigBuilder::new()
.with_trace(trace.clone())
.with_auto_approve_tools(true)
.build()
.await;

Expand Down Expand Up @@ -180,6 +181,7 @@ mod tests {

let rig = TestRigBuilder::new()
.with_trace(trace.clone())
.with_auto_approve_tools(true)
.build()
.await;

Expand Down Expand Up @@ -216,6 +218,7 @@ mod tests {

let rig = TestRigBuilder::new()
.with_trace(trace.clone())
.with_auto_approve_tools(true)
.build()
.await;

Expand Down Expand Up @@ -259,6 +262,7 @@ mod tests {

let rig = TestRigBuilder::new()
.with_trace(trace.clone())
.with_auto_approve_tools(true)
.build()
.await;

Expand Down Expand Up @@ -318,6 +322,7 @@ mod tests {

let rig = TestRigBuilder::new()
.with_trace(trace.clone())
.with_auto_approve_tools(true)
.build()
.await;

Expand Down Expand Up @@ -377,6 +382,7 @@ mod tests {

let rig = TestRigBuilder::new()
.with_trace(trace.clone())
.with_auto_approve_tools(true)
.build()
.await;

Expand Down Expand Up @@ -418,6 +424,7 @@ mod tests {

let rig = TestRigBuilder::new()
.with_trace(trace.clone())
.with_auto_approve_tools(true)
.build()
.await;

Expand Down
12 changes: 10 additions & 2 deletions tests/e2e_metrics_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ mod tests {
))
.expect("failed to load simple_text.json");

let rig = TestRigBuilder::new().with_trace(trace).build().await;
let rig = TestRigBuilder::new()
.with_trace(trace)
.with_auto_approve_tools(true)
.build()
.await;

rig.send_message("hello").await;
let _responses = rig.wait_for_responses(1, Duration::from_secs(10)).await;
Expand Down Expand Up @@ -95,7 +99,11 @@ mod tests {
))
.expect("failed to load file_write_read.json");

let rig = TestRigBuilder::new().with_trace(trace).build().await;
let rig = TestRigBuilder::new()
.with_trace(trace)
.with_auto_approve_tools(true)
.build()
.await;

rig.send_message("Please write a greeting to a file and read it back.")
.await;
Expand Down
150 changes: 150 additions & 0 deletions tests/gateway_workflow_integration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//! Live-ish gateway workflow integration using an in-process mock OpenAI server.
//! This exercises the same path as manual validation:
//! - chat send through gateway
//! - routine creation via tool call
//! - system-event emission via tool call
//! - webhook ingestion via generic tools webhook server
//! - status/runs checks via routines API

#[cfg(feature = "libsql")]
mod support;

#[cfg(feature = "libsql")]
mod tests {
use std::time::Duration;

use crate::support::gateway_workflow_harness::GatewayWorkflowHarness;
use crate::support::mock_openai_server::{
MockOpenAiResponse, MockOpenAiRule, MockOpenAiServerBuilder, MockToolCall,
};

#[tokio::test]
async fn gateway_workflow_harness_chat_and_webhook() {
let mock = MockOpenAiServerBuilder::new()
.with_rule(MockOpenAiRule::on_user_contains(
"create workflow routine",
MockOpenAiResponse::ToolCalls(vec![MockToolCall::new(
"call_create_1",
"routine_create",
serde_json::json!({
"name": "wf-ci-webhook-demo",
"description": "CI webhook workflow demo",
"trigger_type": "system_event",
"event_source": "github",
"event_type": "issue.opened",
"event_filters": {"repository": "nearai/ironclaw"},
"action_type": "lightweight",
"prompt": "Summarize webhook and report issue number"
}),
)]),
))
.with_rule(MockOpenAiRule::on_user_contains(
"emit webhook event",
MockOpenAiResponse::ToolCalls(vec![MockToolCall::new(
"call_emit_1",
"event_emit",
serde_json::json!({
"source": "github",
"event_type": "issue.opened",
"payload": {
"repository": "nearai/ironclaw",
"issue": {"number": 777, "title": "Infra test"}
}
}),
)]),
))
.with_default_response(MockOpenAiResponse::Text("ack".to_string()))
.start()
.await;

let harness =
GatewayWorkflowHarness::start_openai_compatible(&mock.openai_base_url(), "mock-model")
.await;

let thread_id = harness.create_thread().await;
harness
.send_chat(&thread_id, "create workflow routine")
.await;
harness
.wait_for_turns(&thread_id, 1, Duration::from_secs(10))
.await;

let mut routine = None;
for _ in 0..30 {
routine = harness.routine_by_name("wf-ci-webhook-demo").await;
if routine.is_some() {
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
let routine = if let Some(r) = routine {
r
} else {
let history_dbg = harness.history(&thread_id).await;
let started_dbg = harness.test_channel.tool_calls_started();
let requests_dbg = mock.requests().await;
panic!(
"routine not created; tool_calls_started={started_dbg:?}; history={history_dbg}; mock_requests={requests_dbg:?}"
);
};
let routine_id = routine["id"].as_str().expect("routine id missing");

harness.send_chat(&thread_id, "emit webhook event").await;

let history = harness
.wait_for_turns(&thread_id, 2, Duration::from_secs(10))
.await;
let turns = history["turns"].as_array().expect("turns array missing");
assert!(turns.len() >= 2, "expected at least 2 turns");

let runs_before = harness.routine_runs(routine_id).await;
let before_count = runs_before["runs"]
.as_array()
.map(|a| a.len())
.unwrap_or_default();

let hook = harness
.github_webhook(
"issues",
serde_json::json!({
"action": "opened",
"repository": {"full_name": "nearai/ironclaw"},
"issue": {"number": 778, "title": "Webhook endpoint test"}
}),
)
.await;

assert_eq!(hook["status"], "accepted");
assert_eq!(hook["emitted_events"], 1);
assert!(
hook["fired_routines"].as_u64().unwrap_or(0) >= 1,
"expected webhook to fire at least one routine"
);

let mut after_count = before_count;
for _ in 0..50 {
let runs_after = harness.routine_runs(routine_id).await;
after_count = runs_after["runs"]
.as_array()
.map(|a| a.len())
.unwrap_or_default();
if after_count > before_count {
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
assert!(
after_count > before_count,
"expected routine runs to increase after webhook; before={before_count}, after={after_count}"
);

let requests = mock.requests().await;
assert!(
requests.len() >= 2,
"expected mock LLM server to receive requests"
);

harness.shutdown().await;
mock.shutdown().await;
}
}
10 changes: 9 additions & 1 deletion tests/support/assertions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,15 @@ pub fn verify_expects(

// all_tools_succeeded
if expects.all_tools_succeeded == Some(true) {
assert_all_tools_succeeded(completed);
let failed: Vec<&str> = completed
.iter()
.filter(|(_, success)| !*success)
.map(|(name, _)| name.as_str())
.collect();
assert!(
failed.is_empty(),
"[{label}] Expected all tools to succeed, failed={failed:?}, completed={completed:?}, results={results:?}"
);
}

// max_tool_calls
Expand Down
Loading