use super::*;
use crate::iri::{chunk_iri, symbol_iri};
use crate::types::ChunkType;
fn chunk(id: &str, file: &str, t: ChunkType, name: Option<&str>, start: u32) -> Chunk {
Chunk {
id: id.to_string(),
file_path: file.to_string(),
chunk_type: t,
name: name.map(str::to_string),
start_line: start,
end_line: start + 5,
content: "body".to_string(),
language: "rust".to_string(),
tags: String::new(),
}
}
fn edge(source: &str, target_name: &str, t: ChunkEdgeType) -> ChunkEdge {
ChunkEdge {
source_chunk: source.to_string(),
target_chunk: format!("{target_name}-target"),
source_name: source.to_string(),
target_name: target_name.to_string(),
edge_type: t,
file_path: "src/lib.rs".to_string(),
}
}
#[test]
fn named_code_chunk_mentions_its_own_symbol() {
let c = chunk("c1", "src/lib.rs", ChunkType::Function, Some("parse"), 10);
let names = chunk_mention_names(&c, &HashMap::new());
assert_eq!(names, vec!["parse"]);
}
#[test]
fn sections_and_unnamed_chunks_emit_no_self_mention() {
let section = chunk("c1", "docs/a.md", ChunkType::Section, Some("Guide"), 3);
assert!(chunk_mention_names(§ion, &HashMap::new()).is_empty());
let unnamed = chunk("c2", "src/lib.rs", ChunkType::Function, None, 9);
assert!(chunk_mention_names(&unnamed, &HashMap::new()).is_empty());
}
#[test]
fn edge_targets_become_mentions_deduped_and_sorted() {
let c = chunk("impl1", "src/lib.rs", ChunkType::Impl, Some("Foo"), 20);
let edges = vec![
edge("impl1", "Display", ChunkEdgeType::Implements),
edge("impl1", "Foo", ChunkEdgeType::ImplFor), edge("impl1", "Bar", ChunkEdgeType::Extends),
edge("other", "Baz", ChunkEdgeType::Tests), edge("impl1", "Next", ChunkEdgeType::NextChunk), ];
let map = edge_mention_map(&edges);
let names = chunk_mention_names(&c, &map);
assert_eq!(names, vec!["Bar", "Display", "Foo"]);
}
#[test]
fn turtle_carries_mention_literals_for_fixture_chunk() {
let chunks = vec![chunk(
"c1",
"src/lib.rs",
ChunkType::Function,
Some("parse"),
10,
)];
let edges = vec![edge("c1", "Config", ChunkEdgeType::Tests)];
let turtle = super::super::chunks::generate_chunk_turtle(&chunks, &edges, "myrepo");
assert!(turtle.contains("bobbin:mentions \"Config\""));
assert!(turtle.contains("bobbin:mentions \"parse\""));
}
fn seed(store: &mut quipu::Store, turtle: &str) {
quipu::tool_knot(
store,
&serde_json::json!({
"turtle": turtle,
"timestamp": "2026-01-01T00:00:00Z",
"actor": "test",
"source": "test-seed",
}),
)
.expect("seed write");
}
fn seeded_store() -> (quipu::Store, String) {
let mut store = quipu::Store::open_in_memory().expect("in-memory store");
let c_iri = chunk_iri("repo", "src/main.rs", 5);
let unique = symbol_iri("repo", "src/util.rs", "Unique");
let twin_a = symbol_iri("repo", "src/a.rs", "Twin");
let twin_b = symbol_iri("repo", "src/b.rs", "Twin");
let turtle = format!(
"@prefix bobbin: <{ns}> .\n\
<{c_iri}> a bobbin:Chunk ;\n\
bobbin:mentions \"Unique\" ;\n\
bobbin:mentions \"Ghost\" ;\n\
bobbin:mentions \"Twin\" .\n\
<{unique}> bobbin:name \"Unique\" .\n\
<{twin_a}> bobbin:name \"Twin\" .\n\
<{twin_b}> bobbin:name \"Twin\" .\n",
ns = crate::iri::ONTOLOGY_NS,
);
seed(&mut store, &turtle);
(store, unique)
}
#[test]
fn resolves_danglings_and_ambiguous_honestly() {
let (mut store, unique_iri) = seeded_store();
let report = reconcile_mentions(&mut store, "2026-01-02T00:00:00Z").expect("reconcile");
assert_eq!(report.resolved, 1);
assert_eq!(report.dangling, 1);
assert_eq!(report.ambiguous, 1);
assert_eq!(report.edges_written, 1);
let mentions_id = store.lookup(&mentions_iri()).unwrap().unwrap();
let refs: Vec<String> = store
.current_facts()
.unwrap()
.iter()
.filter(|f| f.attribute == mentions_id)
.filter_map(|f| match f.value {
quipu::types::Value::Ref(t) => Some(store.resolve(t).unwrap()),
_ => None,
})
.collect();
assert_eq!(refs, vec![unique_iri]);
let literals = store
.current_facts()
.unwrap()
.iter()
.filter(|f| f.attribute == mentions_id && matches!(f.value, quipu::types::Value::Str(_)))
.count();
assert_eq!(literals, 3);
}
#[test]
fn ambiguity_narrows_to_the_chunks_own_module() {
let mut store = quipu::Store::open_in_memory().expect("in-memory store");
let c_iri = chunk_iri("repo", "src/main.rs", 5);
let local = symbol_iri("repo", "src/main.rs", "Twin");
let remote = symbol_iri("repo", "src/other.rs", "Twin");
let turtle = format!(
"@prefix bobbin: <{ns}> .\n\
<{c_iri}> a bobbin:Chunk ; bobbin:mentions \"Twin\" .\n\
<{local}> bobbin:name \"Twin\" .\n\
<{remote}> bobbin:name \"Twin\" .\n",
ns = crate::iri::ONTOLOGY_NS,
);
seed(&mut store, &turtle);
let report = reconcile_mentions(&mut store, "2026-01-02T00:00:00Z").expect("reconcile");
assert_eq!(report.resolved, 1);
assert_eq!(report.ambiguous, 0);
match &report.details[0] {
MentionResolution::Resolved { entity_iri, .. } => assert_eq!(entity_iri, &local),
other => panic!("expected same-module resolve, got {other:?}"),
}
}
#[test]
fn second_run_is_a_noop_with_identical_report() {
let (mut store, _) = seeded_store();
let first = reconcile_mentions(&mut store, "2026-01-02T00:00:00Z").expect("first run");
let facts_after_first = store.current_facts().unwrap().len();
let second = reconcile_mentions(&mut store, "2026-01-03T00:00:00Z").expect("second run");
assert_eq!(second.resolved, first.resolved);
assert_eq!(second.dangling, first.dangling);
assert_eq!(second.ambiguous, first.ambiguous);
assert_eq!(second.details, first.details);
assert_eq!(second.edges_written, 0);
assert_eq!(store.current_facts().unwrap().len(), facts_after_first);
}
#[test]
fn dangling_resolves_once_the_entity_appears() {
let (mut store, _) = seeded_store();
reconcile_mentions(&mut store, "2026-01-02T00:00:00Z").expect("first run");
let ghost = symbol_iri("repo", "src/ghost.rs", "Ghost");
seed(
&mut store,
&format!(
"@prefix bobbin: <{ns}> .\n<{ghost}> bobbin:name \"Ghost\" .\n",
ns = crate::iri::ONTOLOGY_NS,
),
);
let report = reconcile_mentions(&mut store, "2026-01-04T00:00:00Z").expect("second run");
assert_eq!(report.dangling, 0);
assert_eq!(report.resolved, 2); assert_eq!(report.edges_written, 1); }
#[test]
fn empty_store_reports_all_zeroes() {
let mut store = quipu::Store::open_in_memory().expect("in-memory store");
let report = reconcile_mentions(&mut store, "2026-01-02T00:00:00Z").expect("reconcile");
assert_eq!(
(
report.resolved,
report.dangling,
report.ambiguous,
report.edges_written
),
(0, 0, 0, 0)
);
assert!(report.details.is_empty());
}