#![cfg(feature = "agents")]
use std::path::PathBuf;
use batuta::agent::capability::Capability;
use batuta::agent::driver::mock::MockDriver;
use batuta::agent::manifest::{AgentManifest, ModelConfig, ResourceQuota};
use batuta::agent::memory::InMemorySubstrate;
use batuta::agent::runtime::run_agent_loop;
use batuta::agent::tool::file::{FileEditTool, FileReadTool, FileWriteTool};
use batuta::agent::tool::search::{GlobTool, GrepTool};
use batuta::agent::tool::shell::ShellTool;
use batuta::agent::tool::ToolRegistry;
use batuta::serve::backends::PrivacyTier;
fn code_manifest() -> AgentManifest {
AgentManifest {
name: "apr-code-test".to_string(),
description: "Smoke test".to_string(),
privacy: PrivacyTier::Standard,
model: ModelConfig {
system_prompt: "You are a coding assistant.".to_string(),
max_tokens: 1024,
temperature: 0.0,
..ModelConfig::default()
},
resources: ResourceQuota {
max_iterations: 10,
max_tool_calls: 20,
max_cost_usd: 0.0,
max_tokens_budget: None,
},
capabilities: vec![
Capability::FileRead { allowed_paths: vec!["*".into()] },
Capability::FileWrite { allowed_paths: vec!["*".into()] },
Capability::Shell { allowed_commands: vec!["*".into()] },
Capability::Memory,
],
..AgentManifest::default()
}
}
fn code_tools() -> ToolRegistry {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let mut tools = ToolRegistry::new();
tools.register(Box::new(FileReadTool::new(vec!["*".into()])));
tools.register(Box::new(FileWriteTool::new(vec!["*".into()])));
tools.register(Box::new(FileEditTool::new(vec!["*".into()])));
tools.register(Box::new(GlobTool::new(vec!["*".into()])));
tools.register(Box::new(GrepTool::new(vec!["*".into()])));
tools.register(Box::new(ShellTool::new(vec!["*".into()], cwd)));
tools
}
#[tokio::test]
async fn test_code_smoke_file_read() {
let dir = tempfile::TempDir::new().unwrap();
let test_file = dir.path().join("hello.rs");
std::fs::write(&test_file, "fn main() { println!(\"hello\"); }\n").unwrap();
let manifest = code_manifest();
let memory = InMemorySubstrate::new();
let tools = code_tools();
let driver = MockDriver::tool_then_response(
"file_read",
serde_json::json!({"path": test_file.to_str().unwrap()}),
"The file contains a hello world program.",
);
let result = run_agent_loop(&manifest, "Read hello.rs", &driver, &tools, &memory, None)
.await
.expect("agent loop failed");
assert_eq!(result.text, "The file contains a hello world program.");
assert!(result.iterations >= 2, "expected at least 2 iterations (tool_use + end_turn)");
assert_eq!(result.tool_calls, 1, "expected 1 tool call");
}
#[tokio::test]
async fn test_code_smoke_file_write() {
let dir = tempfile::TempDir::new().unwrap();
let target_file = dir.path().join("output.txt");
let manifest = code_manifest();
let memory = InMemorySubstrate::new();
let tools = code_tools();
let driver = MockDriver::tool_then_response(
"file_write",
serde_json::json!({
"path": target_file.to_str().unwrap(),
"content": "Hello from apr code!"
}),
"File written successfully.",
);
let result = run_agent_loop(&manifest, "Write output.txt", &driver, &tools, &memory, None)
.await
.expect("agent loop failed");
assert_eq!(result.text, "File written successfully.");
assert!(target_file.exists(), "file was not created");
assert_eq!(std::fs::read_to_string(&target_file).unwrap(), "Hello from apr code!");
}
#[tokio::test]
async fn test_code_smoke_file_edit() {
let dir = tempfile::TempDir::new().unwrap();
let test_file = dir.path().join("code.rs");
std::fs::write(&test_file, "fn main() {\n println!(\"hello\");\n}\n").unwrap();
let manifest = code_manifest();
let memory = InMemorySubstrate::new();
let tools = code_tools();
let driver = MockDriver::tool_then_response(
"file_edit",
serde_json::json!({
"path": test_file.to_str().unwrap(),
"old_string": "println!(\"hello\")",
"new_string": "println!(\"world\")"
}),
"Changed hello to world.",
);
let result = run_agent_loop(&manifest, "Fix the greeting", &driver, &tools, &memory, None)
.await
.expect("agent loop failed");
assert_eq!(result.text, "Changed hello to world.");
let content = std::fs::read_to_string(&test_file).unwrap();
assert!(content.contains("println!(\"world\")"), "edit not applied");
assert!(!content.contains("println!(\"hello\")"), "old string still present");
}
#[tokio::test]
async fn test_code_smoke_shell() {
let manifest = code_manifest();
let memory = InMemorySubstrate::new();
let tools = code_tools();
let driver = MockDriver::tool_then_response(
"shell",
serde_json::json!({"command": "echo hello_from_agent"}),
"Shell returned hello.",
);
let result =
run_agent_loop(&manifest, "Run echo", &driver, &tools, &memory, None).await.expect("fail");
assert_eq!(result.text, "Shell returned hello.");
assert_eq!(result.tool_calls, 1);
}
#[tokio::test]
async fn test_code_smoke_glob() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join("a.rs"), "").unwrap();
std::fs::write(dir.path().join("b.rs"), "").unwrap();
let manifest = code_manifest();
let memory = InMemorySubstrate::new();
let tools = code_tools();
let driver = MockDriver::tool_then_response(
"glob",
serde_json::json!({
"pattern": "*.rs",
"path": dir.path().to_str().unwrap()
}),
"Found 2 rust files.",
);
let result = run_agent_loop(&manifest, "Find rs files", &driver, &tools, &memory, None)
.await
.expect("fail");
assert_eq!(result.text, "Found 2 rust files.");
}
#[test]
fn test_code_tool_count() {
let tools = code_tools();
assert_eq!(tools.len(), 6, "expected 6 tools in base code_tools()");
}
#[tokio::test]
async fn test_code_smoke_no_tools() {
let manifest = code_manifest();
let memory = InMemorySubstrate::new();
let tools = code_tools();
let driver = MockDriver::single_response("I can help you with coding tasks.");
let result =
run_agent_loop(&manifest, "Hello", &driver, &tools, &memory, None).await.expect("fail");
assert_eq!(result.text, "I can help you with coding tasks.");
assert_eq!(result.tool_calls, 0);
assert_eq!(result.iterations, 1);
}
#[test]
fn test_session_roundtrip() {
use batuta::agent::driver::{Message, ToolCall, ToolResultMsg};
use batuta::agent::session::SessionStore;
let store = SessionStore::create("integration-test").expect("create");
let messages = vec![
Message::User("Fix the bug".into()),
Message::AssistantToolUse(ToolCall {
id: "t1".into(),
name: "file_read".into(),
input: serde_json::json!({"path": "src/main.rs"}),
}),
Message::ToolResult(ToolResultMsg {
tool_use_id: "t1".into(),
content: "fn main() {}".into(),
is_error: false,
}),
Message::Assistant("I found the bug.".into()),
];
store.append_messages(&messages).expect("append");
let loaded = store.load_messages().expect("load");
assert_eq!(loaded.len(), 4);
assert!(matches!(&loaded[0], Message::User(s) if s == "Fix the bug"));
assert!(matches!(&loaded[1], Message::AssistantToolUse(tc) if tc.name == "file_read"));
assert!(matches!(&loaded[2], Message::ToolResult(tr) if tr.content == "fn main() {}"));
assert!(matches!(&loaded[3], Message::Assistant(s) if s == "I found the bug."));
let _ = std::fs::remove_dir_all(&store.dir);
}
#[test]
fn test_tool_definitions_in_prompt() {
use batuta::agent::driver::chat_template::{format_prompt_with_template, ChatTemplate};
use batuta::agent::driver::{CompletionRequest, Message, ToolDefinition};
let tools = vec![ToolDefinition {
name: "file_read".into(),
description: "Read a file".into(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"path": {"type": "string", "description": "Path to read"}
}
}),
}];
let request = CompletionRequest {
model: String::new(),
messages: vec![Message::User("Read main.rs".into())],
tools,
max_tokens: 1024,
temperature: 0.0,
system: Some("You are a coding assistant.".into()),
};
let prompt = format_prompt_with_template(&request, ChatTemplate::ChatMl);
assert!(prompt.contains("file_read"), "tool name missing from prompt");
assert!(prompt.contains("Read a file"), "tool description missing");
assert!(prompt.contains("path (string)"), "tool schema missing");
assert!(prompt.contains("<tool_call>"), "tool call format missing");
assert!(prompt.contains("tool_result"), "tool result format missing");
}
#[tokio::test]
async fn test_multi_turn_session_integration() {
use batuta::agent::driver::Message;
use batuta::agent::runtime::run_agent_turn;
let manifest = code_manifest();
let memory = InMemorySubstrate::new();
let tools = code_tools();
let driver = MockDriver::new(vec![
batuta::agent::driver::CompletionResponse {
text: "I see the project.".into(),
stop_reason: batuta::agent::result::StopReason::EndTurn,
tool_calls: vec![],
usage: Default::default(),
},
batuta::agent::driver::CompletionResponse {
text: "The bug is on line 42.".into(),
stop_reason: batuta::agent::result::StopReason::EndTurn,
tool_calls: vec![],
usage: Default::default(),
},
]);
let mut history: Vec<Message> = Vec::new();
let r1 = run_agent_turn(
&manifest,
&mut history,
"Look at the project",
&driver,
&tools,
&memory,
None,
)
.await
.expect("turn 1");
assert_eq!(r1.text, "I see the project.");
assert_eq!(history.len(), 2);
let r2 = run_agent_turn(
&manifest,
&mut history,
"Where is the bug?",
&driver,
&tools,
&memory,
None,
)
.await
.expect("turn 2");
assert_eq!(r2.text, "The bug is on line 42.");
assert_eq!(history.len(), 4); }