diff --git a/src/agent/runtime.rs b/src/agent/runtime.rs index a0e0cd9..2d294b8 100644 --- a/src/agent/runtime.rs +++ b/src/agent/runtime.rs @@ -36,6 +36,7 @@ impl Default for RuntimeConfig { pub struct AgentRuntime { client: Arc, tools: ToolRegistry, + #[allow(dead_code)] // Stored for multi-agent orchestration in later phases profile: AgentProfile, config: RuntimeConfig, } diff --git a/src/context/agents_md.rs b/src/context/agents_md.rs index 6c1d521..5a3c050 100644 --- a/src/context/agents_md.rs +++ b/src/context/agents_md.rs @@ -41,8 +41,8 @@ pub fn resolve_agents_md( } } - // Check each directory for AGENTS.md (reverse order: closest to file first) - for dir in dirs_to_check.iter().rev() { + // Check each directory for AGENTS.md (closest to file first) + for dir in &dirs_to_check { let agents_md_path = dir.join("AGENTS.md"); if agents_md_path.exists() && !seen_paths.contains(&agents_md_path) { seen_paths.insert(agents_md_path.clone()); diff --git a/tests/agent_runtime_test.rs b/tests/agent_runtime_test.rs index 0328c7d..9d5a8cb 100644 --- a/tests/agent_runtime_test.rs +++ b/tests/agent_runtime_test.rs @@ -1,10 +1,5 @@ -use async_trait::async_trait; use rustagent::agent::runtime::{AgentRuntime, RuntimeConfig}; use rustagent::agent::{AgentContext, AgentOutcome, AgentProfile}; -use rustagent::graph::GraphNode; -use rustagent::graph::NodeType; -use rustagent::graph::store::WorkGraph; -use rustagent::graph::store::{GraphStore, NodeQuery}; use rustagent::llm::mock::MockLlmClient; use rustagent::security::SecurityScope; use rustagent::tools::ToolRegistry; @@ -133,12 +128,12 @@ async fn test_p1d_ac4_3_token_budget_warning() { // P1d.AC4.3: At warning threshold (80%), inject "wrap up" message let mock_client = Arc::new(MockLlmClient::new()); - // First call: return tokens that reach 80% of budget (800 of 1000) - mock_client.set_token_counts(400, 400); // Total 800 of budget 1000 (80%) + // set_token_counts is global (applies to all responses), not per-response + // First call returns 800 tokens total (400+400), reaching 80% of 1000 budget + mock_client.set_token_counts(400, 400); mock_client.queue_text_response("Processing..."); - // Second call: should have wrap-up message injected, respond with signal_completion - mock_client.set_token_counts(0, 0); + // Second call: wrap-up message should be injected before this call mock_client.queue_tool_call( "signal_completion", json!({ @@ -174,23 +169,23 @@ async fn test_p1d_ac4_3_token_budget_warning() { let ctx = make_test_context(); let outcome = runtime.run(ctx).await.expect("Runtime failed"); - // Verify that the wrap-up logic was engaged + // Verify that the wrap-up logic was engaged by checking recorded LLM calls let calls = mock_client.get_recorded_calls(); - assert!(!calls.is_empty(), "Expected at least 1 LLM call"); + assert!(calls.len() >= 2, "Expected at least 2 LLM calls"); + + // The second call's messages should contain the wrap-up warning injected by the runtime + let second_call_messages = &calls[1].0; + let has_wrap_up_message = second_call_messages + .iter() + .any(|msg| msg.role == rustagent::llm::Role::System && msg.content.contains("Wrap up")); + assert!( + has_wrap_up_message, + "Expected wrap-up system message in second LLM call messages" + ); - // Check that we reached the 80% warning threshold (800 tokens of 1000 budget) + // Verify outcome is valid completion or token exhaustion match outcome { - AgentOutcome::Completed { .. } => { - // Completion is valid - wrap-up logic allowed the agent to gracefully finish - assert!(true, "Wrap-up logic allowed graceful completion"); - } - AgentOutcome::TokenBudgetExhausted { tokens_used, .. } => { - // Also valid - token budget was exhausted after reaching warning threshold - assert!( - tokens_used >= 800, - "Expected to reach at least 80% threshold" - ); - } + AgentOutcome::Completed { .. } | AgentOutcome::TokenBudgetExhausted { .. } => {} _ => panic!("Unexpected outcome: {:?}", outcome), } } diff --git a/tests/agent_types_test.rs b/tests/agent_types_test.rs index e404f20..a72db2e 100644 --- a/tests/agent_types_test.rs +++ b/tests/agent_types_test.rs @@ -1,7 +1,6 @@ use async_trait::async_trait; use rustagent::agent::profile::AgentProfile; use rustagent::agent::{Agent, AgentContext, AgentId, AgentOutcome}; -use rustagent::graph::{EdgeType, GraphNode, NodeStatus}; use rustagent::security::SecurityScope; use std::path::PathBuf; use std::sync::Arc; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index c21129f..49efdab 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,10 +1,205 @@ use anyhow::Result; use async_trait::async_trait; -use rustagent::graph::store::{EdgeDirection, GraphStore, NodeQuery, WorkGraph}; -use rustagent::graph::{EdgeType, GraphEdge, GraphNode, NodeStatus, NodeType}; +use chrono::Utc; +use rustagent::db::Database; +use rustagent::graph::store::{EdgeDirection, GraphStore, NodeQuery, SqliteGraphStore, WorkGraph}; +use rustagent::graph::*; use std::collections::HashMap; +use std::sync::Arc; -/// Mock GraphStore for testing +/// Helper to create a test goal node +pub fn create_test_goal(id: &str, project_id: &str, title: &str) -> GraphNode { + GraphNode { + id: id.to_string(), + project_id: project_id.to_string(), + node_type: NodeType::Goal, + title: title.to_string(), + description: "Test goal".to_string(), + status: NodeStatus::Pending, + priority: Some(Priority::High), + assigned_to: None, + created_by: None, + labels: vec![], + created_at: Utc::now(), + started_at: None, + completed_at: None, + blocked_reason: None, + metadata: HashMap::new(), + } +} + +/// Helper to create a test task node (can optionally accept priority) +pub fn create_test_task(id: &str, project_id: &str, title: &str, status: NodeStatus) -> GraphNode { + create_test_task_with_priority(id, project_id, title, status, Some(Priority::Medium)) +} + +/// Helper to create a test task node with specific priority +pub fn create_test_task_with_priority( + id: &str, + project_id: &str, + title: &str, + status: NodeStatus, + priority: Option, +) -> GraphNode { + GraphNode { + id: id.to_string(), + project_id: project_id.to_string(), + node_type: NodeType::Task, + title: title.to_string(), + description: "Test task".to_string(), + status, + priority, + assigned_to: None, + created_by: None, + labels: vec![], + created_at: Utc::now(), + started_at: None, + completed_at: None, + blocked_reason: None, + metadata: HashMap::new(), + } +} + +/// Helper to create a test observation node +pub fn create_test_observation( + id: &str, + project_id: &str, + title: &str, + description: &str, +) -> GraphNode { + GraphNode { + id: id.to_string(), + project_id: project_id.to_string(), + node_type: NodeType::Observation, + title: title.to_string(), + description: description.to_string(), + status: NodeStatus::Active, + priority: None, + assigned_to: None, + created_by: None, + labels: vec![], + created_at: Utc::now(), + started_at: None, + completed_at: None, + blocked_reason: None, + metadata: HashMap::new(), + } +} + +/// Helper to create a test decision node +pub fn create_test_decision(id: &str, project_id: &str, title: &str) -> GraphNode { + GraphNode { + id: id.to_string(), + project_id: project_id.to_string(), + node_type: NodeType::Decision, + title: title.to_string(), + description: "Test decision".to_string(), + status: NodeStatus::Pending, + priority: None, + assigned_to: None, + created_by: None, + labels: vec![], + created_at: Utc::now(), + started_at: None, + completed_at: None, + blocked_reason: None, + metadata: HashMap::new(), + } +} + +/// Helper to set up a test database with a project (graph store only) +pub async fn setup_test_env() -> Result<(Database, SqliteGraphStore)> { + let db = Database::open_in_memory().await?; + let graph_store = SqliteGraphStore::new(db.clone()); + + // Create a test project by directly inserting into the database + let db_for_project = db.clone(); + db_for_project + .connection() + .call(|conn| { + let now = chrono::Utc::now().to_rfc3339(); + conn.execute( + "INSERT INTO projects (id, name, path, registered_at, config_overrides, metadata) + VALUES (?, ?, ?, ?, ?, ?)", + rusqlite::params![ + "proj-1", + "proj-1", + "/tmp/proj-1", + &now, + None::, + "{}" + ], + )?; + Ok(()) + }) + .await?; + + Ok((db, graph_store)) +} + +/// Helper to set up a test database with a project (includes project store) +pub async fn setup_test_env_with_project() +-> Result<(Database, SqliteGraphStore, rustagent::project::ProjectStore)> { + let db = Database::open_in_memory().await?; + let proj_store = rustagent::project::ProjectStore::new(db.clone()); + let graph_store = SqliteGraphStore::new(db.clone()); + + // Create a test project by directly inserting into the database + let db_for_project = db.clone(); + db_for_project + .connection() + .call(|conn| { + let now = chrono::Utc::now().to_rfc3339(); + conn.execute( + "INSERT INTO projects (id, name, path, registered_at, config_overrides, metadata) + VALUES (?, ?, ?, ?, ?, ?)", + rusqlite::params![ + "proj-1", + "proj-1", + "/tmp/proj-1", + &now, + None::, + "{}" + ], + )?; + Ok(()) + }) + .await?; + + Ok((db, graph_store, proj_store)) +} + +/// Helper to set up a test database with a project (wrapped in Arc for concurrency tests) +pub async fn setup_test_env_concurrent() -> Result<(Database, Arc)> { + let db = Database::open_in_memory().await?; + let graph_store = Arc::new(SqliteGraphStore::new(db.clone())); + + // Create a test project by directly inserting into the database + let db_for_project = db.clone(); + db_for_project + .connection() + .call(|conn| { + let now = chrono::Utc::now().to_rfc3339(); + conn.execute( + "INSERT INTO projects (id, name, path, registered_at, config_overrides, metadata) + VALUES (?, ?, ?, ?, ?, ?)", + rusqlite::params![ + "proj-1", + "proj-1", + "/tmp/proj-1", + &now, + None::, + "{}" + ], + )?; + Ok(()) + }) + .await?; + + Ok((db, graph_store)) +} + +/// Mock GraphStore for testing (returns empty/default values for all operations) pub struct MockGraphStore; #[async_trait]