use rusqlite::Connection;
use semlith::Semlith;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::{Duration, Instant};
const APPEAR_TIMEOUT: Duration = Duration::from_secs(60);
const DEBOUNCE: Duration = Duration::from_millis(200);
fn polyglot(dir: &Path) {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/graph");
for language in fs::read_dir(&root).expect("the fixture corpus is missing") {
let language = language.unwrap().path();
if !language.is_dir() {
continue;
}
for fixture in fs::read_dir(&language).unwrap() {
let fixture = fixture.unwrap().path();
let name = fixture.file_name().unwrap().to_string_lossy().to_string();
write(dir, &name, &fs::read_to_string(&fixture).unwrap());
}
}
write(dir, "notes.txt", "The lock is acquired before the write.");
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn indexing_fills_the_graph_for_every_advertised_language() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
polyglot(corpus.path());
index(store.path(), corpus.path());
let s = Semlith::open(store.path(), None).unwrap();
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/graph");
let mut checked = 0;
for language in fs::read_dir(&root).unwrap() {
let language = language.unwrap().path();
if !language.is_dir() {
continue;
}
for fixture in fs::read_dir(&language).unwrap() {
let file = fixture.unwrap().file_name().to_string_lossy().to_string();
assert!(
symbols_in(s.db(), &file) > 0,
"{file} produced no symbols at all"
);
checked += 1;
}
}
assert_eq!(
checked,
semlith::filter::LANGUAGES.len(),
"every advertised language contributes a fixture to the corpus"
);
assert!(
calls_from(s.db(), "acquire").contains(&"helper".to_string()),
"acquire -> helper was not extracted; calls found: {:?}",
calls_from(s.db(), "acquire")
);
assert_eq!(
symbols_in(s.db(), "notes.txt"),
0,
"a file in no language at all must contribute no symbols, not an error"
);
let bad: i64 = s
.db()
.query_row(
"SELECT COUNT(*) FROM edges WHERE confidence NOT IN ('extracted', 'inferred')",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(bad, 0, "an edge carries a confidence outside the two");
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn editing_a_file_re_extracts_that_file_and_only_that_file() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
polyglot(corpus.path());
index(store.path(), corpus.path());
let before: Vec<(String, i64)> = {
let s = Semlith::open(store.path(), None).unwrap();
per_file_counts(s.db())
};
write(
corpus.path(),
"lock.rs",
"use std::fs::File;\n\
fn helper() {}\n\
fn release() {}\n\
fn acquire() { release(); }\n",
);
index(store.path(), corpus.path());
let s = Semlith::open(store.path(), None).unwrap();
let after = per_file_counts(s.db());
for (path, count) in &before {
if path.ends_with("lock.rs") {
continue;
}
let now = after
.iter()
.find(|(p, _)| p == path)
.map(|(_, c)| *c)
.unwrap_or(-1);
assert_eq!(
*count, now,
"{path} was re-extracted by an edit to another file"
);
}
let calls = calls_from_in(s.db(), "acquire", "lock.rs");
assert!(
calls.contains(&"release".to_string()),
"the new call was not extracted: {calls:?}"
);
assert!(
!calls.contains(&"helper".to_string()),
"the old call survived the edit: {calls:?}"
);
let orphans: i64 = s
.db()
.query_row(
"SELECT COUNT(*) FROM symbols s LEFT JOIN files f ON f.id = s.file_id
WHERE f.id IS NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(
orphans, 0,
"symbols survived the file they were extracted from"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn forgetting_a_file_takes_its_symbols_and_edges() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
polyglot(corpus.path());
index(store.path(), corpus.path());
let mut s = Semlith::open(store.path(), None).unwrap();
s.quiet = true;
assert!(symbols_in(s.db(), "lock.rs") > 0);
let elsewhere = symbols_in(s.db(), "run.py");
s.forget(&corpus.path().join("lock.rs")).unwrap();
assert_eq!(
symbols_in(s.db(), "lock.rs"),
0,
"symbols outlived the file"
);
assert_eq!(
symbols_in(s.db(), "run.py"),
elsewhere,
"forgetting one file disturbed another"
);
let dangling: i64 = s
.db()
.query_row(
"SELECT COUNT(*) FROM edges e LEFT JOIN symbols s ON s.id = e.src
WHERE s.id IS NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(dangling, 0, "edges outlived the symbols they leave");
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn an_edit_under_the_watcher_updates_the_graph_with_the_vectors() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(
corpus.path(),
"lock.rs",
"fn helper() {}\nfn acquire() { helper(); }\n",
);
index(store.path(), corpus.path());
let stop = Arc::new(AtomicBool::new(false));
let handle = {
let (store_path, roots, flag) = (
store.path().to_path_buf(),
vec![corpus.path().to_path_buf()],
Arc::clone(&stop),
);
thread::spawn(move || {
let mut s = Semlith::open(&store_path, None).unwrap();
s.quiet = true;
semlith::watch::run(&mut s, &roots, DEBOUNCE, &flag, |_| {}).unwrap();
})
};
write(
corpus.path(),
"lock.rs",
"fn helper() {}\nfn release() {}\nfn acquire() { release(); }\n",
);
let updated = wait_until(store.path(), |db| {
calls_from_in(db, "acquire", "lock.rs").contains(&"release".to_string())
});
assert!(
updated,
"the watcher re-embedded the file but the graph still describes the old text"
);
let s = Semlith::open(store.path(), None).unwrap();
assert!(
!calls_from_in(s.db(), "acquire", "lock.rs").contains(&"helper".to_string()),
"the edge the edit removed is still in the graph"
);
drop(s);
stop.store(true, Ordering::Relaxed);
handle.join().unwrap();
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn the_three_commands_answer_and_neighbours_agrees_with_a_sweep() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(
corpus.path(),
"lock.rs",
"fn acquire() { hold(); }\n\
fn hold() { release(); }\n\
fn release() {}\n\
fn alone() {}\n",
);
index(store.path(), corpus.path());
let out = cli(store.path(), &["symbol", "hold"]);
assert!(out.contains("lock.rs"), "symbol found nothing: {out}");
let out = cli(store.path(), &["neighbors", "hold"]);
assert!(out.contains("acquire"), "hold's caller is missing: {out}");
assert!(out.contains("release"), "hold's callee is missing: {out}");
let out = cli(store.path(), &["path", "acquire", "release"]);
assert!(out.contains("acquire @"), "{out}");
assert!(out.contains("hold @"), "{out}");
assert!(out.contains("release @"), "{out}");
assert!(
out.contains("2 hops"),
"the trailer counts the chain: {out}"
);
assert!(
!out.contains("hypothesis"),
"every hop here resolves, so there is nothing to qualify: {out}"
);
let out = cli(store.path(), &["path", "acquire", "alone"]);
assert!(
out.contains("not connected"),
"unconnected must say so: {out}"
);
let body = fs::read_to_string(corpus.path().join("lock.rs")).unwrap();
let callers: Vec<&str> = ["acquire", "hold", "alone"]
.into_iter()
.filter(|f| {
let start = body.find(&format!("fn {f}()")).unwrap();
body[start..]
.split_once('}')
.is_some_and(|(b, _)| b.contains("release("))
})
.collect();
assert_eq!(callers, ["hold"], "the sweep itself is wrong");
let s = Semlith::open(store.path(), None).unwrap();
let kinds: Vec<String> = semlith::graph::DEPENDENCY_KINDS
.iter()
.map(|k| k.to_string())
.collect();
let around = semlith::graph::neighbours(s.db(), "release", &kinds, false).unwrap();
let names: Vec<String> = around
.callers
.iter()
.map(|e| e.symbol.name.clone())
.collect();
assert_eq!(
names,
["hold"],
"neighbours disagrees with a full-text sweep"
);
drop(s);
}
#[test]
fn the_impact_command_answers() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_semlith"))
.args(["impact", "--help"])
.output()
.expect("the binary runs");
assert!(
out.status.success(),
"`semlith impact` must be a command: {}",
String::from_utf8_lossy(&out.stderr)
);
let help = String::from_utf8_lossy(&out.stdout);
for flag in ["--depth", "--all-edges", "--strict", "--json"] {
assert!(help.contains(flag), "`semlith impact` is missing {flag}");
}
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn impact_lists_every_caller_by_the_fewest_hops_it_takes() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(corpus.path(), "a.rs", "fn start() { middle(); }\n");
write(corpus.path(), "b.rs", "fn middle() { finish(); }\n");
write(corpus.path(), "c.rs", "fn aside() { finish(); }\n");
write(corpus.path(), "d.rs", "fn finish() {}\n");
index(store.path(), corpus.path());
let out = cli(store.path(), &["impact", "finish"]);
assert!(
out.contains("middle"),
"the direct caller is missing: {out}"
);
assert!(
out.contains("aside"),
"the other direct caller is missing: {out}"
);
assert!(
out.contains("start"),
"the two-hop caller is missing: {out}"
);
assert!(out.contains("1 hop"), "hops are not grouped: {out}");
assert!(out.contains("2 hops"), "hops are not grouped: {out}");
assert!(out.contains("files"), "the files block is missing: {out}");
let one = cli(store.path(), &["impact", "finish", "--depth", "1"]);
assert!(one.contains("middle"), "{one}");
assert!(one.contains("aside"), "{one}");
assert!(!one.contains("start"), "--depth 1 reached two hops: {one}");
let none = cli(store.path(), &["impact", "start"]);
assert!(
none.contains("nothing in this store reaches start"),
"{none}"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn every_impact_row_states_what_its_edge_is_worth() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(corpus.path(), "a.rs", "fn start() { middle(); }\n");
write(corpus.path(), "b.rs", "fn middle() { finish(); }\n");
write(corpus.path(), "c.rs", "fn finish() {}\n");
index(store.path(), corpus.path());
let raw = cli(store.path(), &["impact", "finish", "--json"]);
let parsed: serde_json::Value = serde_json::from_str(&raw).expect("--json is json");
let reached = parsed["reached"].as_array().expect("reached is a list");
assert!(!reached.is_empty(), "{raw}");
for row in reached {
let confidence = row["confidence"].as_str().unwrap_or("");
assert!(
["extracted", "resolved", "inferred", "ambiguous"].contains(&confidence),
"a row reached with no support class: {row}"
);
assert!(row["hop"].as_u64().unwrap_or(0) >= 1, "{row}");
assert!(!row["via"].as_str().unwrap_or("").is_empty(), "{row}");
}
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn a_path_through_a_name_with_two_definitions_is_refused_and_then_labelled() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(corpus.path(), "start.rs", "fn start() { record(); }\n");
write(
corpus.path(),
"one.rs",
"fn record() { nothing(); }\nfn nothing() {}\n",
);
write(
corpus.path(),
"two.rs",
"fn record() { finish(); }\nfn finish() {}\n",
);
index(store.path(), corpus.path());
let refused = cli(store.path(), &["path", "start", "finish"]);
assert!(
refused.contains("not connected"),
"the only route crosses a name with two definitions: {refused}"
);
assert!(refused.contains("--all-edges"), "{refused}");
let walked = cli(store.path(), &["path", "start", "finish", "--all-edges"]);
assert!(walked.contains("seam"), "{walked}");
assert!(walked.contains("record: 2 definitions"), "{walked}");
assert!(walked.contains("A hypothesis, not a finding."), "{walked}");
assert!(
walked.contains("start.rs:1"),
"every hop shows both endpoints: {walked}"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn ambiguous_callees_collapse_to_one_row_and_expand_with_all() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(corpus.path(), "start.rs", "fn start() { record(); }\n");
for (i, name) in ["one", "two", "three"].iter().enumerate() {
write(
corpus.path(),
&format!("{name}.rs"),
&format!("fn record() {{ let _ = {i}; }}\n"),
);
}
index(store.path(), corpus.path());
let collapsed = cli(store.path(), &["neighbors", "start"]);
assert_eq!(
collapsed.matches("record").count(),
1,
"one row stands for all three definitions: {collapsed}"
);
assert!(collapsed.contains("3 definitions"), "{collapsed}");
let expanded = cli(store.path(), &["neighbors", "start", "--all"]);
assert_eq!(
expanded.matches("record").count(),
3,
"--all shows every definition behind the collapsed row: {expanded}"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn targets_outside_the_store_are_counted_and_shown_only_with_all() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(
corpus.path(),
"start.rs",
"fn start() { somewhere_else(); }\n",
);
index(store.path(), corpus.path());
let narrow = cli(store.path(), &["neighbors", "start"]);
assert!(
!narrow.contains("somewhere_else"),
"not listed by default: {narrow}"
);
assert!(
narrow.contains("outside this store"),
"but the reader is told the list is short: {narrow}"
);
let wide = cli(store.path(), &["neighbors", "start", "--all"]);
assert!(wide.contains("somewhere_else"), "{wide}");
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn the_json_output_matches_what_the_library_returns() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(corpus.path(), "lock.rs", "fn a() { b(); }\nfn b() {}\n");
index(store.path(), corpus.path());
let printed: serde_json::Value =
serde_json::from_str(&cli(store.path(), &["path", "a", "b", "--json"])).unwrap();
let s = Semlith::open(store.path(), None).unwrap();
let direct =
serde_json::to_value(semlith::graph::shortest_path(s.db(), "a", "b", 6, false).unwrap())
.unwrap();
assert_eq!(printed, direct);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn a_chunk_reachable_only_through_the_graph_is_returned_and_badged() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(
corpus.path(),
"mixing.rs",
"/// Combine flour and water for the sourdough loaf.\n\
fn knead() { autolyse(); }\n",
);
write(
corpus.path(),
"proving.rs",
"fn autolyse() { let _ = 1; }\n",
);
index(store.path(), corpus.path());
let mut s = Semlith::open(store.path(), None).unwrap();
s.quiet = true;
let hits = s
.search("combine flour and water for the sourdough loaf", 8)
.unwrap();
let reached = hits
.iter()
.find(|h| h.path.ends_with("proving.rs"))
.unwrap_or_else(|| {
panic!(
"the graph-reached chunk is missing; got {:?}",
hits.iter().map(|h| (&h.path, &h.lists)).collect::<Vec<_>>()
)
});
assert!(
reached.lists.contains(&"graph"),
"the chunk came back without saying the graph found it: {:?}",
reached.lists
);
assert!(hits.iter().all(|h| !h.lists.is_empty()));
assert!(
hits[0].path.ends_with("mixing.rs"),
"expansion outranked the actual match"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn the_filter_constrains_the_graph_list_too() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(
corpus.path(),
"mixing.rs",
"/// Combine flour and water for the sourdough loaf.\n\
fn knead() { autolyse(); }\n",
);
write(
corpus.path(),
"proving.rs",
"fn autolyse() { let _ = 1; }\n",
);
index(store.path(), corpus.path());
let mut s = Semlith::open(store.path(), None).unwrap();
s.quiet = true;
let filter = semlith::filter::Filter::new(&["**/mixing.rs".to_string()], &[], &[]).unwrap();
let hits = s
.search_filtered("combine flour and water for the sourdough loaf", 8, &filter)
.unwrap();
assert!(
hits.iter().all(|h| h.path.ends_with("mixing.rs")),
"graph expansion reached outside the filter: {:?}",
hits.iter().map(|h| &h.path).collect::<Vec<_>>()
);
}
fn cli(store: &Path, args: &[&str]) -> String {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_semlith"))
.arg("--store")
.arg(store)
.args(args)
.output()
.expect("the binary runs");
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
)
}
fn symbols_in(db: &Connection, name: &str) -> i64 {
db.query_row(
"SELECT COUNT(*) FROM symbols s JOIN files f ON f.id = s.file_id
WHERE f.path LIKE '%' || ?1",
rusqlite::params![name],
|r| r.get(0),
)
.unwrap()
}
fn per_file_counts(db: &Connection) -> Vec<(String, i64)> {
let mut stmt = db
.prepare(
"SELECT f.path, COUNT(s.id) FROM files f LEFT JOIN symbols s ON s.file_id = f.id
GROUP BY f.id ORDER BY f.path",
)
.unwrap();
let rows = stmt.query_map([], |r| Ok((r.get(0)?, r.get(1)?))).unwrap();
rows.collect::<Result<Vec<_>, _>>().unwrap()
}
fn calls_from(db: &Connection, from: &str) -> Vec<String> {
let mut stmt = db
.prepare(
"SELECT e.dst FROM edges e JOIN symbols s ON s.id = e.src
WHERE s.name = ?1 AND e.kind = 'calls'",
)
.unwrap();
let rows = stmt
.query_map(rusqlite::params![from], |r| r.get::<_, String>(0))
.unwrap();
rows.collect::<Result<Vec<_>, _>>().unwrap()
}
fn calls_from_in(db: &Connection, from: &str, file: &str) -> Vec<String> {
let mut stmt = db
.prepare(
"SELECT e.dst FROM edges e
JOIN symbols s ON s.id = e.src
JOIN files f ON f.id = s.file_id
WHERE s.name = ?1 AND e.kind = 'calls' AND f.path LIKE '%' || ?2",
)
.unwrap();
let rows = stmt
.query_map(rusqlite::params![from, file], |r| r.get::<_, String>(0))
.unwrap();
rows.collect::<Result<Vec<_>, _>>().unwrap()
}
fn wait_until(store: &Path, done: impl Fn(&Connection) -> bool) -> bool {
let deadline = Instant::now() + APPEAR_TIMEOUT;
while Instant::now() < deadline {
let s = Semlith::open(store, None).unwrap();
if done(s.db()) {
return true;
}
drop(s);
thread::sleep(Duration::from_millis(300));
}
false
}
fn index(store: &Path, corpus: &Path) {
let mut s = Semlith::open(store, None).unwrap();
s.quiet = true;
s.index_paths(&[corpus.to_path_buf()], |_, _| {}).unwrap();
}
fn write(dir: &Path, name: &str, body: &str) {
fs::write(dir.join(name), body).unwrap();
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn a_path_crosses_a_re_export_and_neighbours_names_the_call_site() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(
corpus.path(),
"alias.rs",
"pub use real::worker as helper;\n",
);
write(
corpus.path(),
"real.rs",
"fn worker() { finish(); }\nfn finish() {}\n",
);
write(
corpus.path(),
"start.rs",
"fn start() {\n let x = 1;\n let _ = x;\n helper();\n}\n",
);
index(store.path(), corpus.path());
let walked = cli(store.path(), &["path", "start", "finish"]);
assert!(
walked.contains("aliases"),
"the hop through the re-export must say what kind of hop it was: {walked}"
);
assert!(
walked.contains("helper"),
"the alias is a node on the chain: {walked}"
);
assert!(
!walked.contains("not connected"),
"the chain exists once aliases are crossable: {walked}"
);
let out = cli(store.path(), &["neighbors", "start"]);
assert!(
out.contains("called at") && out.contains(":4"),
"the call site is line 4, not the line `start` begins on: {out}"
);
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn read_returns_one_span_and_refuses_to_guess_between_definitions() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(
corpus.path(),
"one.rs",
"fn alpha() {\n let marker = 1;\n}\nfn shared() {}\n",
);
write(corpus.path(), "two.rs", "fn shared() {}\n");
index(store.path(), corpus.path());
let by_name = cli(store.path(), &["read", "alpha"]);
assert!(by_name.contains("let marker = 1;"), "{by_name}");
assert!(
!by_name.contains("fn shared"),
"a read is the span and nothing around it: {by_name}"
);
let by_span = cli(store.path(), &["read", "one.rs:2-2"]);
assert!(by_span.contains("let marker = 1;"), "{by_span}");
assert!(
!by_span.contains("fn alpha"),
"line 2 is not line 1: {by_span}"
);
let ambiguous = cli(store.path(), &["read", "shared"]);
assert!(
ambiguous.contains("2 definitions"),
"two definitions and nothing to choose between them: {ambiguous}"
);
let missing = cli(store.path(), &["read", "nosuchsymbol"]);
assert!(missing.contains("nothing indexed"), "{missing}");
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn a_pattern_finds_the_shape_and_says_which_files_it_parsed() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(
corpus.path(),
"one.rs",
"fn caller() {\n helper();\n}\nfn helper() {}\n",
);
write(corpus.path(), "notes.md", "helper() is called here too\n");
index(store.path(), corpus.path());
let out = cli(
store.path(),
&[
"pattern",
"--lang",
"rust",
"(call_expression function: (identifier) @called)",
],
);
assert!(out.contains("@called"), "{out}");
assert!(out.contains("helper"), "{out}");
assert!(out.contains("one.rs:2"), "the call is on line 2: {out}");
assert!(!out.contains("notes.md"), "{out}");
let broken = cli(store.path(), &["pattern", "--lang", "rust", "(unbalanced"]);
assert!(
broken.contains("not a valid tree-sitter pattern"),
"{broken}"
);
let nolang = cli(store.path(), &["pattern", "--lang", "cobol", "(x) @y"]);
assert!(nolang.contains("no grammar for"), "{nolang}");
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn trace_states_the_answer_the_chain_and_one_line_per_hop() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(corpus.path(), "a.rs", "fn start() { middle(); }\n");
write(corpus.path(), "b.rs", "fn middle() { finish(); }\n");
write(corpus.path(), "c.rs", "fn finish() {}\n");
index(store.path(), corpus.path());
let out = cli(store.path(), &["trace", "start", "finish"]);
assert!(out.contains("answer"), "{out}");
assert!(out.contains("chain"), "{out}");
assert!(out.contains("supporting lines"), "{out}");
assert!(
out.contains("supporting fact") || out.contains("candidate"),
"every hop carries a mark: {out}"
);
let raw = cli(store.path(), &["trace", "start", "finish", "--json"]);
let parsed: serde_json::Value = serde_json::from_str(&raw).expect("--json is json");
let lines = parsed["lines"].as_array().expect("lines is a list");
let hops = parsed["chain"]["steps"].as_array().expect("steps").len();
assert_eq!(lines.len(), hops, "one supporting line per hop");
for line in lines {
let mark = line["mark"].as_str().unwrap_or("");
assert!(
mark == "supporting fact" || mark == "candidate — corroborate before use",
"unexpected mark {mark:?}"
);
}
let evidence = cli(store.path(), &["trace", "start", "finish", "--evidence"]);
assert!(evidence.contains("start → finish"), "{evidence}");
assert!(evidence.starts_with("start → finish"), "{evidence}");
assert!(evidence.contains("answer:"), "{evidence}");
assert!(evidence.contains("supporting lines"), "{evidence}");
let none = cli(store.path(), &["trace", "finish", "start"]);
assert!(none.contains("not connected"), "{none}");
}
#[test]
#[ignore = "downloads an embedding model on first run"]
fn graph_health_says_what_stats_says() {
let corpus = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
write(
corpus.path(),
"a.rs",
"fn start() { middle(); outside(); }\n",
);
write(corpus.path(), "b.rs", "fn middle() { finish(); }\n");
write(corpus.path(), "c.rs", "fn finish() {}\nfn twice() {}\n");
write(corpus.path(), "d.rs", "fn twice() {}\n");
index(store.path(), corpus.path());
let stats = cli(store.path(), &["stats"]);
assert!(
stats.contains("call targets with no definition here"),
"stats does not report unresolved targets: {stats}"
);
assert!(
stats.contains("names with several definitions"),
"stats does not report ambiguous names: {stats}"
);
let db = rusqlite::Connection::open(store.path().join("store.db")).unwrap();
let (top, distinct) = semlith::store::unresolved_targets(&db, 5).unwrap();
let several = semlith::store::names_with_several_definitions(&db).unwrap();
assert!(
stats.contains(&format!("{distinct} call targets with no definition here")),
"stats prints a different unresolved count than the store reports ({distinct}): {stats}"
);
assert!(
stats.contains(&format!("{several} names with several definitions")),
"stats prints a different ambiguous-name count than the store reports ({several}): {stats}"
);
assert!(several >= 1, "`twice` is defined in two files: {several}");
assert!(
top.iter().any(|(name, _)| name == "outside"),
"a call into nothing this store defines is an unresolved target: {top:?}"
);
let months = semlith::store::chunks_by_month(&db).unwrap();
let counted: i64 = months.iter().map(|(_, n)| n).sum();
let chunks: i64 = db
.query_row("SELECT COUNT(*) FROM chunks", [], |r| r.get(0))
.unwrap();
assert_eq!(counted, chunks, "a chunk fell out of the month buckets");
}