#[tokio::test]
async fn test_long_conversation_no_crash() {
let mut responses = Vec::new();
for i in 0..11 {
responses.push(MockProvider::text_response(&format!("Response {}", i)));
}
let provider = MockProvider::with_responses(responses);
let harness = setup_test_agent(provider).await.unwrap();
for i in 0..11 {
let msg = format!(
"Message number {} with some extra text to make it a bit longer",
i
);
let result = harness
.agent
.handle_message(
"long_session",
&msg,
None,
UserRole::Owner,
ChannelContext::private("telegram"),
None,
)
.await;
assert!(
result.is_ok(),
"Message {} should succeed: {:?}",
i,
result.err()
);
let text = result.unwrap();
assert!(!text.is_empty(), "Response {} should not be empty", i);
}
}
#[tokio::test]
async fn test_tool_result_compressed() {
use crate::memory::context_window::compress_tool_result;
let short = "Hello world";
let result = compress_tool_result("terminal", short, 2000);
assert_eq!(result, short);
let large = "x".repeat(5000);
let compressed = compress_tool_result("terminal", &large, 2000);
assert!(compressed.len() < 5000);
assert!(compressed.contains("OUTPUT TRUNCATED"));
assert!(compressed.contains("5000"));
}
#[tokio::test]
async fn test_retries_when_reply_denies_live_access_after_successful_tool_use() {
let provider = MockProvider::with_responses(vec![
MockProvider::tool_call_response(
"http_request",
r#"{"method":"GET","url":"https://clinicaltrials.gov/api/v2/studies"}"#,
),
MockProvider::text_response(
"I can guide you on how to find skin cancer clinical trials, but I cannot perform a live search of current databases.",
),
MockProvider::text_response(
"I checked the live ClinicalTrials.gov results. The first matching study in Fairfax, Virginia is Example Skin Trial.",
),
]);
let http_tool: Arc<dyn crate::traits::Tool> = Arc::new(MockTool::new(
"http_request",
"Make HTTP requests",
"HTTP 200 OK\ncontent-type: application/json\n\n{\"studies\":[{\"briefTitle\":\"Example Skin Trial\",\"city\":\"Fairfax\",\"state\":\"Virginia\"}]}",
));
let harness = crate::testing::setup_test_agent_with_extra_tools_and_llm_timeout(
provider,
vec![http_tool],
None,
)
.await
.unwrap();
let response = harness
.agent
.handle_message(
"http_retry_session",
"Search for clinical trials for skin cancer near Fairfax Virginia for a man.",
None,
UserRole::Owner,
ChannelContext::private("telegram"),
None,
)
.await
.unwrap();
assert!(
response.contains("Example Skin Trial"),
"final response was: {}",
response
);
assert!(
!response
.to_ascii_lowercase()
.contains("cannot perform a live search"),
"Final response should use live results instead of capability denial: {}",
response
);
assert!(
harness.provider.call_count().await >= 3,
"Expected at least one retry after the bad fallback reply"
);
}
#[tokio::test]
async fn test_summary_crud() {
use crate::traits::ConversationSummary;
let provider = MockProvider::with_responses(vec![MockProvider::text_response("Hello")]);
let harness = setup_test_agent(provider).await.unwrap();
let summary = harness
.state
.get_conversation_summary("test_session")
.await
.unwrap();
assert!(summary.is_none());
let summary = ConversationSummary {
session_id: "test_session".to_string(),
summary: "We discussed topic A and decided on approach B.".to_string(),
message_count: 10,
last_message_id: "msg-123".to_string(),
updated_at: Utc::now(),
};
harness
.state
.upsert_conversation_summary(&summary)
.await
.unwrap();
let loaded = harness
.state
.get_conversation_summary("test_session")
.await
.unwrap();
assert!(loaded.is_some());
let loaded = loaded.unwrap();
assert_eq!(loaded.session_id, "test_session");
assert_eq!(
loaded.summary,
"We discussed topic A and decided on approach B."
);
assert_eq!(loaded.message_count, 10);
let updated = ConversationSummary {
summary: "Updated: topic A, approach B, and new topic C.".to_string(),
message_count: 15,
..loaded
};
harness
.state
.upsert_conversation_summary(&updated)
.await
.unwrap();
let reloaded = harness
.state
.get_conversation_summary("test_session")
.await
.unwrap()
.unwrap();
assert_eq!(
reloaded.summary,
"Updated: topic A, approach B, and new topic C."
);
assert_eq!(reloaded.message_count, 15);
harness.state.clear_session("test_session").await.unwrap();
let after_clear = harness
.state
.get_conversation_summary("test_session")
.await
.unwrap();
assert!(
after_clear.is_none(),
"Summary should be deleted after clear_session"
);
}
#[tokio::test]
async fn test_should_extract_facts_filtering() {
use crate::memory::context_window::should_extract_facts;
assert!(!should_extract_facts("ok"));
assert!(!should_extract_facts("thanks"));
assert!(!should_extract_facts("👍"));
assert!(!should_extract_facts("hi"));
assert!(should_extract_facts(
"My dog's name is Bella and she's 3 years old"
));
assert!(should_extract_facts(
"I work at Acme Corp as a senior engineer"
));
}
async fn collect_status_updates(
mut rx: tokio::sync::mpsc::Receiver<StatusUpdate>,
) -> Vec<StatusUpdate> {
let mut updates = Vec::new();
while let Ok(update) = rx.try_recv() {
updates.push(update);
}
updates
}
#[tokio::test]
async fn test_task_budget_auto_extends_on_progress() {
let provider = MockProvider::with_responses(vec![
MockProvider::tool_call_response("system_info", "{}"),
MockProvider::tool_call_response(
"remember_fact",
r#"{"category":"test","key":"k1","value":"v1"}"#,
),
MockProvider::tool_call_response("system_info", r#"{"verbose": true}"#),
MockProvider::tool_call_response(
"remember_fact",
r#"{"category":"test","key":"k2","value":"v2"}"#,
),
MockProvider::tool_call_response("system_info", r#"{"check": "os"}"#),
MockProvider::tool_call_response(
"remember_fact",
r#"{"category":"test","key":"k3","value":"v3"}"#,
),
MockProvider::tool_call_response("system_info", r#"{"check": "mem"}"#),
MockProvider::tool_call_response(
"remember_fact",
r#"{"category":"test","key":"k4","value":"v4"}"#,
),
MockProvider::tool_call_response("system_info", r#"{"check": "cpu"}"#),
MockProvider::tool_call_response(
"remember_fact",
r#"{"category":"test","key":"k5","value":"v5"}"#,
),
MockProvider::tool_call_response("system_info", r#"{"check": "final"}"#),
MockProvider::text_response("Task completed successfully."),
]);
let mut harness = setup_test_agent(provider).await.unwrap();
harness.agent.set_test_executor_mode();
harness.agent.set_test_task_token_budget(Some(150));
let (status_tx, status_rx) = tokio::sync::mpsc::channel::<StatusUpdate>(64);
let response = harness
.agent
.handle_message(
"budget_test",
"Run a complex analysis requiring multiple steps",
Some(status_tx),
UserRole::Owner,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert!(
!response.is_empty(),
"Agent should return a non-empty response"
);
let updates = collect_status_updates(status_rx).await;
if response.contains("Task completed successfully.") {
let budget_extended = updates
.iter()
.any(|u| matches!(u, StatusUpdate::BudgetExtended { .. }));
assert!(
budget_extended,
"Expected BudgetExtended status update when agent completes normally"
);
}
}
#[tokio::test]
async fn test_task_budget_auto_extends_on_short_productive_run() {
let provider = MockProvider::with_responses(vec![
MockProvider::tool_call_response("system_info", "{}"),
MockProvider::tool_call_response("system_info", r#"{"verbose": true}"#),
MockProvider::tool_call_response("system_info", r#"{"check": "os"}"#),
MockProvider::text_response("Short task completed."),
]);
let mut harness = setup_test_agent(provider).await.unwrap();
harness.agent.set_test_executor_mode();
harness.agent.set_test_task_token_budget(Some(60));
let response = harness
.agent
.handle_message(
"short_budget_test",
"Run a short multi-step check",
None,
UserRole::Owner,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert_eq!(response, "Short task completed.");
}
#[tokio::test]
async fn test_global_daily_budget_auto_extends_on_short_productive_run() {
let provider = MockProvider::with_responses(vec![
MockProvider::tool_call_response("system_info", "{}"),
MockProvider::tool_call_response("system_info", r#"{"verbose": true}"#),
MockProvider::tool_call_response("system_info", r#"{"check": "os"}"#),
MockProvider::tool_call_response("system_info", r#"{"check": "mem"}"#),
MockProvider::text_response("Daily budget task completed."),
]);
let mut harness = setup_test_agent(provider).await.unwrap();
harness.agent.set_test_executor_mode();
harness.agent.set_test_daily_token_budget(Some(60));
let response = harness
.agent
.handle_message(
"daily_budget_test",
"Run a short multi-step check against the daily budget",
None,
UserRole::Owner,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert_eq!(response, "Daily budget task completed.");
}
#[tokio::test]
async fn test_task_budget_stops_when_not_productive() {
let mut responses = Vec::new();
for _ in 0..5 {
responses.push(MockProvider::tool_call_response("system_info", "{}"));
}
responses.push(MockProvider::text_response("Should not reach this."));
let provider = MockProvider::with_responses(responses);
let mut harness = setup_test_agent(provider).await.unwrap();
harness.agent.set_test_executor_mode();
harness.agent.set_test_task_token_budget(Some(45));
let (status_tx, status_rx) = tokio::sync::mpsc::channel::<StatusUpdate>(64);
let response = harness
.agent
.handle_message(
"stall_test",
"Do something",
Some(status_tx),
UserRole::Owner,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert_ne!(response, "Should not reach this.");
let updates = collect_status_updates(status_rx).await;
let budget_extended = updates
.iter()
.any(|u| matches!(u, StatusUpdate::BudgetExtended { .. }));
assert!(
!budget_extended,
"BudgetExtended should NOT be emitted when agent is stalling"
);
}
#[tokio::test]
async fn test_goal_budget_auto_extends_and_persists() {
let provider = MockProvider::with_responses(vec![
MockProvider::tool_call_response("system_info", "{}"),
MockProvider::tool_call_response("system_info", r#"{"verbose": true}"#),
MockProvider::tool_call_response("system_info", r#"{"check": "os"}"#),
MockProvider::tool_call_response("system_info", r#"{"check": "mem"}"#),
MockProvider::text_response("Goal task completed."),
]);
let mut harness = setup_test_agent(provider).await.unwrap();
harness.agent.set_test_executor_mode();
let mut goal = Goal::new_finite("Test goal for budget extension", "goal_budget_session");
goal.status = "active".to_string();
goal.budget_daily = Some(60);
goal.budget_per_check = Some(500);
harness.state.create_goal(&goal).await.unwrap();
harness.agent.set_test_goal_id(Some(goal.id.clone()));
let (status_tx, _status_rx) = tokio::sync::mpsc::channel::<StatusUpdate>(64);
let response = harness
.agent
.handle_message(
"goal_budget_session",
"Execute the goal task",
Some(status_tx),
UserRole::Owner,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert_eq!(response, "Goal task completed.");
let updated_goal = harness.state.get_goal(&goal.id).await.unwrap().unwrap();
assert_eq!(
updated_goal.budget_daily.unwrap(),
60,
"Budget should NOT be ratcheted up in DB — expected 60, got {:?}",
updated_goal.budget_daily
);
}
struct TrustProbeTool {
seen: Arc<tokio::sync::Mutex<Vec<bool>>>,
}
impl TrustProbeTool {
fn new(seen: Arc<tokio::sync::Mutex<Vec<bool>>>) -> Self {
Self { seen }
}
}
#[async_trait::async_trait]
impl crate::traits::Tool for TrustProbeTool {
fn name(&self) -> &str {
"trust_probe"
}
fn description(&self) -> &str {
"Inspect whether the agent injected _trusted_session"
}
fn schema(&self) -> serde_json::Value {
json!({
"name": "trust_probe",
"description": "Inspect whether the agent injected _trusted_session",
"parameters": {
"type": "object",
"properties": {},
"additionalProperties": false
}
})
}
async fn call(&self, arguments: &str) -> anyhow::Result<String> {
let args: serde_json::Value = serde_json::from_str(arguments)?;
let trusted = args["_trusted_session"].as_bool().unwrap_or(false);
self.seen.lock().await.push(trusted);
Ok(if trusted { "trusted" } else { "untrusted" }.to_string())
}
}
#[tokio::test]
async fn test_scheduled_goal_tool_calls_are_marked_trusted() {
let seen = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let provider = MockProvider::with_responses(vec![
MockProvider::tool_call_response("trust_probe", "{}"),
MockProvider::text_response("done"),
]);
let mut harness = crate::testing::setup_test_agent_with_extra_tools_and_llm_timeout(
provider,
vec![Arc::new(TrustProbeTool::new(seen.clone())) as Arc<dyn crate::traits::Tool>],
None,
)
.await
.unwrap();
let mut goal = Goal::new_continuous(
"Scheduled trust test",
"scheduled_trust_session",
None,
None,
);
goal.status = "active".to_string();
harness.state.create_goal(&goal).await.unwrap();
let now = chrono::Utc::now().to_rfc3339();
let schedule = crate::traits::GoalSchedule {
id: uuid::Uuid::new_v4().to_string(),
goal_id: goal.id.clone(),
cron_expr: "0 * * * *".to_string(),
tz: "local".to_string(),
original_schedule: Some("hourly".to_string()),
fire_policy: "coalesce".to_string(),
is_one_shot: false,
is_paused: false,
last_run_at: None,
next_run_at: now.clone(),
created_at: now.clone(),
updated_at: now,
};
harness.state.create_goal_schedule(&schedule).await.unwrap();
harness.agent.set_test_goal_id(Some(goal.id.clone()));
let response = harness
.agent
.handle_message(
"scheduled_trust_session",
"Run the scheduled goal",
None,
UserRole::Owner,
ChannelContext::internal(),
None,
)
.await
.unwrap();
assert_eq!(response, "done");
assert_eq!(*seen.lock().await, vec![true]);
}
#[tokio::test]
async fn test_scheduled_goal_executor_tool_calls_inherit_trust_from_goal() {
let seen = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let provider = MockProvider::with_responses(vec![
MockProvider::tool_call_response("trust_probe", "{}"),
MockProvider::text_response("done"),
]);
let mut harness = crate::testing::setup_test_agent_with_extra_tools_and_llm_timeout(
provider,
vec![Arc::new(TrustProbeTool::new(seen.clone())) as Arc<dyn crate::traits::Tool>],
None,
)
.await
.unwrap();
harness.agent.set_test_executor_mode();
let mut goal = Goal::new_continuous(
"Scheduled executor trust test",
"scheduled_executor_trust_session",
None,
None,
);
goal.status = "active".to_string();
harness.state.create_goal(&goal).await.unwrap();
let now = chrono::Utc::now().to_rfc3339();
let schedule = crate::traits::GoalSchedule {
id: uuid::Uuid::new_v4().to_string(),
goal_id: goal.id.clone(),
cron_expr: "0 * * * *".to_string(),
tz: "local".to_string(),
original_schedule: Some("hourly".to_string()),
fire_policy: "coalesce".to_string(),
is_one_shot: false,
is_paused: false,
last_run_at: None,
next_run_at: now.clone(),
created_at: now.clone(),
updated_at: now.clone(),
};
harness.state.create_goal_schedule(&schedule).await.unwrap();
let child_task = crate::traits::Task {
id: uuid::Uuid::new_v4().to_string(),
goal_id: goal.id.clone(),
description: "Post the composed tweet to Twitter/X".to_string(),
status: "pending".to_string(),
priority: "medium".to_string(),
task_order: 1,
parallel_group: None,
depends_on: None,
agent_id: None,
context: None,
result: None,
error: None,
blocker: None,
idempotent: true,
retry_count: 0,
max_retries: 1,
created_at: now.clone(),
started_at: None,
completed_at: None,
};
harness.state.create_task(&child_task).await.unwrap();
harness.agent.set_test_goal_id(None);
harness.agent.set_test_task_id(Some(child_task.id.clone()));
let response = harness
.agent
.handle_message(
"scheduled_executor_trust_session",
"Run the executor task",
None,
UserRole::Owner,
ChannelContext::internal(),
None,
)
.await
.unwrap();
assert_eq!(response, "done");
assert_eq!(*seen.lock().await, vec![true]);
}
#[tokio::test]
async fn test_scheduled_goal_spawned_executor_tool_calls_inherit_trust_from_goal() {
let seen = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let provider = MockProvider::with_responses(vec![
MockProvider::tool_call_response("trust_probe", "{}"),
MockProvider::text_response("done"),
]);
let harness = crate::testing::setup_test_agent_with_extra_tools_and_llm_timeout(
provider,
vec![Arc::new(TrustProbeTool::new(seen.clone())) as Arc<dyn crate::traits::Tool>],
None,
)
.await
.unwrap();
let mut goal = Goal::new_continuous(
"Scheduled spawned executor trust test",
"scheduled_spawned_executor_trust_session",
None,
None,
);
goal.status = "active".to_string();
harness.state.create_goal(&goal).await.unwrap();
let now = chrono::Utc::now().to_rfc3339();
let schedule = crate::traits::GoalSchedule {
id: uuid::Uuid::new_v4().to_string(),
goal_id: goal.id.clone(),
cron_expr: "0 * * * *".to_string(),
tz: "local".to_string(),
original_schedule: Some("hourly".to_string()),
fire_policy: "coalesce".to_string(),
is_one_shot: false,
is_paused: false,
last_run_at: None,
next_run_at: now.clone(),
created_at: now.clone(),
updated_at: now.clone(),
};
harness.state.create_goal_schedule(&schedule).await.unwrap();
let child_task = crate::traits::Task {
id: uuid::Uuid::new_v4().to_string(),
goal_id: goal.id.clone(),
description: "Post the composed tweet to Twitter/X".to_string(),
status: "pending".to_string(),
priority: "medium".to_string(),
task_order: 1,
parallel_group: None,
depends_on: None,
agent_id: None,
context: None,
result: None,
error: None,
blocker: None,
idempotent: true,
retry_count: 0,
max_retries: 1,
created_at: now.clone(),
started_at: None,
completed_at: None,
};
harness.state.create_task(&child_task).await.unwrap();
let agent = Arc::new(harness.agent);
let response = agent
.spawn_child(
"Post the scheduled tweet",
"Post the scheduled tweet",
None,
ChannelContext::internal(),
UserRole::Owner,
Some(crate::traits::AgentRole::Executor),
Some(goal.id.as_str()),
Some(child_task.id.as_str()),
None,
None,
)
.await
.unwrap();
assert_eq!(response, "done");
assert_eq!(*seen.lock().await, vec![true]);
}
#[tokio::test]
async fn test_non_scheduled_goal_tool_calls_remain_untrusted() {
let seen = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let provider = MockProvider::with_responses(vec![
MockProvider::tool_call_response("trust_probe", "{}"),
MockProvider::text_response("done"),
]);
let mut harness = crate::testing::setup_test_agent_with_extra_tools_and_llm_timeout(
provider,
vec![Arc::new(TrustProbeTool::new(seen.clone())) as Arc<dyn crate::traits::Tool>],
None,
)
.await
.unwrap();
let mut goal = Goal::new_finite("Plain goal", "plain_goal_session");
goal.status = "active".to_string();
harness.state.create_goal(&goal).await.unwrap();
harness.agent.set_test_goal_id(Some(goal.id.clone()));
let response = harness
.agent
.handle_message(
"plain_goal_session",
"Run the goal",
None,
UserRole::Owner,
ChannelContext::internal(),
None,
)
.await
.unwrap();
assert_eq!(response, "done");
assert_eq!(*seen.lock().await, vec![false]);
}
#[tokio::test]
async fn test_scheduled_goal_daily_budget_is_backstop_only_during_active_run() {
let provider = MockProvider::with_responses(vec![
MockProvider::tool_call_response("system_info", "{}"),
MockProvider::tool_call_response("system_info", r#"{"verbose": true}"#),
MockProvider::tool_call_response("system_info", r#"{"check": "os"}"#),
MockProvider::tool_call_response("system_info", r#"{"check": "mem"}"#),
MockProvider::text_response("Goal task completed."),
]);
let mut harness = setup_test_agent(provider).await.unwrap();
harness.agent.set_test_executor_mode();
let mut goal = Goal::new_continuous(
"Scheduled build task",
"scheduled_goal_budget_session",
None,
None,
);
goal.status = "active".to_string();
goal.budget_daily = Some(60);
goal.budget_per_check = Some(500);
harness.state.create_goal(&goal).await.unwrap();
let now = chrono::Utc::now().to_rfc3339();
let schedule = crate::traits::GoalSchedule {
id: uuid::Uuid::new_v4().to_string(),
goal_id: goal.id.clone(),
cron_expr: "0 * * * *".to_string(),
tz: "local".to_string(),
original_schedule: Some("hourly".to_string()),
fire_policy: "coalesce".to_string(),
is_one_shot: false,
is_paused: false,
last_run_at: None,
next_run_at: now.clone(),
created_at: now.clone(),
updated_at: now,
};
harness.state.create_goal_schedule(&schedule).await.unwrap();
harness.agent.set_test_goal_id(Some(goal.id.clone()));
let (status_tx, status_rx) = tokio::sync::mpsc::channel::<StatusUpdate>(64);
let response = harness
.agent
.handle_message(
"scheduled_goal_budget_session",
"Execute the scheduled goal task",
Some(status_tx),
UserRole::Owner,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert_eq!(response, "Goal task completed.");
let updates = collect_status_updates(status_rx).await;
assert!(
!updates
.iter()
.any(|u| matches!(u, StatusUpdate::BudgetExtended { .. })),
"Daily backstop should not trigger an in-run budget extension for scheduled goals"
);
let updated_goal = harness.state.get_goal(&goal.id).await.unwrap().unwrap();
assert_eq!(
updated_goal.budget_daily,
Some(60),
"The daily backstop should remain unchanged in the database"
);
}
#[tokio::test]
async fn test_scheduled_goal_run_budget_stops_unproductive_run() {
let provider = MockProvider::with_responses(vec![
MockProvider::tool_call_response("no_such_tool", "{}"),
MockProvider::text_response("Should not reach this."),
]);
let mut harness = setup_test_agent(provider).await.unwrap();
harness.agent.set_test_executor_mode();
let mut goal = Goal::new_continuous(
"Scheduled noisy task",
"scheduled_goal_run_budget_session",
Some(10),
Some(500),
);
goal.status = "active".to_string();
harness.state.create_goal(&goal).await.unwrap();
let now = chrono::Utc::now().to_rfc3339();
let schedule = crate::traits::GoalSchedule {
id: uuid::Uuid::new_v4().to_string(),
goal_id: goal.id.clone(),
cron_expr: "0 * * * *".to_string(),
tz: "local".to_string(),
original_schedule: Some("hourly".to_string()),
fire_policy: "coalesce".to_string(),
is_one_shot: false,
is_paused: false,
last_run_at: None,
next_run_at: now.clone(),
created_at: now.clone(),
updated_at: now,
};
harness.state.create_goal_schedule(&schedule).await.unwrap();
harness.agent.set_test_goal_id(Some(goal.id.clone()));
let response = harness
.agent
.handle_message(
"scheduled_goal_run_budget_session",
"Execute the scheduled goal task",
None,
UserRole::Owner,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert!(
response.contains("per-run processing budget"),
"Expected scheduled run budget stop message, got: {}",
response
);
let updated_goal = harness.state.get_goal(&goal.id).await.unwrap().unwrap();
assert_eq!(updated_goal.budget_per_check, Some(10));
assert_eq!(updated_goal.budget_daily, Some(500));
}
#[tokio::test]
async fn test_scheduled_goal_run_budget_resets_between_runs() {
let provider = MockProvider::with_responses(vec![
crate::traits::ProviderResponse {
content: None,
tool_calls: vec![crate::traits::ToolCall {
id: format!("call_{}", uuid::Uuid::new_v4()),
name: "no_such_tool".to_string(),
arguments: "{}".to_string(),
extra_content: None,
}],
usage: Some(crate::traits::TokenUsage {
input_tokens: 1,
output_tokens: 1,
cached_input_tokens: None,
cache_creation_input_tokens: None,
model: "mock".to_string(),
}),
thinking: None,
response_note: None,
},
crate::traits::ProviderResponse {
content: Some("Should not reach the first run.".to_string()),
tool_calls: vec![],
usage: Some(crate::traits::TokenUsage {
input_tokens: 10,
output_tokens: 5,
cached_input_tokens: None,
cache_creation_input_tokens: None,
model: "mock".to_string(),
}),
thinking: None,
response_note: None,
},
crate::traits::ProviderResponse {
content: Some("Second scheduled run completed.".to_string()),
tool_calls: vec![],
usage: Some(crate::traits::TokenUsage {
input_tokens: 1,
output_tokens: 1,
cached_input_tokens: None,
cache_creation_input_tokens: None,
model: "mock".to_string(),
}),
thinking: None,
response_note: None,
},
]);
let mut harness = setup_test_agent(provider).await.unwrap();
harness.agent.set_test_executor_mode();
let mut goal = Goal::new_continuous(
"Scheduled repeated task",
"scheduled_goal_reset_session",
Some(10),
Some(500),
);
goal.status = "active".to_string();
harness.state.create_goal(&goal).await.unwrap();
let now = chrono::Utc::now().to_rfc3339();
let schedule = crate::traits::GoalSchedule {
id: uuid::Uuid::new_v4().to_string(),
goal_id: goal.id.clone(),
cron_expr: "0 * * * *".to_string(),
tz: "local".to_string(),
original_schedule: Some("hourly".to_string()),
fire_policy: "coalesce".to_string(),
is_one_shot: false,
is_paused: false,
last_run_at: None,
next_run_at: now.clone(),
created_at: now.clone(),
updated_at: now,
};
harness.state.create_goal_schedule(&schedule).await.unwrap();
harness.agent.set_test_goal_id(Some(goal.id.clone()));
let first_response = harness
.agent
.handle_message(
"scheduled_goal_reset_session",
"Execute the scheduled goal task",
None,
UserRole::Owner,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert!(
first_response.contains("per-run processing budget"),
"Expected first run to stop on per-run budget, got: {}",
first_response
);
let second_response = harness
.agent
.handle_message(
"scheduled_goal_reset_session",
"Execute the scheduled goal task",
None,
UserRole::Owner,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert_eq!(second_response, "Second scheduled run completed.");
}
#[tokio::test]
async fn test_scheduled_goal_ignores_hard_iteration_cap() {
let provider = MockProvider::with_responses(vec![
MockProvider::tool_call_response("system_info", "{}"),
MockProvider::tool_call_response("system_info", r#"{"verbose": true}"#),
MockProvider::text_response("Scheduled run finished after multiple steps."),
]);
let mut harness = setup_test_agent(provider).await.unwrap();
harness.agent.set_test_executor_mode();
harness
.agent
.set_test_iteration_config(crate::config::IterationLimitConfig::Hard {
initial: 1,
cap: 1,
});
let mut goal = Goal::new_continuous(
"Scheduled iterative task",
"scheduled_goal_iteration_session",
Some(500),
Some(5000),
);
goal.status = "active".to_string();
harness.state.create_goal(&goal).await.unwrap();
let now = chrono::Utc::now().to_rfc3339();
let schedule = crate::traits::GoalSchedule {
id: uuid::Uuid::new_v4().to_string(),
goal_id: goal.id.clone(),
cron_expr: "0 * * * *".to_string(),
tz: "local".to_string(),
original_schedule: Some("hourly".to_string()),
fire_policy: "coalesce".to_string(),
is_one_shot: false,
is_paused: false,
last_run_at: None,
next_run_at: now.clone(),
created_at: now.clone(),
updated_at: now,
};
harness.state.create_goal_schedule(&schedule).await.unwrap();
harness.agent.set_test_goal_id(Some(goal.id.clone()));
let response = harness
.agent
.handle_message(
"scheduled_goal_iteration_session",
"Execute the scheduled goal task",
None,
UserRole::Owner,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert_eq!(response, "Scheduled run finished after multiple steps.");
}
#[tokio::test]
async fn test_scheduled_goal_restores_run_state_after_restart_like_resume() {
let provider = MockProvider::with_responses(vec![
MockProvider::tool_call_response("no_such_tool", "{}"),
MockProvider::text_response("Should not reach this."),
]);
let mut harness = setup_test_agent(provider).await.unwrap();
harness.agent.set_test_executor_mode();
let mut goal = Goal::new_continuous(
"Scheduled resumed task",
"scheduled_goal_resume_session",
Some(100),
Some(500),
);
goal.status = "active".to_string();
harness.state.create_goal(&goal).await.unwrap();
let now = chrono::Utc::now().to_rfc3339();
let schedule = crate::traits::GoalSchedule {
id: uuid::Uuid::new_v4().to_string(),
goal_id: goal.id.clone(),
cron_expr: "0 * * * *".to_string(),
tz: "local".to_string(),
original_schedule: Some("hourly".to_string()),
fire_policy: "coalesce".to_string(),
is_one_shot: false,
is_paused: false,
last_run_at: None,
next_run_at: now.clone(),
created_at: now.clone(),
updated_at: now.clone(),
};
harness.state.create_goal_schedule(&schedule).await.unwrap();
let root_task = crate::traits::Task {
id: uuid::Uuid::new_v4().to_string(),
goal_id: goal.id.clone(),
description:
"Execute scheduled goal: Scheduled resumed task [SYSTEM: already scheduled and firing now; do not reschedule.]"
.to_string(),
status: "pending".to_string(),
priority: "low".to_string(),
task_order: 0,
parallel_group: None,
depends_on: None,
agent_id: None,
context: None,
result: None,
error: None,
blocker: None,
idempotent: true,
retry_count: 0,
max_retries: 1,
created_at: now.clone(),
started_at: None,
completed_at: None,
};
harness.state.create_task(&root_task).await.unwrap();
harness
.state
.upsert_scheduled_run_state(&crate::traits::ScheduledRunState {
goal_id: goal.id.clone(),
root_task_id: root_task.id.clone(),
effective_budget_per_check: 20,
tokens_used: 15,
budget_extensions_count: 0,
health: crate::traits::ScheduledRunHealth {
evidence_gain_count: 0,
total_successful_tool_calls: 0,
stall_count: 0,
consecutive_same_tool_count: 0,
consecutive_same_tool_unique_args: 0,
unrecovered_error_count: 1,
},
created_at: now.clone(),
updated_at: now,
})
.await
.unwrap();
harness.agent.set_test_goal_id(Some(goal.id.clone()));
let response = harness
.agent
.handle_message(
"scheduled_goal_resume_session",
"Execute the scheduled goal task",
None,
UserRole::Owner,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert!(
response.contains("per-run processing budget"),
"Expected restored scheduled run state to stop the resumed run, got: {}",
response
);
assert!(
harness
.state
.get_scheduled_run_state(&goal.id)
.await
.unwrap()
.is_none(),
"Scheduled run state should be cleared after the resumed run exits"
);
}
#[tokio::test]
async fn test_non_owner_cannot_schedule_goal() {
let provider = MockProvider::with_responses(vec![
MockProvider::text_response("I'll handle this request now."),
MockProvider::text_response("Here is the information you requested."),
]);
let harness = setup_test_agent(provider).await.unwrap();
let response = harness
.agent
.handle_message(
"guest_schedule_session",
"Every day at 9am, check the weather forecast",
None,
UserRole::Guest,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert!(!response.is_empty());
let call_log = harness.provider.call_log.lock().await;
assert!(
call_log.iter().all(|call| call.tools.is_empty()),
"Guest schedule requests should be handled without tools"
);
let goals = harness
.state
.get_pending_confirmation_goals("guest_schedule_session")
.await
.unwrap();
assert!(
goals.is_empty(),
"Guest should not be able to create scheduled goals"
);
}
#[tokio::test]
async fn test_non_owner_cannot_confirm_scheduled_goal() {
let harness = setup_test_agent(MockProvider::new()).await.unwrap();
let mut goal = Goal::new_finite("Check weather daily", "confirm_test_session");
goal.status = "pending_confirmation".to_string();
harness.state.create_goal(&goal).await.unwrap();
let response = harness
.agent
.handle_message(
"confirm_test_session",
"confirm",
None,
UserRole::Guest,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert!(
response.contains("Only the owner"),
"Expected owner-only message, got: {}",
response
);
let pending = harness
.state
.get_pending_confirmation_goals("confirm_test_session")
.await
.unwrap();
assert_eq!(pending.len(), 1, "Goal should remain pending_confirmation");
assert_eq!(pending[0].status, "pending_confirmation");
}
#[tokio::test]
async fn test_non_owner_unrelated_message_does_not_cancel_pending_goal() {
let harness = setup_test_agent(MockProvider::new()).await.unwrap();
let mut goal = Goal::new_finite("Deploy app nightly", "shared_session");
goal.status = "pending_confirmation".to_string();
harness.state.create_goal(&goal).await.unwrap();
let _response = harness
.agent
.handle_message(
"shared_session",
"What's the weather today?",
None,
UserRole::Guest,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
let pending = harness
.state
.get_pending_confirmation_goals("shared_session")
.await
.unwrap();
assert_eq!(
pending.len(),
1,
"Pending goal should not be auto-cancelled by non-owner message"
);
assert_eq!(pending[0].status, "pending_confirmation");
}
fn test_reload_config(
kind: &str,
base_url: &str,
api_key: &str,
primary: &str,
fast: &str,
smart: &str,
) -> crate::config::AppConfig {
let toml = format!(
r#"
[provider]
kind = "{kind}"
base_url = "{base_url}"
api_key = "{api_key}"
[provider.models]
primary = "{primary}"
fast = "{fast}"
smart = "{smart}"
"#
);
toml::from_str(&toml).expect("reload test config should parse")
}
#[tokio::test]
async fn test_reload_provider_switches_backend_and_models_endpoint() {
let harness = setup_test_agent(MockProvider::new()).await.unwrap();
let before_models = harness.agent.list_models().await.unwrap();
assert_eq!(before_models, vec!["mock-model".to_string()]);
assert_eq!(harness.agent.current_model().await, "mock-model");
let anthropic = test_reload_config(
"anthropic",
"https://api.openai.com/v1",
"test-anthropic-key",
"claude-3-haiku-20240307",
"claude-3-haiku-20240307",
"claude-3-opus-20240229",
);
let status = harness.agent.reload_provider(&anthropic).await.unwrap();
assert!(
status.contains("OpenaiCompatible -> Anthropic"),
"unexpected reload status: {}",
status
);
assert!(
status.contains("mock-model -> claude-3-haiku-20240307"),
"unexpected reload status: {}",
status
);
assert_eq!(
harness.agent.current_model().await,
"claude-3-haiku-20240307"
);
let after_models = harness.agent.list_models().await.unwrap();
assert!(
after_models.iter().any(|m| m.starts_with("claude")),
"expected Anthropic known models, got {:?}",
after_models
);
assert!(
!after_models.iter().any(|m| m == "mock-model"),
"expected list_models source to switch away from mock provider, got {:?}",
after_models
);
}
#[tokio::test]
async fn test_reload_provider_resets_manual_model_and_supports_second_reload() {
let harness = setup_test_agent(MockProvider::new()).await.unwrap();
harness.agent.set_model("manual-override".to_string()).await;
assert_eq!(harness.agent.current_model().await, "manual-override");
let anthropic = test_reload_config(
"anthropic",
"https://api.openai.com/v1",
"test-anthropic-key",
"claude-3-opus-20240229",
"claude-3-haiku-20240307",
"claude-3-opus-20240229",
);
let status_1 = harness.agent.reload_provider(&anthropic).await.unwrap();
assert!(
status_1.contains("manual-override -> claude-3-opus-20240229"),
"unexpected reload status: {}",
status_1
);
assert_eq!(
harness.agent.current_model().await,
"claude-3-opus-20240229"
);
let openai = test_reload_config(
"openai_compatible",
"https://api.openai.com/v1",
"test-openai-key",
"openai/gpt-4o-mini",
"openai/gpt-4o-mini",
"openai/gpt-4o",
);
let status_2 = harness.agent.reload_provider(&openai).await.unwrap();
assert!(
status_2.contains("Anthropic -> OpenaiCompatible"),
"unexpected second reload status: {}",
status_2
);
assert!(
status_2.contains("claude-3-opus-20240229 -> openai/gpt-4o-mini"),
"unexpected second reload status: {}",
status_2
);
assert_eq!(harness.agent.current_model().await, "openai/gpt-4o-mini");
}
#[tokio::test]
async fn test_no_router_auto_mode_uses_runtime_primary_over_stale_model_field() {
let provider = MockProvider::with_responses(vec![MockProvider::text_response("ok")]);
let mut harness = setup_test_agent(provider).await.unwrap();
harness.agent.set_test_orchestrator_mode();
harness
.agent
.set_model("manual-stale-model".to_string())
.await;
harness.agent.clear_model_override().await;
let response = harness
.agent
.handle_message(
"no_router_auto_primary",
"hello",
None,
UserRole::Owner,
ChannelContext::private("test"),
None,
)
.await
.unwrap();
assert!(
!response.is_empty(),
"agent should return a non-empty response"
);
let calls = harness.provider.call_log.lock().await;
assert!(
!calls.is_empty(),
"expected at least one LLM call in orchestrator mode"
);
assert_eq!(
calls[0].model, "mock-model",
"top-level no-router auto mode should use runtime primary model, not stale self.model"
);
}