use super::*;
#[test]
fn find_text_returns_structured_hits() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("src/lib.harn"),
"alpha\nneedle here\nanother needle\n",
)
.unwrap();
std::fs::write(dir.path().join("README.md"), "needle docs\n").unwrap();
let mut vm = vm();
let response = call(
&mut vm,
"find_text",
vec![s(&dir.path().to_string_lossy()), s("needle")],
)
.unwrap();
let VmValue::List(hits) = response else {
panic!("find_text returns list");
};
assert_eq!(hits.len(), 3);
let first = hits[0].as_dict().expect("hit dict");
assert!(first["path"].display().ends_with("README.md"));
assert_eq!(first["line"].as_int(), Some(1));
assert_eq!(first["col"].as_int(), Some(1));
assert_eq!(first["column"].as_int(), Some(1));
assert_eq!(first["text"].display(), "needle docs");
}
#[test]
fn find_text_filters_case_and_limits() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(dir.path().join("src/a.harn"), "Needle\nneedle\n").unwrap();
std::fs::write(dir.path().join("src/b.txt"), "needle\n").unwrap();
let mut vm = vm();
let response = call(
&mut vm,
"find_text",
vec![
s(&dir.path().to_string_lossy()),
s("needle"),
dict(vec![
("include", s("**/*.harn")),
("case_insensitive", b(true)),
("max_matches", VmValue::Int(1)),
]),
],
)
.unwrap();
let VmValue::List(hits) = response else {
panic!("find_text returns list");
};
assert_eq!(hits.len(), 1);
let hit = hits[0].as_dict().expect("hit dict");
assert!(hit["path"].display().ends_with("src/a.harn"));
assert_eq!(hit["text"].display(), "Needle");
}
#[test]
fn find_text_summary_modes_and_source_preset() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::create_dir_all(dir.path().join("node_modules/pkg")).unwrap();
std::fs::write(dir.path().join("src/a.harn"), "needle\nneedle\n").unwrap();
std::fs::write(dir.path().join("node_modules/pkg/a.harn"), "needle\n").unwrap();
let mut vm = vm();
let exists = call(
&mut vm,
"find_text",
vec![
s(&dir.path().to_string_lossy()),
s("needle"),
dict(vec![
("mode", s("exists")),
("preset", s("source")),
("parallel", b(true)),
]),
],
)
.unwrap();
assert!(matches!(exists, VmValue::Bool(true)));
let count = call(
&mut vm,
"find_text",
vec![
s(&dir.path().to_string_lossy()),
s("needle"),
dict(vec![
("mode", s("count")),
("preset", s("source")),
("parallel", b(true)),
]),
],
)
.unwrap();
assert_eq!(count.as_int(), Some(2));
}
#[test]
fn find_text_background_returns_handle_and_feedback() {
let _guard = LONG_RUNNING_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let _inbox_reset_guard = crate::orchestration::agent_inbox::lock_reset_for_test();
crate::stdlib::long_running::reset_state();
let session_id = format!("fs-long-running-{}", uuid::Uuid::now_v7());
let _session_guard = crate::agent_sessions::enter_current_session(session_id.clone());
let _ = crate::orchestration::agent_inbox::drain(&session_id);
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(dir.path().join("src/lib.harn"), "needle\n").unwrap();
let mut vm = vm();
let response = call(
&mut vm,
"find_text",
vec![
s(&dir.path().to_string_lossy()),
s("needle"),
dict(vec![("background", b(true))]),
],
)
.unwrap();
let response = response.as_dict().expect("handle dict");
assert_eq!(response["status"].display(), "running");
assert_eq!(response["operation"].display(), "find_text");
let handle_id = response["handle_id"].display();
let payload = drain_feedback(&session_id, &handle_id);
assert_eq!(payload["status"], "completed");
assert_eq!(payload["operation"], "find_text");
let result = payload["result"].as_array().unwrap();
assert_eq!(result.len(), 1);
assert!(result[0]["path"]
.as_str()
.unwrap()
.ends_with("src/lib.harn"));
assert_eq!(result[0]["line"], 1);
assert_eq!(result[0]["col"], 1);
}
#[test]
fn find_evidence_is_labeled_overlapping_and_worker_deterministic() {
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
std::fs::write(first.path().join("a.txt"), "needle\n").unwrap();
std::fs::write(second.path().join("b.txt"), "a needle twice needle\n").unwrap();
let roots = VmValue::List(Arc::new(vec![
evidence_root("z-root", second.path()),
evidence_root("a-root", first.path()),
]));
let patterns = VmValue::List(Arc::new(vec![
evidence_pattern("whole", "needle"),
evidence_pattern("prefix", "need"),
]));
let mut vm = vm();
let sequential = call(
&mut vm,
"find_evidence",
vec![
roots.clone(),
patterns.clone(),
dict(vec![("threads", VmValue::Int(1))]),
],
)
.unwrap();
let parallel = call(
&mut vm,
"find_evidence",
vec![roots, patterns, dict(vec![("threads", VmValue::Int(4))])],
)
.unwrap();
assert_eq!(
crate::llm::vm_value_to_json(&sequential),
crate::llm::vm_value_to_json(¶llel)
);
assert_eq!(field(¶llel, "status").display(), "complete");
assert_eq!(field(¶llel, "match_count").as_int(), Some(6));
assert_eq!(field(¶llel, "files_scanned").as_int(), Some(2));
let VmValue::List(roots) = field(¶llel, "roots") else {
panic!("roots must be a list");
};
assert_eq!(field(&roots[0], "id").display(), "a-root");
let VmValue::List(groups) = field(&roots[1], "patterns") else {
panic!("patterns must be a list");
};
assert_eq!(field(&groups[0], "id").display(), "prefix");
assert_eq!(field(&groups[0], "count").as_int(), Some(2));
assert_eq!(field(&groups[1], "count").as_int(), Some(2));
}
#[cfg(unix)]
#[test]
fn find_evidence_follows_symlinks_only_when_requested() {
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().unwrap();
let external = tempfile::tempdir().unwrap();
std::fs::write(external.path().join("linked.txt"), "needle\n").unwrap();
symlink(external.path(), root.path().join("linked")).unwrap();
let roots = VmValue::List(Arc::new(vec![evidence_root("repo", root.path())]));
let patterns = VmValue::List(Arc::new(vec![evidence_pattern("needle", "needle")]));
let mut vm = vm();
let default_receipt = call(
&mut vm,
"find_evidence",
vec![roots.clone(), patterns.clone()],
)
.unwrap();
assert_eq!(field(&default_receipt, "match_count").as_int(), Some(0));
let followed = call(
&mut vm,
"find_evidence",
vec![roots, patterns, dict(vec![("follow_symlinks", b(true))])],
)
.unwrap();
assert_eq!(field(&followed, "status").display(), "complete");
assert_eq!(field(&followed, "match_count").as_int(), Some(1));
let VmValue::List(roots) = field(&followed, "roots") else {
panic!("roots must be a list");
};
let VmValue::List(groups) = field(&roots[0], "patterns") else {
panic!("patterns must be a list");
};
let VmValue::List(hits) = field(&groups[0], "hits") else {
panic!("hits must be a list");
};
assert_eq!(field(&hits[0], "path").display(), "linked/linked.txt");
}
#[test]
fn find_evidence_background_returns_typed_feedback() {
let _guard = LONG_RUNNING_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let _inbox_reset_guard = crate::orchestration::agent_inbox::lock_reset_for_test();
crate::stdlib::long_running::reset_state();
let session_id = format!("fs-evidence-long-running-{}", uuid::Uuid::now_v7());
let _session_guard = crate::agent_sessions::enter_current_session(session_id.clone());
let _ = crate::orchestration::agent_inbox::drain(&session_id);
let root = tempfile::tempdir().unwrap();
std::fs::write(root.path().join("source.harn"), "needle\n").unwrap();
let mut vm = vm();
let response = call(
&mut vm,
"find_evidence",
vec![
VmValue::List(Arc::new(vec![evidence_root("repo", root.path())])),
VmValue::List(Arc::new(vec![evidence_pattern("needle", "needle")])),
dict(vec![("background", b(true))]),
],
)
.unwrap();
let response = response.as_dict().expect("handle dict");
assert_eq!(response["status"].display(), "running");
assert_eq!(response["operation"].display(), "find_evidence");
let payload = drain_feedback(&session_id, &response["handle_id"].display());
assert_eq!(payload["status"], "completed");
assert_eq!(payload["operation"], "find_evidence");
assert_eq!(payload["result"]["schema_version"], "harn.fs.evidence.v1");
assert_eq!(payload["result"]["status"], "complete");
assert_eq!(payload["result"]["match_count"], 1);
}
#[test]
fn find_evidence_settles_failed_roots_and_handles_non_utf8() {
let readable = tempfile::tempdir().unwrap();
std::fs::write(
readable.path().join("bytes.bin"),
b"before\xffneedle after\n",
)
.unwrap();
std::fs::create_dir_all(readable.path().join("node_modules/pkg")).unwrap();
std::fs::write(
readable.path().join("node_modules/pkg/generated.js"),
"needle\n",
)
.unwrap();
let missing = readable.path().join("missing");
let roots = VmValue::List(Arc::new(vec![
evidence_root("missing", &missing),
evidence_root("readable", readable.path()),
]));
let patterns = VmValue::List(Arc::new(vec![evidence_pattern("needle", "needle")]));
let mut vm = vm();
let receipt = call(
&mut vm,
"find_evidence",
vec![roots, patterns, dict(vec![("preset", s("source"))])],
)
.unwrap();
assert_eq!(field(&receipt, "status").display(), "partial");
assert_eq!(field(&receipt, "failed_root_count").as_int(), Some(1));
assert_eq!(field(&receipt, "match_count").as_int(), Some(1));
let VmValue::List(roots) = field(&receipt, "roots") else {
panic!("roots must be a list");
};
assert_eq!(field(&roots[0], "status").display(), "failed");
assert_eq!(field(&roots[1], "status").display(), "complete");
}
#[test]
fn find_evidence_applies_stable_global_and_root_budgets() {
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
std::fs::write(first.path().join("a.txt"), "x x x\n").unwrap();
std::fs::write(second.path().join("b.txt"), "x x x\n").unwrap();
let roots = VmValue::List(Arc::new(vec![
evidence_root("b", second.path()),
evidence_root("a", first.path()),
]));
let patterns = VmValue::List(Arc::new(vec![evidence_pattern("x", "x")]));
let mut vm = vm();
let receipt = call(
&mut vm,
"find_evidence",
vec![
roots,
patterns,
dict(vec![
("max_matches", VmValue::Int(3)),
("max_matches_per_root", VmValue::Int(2)),
("threads", VmValue::Int(2)),
]),
],
)
.unwrap();
assert_eq!(field(&receipt, "status").display(), "truncated");
assert_eq!(field(&receipt, "match_count").as_int(), Some(3));
let VmValue::List(roots) = field(&receipt, "roots") else {
panic!("roots must be a list");
};
assert_eq!(field(&roots[0], "match_count").as_int(), Some(2));
assert_eq!(field(&roots[1], "match_count").as_int(), Some(1));
}
#[test]
fn find_evidence_rejects_duplicate_or_empty_labels() {
let root = tempfile::tempdir().unwrap();
let duplicate_roots = VmValue::List(Arc::new(vec![
evidence_root("same", root.path()),
evidence_root("same", root.path()),
]));
let patterns = VmValue::List(Arc::new(vec![evidence_pattern("p", "x")]));
let mut vm = vm();
let error = call(&mut vm, "find_evidence", vec![duplicate_roots, patterns]).unwrap_err();
assert!(error.to_string().contains("duplicate roots id `same`"));
let roots = VmValue::List(Arc::new(vec![evidence_root("root", root.path())]));
let empty_patterns = VmValue::List(Arc::new(vec![evidence_pattern("", "x")]));
let error = call(&mut vm, "find_evidence", vec![roots, empty_patterns]).unwrap_err();
assert!(error.to_string().contains("patterns id must not be empty"));
}