use super::*;
fn seed_mobility_claim(
db: &YantrikDB,
proposition_id: &str,
extractor: &str,
polarity: i32,
source_lineage_json: &str,
self_gen: i32,
modality: &str,
regime: &str,
) {
let claim_id = format!("c_{}", uuid_like(extractor, source_lineage_json, polarity));
db.conn()
.execute(
"INSERT INTO claims (claim_id, src, dst, rel_type, created_at, \
extractor, polarity, namespace, proposition_id, regime_tag, \
self_generated, source_lineage, modality_signal, weight) \
VALUES (?1, 'X', 'Y', 'rel', 0.0, ?2, ?3, 'default', ?4, ?5, ?6, ?7, ?8, 1.0)",
rusqlite::params![
claim_id,
extractor,
polarity,
proposition_id,
regime,
self_gen,
source_lineage_json,
modality,
],
)
.unwrap();
}
fn uuid_like(a: &str, b: &str, pol: i32) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut h = DefaultHasher::new();
(a, b, pol).hash(&mut h);
format!("c_{:x}", h.finish())
}
#[test]
fn test_mobility_single_support_claim() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_single");
seed_mobility_claim(
&db,
"p_single",
"ext_a",
1,
"[\"src_1\"]",
0,
"text",
"default",
);
let state = db
.compute_write_tier_mobility("p_single", "default")
.unwrap();
assert_eq!(state.support_mass, Some(1.0));
assert_eq!(state.attack_mass, Some(0.0));
assert_eq!(state.self_gen_local, Some(0.0));
assert!((state.modality_consilience.unwrap() - 1.0 / 6.0).abs() < 1e-9);
}
#[test]
fn test_mobility_three_independent_sources() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_ind");
seed_mobility_claim(
&db,
"p_ind",
"ext_a",
1,
"[\"src_a\"]",
0,
"text",
"default",
);
seed_mobility_claim(
&db,
"p_ind",
"ext_b",
1,
"[\"src_b\"]",
0,
"image",
"default",
);
seed_mobility_claim(
&db,
"p_ind",
"ext_c",
1,
"[\"src_c\"]",
0,
"numeric",
"default",
);
let state = db.compute_write_tier_mobility("p_ind", "default").unwrap();
assert!(
(state.support_mass.unwrap() - 3.0).abs() < 1e-9,
"expected 3.0 for independent claims, got {:?}",
state.support_mass
);
assert!((state.modality_consilience.unwrap() - 0.5).abs() < 1e-9);
}
#[test]
fn test_mobility_duplicate_sources_discounted() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_dup");
seed_mobility_claim(
&db,
"p_dup",
"ext_a",
1,
"[\"src_shared\"]",
0,
"text",
"default",
);
seed_mobility_claim(
&db,
"p_dup",
"ext_b",
1,
"[\"src_shared\"]",
0,
"text",
"default",
);
seed_mobility_claim(
&db,
"p_dup",
"ext_c",
1,
"[\"src_shared\"]",
0,
"text",
"default",
);
let state = db.compute_write_tier_mobility("p_dup", "default").unwrap();
let s = state.support_mass.unwrap();
assert!(
s < 2.5,
"shared-source claims should discount below 2.5, got {}",
s
);
assert!(s > 1.8, "discount shouldn't be excessive, got {}", s);
}
#[test]
fn test_mobility_support_and_attack_separate() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_mixed");
seed_mobility_claim(
&db,
"p_mixed",
"ext_a",
1,
"[\"src_a\"]",
0,
"text",
"default",
);
seed_mobility_claim(
&db,
"p_mixed",
"ext_b",
1,
"[\"src_b\"]",
0,
"image",
"default",
);
seed_mobility_claim(
&db,
"p_mixed",
"ext_c",
-1,
"[\"src_c\"]",
0,
"text",
"default",
);
let state = db
.compute_write_tier_mobility("p_mixed", "default")
.unwrap();
assert!((state.support_mass.unwrap() - 2.0).abs() < 1e-9);
assert!((state.attack_mass.unwrap() - 1.0).abs() < 1e-9);
assert_eq!(state.self_gen_local, Some(0.0));
}
#[test]
fn test_mobility_upsert_and_read_back() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_rw");
seed_mobility_claim(&db, "p_rw", "ext_a", 1, "[\"src_1\"]", 0, "text", "default");
let computed = db.compute_write_tier_mobility("p_rw", "default").unwrap();
db.upsert_mobility_state(&computed).unwrap();
let read_back = db.get_mobility_state("p_rw", "default").unwrap().unwrap();
assert_eq!(read_back.support_mass, computed.support_mass);
assert_eq!(read_back.attack_mass, computed.attack_mass);
assert_eq!(read_back.tier_write_components.len(), 4);
assert!(read_back.self_gen_ancestral.is_none());
assert!(read_back.novelty_isolation.is_none());
}
#[test]
fn test_mobility_missing_returns_none() {
let db = YantrikDB::new(":memory:", 8).unwrap();
let result = db.get_mobility_state("nonexistent", "default").unwrap();
assert!(result.is_none());
}
#[test]
fn test_mobility_self_generated_discount() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_self");
seed_mobility_claim(
&db,
"p_self",
"self_mode_a",
1,
"[\"self_1\"]",
1,
"text",
"default",
);
seed_mobility_claim(
&db,
"p_self",
"self_mode_b",
1,
"[\"self_1\"]",
1,
"text",
"default",
);
let state = db.compute_write_tier_mobility("p_self", "default").unwrap();
let s = state.support_mass.unwrap();
assert!(
s < 1.2,
"self-gen shared-lineage claims should collapse, got {}",
s
);
assert_eq!(state.self_gen_local, Some(1.0));
}
#[test]
fn test_m3_schema_v21_columns_present() {
let db = YantrikDB::new(":memory:", 8).unwrap();
let conn = db.conn();
let mut stmt = conn.prepare("PRAGMA table_info(mobility_state)").unwrap();
let rows = stmt
.query_map([], |r| r.get::<_, String>(1))
.unwrap()
.collect::<std::result::Result<Vec<_>, _>>()
.unwrap();
for expected in [
"formula_version",
"content_hash",
"live_claim_count",
"state_status",
"computed_at",
] {
assert!(
rows.iter().any(|c| c == expected),
"V21 column {} missing from mobility_state",
expected
);
}
}
#[test]
fn test_m3_state_populated_with_hash_and_status() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_hash");
seed_mobility_claim(
&db,
"p_hash",
"ext_a",
1,
"[\"src_1\"]",
0,
"text",
"default",
);
let state = db.compute_write_tier_mobility("p_hash", "default").unwrap();
assert_eq!(
state.formula_version,
crate::engine::warrant::FORMULA_VERSION
);
assert!(
!state.content_hash.is_empty(),
"content_hash should be populated"
);
assert_eq!(state.state_status, "fresh");
assert_eq!(state.live_claim_count, 1);
assert!(state.computed_at > 0, "computed_at should be set");
let read = db.get_mobility_state("p_hash", "default").unwrap().unwrap();
assert_eq!(read.content_hash, state.content_hash);
assert_eq!(read.state_status, "fresh");
assert_eq!(read.live_claim_count, 1);
}
#[test]
fn test_m3_idempotent_recompute_on_unchanged_live_set() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_idem");
seed_mobility_claim(
&db,
"p_idem",
"ext_a",
1,
"[\"src_1\"]",
0,
"text",
"default",
);
let first = db.compute_write_tier_mobility("p_idem", "default").unwrap();
let second = db.compute_write_tier_mobility("p_idem", "default").unwrap();
assert_eq!(
first.content_hash, second.content_hash,
"hash must be stable on unchanged set"
);
assert_eq!(
first.snapshot_ts, second.snapshot_ts,
"idempotent call should return the same row"
);
}
#[test]
fn test_m3_hash_discriminates_on_claim_change() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_disc");
seed_mobility_claim(
&db,
"p_disc",
"ext_a",
1,
"[\"src_a\"]",
0,
"text",
"default",
);
let h1 = db
.compute_write_tier_mobility("p_disc", "default")
.unwrap()
.content_hash;
seed_mobility_claim(
&db,
"p_disc",
"ext_b",
1,
"[\"src_b\"]",
0,
"image",
"default",
);
let h2 = db
.compute_write_tier_mobility("p_disc", "default")
.unwrap()
.content_hash;
assert_ne!(h1, h2, "content_hash must change when live set changes");
}
#[test]
fn test_m3_order_invariant_through_db() {
fn setup_and_compute(order: &[(&str, &str, &str)]) -> (f64, String) {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_ord");
for (ext, src, modality) in order {
let lineage = format!("[\"{}\"]", src);
seed_mobility_claim(&db, "p_ord", ext, 1, &lineage, 0, modality, "default");
}
let state = db.compute_write_tier_mobility("p_ord", "default").unwrap();
(state.support_mass.unwrap(), state.content_hash)
}
let (mass_abc, hash_abc) = setup_and_compute(&[
("ext_a", "src_a", "text"),
("ext_b", "src_b", "image"),
("ext_c", "src_c", "numeric"),
]);
let (mass_cba, hash_cba) = setup_and_compute(&[
("ext_c", "src_c", "numeric"),
("ext_b", "src_b", "image"),
("ext_a", "src_a", "text"),
]);
assert!(
(mass_abc - mass_cba).abs() < 1e-9,
"order invariance broken: {} vs {}",
mass_abc,
mass_cba
);
assert_eq!(hash_abc, hash_cba, "content_hash must be order-invariant");
}
#[test]
fn test_m3_ingest_claim_triggers_mobility() {
let db = YantrikDB::new(":memory:", 8).unwrap();
let claim_id = db
.ingest_claim(
"Alice", "works_at", "Acme", "default", 1, "asserted", None, None, "manual", None,
"medium", None, None, None, 1.0,
)
.unwrap();
assert!(!claim_id.is_empty());
let prop_id: String = db
.conn()
.query_row(
"SELECT proposition_id FROM propositions \
WHERE src = 'Alice' AND rel_type = 'works_at' AND dst = 'Acme' AND namespace = 'default'",
[],
|row| row.get(0),
)
.unwrap();
assert!(!prop_id.is_empty());
let claim_prop_id: String = db
.conn()
.query_row(
"SELECT proposition_id FROM claims WHERE claim_id = ?1",
rusqlite::params![claim_id],
|row| row.get(0),
)
.unwrap();
assert_eq!(claim_prop_id, prop_id);
let state = db.get_mobility_state(&prop_id, "default").unwrap().unwrap();
assert_eq!(state.state_status, "fresh");
assert_eq!(state.live_claim_count, 1);
assert!(state.content_hash.len() >= 32);
assert!((state.support_mass.unwrap() - 1.0).abs() < 1e-9);
}
#[test]
fn test_m4_schema_v22_contest_state_table() {
let db = YantrikDB::new(":memory:", 8).unwrap();
let conn = db.conn();
let mut stmt = conn.prepare("PRAGMA table_info(contest_state)").unwrap();
let rows: Vec<String> = stmt
.query_map([], |r| r.get::<_, String>(1))
.unwrap()
.collect::<std::result::Result<Vec<_>, _>>()
.unwrap();
for expected in [
"proposition_id",
"regime",
"support_mass",
"attack_mass",
"support_effective_independence",
"attack_effective_independence",
"support_distinct_source_count",
"attack_distinct_source_count",
"same_source_opposite_polarity_count",
"same_artifact_extractor_polarity_conflict_count",
"temporal_overlap_conflict_count",
"temporal_separable_opposition_count",
"referent_schema_heterogeneity_count",
"heuristic_flags",
"derivation_version",
"content_hash",
"live_claim_count",
"state_status",
"computed_at",
] {
assert!(
rows.iter().any(|c| c == expected),
"contest_state column {} missing",
expected
);
}
}
#[test]
fn test_m4_contest_state_missing_returns_none() {
let db = YantrikDB::new(":memory:", 8).unwrap();
assert!(db
.get_contest_state("nonexistent", "default")
.unwrap()
.is_none());
}
#[test]
fn test_m4_contest_single_support_basic_fields() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_c1");
seed_contest_claim(
&db,
"p_c1",
"c1",
"ext_a",
1,
"[\"src_1\"]",
None,
"default",
None,
None,
);
let c = db.compute_contest_state("p_c1", "default").unwrap();
assert_eq!(c.live_claim_count, 1);
assert_eq!(c.state_status, "fresh");
assert!(!c.content_hash.is_empty());
assert!((c.support_mass - 1.0).abs() < 1e-9);
assert_eq!(c.attack_mass, 0.0);
assert_eq!(c.support_distinct_source_count, 1);
assert_eq!(c.attack_distinct_source_count, 0);
assert_eq!(
c.heuristic_flags, 0,
"no flags should fire for a lone claim"
);
}
#[test]
fn test_m4_contest_same_source_opposite_polarity() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_same_src");
seed_contest_claim(
&db,
"p_same_src",
"c_sup",
"ext_a",
1,
"[\"src_x\"]",
None,
"default",
None,
None,
);
seed_contest_claim(
&db,
"p_same_src",
"c_att",
"ext_b",
-1,
"[\"src_x\"]",
None,
"default",
None,
None,
);
let c = db.compute_contest_state("p_same_src", "default").unwrap();
assert_eq!(c.same_source_opposite_polarity_count, 1);
assert!(c.heuristic_flags & crate::engine::warrant::contest_flags::SAME_SOURCE_CONFLICT != 0);
}
#[test]
fn test_m4_contest_same_artifact_extractor_conflict() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_artifact");
seed_contest_claim(
&db,
"p_artifact",
"c1",
"extractor_a",
1,
"[]",
Some("doc_42"),
"default",
None,
None,
);
seed_contest_claim(
&db,
"p_artifact",
"c2",
"extractor_b",
-1,
"[]",
Some("doc_42"),
"default",
None,
None,
);
let c = db.compute_contest_state("p_artifact", "default").unwrap();
assert_eq!(c.same_artifact_extractor_polarity_conflict_count, 1);
assert!(
c.heuristic_flags & crate::engine::warrant::contest_flags::SAME_ARTIFACT_EXTRACTOR_CONFLICT
!= 0
);
}
#[test]
fn test_m4_contest_same_artifact_same_extractor_is_not_conflict() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_same_ext");
seed_contest_claim(
&db,
"p_same_ext",
"c1",
"extractor_a",
1,
"[]",
Some("doc_42"),
"default",
None,
None,
);
seed_contest_claim(
&db,
"p_same_ext",
"c2",
"extractor_a",
-1,
"[]",
Some("doc_42"),
"default",
None,
None,
);
let c = db.compute_contest_state("p_same_ext", "default").unwrap();
assert_eq!(
c.same_artifact_extractor_polarity_conflict_count, 0,
"same-extractor same-artifact conflict is not an EXTRACTOR conflict"
);
}
#[test]
fn test_m4_contest_temporal_separable_vs_overlap() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_temporal");
seed_contest_claim(
&db,
"p_temporal",
"c_sup",
"ext_a",
1,
"[\"s1\"]",
None,
"default",
Some(0.0),
Some(10.0),
);
seed_contest_claim(
&db,
"p_temporal",
"c_att",
"ext_b",
-1,
"[\"s2\"]",
None,
"default",
Some(20.0),
Some(30.0),
);
let c = db.compute_contest_state("p_temporal", "default").unwrap();
assert_eq!(c.temporal_separable_opposition_count, 1);
assert_eq!(c.temporal_overlap_conflict_count, 0);
assert!(
c.heuristic_flags & crate::engine::warrant::contest_flags::PRESENT_TENSE_CONFLICT == 0,
"disjoint intervals should NOT set PRESENT_TENSE_CONFLICT"
);
}
#[test]
fn test_m4_contest_temporal_overlap_is_present_tense_conflict() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_overlap");
seed_contest_claim(
&db,
"p_overlap",
"c_sup",
"ext_a",
1,
"[\"s1\"]",
None,
"default",
Some(0.0),
Some(20.0),
);
seed_contest_claim(
&db,
"p_overlap",
"c_att",
"ext_b",
-1,
"[\"s2\"]",
None,
"default",
Some(10.0),
Some(30.0),
);
let c = db.compute_contest_state("p_overlap", "default").unwrap();
assert_eq!(c.temporal_overlap_conflict_count, 1);
assert_eq!(c.temporal_separable_opposition_count, 0);
assert!(c.heuristic_flags & crate::engine::warrant::contest_flags::PRESENT_TENSE_CONFLICT != 0);
}
#[test]
fn test_m4_contest_referent_heterogeneity() {
let db = YantrikDB::new(":memory:", 8).unwrap();
db.conn()
.execute(
"INSERT INTO propositions (proposition_id, src, rel_type, dst, namespace, created_at) \
VALUES ('p_het', 'X', 'rel', 'Y', 'default', 0.0)",
[],
)
.unwrap();
seed_contest_claim(
&db, "p_het", "c1", "ext_a", 1, "[\"s1\"]", None, "ns_a", None, None,
);
seed_contest_claim(
&db, "p_het", "c2", "ext_b", 1, "[\"s2\"]", None, "ns_b", None, None,
);
let c = db.compute_contest_state("p_het", "default").unwrap();
assert!(
c.referent_schema_heterogeneity_count > 0,
"two namespaces should register heterogeneity"
);
assert!(
c.heuristic_flags & crate::engine::warrant::contest_flags::REFERENT_HETEROGENEITY_PRESENT
!= 0
);
}
#[test]
fn test_m4_contest_duplication_risk_flag() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_dup_risk");
for i in 0..4 {
let cid = format!("c_dup_{}", i);
let ext = format!("ext_{}", i);
seed_contest_claim(
&db,
"p_dup_risk",
&cid,
&ext,
1,
"[\"shared\"]",
None,
"default",
None,
None,
);
}
let c = db.compute_contest_state("p_dup_risk", "default").unwrap();
assert!(
c.support_distinct_source_count == 1,
"all share the same lineage element"
);
assert!(
c.support_mass > 2.0,
"four-way shared lineage should still give σ > 2"
);
let _ = c.heuristic_flags;
}
#[test]
fn test_m4_contest_order_invariant_through_db() {
fn setup(order: &[(&str, i32, &str)]) -> (f64, i64, String) {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_ord");
for (ext, pol, lineage) in order {
let cid = format!("c_{}_{}", ext, pol);
seed_contest_claim(
&db, "p_ord", &cid, ext, *pol, lineage, None, "default", None, None,
);
}
let c = db.compute_contest_state("p_ord", "default").unwrap();
(
c.support_mass,
c.same_source_opposite_polarity_count,
c.content_hash,
)
}
let (m1, n1, h1) = setup(&[
("ext_a", 1, "[\"x\"]"),
("ext_b", -1, "[\"x\"]"),
("ext_c", 1, "[\"y\"]"),
]);
let (m2, n2, h2) = setup(&[
("ext_c", 1, "[\"y\"]"),
("ext_b", -1, "[\"x\"]"),
("ext_a", 1, "[\"x\"]"),
]);
assert!(
(m1 - m2).abs() < 1e-9,
"support_mass must be order-invariant"
);
assert_eq!(n1, n2, "counters must be order-invariant");
assert_eq!(h1, h2, "content_hash must be order-invariant");
}
#[test]
fn test_m4_contest_idempotent_recompute() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_idem");
seed_contest_claim(
&db, "p_idem", "c1", "ext_a", 1, "[\"s1\"]", None, "default", None, None,
);
let first = db.compute_contest_state("p_idem", "default").unwrap();
let second = db.compute_contest_state("p_idem", "default").unwrap();
assert_eq!(first.content_hash, second.content_hash);
assert_eq!(
first.computed_at, second.computed_at,
"idempotent recompute should not re-stamp"
);
}
#[test]
fn test_m4_ingest_claim_triggers_contest_state() {
let db = YantrikDB::new(":memory:", 8).unwrap();
db.ingest_claim(
"Alice", "works_at", "Acme", "default", 1, "asserted", None, None, "manual", None,
"medium", None, None, None, 1.0,
)
.unwrap();
let prop_id: String = db
.conn()
.query_row(
"SELECT proposition_id FROM propositions \
WHERE src = 'Alice' AND rel_type = 'works_at' AND dst = 'Acme' AND namespace = 'default'",
[],
|row| row.get(0),
)
.unwrap();
let c = db.get_contest_state(&prop_id, "default").unwrap().unwrap();
assert_eq!(c.state_status, "fresh");
assert_eq!(c.live_claim_count, 1);
assert!(!c.content_hash.is_empty());
assert_eq!(
c.derivation_version,
crate::engine::warrant::CONTEST_DERIVATION_VERSION
);
}
#[test]
fn test_m4_contest_independence_matches_omega_sum() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_ind");
for i in 0..3 {
let cid = format!("c_ind_{}", i);
let ext = format!("ext_{}", i);
seed_contest_claim(
&db,
"p_ind",
&cid,
&ext,
1,
"[\"shared\"]",
None,
"default",
None,
None,
);
}
let c = db.compute_contest_state("p_ind", "default").unwrap();
assert!(
(c.support_effective_independence - 2.0).abs() < 0.01,
"expected ≈ 2.0, got {}",
c.support_effective_independence
);
}
#[test]
fn test_m45_list_flagged_propositions_empty_when_nothing_flagged() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_clean");
seed_contest_claim(
&db,
"p_clean",
"c1",
"ext_a",
1,
"[\"src_1\"]",
None,
"default",
None,
None,
);
db.compute_contest_state("p_clean", "default").unwrap();
let flagged = db
.list_flagged_propositions(
crate::engine::warrant::contest_flags::SAME_SOURCE_CONFLICT,
10,
)
.unwrap();
assert!(
flagged.is_empty(),
"clean proposition should not be flagged"
);
}
#[test]
fn test_m45_list_flagged_propositions_returns_matching() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_flag");
seed_contest_claim(
&db,
"p_flag",
"c_s",
"ext_a",
1,
"[\"src_x\"]",
None,
"default",
None,
None,
);
seed_contest_claim(
&db,
"p_flag",
"c_a",
"ext_b",
-1,
"[\"src_x\"]",
None,
"default",
None,
None,
);
db.compute_contest_state("p_flag", "default").unwrap();
seed_proposition(&db, "p_clean");
seed_contest_claim(
&db,
"p_clean",
"c1",
"ext_a",
1,
"[\"src_y\"]",
None,
"default",
None,
None,
);
db.compute_contest_state("p_clean", "default").unwrap();
let flagged = db
.list_flagged_propositions(
crate::engine::warrant::contest_flags::SAME_SOURCE_CONFLICT,
10,
)
.unwrap();
assert_eq!(flagged.len(), 1);
assert_eq!(flagged[0].proposition_id, "p_flag");
}
#[test]
fn test_m45_list_flagged_propositions_combined_mask() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_same_src");
seed_contest_claim(
&db,
"p_same_src",
"c_src_s",
"ext_a",
1,
"[\"src_x\"]",
None,
"default",
None,
None,
);
seed_contest_claim(
&db,
"p_same_src",
"c_src_a",
"ext_b",
-1,
"[\"src_x\"]",
None,
"default",
None,
None,
);
db.compute_contest_state("p_same_src", "default").unwrap();
seed_proposition(&db, "p_artifact");
seed_contest_claim(
&db,
"p_artifact",
"c_art_s",
"extractor_a",
1,
"[]",
Some("doc_1"),
"default",
None,
None,
);
seed_contest_claim(
&db,
"p_artifact",
"c_art_a",
"extractor_b",
-1,
"[]",
Some("doc_1"),
"default",
None,
None,
);
db.compute_contest_state("p_artifact", "default").unwrap();
let mask = crate::engine::warrant::contest_flags::SAME_SOURCE_CONFLICT
| crate::engine::warrant::contest_flags::SAME_ARTIFACT_EXTRACTOR_CONFLICT;
let flagged = db.list_flagged_propositions(mask, 10).unwrap();
assert_eq!(
flagged.len(),
2,
"combined mask should match both propositions"
);
let prop_ids: Vec<&str> = flagged.iter().map(|c| c.proposition_id.as_str()).collect();
assert!(prop_ids.contains(&"p_same_src"));
assert!(prop_ids.contains(&"p_artifact"));
}
#[test]
fn test_m45_list_flagged_propositions_zero_mask_returns_empty() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_any");
seed_contest_claim(
&db,
"p_any",
"c_s",
"ext_a",
1,
"[\"src_x\"]",
None,
"default",
None,
None,
);
seed_contest_claim(
&db,
"p_any",
"c_a",
"ext_b",
-1,
"[\"src_x\"]",
None,
"default",
None,
None,
);
db.compute_contest_state("p_any", "default").unwrap();
let flagged = db.list_flagged_propositions(0, 10).unwrap();
assert!(flagged.is_empty(), "mask = 0 should match nothing");
}
#[test]
fn test_m45_inspect_contest_conflicts_missing_returns_none() {
let db = YantrikDB::new(":memory:", 8).unwrap();
let report = db
.inspect_contest_conflicts("nonexistent", "default")
.unwrap();
assert!(report.is_none());
}
#[test]
fn test_m45_inspect_contest_returns_exemplar_pairs() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_insp");
seed_contest_claim(
&db,
"p_insp",
"c_support",
"ext_a",
1,
"[\"src_shared\"]",
None,
"default",
None,
None,
);
seed_contest_claim(
&db,
"p_insp",
"c_attack",
"ext_b",
-1,
"[\"src_shared\"]",
None,
"default",
None,
None,
);
db.compute_contest_state("p_insp", "default").unwrap();
let report = db
.inspect_contest_conflicts("p_insp", "default")
.unwrap()
.unwrap();
assert!(
report.heuristic_flags & crate::engine::warrant::contest_flags::SAME_SOURCE_CONFLICT != 0
);
assert_eq!(report.same_source_opposite_polarity_pairs.len(), 1);
let pair = &report.same_source_opposite_polarity_pairs[0];
assert!(
(pair.0 == "c_support" && pair.1 == "c_attack")
|| (pair.0 == "c_attack" && pair.1 == "c_support"),
"exemplar pair should contain the actual conflicting claim_ids, got ({}, {})",
pair.0,
pair.1
);
}
#[test]
fn test_m45_inspect_temporal_overlap_pairs() {
let db = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db, "p_temp");
seed_contest_claim(
&db,
"p_temp",
"c_s",
"ext_a",
1,
"[\"s1\"]",
None,
"default",
Some(0.0),
Some(20.0),
);
seed_contest_claim(
&db,
"p_temp",
"c_a",
"ext_b",
-1,
"[\"s2\"]",
None,
"default",
Some(10.0),
Some(30.0),
);
db.compute_contest_state("p_temp", "default").unwrap();
let report = db
.inspect_contest_conflicts("p_temp", "default")
.unwrap()
.unwrap();
assert_eq!(report.temporal_overlap_conflict_pairs.len(), 1);
let db2 = YantrikDB::new(":memory:", 8).unwrap();
seed_proposition(&db2, "p_sep");
seed_contest_claim(
&db2,
"p_sep",
"c_s",
"ext_a",
1,
"[\"s1\"]",
None,
"default",
Some(0.0),
Some(10.0),
);
seed_contest_claim(
&db2,
"p_sep",
"c_a",
"ext_b",
-1,
"[\"s2\"]",
None,
"default",
Some(20.0),
Some(30.0),
);
db2.compute_contest_state("p_sep", "default").unwrap();
let report2 = db2
.inspect_contest_conflicts("p_sep", "default")
.unwrap()
.unwrap();
assert_eq!(
report2.temporal_overlap_conflict_pairs.len(),
0,
"disjoint intervals should not appear as overlap conflicts"
);
}
#[test]
fn test_m45_end_to_end_ingest_then_flagged_query() {
let db = YantrikDB::new(":memory:", 8).unwrap();
db.ingest_claim(
"Alice", "works_at", "Acme", "default", 1, "asserted", None, None, "manual", None,
"medium", None, None, None, 1.0,
)
.unwrap();
db.ingest_claim(
"Alice",
"works_at",
"Acme",
"default",
-1,
"denied",
None,
None,
"alt_manual",
None,
"medium",
None,
None,
None,
1.0,
)
.unwrap();
let prop_id: String = db
.conn()
.query_row(
"SELECT proposition_id FROM propositions \
WHERE src = 'Alice' AND rel_type = 'works_at' AND dst = 'Acme' AND namespace = 'default'",
[],
|row| row.get(0),
)
.unwrap();
let state = db.get_contest_state(&prop_id, "default").unwrap().unwrap();
assert_eq!(state.live_claim_count, 2);
assert!(
state.heuristic_flags & crate::engine::warrant::contest_flags::PRESENT_TENSE_CONFLICT != 0,
"opposite-polarity claims without temporal bounds should flag as present-tense conflict"
);
let flagged = db
.list_flagged_propositions(
crate::engine::warrant::contest_flags::PRESENT_TENSE_CONFLICT,
10,
)
.unwrap();
assert_eq!(flagged.len(), 1);
assert_eq!(flagged[0].proposition_id, prop_id);
}
#[test]
fn test_m3_second_ingest_updates_mobility_deterministically() {
let db = YantrikDB::new(":memory:", 8).unwrap();
db.ingest_claim(
"Alice", "works_at", "Acme", "default", 1, "asserted", None, None, "source_a", None,
"medium", None, None, None, 1.0,
)
.unwrap();
let prop_id: String = db
.conn()
.query_row(
"SELECT proposition_id FROM propositions \
WHERE src = 'Alice' AND rel_type = 'works_at' AND dst = 'Acme' AND namespace = 'default'",
[],
|row| row.get(0),
)
.unwrap();
let h1 = db
.get_mobility_state(&prop_id, "default")
.unwrap()
.unwrap()
.content_hash;
db.ingest_claim(
"Alice", "works_at", "Acme", "default", 1, "asserted", None, None, "source_b", None,
"medium", None, None, None, 1.0,
)
.unwrap();
let state2 = db.get_mobility_state(&prop_id, "default").unwrap().unwrap();
assert_eq!(state2.live_claim_count, 2);
assert_ne!(h1, state2.content_hash, "hash must change after new claim");
assert!((state2.support_mass.unwrap() - 2.0).abs() < 1e-9);
}
#[test]
fn test_schema_v20_migration_from_v19() {
use rusqlite::Connection;
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"
CREATE TABLE propositions (
proposition_id TEXT PRIMARY KEY,
src TEXT NOT NULL, rel_type TEXT NOT NULL, dst TEXT NOT NULL,
namespace TEXT NOT NULL, created_at REAL NOT NULL,
UNIQUE(src, rel_type, dst, namespace)
);
CREATE TABLE claims (
claim_id TEXT PRIMARY KEY,
src TEXT NOT NULL, dst TEXT NOT NULL, rel_type TEXT NOT NULL,
weight REAL NOT NULL DEFAULT 1.0,
created_at REAL NOT NULL,
tombstoned INTEGER NOT NULL DEFAULT 0,
polarity INTEGER NOT NULL DEFAULT 1,
modality TEXT NOT NULL DEFAULT 'asserted',
valid_from REAL, valid_to REAL,
extractor TEXT NOT NULL DEFAULT 'manual',
extractor_version TEXT,
confidence_band TEXT NOT NULL DEFAULT 'medium',
source_memory_rid TEXT,
span_start INTEGER, span_end INTEGER,
namespace TEXT NOT NULL DEFAULT 'default',
proposition_id TEXT
);
",
)
.unwrap();
conn.execute(
"INSERT INTO propositions (proposition_id, src, rel_type, dst, namespace, created_at) \
VALUES ('p1', 'Alice', 'works_at', 'Acme', 'default', 0.0)",
[],
)
.unwrap();
conn.execute("INSERT INTO claims (claim_id, src, dst, rel_type, created_at, extractor, namespace, proposition_id) \
VALUES ('c1', 'Alice', 'Acme', 'works_at', 0.0, 'manual', 'default', 'p1')", []).unwrap();
conn.execute_batch(crate::schema::MIGRATE_V19_TO_V20)
.unwrap();
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN \
('mobility_state', 'actor_profile', 'compression_artifact')",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(count, 3);
let (regime, self_gen, lineage, modality): (String, i64, String, String) = conn
.query_row(
"SELECT regime_tag, self_generated, source_lineage, modality_signal \
FROM claims WHERE claim_id='c1'",
[],
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
)
.unwrap();
assert_eq!(regime, "default");
assert_eq!(self_gen, 0);
assert_eq!(lineage, "[]");
assert_eq!(modality, "text");
}
#[test]
fn test_schema_v19_backfill_from_v18() {
use rusqlite::Connection;
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"
CREATE TABLE claims (
claim_id TEXT PRIMARY KEY,
src TEXT NOT NULL,
dst TEXT NOT NULL,
rel_type TEXT NOT NULL,
weight REAL NOT NULL DEFAULT 1.0,
created_at REAL NOT NULL,
tombstoned INTEGER NOT NULL DEFAULT 0,
polarity INTEGER NOT NULL DEFAULT 1,
modality TEXT NOT NULL DEFAULT 'asserted',
valid_from REAL, valid_to REAL,
extractor TEXT NOT NULL DEFAULT 'manual',
extractor_version TEXT,
confidence_band TEXT NOT NULL DEFAULT 'medium',
source_memory_rid TEXT,
span_start INTEGER, span_end INTEGER,
namespace TEXT NOT NULL DEFAULT 'default'
);
",
)
.unwrap();
conn.execute(
"INSERT INTO claims (claim_id, src, dst, rel_type, created_at, extractor, namespace) \
VALUES ('c1', 'Alice', 'Acme', 'works_at', 0.0, 'source_a', 'ns1')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO claims (claim_id, src, dst, rel_type, created_at, extractor, namespace) \
VALUES ('c2', 'Alice', 'Acme', 'works_at', 0.0, 'source_b', 'ns1')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO claims (claim_id, src, dst, rel_type, created_at, extractor, namespace) \
VALUES ('c3', 'Bob', 'Beta', 'works_at', 0.0, 'source_a', 'ns1')",
[],
)
.unwrap();
conn.execute("INSERT INTO claims (claim_id, src, dst, rel_type, created_at, extractor, namespace, tombstoned) \
VALUES ('c4', 'Carol', 'Gamma', 'works_at', 0.0, 'source_a', 'ns1', 1)", []).unwrap();
conn.execute_batch(crate::schema::MIGRATE_V18_TO_V19)
.unwrap();
let prop_count: i64 = conn
.query_row("SELECT COUNT(*) FROM propositions", [], |r| r.get(0))
.unwrap();
assert_eq!(
prop_count, 2,
"backfill should create one proposition per unique non-tombstoned tuple"
);
let alice_props: Vec<String> = conn
.prepare("SELECT proposition_id FROM claims WHERE src='Alice'")
.unwrap()
.query_map([], |r| r.get::<_, String>(0))
.unwrap()
.filter_map(|r| r.ok())
.collect();
assert_eq!(alice_props.len(), 2);
assert_eq!(
alice_props[0], alice_props[1],
"claims on the same tuple should share a proposition_id"
);
let bob_prop: String = conn
.query_row(
"SELECT proposition_id FROM claims WHERE src='Bob'",
[],
|r| r.get(0),
)
.unwrap();
assert_ne!(alice_props[0], bob_prop);
let carol_prop: Option<String> = conn
.query_row(
"SELECT proposition_id FROM claims WHERE src='Carol'",
[],
|r| r.get(0),
)
.unwrap();
assert!(
carol_prop.is_none(),
"tombstoned claims should not be backfilled"
);
}