use std::path::Path;
use std::sync::Arc;
use sqry_core::graph::Language;
use sqry_core::graph::unified::concurrent::{CodeGraph, GraphSnapshot};
use sqry_core::graph::unified::edge::kind::EdgeKind;
use sqry_core::graph::unified::file::id::FileId;
use sqry_core::graph::unified::node::id::NodeId;
use sqry_core::graph::unified::node::kind::NodeKind;
use sqry_core::graph::unified::storage::arena::NodeEntry;
use sqry_db::persistence::{
DerivedManifest, compute_file_sha256, derived_path_for_snapshot, load_manifest, save_manifest,
};
use sqry_db::queries::{IsInCycleQuery, RelationKey, mcp_callers_query};
use sqry_db::{LoadOutcome, QueryDb, QueryDbConfig};
use tempfile::TempDir;
fn add_node(graph: &mut CodeGraph, entry: NodeEntry) -> NodeId {
let id = graph.nodes_mut().alloc(entry.clone()).expect("alloc node");
graph
.indices_mut()
.add(id, entry.kind, entry.name, entry.qualified_name, entry.file);
id
}
fn build_3_file_fixture() -> (
Arc<GraphSnapshot>,
Vec<sqry_db::queries::IsInCycleKey>,
FileId,
FileId,
FileId,
) {
use sqry_core::query::CircularType;
use sqry_db::queries::{CycleBounds, IsInCycleKey};
let mut graph = CodeGraph::new();
let add_self_loop = |graph: &mut CodeGraph, node: NodeId, file: FileId| {
graph.edges().add_edge(
node,
node,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file,
);
};
let file_a = graph
.files_mut()
.register_with_language(Path::new("src/file_a.rs"), Some(Language::Rust))
.expect("register file_a");
let sym_a = graph.strings_mut().intern("sym_a").expect("intern sym_a");
let node_a = add_node(
&mut graph,
NodeEntry::new(NodeKind::Function, sym_a, file_a)
.with_qualified_name(sym_a)
.with_byte_range(0, 80),
);
add_self_loop(&mut graph, node_a, file_a);
let file_b = graph
.files_mut()
.register_with_language(Path::new("src/file_b.rs"), Some(Language::Rust))
.expect("register file_b");
let sym_b = graph.strings_mut().intern("sym_b").expect("intern sym_b");
let node_b = add_node(
&mut graph,
NodeEntry::new(NodeKind::Function, sym_b, file_b)
.with_qualified_name(sym_b)
.with_byte_range(0, 80),
);
add_self_loop(&mut graph, node_b, file_b);
let sym_c = graph.strings_mut().intern("sym_c").expect("intern sym_c");
let node_c = add_node(
&mut graph,
NodeEntry::new(NodeKind::Function, sym_c, file_b)
.with_qualified_name(sym_c)
.with_byte_range(100, 180),
);
add_self_loop(&mut graph, node_c, file_b);
let sym_d = graph.strings_mut().intern("sym_d").expect("intern sym_d");
let node_d = add_node(
&mut graph,
NodeEntry::new(NodeKind::Function, sym_d, file_b)
.with_qualified_name(sym_d)
.with_byte_range(200, 280),
);
add_self_loop(&mut graph, node_d, file_b);
let file_c = graph
.files_mut()
.register_with_language(Path::new("src/file_c.rs"), Some(Language::Rust))
.expect("register file_c");
let sym_e = graph.strings_mut().intern("sym_e").expect("intern sym_e");
let node_e = add_node(
&mut graph,
NodeEntry::new(NodeKind::Function, sym_e, file_c)
.with_qualified_name(sym_e)
.with_byte_range(0, 80),
);
add_self_loop(&mut graph, node_e, file_c);
let bounds = CycleBounds {
min_depth: 2,
max_depth: None,
max_results: 100,
should_include_self_loops: true,
};
let keys = vec![
IsInCycleKey {
node_id: node_a,
circular_type: CircularType::Calls,
bounds,
},
IsInCycleKey {
node_id: node_b,
circular_type: CircularType::Calls,
bounds,
},
IsInCycleKey {
node_id: node_c,
circular_type: CircularType::Calls,
bounds,
},
IsInCycleKey {
node_id: node_d,
circular_type: CircularType::Calls,
bounds,
},
IsInCycleKey {
node_id: node_e,
circular_type: CircularType::Calls,
bounds,
},
];
(Arc::new(graph.snapshot()), keys, file_a, file_b, file_c)
}
fn build_fixture() -> (Arc<GraphSnapshot>, Vec<String>) {
let mut graph = CodeGraph::new();
let file = graph
.files_mut()
.register_with_language(Path::new("src/lib.rs"), Some(Language::Rust))
.expect("register file");
let caller_name = graph.strings_mut().intern("driver").expect("intern driver");
let caller = add_node(
&mut graph,
NodeEntry::new(NodeKind::Function, caller_name, file)
.with_qualified_name(caller_name)
.with_byte_range(0, 60),
);
let symbols = ["sym_a", "sym_b", "sym_c", "sym_d", "sym_e"];
for (i, s) in symbols.iter().enumerate() {
let name = graph.strings_mut().intern(s).expect("intern symbol");
let start = 100 + (i as u32) * 200;
let node = add_node(
&mut graph,
NodeEntry::new(NodeKind::Function, name, file)
.with_qualified_name(name)
.with_byte_range(start, start + 80),
);
graph.edges().add_edge(
caller,
node,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file,
);
}
(
Arc::new(graph.snapshot()),
symbols.iter().map(|s| (*s).to_string()).collect(),
)
}
#[test]
fn proof3_warm_cache_has_zero_recomputation_for_repeated_queries() {
let (snapshot, symbols) = build_fixture();
let db = QueryDb::new(snapshot, QueryDbConfig::default());
for s in &symbols {
let _ = mcp_callers_query(&db, &RelationKey::exact(s));
}
let cold = db.metrics();
assert_eq!(cold.cache_misses, 5, "5 cold queries => 5 misses");
assert_eq!(cold.cache_hits, 0);
for s in &symbols {
let _ = mcp_callers_query(&db, &RelationKey::exact(s));
}
let warm = db.metrics();
assert_eq!(
warm.cache_misses, cold.cache_misses,
"warm repeat must NOT produce additional misses (zero recomputation)"
);
assert_eq!(
warm.cache_hits - cold.cache_hits,
5,
"warm repeat must produce exactly 5 hits"
);
}
#[test]
fn proof3_snapshot_sha256_matches_across_identical_payloads() {
let tmp = TempDir::new().expect("tempdir");
let snap1 = tmp.path().join("snap1.sqry");
let snap2 = tmp.path().join("snap2.sqry");
std::fs::write(&snap1, b"snapshot-payload-v1").expect("write snap1");
std::fs::write(&snap2, b"snapshot-payload-v1").expect("write snap2");
let h1 = compute_file_sha256(&snap1).expect("sha snap1");
let h2 = compute_file_sha256(&snap2).expect("sha snap2");
assert_eq!(h1, h2, "identical bytes must hash to the same SHA-256");
let snap3 = tmp.path().join("snap3.sqry");
std::fs::write(&snap3, b"snapshot-payload-v2").expect("write snap3");
let h3 = compute_file_sha256(&snap3).expect("sha snap3");
assert_ne!(h1, h3, "different bytes must hash differently");
}
#[test]
fn proof3_manifest_round_trip_and_graph_identity_gating() {
let tmp = TempDir::new().expect("tempdir");
let snapshot_path = tmp.path().join(".sqry").join("graph").join("snapshot.sqry");
std::fs::create_dir_all(snapshot_path.parent().unwrap()).expect("mkdir");
std::fs::write(&snapshot_path, b"snapshot-bytes").expect("write snapshot");
let snap_hash = compute_file_sha256(&snapshot_path).expect("hash snapshot");
let derived_path = derived_path_for_snapshot(&snapshot_path, "derived.sqry");
assert_eq!(
derived_path,
snapshot_path.parent().unwrap().join("derived.sqry")
);
let manifest = DerivedManifest::new(snap_hash, 0, 0, vec![], 5);
save_manifest(&derived_path, &manifest).expect("save manifest");
let loaded = load_manifest(&derived_path).expect("load manifest");
assert_eq!(loaded.snapshot_sha256, snap_hash);
assert_eq!(loaded.entry_count, 5);
assert!(
loaded.matches_snapshot(&snap_hash),
"manifest must authorise reload when the snapshot hash matches"
);
std::fs::write(&snapshot_path, b"snapshot-bytes-changed").expect("rewrite snapshot");
let new_hash = compute_file_sha256(&snapshot_path).expect("rehash snapshot");
assert_ne!(new_hash, snap_hash);
assert!(
!loaded.matches_snapshot(&new_hash),
"manifest must reject reload when the snapshot hash has changed"
);
}
#[test]
fn proof3_file_level_revision_validation_is_monotonic() {
use smallvec::SmallVec;
use sqry_core::graph::unified::file::id::FileId;
use sqry_db::cache::CachedResult;
use sqry_db::dependency::FileDep;
use sqry_db::input::{FileInput, FileInputStore};
let mut store = FileInputStore::new();
store.insert(FileId::new(1), FileInput::new(Default::default()));
store.insert(FileId::new(2), FileInput::new(Default::default()));
let mut deps: SmallVec<[FileDep; 8]> = SmallVec::new();
deps.push((FileId::new(1), 1));
deps.push((FileId::new(2), 1));
let cached = CachedResult::new(vec![1u32, 2, 3], deps, None, None);
assert!(
cached.validate_file_deps(&store),
"baseline revisions (1,1) must validate"
);
store
.get_mut(FileId::new(1))
.unwrap()
.update(Default::default());
assert!(
!cached.validate_file_deps(&store),
"after file 1's revision bump, validation must fail"
);
}
#[test]
fn proof3_load_manifest_returns_none_for_missing_file() {
let tmp = TempDir::new().expect("tempdir");
let derived_path = tmp.path().join("nonexistent-derived.sqry");
assert!(load_manifest(&derived_path).is_none());
}
#[test]
fn proof3_dependency_impact_queries_are_independent_cache_entries() {
let (snapshot, symbols) = build_fixture();
let db = QueryDb::new(snapshot, QueryDbConfig::default());
let before = db.metrics();
for s in &symbols {
let _ = mcp_callers_query(&db, &RelationKey::exact(s));
}
let after_first = db.metrics();
assert_eq!(after_first.cache_misses - before.cache_misses, 5);
for s in &symbols {
let _ = mcp_callers_query(&db, &RelationKey::exact(s));
}
let after_second = db.metrics();
assert_eq!(after_second.cache_hits - after_first.cache_hits, 5);
assert_eq!(after_second.cache_misses, after_first.cache_misses);
}
#[test]
fn proof3_cold_start_full_restore() {
let (snapshot, keys, file_a, _file_b, _file_c) = build_3_file_fixture();
let workspace = TempDir::new().expect("create workspace tempdir");
let graph_dir = workspace.path().join(".sqry").join("graph");
std::fs::create_dir_all(&graph_dir).expect("create .sqry/graph/");
let snapshot_path = graph_dir.join("snapshot.sqry");
std::fs::write(&snapshot_path, b"proof3-cold-start-anchor-bytes").expect("write snapshot stub");
let snapshot_sha256 = compute_file_sha256(&snapshot_path).expect("sha256 snapshot stub");
let config = QueryDbConfig::default();
let derived = sqry_db::queries::derived_path(workspace.path(), &config);
{
let db = QueryDb::new(Arc::clone(&snapshot), config.clone());
for key in &keys {
let result = db.get::<IsInCycleQuery>(key);
assert!(
result,
"every node has a self-loop — IsInCycleQuery must return true"
);
}
let after_warm = db.metrics();
assert_eq!(
after_warm.cache_misses, 5,
"session A: 5 IsInCycleQuery queries must produce 5 cold misses"
);
sqry_db::persistence::save_derived(&db, snapshot_sha256, &derived, workspace.path())
.expect("save_derived must succeed");
}
let mut db_b = QueryDb::new(Arc::clone(&snapshot), config.clone());
assert!(
db_b.cold_load_allowed(),
"cold_load_allowed must be true before the first load"
);
let outcome =
sqry_db::persistence::load_derived(&mut db_b, snapshot_sha256, &derived, workspace.path())
.expect("load_derived must succeed");
match outcome {
LoadOutcome::Applied { entries } => {
assert_eq!(entries, 5, "cold-load must restore exactly 5 cache entries");
}
other => panic!("expected LoadOutcome::Applied {{ entries: 5 }}, got {other:?}"),
}
assert!(
!db_b.cold_load_allowed(),
"cold_load_allowed must be false after a successful load_derived"
);
assert_eq!(
db_b.metrics().cache_misses,
0,
"no misses should have accumulated before the post-load pass in session B"
);
for key in &keys {
let _ = db_b.get::<IsInCycleQuery>(key);
}
let after_first_pass = db_b.metrics();
assert_eq!(
after_first_pass.cache_misses, 0,
"first typed query pass after cold-load MUST be ZERO misses (spec §2 \
'first query after a cold start is free')"
);
assert_eq!(
after_first_pass.cache_hits, 5,
"first typed query pass after cold-load MUST be exactly 5 cache hits \
(rehydrated entries landed in the same (shard, QueryKey) slot that \
`get::<Q>` probes)"
);
for key in &keys {
let _ = db_b.get::<IsInCycleQuery>(key);
}
let after_second_pass = db_b.metrics();
assert_eq!(
after_second_pass.cache_misses - after_first_pass.cache_misses,
0,
"second pass must also produce zero misses"
);
assert_eq!(
after_second_pass.cache_hits - after_first_pass.cache_hits,
5,
"second pass must produce exactly 5 more cache hits"
);
db_b.inputs_mut()
.get_mut(file_a)
.expect("file_a must be present in the input store after cold-load")
.update(Default::default());
let pre_mutation_reissue = db_b.metrics();
for key in &keys {
let _ = db_b.get::<IsInCycleQuery>(key);
}
let post_mutation_reissue = db_b.metrics();
assert_eq!(
post_mutation_reissue.cache_misses - pre_mutation_reissue.cache_misses,
1,
"after file_a revision bump, exactly 1 IsInCycleQuery entry must invalidate \
(only node_a records file_a as its Tier 1 dep)"
);
assert_eq!(
post_mutation_reissue.cache_hits - pre_mutation_reissue.cache_hits,
4,
"after file_a revision bump, the 4 entries on file_b and file_c must stay warm"
);
let pre_final = db_b.metrics();
for key in &keys {
let _ = db_b.get::<IsInCycleQuery>(key);
}
let post_final = db_b.metrics();
assert_eq!(
post_final.cache_hits - pre_final.cache_hits,
5,
"after recomputation, a second repeat must be 5 cache hits"
);
assert_eq!(
post_final.cache_misses - pre_final.cache_misses,
0,
"after recomputation, a second repeat must have 0 new misses"
);
}