#![cfg(feature = "code-search")]
use std::process::Command;
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_basemind")
}
const FIXTURE: &str = "/// Parse a configuration file's text into a typed Config value.\n\
pub fn parse_config(text: &str) -> Config {\n\
\x20 let _ = text;\n\
\x20 Config { name: String::new() }\n\
}\n\
\n\
pub struct Config {\n\
\x20 pub name: String,\n\
}\n";
#[test]
fn search_code_finds_chunk_then_get_chunk_fetches_body() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(root.join("lib.rs"), FIXTURE).expect("write fixture");
let scan = Command::new(bin())
.current_dir(root)
.arg("scan")
.output()
.expect("spawn scan");
assert!(
scan.status.success(),
"basemind scan failed: {}",
String::from_utf8_lossy(&scan.stderr)
);
let out = Command::new(bin())
.current_dir(root)
.args([
"--json",
"query",
"search-code",
"parse a configuration file into a struct",
])
.output()
.expect("spawn search-code");
if !out.status.success() {
eprintln!(
"SKIP: search-code errored (embedder unavailable / offline): {}",
String::from_utf8_lossy(&out.stderr)
);
return;
}
let stdout = String::from_utf8_lossy(&out.stdout);
let value: serde_json::Value = match serde_json::from_str(&stdout) {
Ok(v) => v,
Err(e) => {
eprintln!("SKIP: search-code produced non-JSON output ({e}): {stdout}");
return;
}
};
let hits = value.get("hits").and_then(|h| h.as_array());
let Some(hits) = hits else {
eprintln!("SKIP: search-code response has no `hits` array: {value}");
return;
};
if hits.is_empty() {
eprintln!("SKIP: zero hits (grammar cold or embedder offline) — code-search path exercised without a corpus");
return;
}
let top = &hits[0];
assert_eq!(
top.get("path").and_then(|p| p.as_str()),
Some("lib.rs"),
"top hit must point at the only indexed file: {top}"
);
let chunk_id = top
.get("chunk_id")
.and_then(|c| c.as_str())
.expect("hit carries a chunk_id pointer");
assert!(
chunk_id.contains(':'),
"chunk_id is content-addressed `<hash>:<ordinal>`: {chunk_id}"
);
let gc = Command::new(bin())
.current_dir(root)
.args(["--json", "query", "get-chunk", "lib.rs", "--chunk-id", chunk_id])
.output()
.expect("spawn get-chunk");
assert!(
gc.status.success(),
"get-chunk failed: {}",
String::from_utf8_lossy(&gc.stderr)
);
let gv: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&gc.stdout)).expect("get-chunk emits JSON");
let text = gv.get("text").and_then(|t| t.as_str()).unwrap_or("");
assert!(!text.is_empty(), "get_chunk must return a non-empty body: {gv}");
assert_eq!(
gv.get("chunk_id").and_then(|c| c.as_str()),
Some(chunk_id),
"get_chunk echoes the requested chunk_id"
);
}
#[test]
fn stale_sidecar_rechunked_when_content_unchanged() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(root.join("lib.rs"), FIXTURE).expect("write fixture");
let scan1 = Command::new(bin())
.current_dir(root)
.arg("scan")
.output()
.expect("spawn first scan");
assert!(
scan1.status.success(),
"first scan failed: {}",
String::from_utf8_lossy(&scan1.stderr)
);
let sidecar = find_chunk_sidecar(root);
let Some(sidecar) = sidecar else {
eprintln!(
"SKIP: no .chunk.msgpack sidecar found after first scan \
(chunker may be disabled or model unavailable)"
);
return;
};
assert!(
sidecar.exists(),
"sidecar must exist after first scan: {}",
sidecar.display()
);
std::fs::remove_file(&sidecar).expect("remove sidecar");
assert!(!sidecar.exists(), "sidecar must be gone after manual deletion");
let scan2 = Command::new(bin())
.current_dir(root)
.arg("scan")
.output()
.expect("spawn second scan");
assert!(
scan2.status.success(),
"second scan failed: {}",
String::from_utf8_lossy(&scan2.stderr)
);
assert!(
sidecar.exists(),
"re-scan must regenerate the .chunk.msgpack sidecar after it was deleted \
(the stale-sidecar guard should force re-chunking despite unchanged content hash): \
sidecar={}",
sidecar.display()
);
}
#[test]
fn search_code_keyword_mode_ranks_by_bm25() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(root.join("lib.rs"), FIXTURE).expect("write fixture");
std::fs::create_dir_all(root.join(".basemind")).expect("mkdir .basemind");
std::fs::write(
root.join(".basemind/basemind.toml"),
"\"$schema\" = \"v1\"\n\n[code_search]\nembed = false\n",
)
.expect("write config");
let scan = Command::new(bin())
.current_dir(root)
.arg("scan")
.output()
.expect("spawn scan");
assert!(
scan.status.success(),
"basemind scan failed: {}",
String::from_utf8_lossy(&scan.stderr)
);
let out = Command::new(bin())
.current_dir(root)
.args(["--json", "query", "search-code", "--mode", "keyword", "config parser"])
.output()
.expect("spawn keyword search-code");
assert!(
out.status.success(),
"keyword search-code failed (should not need an embedder): {}",
String::from_utf8_lossy(&out.stderr)
);
let value: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).expect("keyword search-code emits JSON");
let hits = value
.get("hits")
.and_then(|h| h.as_array())
.expect("keyword response carries a hits array");
assert!(
!hits.is_empty(),
"keyword search must find the `config`-bearing chunk (embed-free, deterministic): {value}"
);
let top = &hits[0];
assert_eq!(
top.get("path").and_then(|p| p.as_str()),
Some("lib.rs"),
"top keyword hit must point at the only indexed file: {top}"
);
let score = top
.get("score")
.and_then(serde_json::Value::as_f64)
.expect("keyword hit carries a BM25 score");
assert!(
score > 0.0,
"a matching keyword hit must have a positive BM25 score: {top}"
);
assert!(
top.get("distance").is_none(),
"keyword hit must not carry a vector distance: {top}"
);
let chunk_id = top
.get("chunk_id")
.and_then(|c| c.as_str())
.expect("keyword hit carries a chunk_id pointer");
let gc = Command::new(bin())
.current_dir(root)
.args(["--json", "query", "get-chunk", "lib.rs", "--chunk-id", chunk_id])
.output()
.expect("spawn get-chunk");
assert!(
gc.status.success(),
"get-chunk failed: {}",
String::from_utf8_lossy(&gc.stderr)
);
let gv: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&gc.stdout)).expect("get-chunk emits JSON");
assert!(
gv.get("text").and_then(|t| t.as_str()).is_some_and(|t| !t.is_empty()),
"get_chunk must return a non-empty body for the keyword hit: {gv}"
);
}
#[test]
fn search_code_hybrid_ranks_exact_symbol_first() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(root.join("lib.rs"), FIXTURE).expect("write fixture");
std::fs::create_dir_all(root.join(".basemind")).expect("mkdir .basemind");
std::fs::write(
root.join(".basemind/basemind.toml"),
"\"$schema\" = \"v1\"\n\n[code_search]\nembed = false\n",
)
.expect("write config");
let scan = Command::new(bin())
.current_dir(root)
.arg("scan")
.output()
.expect("spawn scan");
assert!(
scan.status.success(),
"basemind scan failed: {}",
String::from_utf8_lossy(&scan.stderr)
);
let out = Command::new(bin())
.current_dir(root)
.args(["--json", "query", "search-code", "parse_config"])
.output()
.expect("spawn hybrid search-code");
assert!(
out.status.success(),
"default (hybrid) search-code failed without an embedder: {}",
String::from_utf8_lossy(&out.stderr)
);
let value: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).expect("hybrid search-code emits JSON");
let hits = value
.get("hits")
.and_then(|h| h.as_array())
.expect("hybrid response carries a hits array");
assert!(
!hits.is_empty(),
"hybrid search must find the parse_config chunk: {value}"
);
let top = &hits[0];
assert_eq!(
top.get("symbol").and_then(|s| s.as_str()),
Some("parse_config"),
"the exact symbol lane must float parse_config's defining chunk to rank #1: {top}"
);
assert!(
top.get("score")
.and_then(serde_json::Value::as_f64)
.is_some_and(|s| s > 0.0),
"hybrid hit must carry a positive fused RRF score: {top}"
);
let lanes: Vec<&str> = top
.get("matched_lanes")
.and_then(|v| v.as_array())
.expect("hybrid hit carries matched_lanes")
.iter()
.filter_map(serde_json::Value::as_str)
.collect();
assert!(
lanes.contains(&"exact"),
"the exact lane must be credited in matched_lanes for an identifier query: {top}"
);
assert_eq!(
top.get("exact_rank").and_then(serde_json::Value::as_u64),
Some(1),
"the defining chunk must be exact-lane rank #1: {top}"
);
assert!(
top.get("vector_rank").is_none(),
"no vector lane under embed=false, so vector_rank must be absent: {top}"
);
}
fn find_chunk_sidecar(root: &std::path::Path) -> Option<std::path::PathBuf> {
fn walk(dir: &std::path::Path) -> Option<std::path::PathBuf> {
let entries = std::fs::read_dir(dir).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(found) = walk(&path) {
return Some(found);
}
} else if path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.ends_with(".chunk.msgpack"))
{
return Some(path);
}
}
None
}
walk(&root.join(".basemind"))
}