use crate::e2e::expectations::{load_expectations, LangExpectation};
use codescout::agent::Agent;
use codescout::lsp::manager::LspManager;
use codescout::tools::ast::ListFunctions;
use codescout::tools::grep::Grep;
use codescout::tools::symbol::{References, Symbols};
use codescout::tools::{Tool, ToolContext};
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
use std::sync::Arc;
fn fixtures_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
}
fn fixture_dir(language: &str) -> PathBuf {
fixtures_root().join(format!("{language}-library"))
}
async fn fixture_context(language: &str) -> Arc<ToolContext> {
let dir = fixture_dir(language);
assert!(
dir.exists(),
"Fixture directory not found: {}",
dir.display()
);
let agent = Agent::new(Some(dir.clone()))
.await
.unwrap_or_else(|e| panic!("Failed to create Agent for {language}: {e}"));
let lsp = LspManager::new_arc();
Arc::new(ToolContext {
agent,
lsp,
output_buffer: 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(),
)),
guide_hints_emitted: std::sync::Arc::new(parking_lot::Mutex::new(Default::default())),
})
}
fn language_extensions(language: &str) -> &'static [&'static str] {
match language {
"rust" => &["rs"],
"kotlin" => &["kt", "kts"],
"java" => &["java"],
"python" => &["py"],
"typescript" => &["ts", "tsx", "js", "jsx"],
_ => &[],
}
}
async fn prime_lsp(ctx: &ToolContext, language: &str) {
let root = fixture_dir(language);
let exts = language_extensions(language);
if exts.is_empty() {
return;
}
fn walk(dir: &std::path::Path, exts: &[&str], out: &mut Vec<std::path::PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if name == "target" || name == "build" || name == "node_modules" || name == ".git" {
continue;
}
walk(&path, exts, out);
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if exts.contains(&ext) {
out.push(path);
}
}
}
}
let mut files = Vec::new();
walk(&root, exts, &mut files);
for abs in files {
let rel = match abs.strip_prefix(&root) {
Ok(r) => r.to_string_lossy().into_owned(),
Err(_) => continue,
};
let _ = Symbols.call(json!({ "path": rel }), ctx).await;
}
}
pub async fn run_all_expectations(language: &str, toml_filenames: &[&str]) {
let ctx = fixture_context(language).await;
prime_lsp(&ctx, language).await;
let mut total_pass = 0;
let mut total_failures = Vec::new();
for toml_filename in toml_filenames {
let toml_path = fixtures_root().join(toml_filename);
let expectations = load_expectations(&toml_path, language);
if expectations.is_empty() {
total_failures.push((
format!("{toml_filename}::<no tests>"),
format!(
"No expectations found for language '{language}' in {toml_filename}. \
Check TOML for [{language}] sub-tables or top-level path/file."
),
));
continue;
}
eprintln!("\n--- {toml_filename} ---");
let (pass, failures) = run_expectations_inner(&ctx, &expectations).await;
total_pass += pass;
for (name, err) in failures {
total_failures.push((format!("{toml_filename}::{name}"), err));
}
}
let total = total_pass + total_failures.len();
eprintln!("\n{total_pass}/{total} passed for {language}");
if !total_failures.is_empty() {
panic!(
"{} of {total} expectations failed for {language}:\n{}",
total_failures.len(),
total_failures
.iter()
.map(|(n, e)| format!(" - {n}: {e}"))
.collect::<Vec<_>>()
.join("\n")
);
}
}
async fn run_expectations_inner(
ctx: &ToolContext,
expectations: &[(String, LangExpectation, String)],
) -> (usize, Vec<(String, String)>) {
let mut sorted: Vec<_> = expectations.iter().collect();
sorted.sort_by_key(|(_, _, tool)| {
if tool == "find_referencing_symbols" {
1
} else {
0
}
});
let mut pass = 0;
let mut failures = Vec::new();
for (name, expectation, tool) in sorted {
match run_single(ctx, expectation, tool).await {
Ok(()) => {
pass += 1;
eprintln!(" PASS {name}");
}
Err(e) => {
eprintln!(" FAIL {name}: {e}");
failures.push((name.clone(), e));
}
}
}
(pass, failures)
}
async fn run_single(ctx: &ToolContext, exp: &LangExpectation, tool: &str) -> Result<(), String> {
match tool {
"get_symbols_overview" => run_symbols_overview(ctx, exp).await,
"symbols" => run_symbols(ctx, exp).await,
"find_referencing_symbols" => run_find_references(ctx, exp).await,
"list_functions" => run_list_functions(ctx, exp).await,
"search_for_pattern" => run_search_pattern(ctx, exp).await,
other => Err(format!("Unknown tool: {other}")),
}
}
async fn run_symbols_overview(ctx: &ToolContext, exp: &LangExpectation) -> Result<(), String> {
let path = exp.path.as_deref().ok_or("Missing 'path'")?;
let result = Symbols
.call(json!({ "path": path }), ctx)
.await
.map_err(|e| format!("Tool error: {e}"))?;
if let Some(expected) = &exp.contains_symbols {
assert_contains_symbols(&result, expected)?;
}
Ok(())
}
async fn run_symbols(ctx: &ToolContext, exp: &LangExpectation) -> Result<(), String> {
let symbol = exp.symbol.as_deref().ok_or("Missing 'symbol'")?;
let mut params = json!({ "query": symbol });
if let Some(path) = &exp.path {
params["path"] = json!(path);
}
if exp.body_contains.is_some() {
params["include_body"] = json!(true);
}
let expected_children = exp.contains_symbols.as_deref().unwrap_or(&[]);
let expected_body = exp.body_contains.as_deref().unwrap_or(&[]);
let mut last_result = serde_json::Value::Null;
let mut last_err: Option<String> = None;
for attempt in 0..8 {
if attempt > 0 {
tokio::time::sleep(std::time::Duration::from_millis(500 * attempt as u64)).await;
}
match Symbols.call(params.clone(), ctx).await {
Ok(result) => {
last_err = None;
last_result = result;
let result_str = serde_json::to_string(&last_result).unwrap_or_default();
let children_ok = expected_children.is_empty()
|| assert_contains_symbols(&last_result, expected_children).is_ok();
let body_ok = expected_body.iter().all(|n| result_str.contains(n));
if children_ok && body_ok {
return Ok(());
}
}
Err(e) => {
last_err = Some(format!("Tool error: {e}"));
}
}
}
if let Some(err) = last_err {
return Err(err);
}
if !expected_children.is_empty() {
assert_contains_symbols(&last_result, expected_children)?;
}
let result_str = serde_json::to_string(&last_result).unwrap_or_default();
for needle in expected_body {
if !result_str.contains(needle) {
return Err(format!("symbols(\"{symbol}\") body missing \"{needle}\""));
}
}
Ok(())
}
async fn run_find_references(ctx: &ToolContext, exp: &LangExpectation) -> Result<(), String> {
let symbol = exp.symbol.as_deref().ok_or("Missing 'symbol'")?;
let file = exp.path.as_deref().ok_or("Missing 'path'/'file'")?;
let expected = exp.expected_refs_contain.as_deref().unwrap_or(&[]);
let mut last_result_str = String::new();
let mut last_err: Option<String> = None;
for attempt in 0..16 {
if attempt > 0 {
tokio::time::sleep(std::time::Duration::from_millis(750 * attempt as u64)).await;
}
match References
.call(json!({ "symbol": symbol, "path": file }), ctx)
.await
{
Ok(result) => {
last_err = None;
last_result_str = serde_json::to_string(&result).unwrap_or_default();
let all_found = expected
.iter()
.all(|n| last_result_str.contains(n.as_str()));
if all_found {
return Ok(());
}
}
Err(e) => {
last_err = Some(format!("Tool error: {e}"));
}
}
}
if let Some(err) = last_err {
return Err(err);
}
for needle in expected {
if !last_result_str.contains(needle.as_str()) {
return Err(format!(
"find_referencing_symbols(\"{symbol}\") missing reference to \"{needle}\""
));
}
}
Ok(())
}
async fn run_list_functions(ctx: &ToolContext, exp: &LangExpectation) -> Result<(), String> {
let path = exp.path.as_deref().ok_or("Missing 'path'")?;
let result = ListFunctions
.call(json!({ "path": path }), ctx)
.await
.map_err(|e| format!("Tool error: {e}"))?;
if let Some(expected) = &exp.contains_functions {
let result_str = serde_json::to_string(&result).unwrap_or_default();
for needle in expected {
if !result_str.contains(needle) {
return Err(format!("list_functions(\"{path}\") missing \"{needle}\""));
}
}
}
Ok(())
}
async fn run_search_pattern(ctx: &ToolContext, exp: &LangExpectation) -> Result<(), String> {
let pattern = exp.pattern.as_deref().ok_or("Missing 'pattern'")?;
let result = Grep
.call(json!({ "pattern": pattern }), ctx)
.await
.map_err(|e| format!("Tool error: {e}"))?;
if let Some(expected_files) = &exp.expected_files {
let result_str = serde_json::to_string(&result).unwrap_or_default();
for needle in expected_files {
if !result_str.contains(needle) {
return Err(format!(
"search_for_pattern(\"{pattern}\") missing file \"{needle}\""
));
}
}
}
Ok(())
}
fn assert_contains_symbols(result: &Value, expected: &[String]) -> Result<(), String> {
let result_str = serde_json::to_string(result).unwrap_or_default();
let mut missing = Vec::new();
for name in expected {
if !result_str.contains(name.as_str()) {
missing.push(name.as_str());
}
}
if missing.is_empty() {
Ok(())
} else {
Err(format!("Missing symbols: {:?}", missing))
}
}