use codescout::agent::Agent;
use codescout::lsp::LspManager;
use codescout::tools::{Tool, ToolContext};
use serde_json::json;
use tempfile::tempdir;
async fn project_with_files(files: &[(&str, &str)]) -> (tempfile::TempDir, ToolContext) {
let dir = tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".codescout")).unwrap();
for (name, content) in files {
let path = dir.path().join(name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(path, content).unwrap();
}
let agent = Agent::new(Some(dir.path().to_path_buf())).await.unwrap();
let ctx = ToolContext {
agent,
lsp: LspManager::new_arc(),
output_buffer: std::sync::Arc::new(codescout::tools::output_buffer::OutputBuffer::new(20)),
progress: None,
peer: None,
section_coverage: std::sync::Arc::new(std::sync::Mutex::new(
codescout::tools::section_coverage::SectionCoverage::new(),
)),
};
(dir, ctx)
}
#[tokio::test]
async fn workflow_read_search_replace() {
use codescout::tools::edit_file::EditFile;
use codescout::tools::grep::Grep;
use codescout::tools::read_file::ReadFile;
let (dir, ctx) = project_with_files(&[
(
"src/main.txt",
"fn main() {\n println!(\"Hello, world!\");\n}\n",
),
(
"src/lib.txt",
"pub fn greet(name: &str) -> String {\n format!(\"Hello, {}!\", name)\n}\n",
),
])
.await;
let search_result = Grep
.call(
json!({ "pattern": "Hello", "path": dir.path().display().to_string() }),
&ctx,
)
.await
.unwrap();
let matches = search_result["matches"].as_array().unwrap();
assert!(
matches.len() >= 2,
"expected matches in both files: {:?}",
search_result
);
let lib_path = dir.path().join("src/lib.txt").display().to_string();
let read_result = ReadFile
.call(json!({ "path": &lib_path }), &ctx)
.await
.unwrap();
assert!(read_result["content"].as_str().unwrap().contains("Hello"));
let replace_result = EditFile
.call(
json!({
"path": &lib_path,
"old_string": " format!(\"Hello, {}!\", name)",
"new_string": " format!(\"Greetings, {}!\", name)",
}),
&ctx,
)
.await
.unwrap();
assert_eq!(replace_result, json!("ok"));
let read_after = ReadFile
.call(json!({ "path": &lib_path }), &ctx)
.await
.unwrap();
assert!(read_after["content"]
.as_str()
.unwrap()
.contains("Greetings"));
assert!(!read_after["content"].as_str().unwrap().contains("Hello"));
drop(dir);
}
#[tokio::test]
async fn workflow_analyze_ast() {
use codescout::tools::ast::{ListDocs, ListFunctions};
let (dir, ctx) = project_with_files(&[
(
"math.rs",
"/// Add two numbers.\nfn add(a: i32, b: i32) -> i32 { a + b }\n\n\
/// Subtract two numbers.\nfn sub(a: i32, b: i32) -> i32 { a - b }\n",
),
(
"util.py",
"def helper():\n \"\"\"A helper function.\"\"\"\n pass\n",
),
])
.await;
let list_result = ListFunctions
.call(json!({ "path": "math.rs" }), &ctx)
.await
.unwrap();
assert_eq!(list_result["total"], 2);
let func_names: Vec<&str> = list_result["functions"]
.as_array()
.unwrap()
.iter()
.map(|f| f["name"].as_str().unwrap())
.collect();
assert!(func_names.contains(&"add"));
assert!(func_names.contains(&"sub"));
let doc_result = ListDocs
.call(json!({ "path": "math.rs" }), &ctx)
.await
.unwrap();
assert_eq!(doc_result["total"], 2);
let docs = doc_result["docstrings"].as_array().unwrap();
assert_eq!(docs[0]["symbol_name"], "add");
assert!(docs[0]["content"].as_str().unwrap().contains("Add two"));
let py_list = ListFunctions
.call(json!({ "path": "util.py" }), &ctx)
.await
.unwrap();
assert_eq!(py_list["total"], 1);
let py_docs = ListDocs
.call(json!({ "path": "util.py" }), &ctx)
.await
.unwrap();
assert!(py_docs["total"].as_u64().unwrap() >= 1);
drop(dir);
}
#[tokio::test]
async fn workflow_project_memory_config() {
use codescout::tools::config::{ActivateProject, ProjectStatus};
use codescout::tools::memory::Memory;
let (dir, ctx) = project_with_files(&[("src/main.rs", "fn main() {}\n")]).await;
let activate_result = ActivateProject
.call(json!({ "path": dir.path().display().to_string() }), &ctx)
.await
.unwrap();
assert_eq!(activate_result["status"], "ok");
let config = ProjectStatus.call(json!({}), &ctx).await.unwrap();
assert!(config["languages"].is_array());
assert!(config["embeddings_model"].is_string());
assert!(config["project_root"].is_string());
Memory
.call(
json!({
"action": "write",
"topic": "architecture/decisions",
"content": "We chose Rust for performance."
}),
&ctx,
)
.await
.unwrap();
let read = Memory
.call(
json!({ "action": "read", "topic": "architecture/decisions" }),
&ctx,
)
.await
.unwrap();
assert!(read["content"]
.as_str()
.unwrap()
.contains("Rust for performance"));
let list = Memory
.call(json!({ "action": "list" }), &ctx)
.await
.unwrap();
let topics: Vec<&str> = list["topics"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
assert!(topics.contains(&"architecture/decisions"));
drop(dir);
}
#[tokio::test]
async fn workflow_onboarding_explore() {
use codescout::tools::onboarding::Onboarding;
use codescout::tools::tree::Tree;
let (dir, ctx) = project_with_files(&[
("src/main.rs", "fn main() {}\n"),
("src/lib.rs", "pub mod utils;\n"),
("Cargo.toml", "[package]\nname = \"test\"\n"),
])
.await;
let onboard = Onboarding.call(json!({}), &ctx).await.unwrap();
assert!(onboard["languages"].is_array());
let status = Onboarding.call(json!({}), &ctx).await.unwrap();
assert_eq!(status["onboarded"], true);
let list = Tree
.call(json!({ "path": dir.path().display().to_string() }), &ctx)
.await
.unwrap();
let entries = list["entries"].as_array().unwrap();
let entry_strs: Vec<&str> = entries.iter().filter_map(|e| e.as_str()).collect();
assert!(
entry_strs
.iter()
.any(|e| e.contains("src") && e.ends_with('/')),
"missing src dir: {:?}",
entry_strs
);
assert!(
entry_strs.iter().any(|e| e.ends_with("Cargo.toml")),
"missing Cargo.toml: {:?}",
entry_strs
);
drop(dir);
}
#[tokio::test]
async fn workflow_symbols_path_types() {
use codescout::tools::symbol::Symbols;
let (_dir, ctx) = project_with_files(&[
(
"src/main.rs",
"fn main() {}\nfn add(a: i32, b: i32) -> i32 { a + b }\n",
),
(
"src/lib.rs",
"pub fn helper() -> bool { true }\npub struct Calculator;\nimpl Calculator { pub fn compute() -> i32 { 42 } }\n",
),
(
"src/utils/math.rs",
"pub fn multiply(a: i32, b: i32) -> i32 { a * b }\n",
),
])
.await;
let r = Symbols
.call(json!({ "query": "add", "path": "src/main.rs" }), &ctx)
.await
.unwrap();
let syms = r["symbols"].as_array().unwrap();
assert!(
syms.iter().any(|s| s["name"] == "add"),
"file path should find 'add': {r:?}"
);
let r = Symbols
.call(json!({ "query": "helper", "path": "src" }), &ctx)
.await
.unwrap();
let syms = r["symbols"].as_array().unwrap();
assert!(
syms.iter().any(|s| s["name"] == "helper"),
"directory path should find 'helper': {r:?}"
);
let r = Symbols
.call(json!({ "query": "multiply", "path": "src/utils" }), &ctx)
.await
.unwrap();
let syms = r["symbols"].as_array().unwrap();
assert!(
syms.iter().any(|s| s["name"] == "multiply"),
"nested directory path should find 'multiply': {r:?}"
);
let r = Symbols
.call(json!({ "query": "add", "path": "src/**/*.rs" }), &ctx)
.await
.unwrap();
let syms = r["symbols"].as_array().unwrap();
assert!(
syms.iter().any(|s| s["name"] == "add"),
"glob path should find 'add': {r:?}"
);
let r = Symbols
.call(json!({ "query": "Calculator/compute" }), &ctx)
.await
.unwrap();
let syms = r["symbols"].as_array().unwrap();
assert!(
syms.iter().any(|s| s["name"] == "compute"),
"name_path pattern should find 'compute' project-wide: {r:?}"
);
}
#[tokio::test]
async fn write_allowed_when_project_provided_at_startup_even_with_worktrees() {
use codescout::tools::create_file::CreateFile;
let dir = tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".codescout")).unwrap();
let wt_entry = dir.path().join(".git").join("worktrees").join("feat");
std::fs::create_dir_all(&wt_entry).unwrap();
let fake_wt_root = dir.path().join("..").join("my-worktree");
std::fs::create_dir_all(&fake_wt_root).unwrap();
let gitdir_content = format!("{}/.git\n", fake_wt_root.display());
std::fs::write(wt_entry.join("gitdir"), &gitdir_content).unwrap();
let agent = Agent::new(Some(dir.path().to_path_buf())).await.unwrap();
assert!(agent.is_project_explicitly_activated().await);
let ctx = ToolContext {
agent,
lsp: LspManager::new_arc(),
output_buffer: std::sync::Arc::new(codescout::tools::output_buffer::OutputBuffer::new(20)),
progress: None,
peer: None,
section_coverage: std::sync::Arc::new(std::sync::Mutex::new(
codescout::tools::section_coverage::SectionCoverage::new(),
)),
};
let result = CreateFile
.call(json!({ "path": "test.txt", "content": "hello" }), &ctx)
.await;
assert!(result.is_ok(), "expected write to succeed, got: {result:?}");
drop(dir);
}
#[cfg(unix)]
#[tokio::test]
async fn integration_run_command_buffer_round_trip() {
use codescout::tools::run_command::RunCommand;
let (dir, ctx) = project_with_files(&[("README.md", "# test\n")]).await;
let r1 = RunCommand
.call(json!({ "command": "seq 1 3000", "timeout_secs": 10 }), &ctx)
.await
.unwrap();
let output_id = r1["output_id"]
.as_str()
.expect("seq 1 3000 should be buffered and return output_id");
assert!(
output_id.starts_with("@cmd_"),
"output_id should start with @cmd_, got: {}",
output_id
);
let r2 = RunCommand
.call(
json!({
"command": format!("grep '^50$' {}", output_id),
"timeout_secs": 10
}),
&ctx,
)
.await
.unwrap();
assert_eq!(r2["exit_code"], 0, "grep should succeed: {r2:?}");
assert_eq!(
r2["stdout"].as_str().unwrap().trim(),
"50",
"grep result should be '50': {r2:?}"
);
drop(dir);
}
#[cfg(unix)]
#[tokio::test]
async fn integration_read_file_large_then_query_via_buffer() {
use codescout::tools::read_file::ReadFile;
use codescout::tools::run_command::RunCommand;
let content: String = (1..=250)
.map(|i| format!("entry {:04} {}\n", i, "x".repeat(35)))
.collect();
let (dir, ctx) = project_with_files(&[("big.txt", &content)]).await;
let path = dir.path().join("big.txt").display().to_string();
let r1 = ReadFile.call(json!({ "path": &path }), &ctx).await.unwrap();
let file_id = r1["file_id"]
.as_str()
.expect("250-line file should be buffered and return file_id");
assert!(
file_id.starts_with("@file_"),
"file_id should start with @file_, got: {}",
file_id
);
let r2 = RunCommand
.call(
json!({
"command": format!("grep 'entry 0200' {}", file_id),
"timeout_secs": 10
}),
&ctx,
)
.await
.unwrap();
assert_eq!(r2["exit_code"], 0, "grep should succeed: {r2:?}");
assert!(
r2["stdout"].as_str().unwrap().contains("entry 0200"),
"grep result should contain 'entry 0200': {r2:?}"
);
drop(dir);
}
#[cfg(unix)]
#[tokio::test]
async fn integration_speed_bump_two_round_trips() {
use codescout::tools::run_command::RunCommand;
let (dir, ctx) = project_with_files(&[("README.md", "# test\n")]).await;
let r1 = RunCommand
.call(
json!({ "command": "rm -rf /tmp/ce_integration_test_nonexistent_dir" }),
&ctx,
)
.await
.expect("dangerous command should return Ok(pending_ack), not Err");
let handle = r1["pending_ack"]
.as_str()
.expect("result should have pending_ack string field");
assert!(
handle.starts_with("@ack_"),
"handle should start with @ack_, got: {handle}"
);
assert!(
r1.get("reason").is_some(),
"result should include reason, got: {r1}"
);
assert!(
r1["hint"].as_str().unwrap_or("").contains("@ack_"),
"hint should reference the ack handle, got: {r1}"
);
let r2 = RunCommand.call(json!({ "command": handle }), &ctx).await;
match &r2 {
Ok(v) => {
assert!(
v.get("pending_ack").is_none(),
"ack'd command should not re-block, got: {v}"
);
}
Err(e) => {
let msg = e.to_string().to_lowercase();
assert!(
!msg.contains("dangerous"),
"ack'd command should not be blocked as dangerous, got: {msg}"
);
}
}
drop(dir);
}