use super::*;
#[test]
fn test_proof_memo_deduplication() {
let kb = new_kb();
assert_buf(&kb, make_temporal_event_assertion("bob", "mlatu", present));
assert_buf(&kb, make_event_universal("mlatu", "danlu"));
assert_buf(&kb, make_event_universal("danlu", "jmive"));
let (holds, trace) = kb
.query_entailment_with_proof_inner(make_temporal_event_query("bob", "jmive", present))
.unwrap();
assert!(holds.is_true(), "Present(jmive(bob)) should hold");
let proof_ref_count = trace
.steps
.iter()
.filter(|step| matches!(&step.rule, ProofRule::ProofRef { .. }))
.count();
assert!(
proof_ref_count > 0,
"2-hop event-decomposed trace should have ProofRef nodes for deduplicated sub-proofs, got {}",
proof_ref_count
);
assert!(
proof_ref_count >= 3,
"2-hop event trace should have at least 3 ProofRef nodes (deduplicated conditions), got {}",
proof_ref_count
);
}
#[test]
fn proof_ref_children_are_holds_true() {
{
let kb = new_kb();
assert_buf(&kb, make_temporal_event_assertion("bob", "mlatu", present));
assert_buf(&kb, make_event_universal("mlatu", "danlu"));
assert_buf(&kb, make_event_universal("danlu", "jmive"));
let (_holds, trace) = kb
.query_entailment_with_proof_inner(make_temporal_event_query("bob", "jmive", present))
.unwrap();
assert_proof_refs_resolve_to_holds_true(&trace);
}
{
let kb = new_kb();
assert_buf(&kb, make_assertion("adam", "gerku"));
assert_buf(&kb, make_equals("adam", "betty"));
let (_holds, trace) = kb
.query_entailment_with_proof_inner(make_query("betty", "gerku"))
.unwrap();
assert_proof_refs_resolve_to_holds_true(&trace);
}
{
let kb = new_kb();
assert_buf(&kb, make_universal("gerku", "danlu"));
assert_buf(&kb, make_assertion("alis", "gerku"));
let (_holds, trace) = kb
.query_entailment_with_proof_inner(make_query("alis", "danlu"))
.unwrap();
assert_proof_refs_resolve_to_holds_true(&trace);
}
}
#[test]
fn proof_trace_byte_deterministic_3x() {
let render = || {
let kb = new_kb();
assert_buf(&kb, make_temporal_event_assertion("bob", "mlatu", present));
assert_buf(&kb, make_event_universal("mlatu", "danlu"));
assert_buf(&kb, make_event_universal("danlu", "jmive"));
let (_holds, trace) = kb
.query_entailment_with_proof_inner(make_temporal_event_query("bob", "jmive", present))
.unwrap();
format!("{trace:?}")
};
let a = render();
let b = render();
let c = render();
assert_eq!(a, b, "proof trace not deterministic (run 1 vs run 2)");
assert_eq!(b, c, "proof trace not deterministic (run 2 vs run 3)");
}
#[test]
fn proof_trace_equals_pinned_resolving_depth() {
let kb_pin = new_kb();
assert_buf(&kb_pin, make_assertion("alis", "gerku"));
assert_buf(&kb_pin, make_universal("gerku", "danlu"));
assert_buf(&kb_pin, make_universal("danlu", "xanlu"));
let configured_max = {
let inner = kb_pin.inner.borrow();
clear_and_enable_pred_cache(&inner);
inner.max_chain_depth
};
let mut resolving_depth = configured_max;
for depth_limit in 1..=configured_max {
kb_pin.inner.borrow_mut().max_chain_depth = depth_limit;
let r = kb_pin
.run_entailment_check(&make_query("alis", "xanlu"))
.unwrap();
if !matches!(r, QueryResult::ResourceExceeded(ResourceKind::Depth)) {
resolving_depth = depth_limit;
break;
}
}
kb_pin.inner.borrow_mut().max_chain_depth = resolving_depth;
let (r_pin, t_pin) = kb_pin
.run_entailment_check_with_proof(&make_query("alis", "xanlu"))
.unwrap();
kb_pin.inner.borrow_mut().max_chain_depth = configured_max;
let kb_loop = new_kb();
assert_buf(&kb_loop, make_assertion("alis", "gerku"));
assert_buf(&kb_loop, make_universal("gerku", "danlu"));
assert_buf(&kb_loop, make_universal("danlu", "xanlu"));
let (r_loop, t_loop) = kb_loop
.query_entailment_with_proof_inner(make_query("alis", "xanlu"))
.unwrap();
assert!(
r_loop.is_true(),
"xanlu(alis) should hold via the 2-hop chain"
);
assert!(
resolving_depth > 1,
"expected a multi-hop proof (resolving depth >1), got {resolving_depth}"
);
assert_eq!(
r_loop, r_pin,
"verdict mismatch: deepening loop vs pinned at resolving depth"
);
assert_eq!(
format!("{t_loop:?}"),
format!("{t_pin:?}"),
"trace mismatch: deepening loop vs single build at resolving depth {resolving_depth}"
);
}
#[test]
fn test_proof_memo_correctness() {
let kb = new_kb();
assert_buf(&kb, make_temporal_event_assertion("alis", "gerku", past));
assert_buf(&kb, make_event_universal("gerku", "danlu"));
let (holds, trace) = kb
.query_entailment_with_proof_inner(make_temporal_event_query("alis", "danlu", past))
.unwrap();
assert!(holds.is_true(), "Past(danlu(alis)) should hold");
let derived_count = trace
.steps
.iter()
.filter(|step| matches!(&step.rule, ProofRule::Derived { .. }))
.count();
assert!(
derived_count >= 1,
"should have at least 1 Derived step from gerku→danlu rule, got {}",
derived_count
);
let has_asserted_or_check = trace.steps.iter().any(|step| {
matches!(&step.rule, ProofRule::Asserted { .. })
|| matches!(&step.rule, ProofRule::PredicateCheck { .. })
});
assert!(
has_asserted_or_check,
"first occurrence of base facts should be Asserted or PredicateCheck, not ProofRef"
);
}
#[test]
fn test_proof_memo_single_hop_no_unnecessary_refs() {
let kb = new_kb();
assert_buf(&kb, make_temporal_event_assertion("alis", "gerku", past));
assert_buf(&kb, make_event_universal("gerku", "danlu"));
let (holds, trace) = kb
.query_entailment_with_proof_inner(make_temporal_event_query("alis", "danlu", past))
.unwrap();
assert!(holds.is_true(), "Past(danlu(alis)) should hold");
let proof_ref_count = trace
.steps
.iter()
.filter(|step| matches!(&step.rule, ProofRule::ProofRef { .. }))
.count();
assert!(
proof_ref_count <= 3,
"single-hop trace should have very few ProofRef nodes (unique conditions), got {}",
proof_ref_count
);
}
fn make_event_assertion_2arg(entity1: &str, entity2: &str, predicate: &str) -> LogicBuffer {
let mut nodes = Vec::new();
let p_type = pred(
&mut nodes,
predicate,
vec![LogicalTerm::Variable("_ev0".to_string())],
);
let p_role1 = pred(
&mut nodes,
&format!("{}_x1", predicate),
vec![
LogicalTerm::Variable("_ev0".to_string()),
LogicalTerm::Constant(entity1.to_string()),
],
);
let p_role2 = pred(
&mut nodes,
&format!("{}_x2", predicate),
vec![
LogicalTerm::Variable("_ev0".to_string()),
LogicalTerm::Constant(entity2.to_string()),
],
);
let a1 = and(&mut nodes, p_type, p_role1);
let a2 = and(&mut nodes, a1, p_role2);
let root = exists(&mut nodes, "_ev0", a2);
LogicBuffer {
nodes,
roots: vec![root],
}
}
#[test]
fn test_book_example_no_oom() {
let kb = new_kb();
assert_buf(
&kb,
make_event_assertion_2arg("prenu_sk", "datni_sk", "ponse"),
);
assert_buf(&kb, make_event_assertion("prenu_sk", "prenu"));
assert_buf(&kb, make_event_assertion("datni_sk", "datni"));
assert_buf(&kb, make_event_universal("prenu", "bilga"));
assert_buf(&kb, make_event_universal("bilga", "zukte"));
assert!(
query(&kb, make_event_assertion("prenu_sk", "bilga")),
"prenu_sk should be derived as bilga via universal rule"
);
let (holds, _trace) = kb
.query_entailment_with_proof_inner(make_event_assertion("prenu_sk", "bilga"))
.unwrap();
assert!(
holds.is_true(),
"proof-traced query should hold for bilga(prenu_sk)"
);
assert!(
query(&kb, make_event_assertion("prenu_sk", "zukte")),
"multi-hop prenu→bilga→zukte should derive zukte(prenu_sk)"
);
let (holds2, _trace2) = kb
.query_entailment_with_proof_inner(make_event_assertion("prenu_sk", "zukte"))
.unwrap();
assert!(
holds2.is_true(),
"proof-traced multi-hop should hold for zukte(prenu_sk)"
);
}
#[test]
fn test_and_flattening_prevents_rewrite_explosion() {
let kb = new_kb();
let mut nodes = Vec::new();
let p1 = pred(
&mut nodes,
"ponse",
vec![LogicalTerm::Variable("_ev0".into())],
);
let p2 = pred(
&mut nodes,
"ponse_x1",
vec![
LogicalTerm::Variable("_ev0".into()),
LogicalTerm::Variable("_v0".into()),
],
);
let p3 = pred(
&mut nodes,
"ponse_x2",
vec![
LogicalTerm::Variable("_ev0".into()),
LogicalTerm::Variable("_v1".into()),
],
);
let p4 = pred(
&mut nodes,
"prenu",
vec![LogicalTerm::Variable("_v0".into())],
);
let p5 = pred(
&mut nodes,
"datni",
vec![LogicalTerm::Variable("_v1".into())],
);
let p6 = pred(
&mut nodes,
"prenu_x1",
vec![
LogicalTerm::Variable("_ev1".into()),
LogicalTerm::Variable("_v0".into()),
],
);
let p7 = pred(
&mut nodes,
"datni_x1",
vec![
LogicalTerm::Variable("_ev2".into()),
LogicalTerm::Variable("_v1".into()),
],
);
let a1 = and(&mut nodes, p1, p2);
let a2 = and(&mut nodes, a1, p3);
let a3 = and(&mut nodes, a2, p4);
let a4 = and(&mut nodes, a3, p5);
let a5 = and(&mut nodes, a4, p6);
let a6 = and(&mut nodes, a5, p7);
let e0 = exists(&mut nodes, "_ev0", a6);
let e1 = exists(&mut nodes, "_ev1", e0);
let e2 = exists(&mut nodes, "_ev2", e1);
let e3 = exists(&mut nodes, "_v0", e2);
let root = exists(&mut nodes, "_v1", e3);
let buf = LogicBuffer {
nodes,
roots: vec![root],
};
assert_buf(&kb, buf);
let inner = kb.inner.borrow();
let fact_count = inner.fact_store.len();
eprintln!("[Test] fact_store count: {}", fact_count);
assert!(
fact_count < 100,
"Asserted facts should be < 100 after flattening, got {}",
fact_count
);
}
fn tenfa_query() -> LogicBuffer {
let mut nodes = Vec::new();
let root = compute(
&mut nodes,
"exponential", vec![
LogicalTerm::Number(8.0),
LogicalTerm::Number(2.0),
LogicalTerm::Number(3.0),
],
);
LogicBuffer {
nodes,
roots: vec![root],
}
}
fn failing_eval(_rel: &str, _args: &[LogicalTerm]) -> Result<bool, String> {
Err("backend unreachable".to_string())
}
fn failing_batch(reqs: &[ComputeRequest]) -> Vec<Result<bool, String>> {
reqs.iter()
.map(|_| Err("backend unreachable".to_string()))
.collect()
}
fn ok_eval(_rel: &str, _args: &[LogicalTerm]) -> Result<bool, String> {
Ok(true)
}
fn ok_batch(reqs: &[ComputeRequest]) -> Vec<Result<bool, String>> {
reqs.iter().map(|_| Ok(true)).collect()
}
#[test]
fn test_compute_no_backend_is_unknown_not_false() {
let kb = new_kb();
let r = query_result(&kb, tenfa_query());
assert!(
matches!(r, QueryResult::Unknown(UnknownReason::BackendUnavailable)),
"no backend → Unknown(BackendUnavailable), got {r:?}"
);
}
#[test]
fn test_compute_backend_failure_is_unknown_not_false() {
let kb = new_kb();
kb.set_compute_dispatch(failing_eval, failing_batch);
let r = query_result(&kb, tenfa_query());
assert!(
matches!(r, QueryResult::Unknown(UnknownReason::BackendUnavailable)),
"backend error → Unknown(BackendUnavailable), got {r:?}"
);
}
#[test]
fn test_cached_compute_result_survives_backend_outage() {
let kb = new_kb();
kb.set_compute_dispatch(ok_eval, ok_batch);
assert!(
query(&kb, tenfa_query()),
"working backend → TRUE (auto-asserted into the KB)"
);
kb.set_compute_dispatch(failing_eval, failing_batch);
let r = query_result(&kb, tenfa_query());
assert!(
matches!(r, QueryResult::True),
"the auto-asserted prior result survives the outage, got {r:?}"
);
}
#[test]
fn test_proof_ref_carries_cached_index() {
let kb = new_kb();
assert_buf(&kb, make_temporal_event_assertion("bob", "mlatu", present));
assert_buf(&kb, make_event_universal("mlatu", "danlu"));
assert_buf(&kb, make_event_universal("danlu", "jmive"));
let (holds, trace) = kb
.query_entailment_with_proof_inner(make_temporal_event_query("bob", "jmive", present))
.unwrap();
assert!(holds.is_true());
for step in &trace.steps {
if let ProofRule::ProofRef { .. } = &step.rule {
assert_eq!(
step.children.len(),
1,
"ProofRef should have exactly 1 child (back-reference)"
);
let referenced_idx = step.children[0] as usize;
assert!(
referenced_idx < trace.steps.len(),
"ProofRef back-reference {} out of bounds ({})",
referenced_idx,
trace.steps.len()
);
assert_eq!(
step.holds, trace.steps[referenced_idx].holds,
"ProofRef.holds should match the referenced step's holds"
);
}
}
}
#[test]
fn depth_cut_table_recovers_deep_surface_chain() {
let kb = new_kb();
let chain = [
"dog", "animal", "alive", "big", "fast", "healthy", "thin", "eats", "goes",
];
assert_buf(&kb, compile_surface("dog(Adam)."));
for w in chain.windows(2) {
assert_buf(&kb, compile_surface(&format!("{}(every {}).", w[1], w[0])));
}
assert!(
query(&kb, compile_surface("goes(Adam).")),
"the 8-hop chain must derive TRUE"
);
}
#[test]
fn cycle_plus_deep_chain_stays_complete() {
let kb = new_kb();
assert_buf(&kb, make_universal("bbb", "aaa"));
assert_buf(&kb, make_universal("aaa", "bbb"));
assert_buf(&kb, make_universal("ccc3", "bbb"));
assert_buf(&kb, make_universal("ccc2", "ccc3"));
assert_buf(&kb, make_universal("ccc1", "ccc2"));
assert_buf(&kb, make_assertion("adam", "ccc1"));
assert!(
query(&kb, make_query("adam", "aaa")),
"the chain entry point must prove aaa despite the aaa/bbb cycle"
);
}
fn make_universal_2cond(c1: &str, c2: &str, concl: &str) -> LogicBuffer {
let mut nodes = Vec::new();
let mk = |nodes: &mut Vec<LogicNode>, rel: &str| {
pred(
nodes,
rel,
vec![
LogicalTerm::Variable("_v0".to_string()),
LogicalTerm::Unspecified,
],
)
};
let a = mk(&mut nodes, c1);
let b = mk(&mut nodes, c2);
let ante = and(&mut nodes, a, b);
let head = mk(&mut nodes, concl);
let neg_ante = not(&mut nodes, ante);
let disj = or(&mut nodes, neg_ante, head);
let root = forall(&mut nodes, "_v0", disj);
LogicBuffer {
nodes,
roots: vec![root],
}
}
#[test]
fn contaminated_depth_cut_is_not_tabled_for_replay() {
let kb = new_kb();
kb.set_max_chain_depth(5);
assert_buf(&kb, make_universal_2cond("aaa", "fff", "rrr"));
assert_buf(&kb, make_universal("xxx1", "rrr"));
assert_buf(&kb, make_universal("xxx2", "xxx1"));
assert_buf(&kb, make_universal("ggg", "xxx2"));
assert_buf(&kb, make_universal("ggg", "aaa"));
assert_buf(&kb, make_universal("yyy1", "aaa"));
assert_buf(&kb, make_universal("yyy2", "yyy1"));
assert_buf(&kb, make_universal("yyy3", "yyy2"));
assert_buf(&kb, make_universal("aaa", "ggg"));
assert_buf(&kb, make_universal("ddd1", "ggg"));
for i in 1..12 {
assert_buf(
&kb,
make_universal(&format!("ddd{}", i + 1), &format!("ddd{i}")),
);
}
assert_buf(&kb, make_universal("fff", "fff"));
assert_buf(&kb, make_assertion("adam", "yyy3"));
assert!(
query(&kb, make_query("adam", "rrr")),
"the clean branch-2 derivation must prove rrr despite the contaminated \
depth cut recorded (and correctly NOT tabled) under branch 1"
);
}
#[test]
fn depth_cut_table_invalidated_by_assert() {
let kb = new_kb();
for i in 0..8 {
assert_buf(
&kb,
make_universal(&format!("qqq{i}"), &format!("qqq{}", i + 1)),
);
}
let before = query_result(&kb, make_query("adam", "qqq8"));
assert!(
!before.is_true(),
"no base fact: the chain head cannot be TRUE"
);
assert_buf(&kb, make_assertion("adam", "qqq0"));
assert!(
query(&kb, make_query("adam", "qqq8")),
"post-assert the chain must fire (a stale depth-cut table would block it)"
);
}