use chrono::{DateTime, TimeZone, Utc};
use cortex_core::{
compose_policy_outcomes, CrossSessionSalience, EventId, MemoryId, PolicyContribution,
PolicyOutcome, SourceAuthority, SummarySpan,
};
use cortex_store::migrate::apply_pending;
use cortex_store::migrate_v2::apply_expand_backfill_skeleton;
use cortex_store::repo::memories::{
cross_session_salience_contribution, insert_candidate_v2_policy_decision_test_allow,
summary_span_proof_contribution, V2_CROSS_SESSION_SALIENCE_RULE_ID,
V2_SUMMARY_SPAN_PROOF_RULE_ID,
};
use cortex_store::repo::{MemoryCandidate, MemoryRepo};
use cortex_store::Pool;
use rusqlite::Connection;
use serde_json::json;
fn test_pool() -> Pool {
let pool = Connection::open_in_memory().expect("open in-memory sqlite");
apply_pending(&pool).expect("apply migrations");
apply_expand_backfill_skeleton(&pool, "2026-05-04T13:00:00Z".parse().unwrap())
.expect("expand/backfill skeleton before v2 inserts");
pool
}
fn at(second: u32) -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 1, 1, 12, 0, second).unwrap()
}
fn memory_id() -> MemoryId {
"mem_01ARZ3NDEKTSV4RRFFQ69G5V60".parse().unwrap()
}
fn source_event_id() -> EventId {
"evt_01ARZ3NDEKTSV4RRFFQ69G5V60".parse().unwrap()
}
fn summary_claim() -> &'static str {
"v2 summary memory claim."
}
fn valid_span() -> SummarySpan {
let len: u32 = u32::try_from(summary_claim().len()).expect("claim fits u32");
SummarySpan {
byte_start: 0,
byte_end: len,
derived_from_event_ids: vec![source_event_id()],
max_source_authority: SourceAuthority::Derived,
}
}
fn candidate() -> MemoryCandidate {
MemoryCandidate {
id: memory_id(),
memory_type: "summary".into(),
claim: summary_claim().into(),
source_episodes_json: json!([]),
source_events_json: json!([source_event_id().to_string()]),
domains_json: json!(["v2"]),
salience_json: json!({"score": 0.5}),
confidence: 0.7,
authority: "candidate".into(),
applies_when_json: json!({}),
does_not_apply_when_json: json!({}),
created_at: at(1),
updated_at: at(1),
}
}
fn fresh_salience() -> CrossSessionSalience {
CrossSessionSalience {
cross_session_use_count: 0,
first_used_at: None,
last_cross_session_use_at: None,
last_validation_at: None,
validation_epoch: 0,
blessed_until: None,
}
}
fn count_memories(pool: &Pool) -> i64 {
pool.query_row("SELECT COUNT(*) FROM memories;", [], |row| row.get(0))
.expect("count memories")
}
#[test]
fn insert_candidate_with_v2_fields_refuses_uncovered_summary_text() {
let pool = test_pool();
let repo = MemoryRepo::new(&pool);
let invalid_span = SummarySpan {
byte_start: 0,
byte_end: 5,
derived_from_event_ids: vec![source_event_id()],
max_source_authority: SourceAuthority::Derived,
};
let memory = candidate();
let salience = fresh_salience();
let span_contribution =
summary_span_proof_contribution(&memory, std::slice::from_ref(&invalid_span), |_| {
SourceAuthority::Derived
});
assert_eq!(span_contribution.outcome, PolicyOutcome::Reject);
let salience_contribution = cross_session_salience_contribution(&salience);
let decision = compose_policy_outcomes(vec![span_contribution, salience_contribution], None);
assert_eq!(decision.final_outcome, PolicyOutcome::Reject);
let err = repo
.insert_candidate_with_v2_fields(
&memory,
std::slice::from_ref(&invalid_span),
&salience,
&decision,
)
.expect_err("Reject policy must fail closed for invalid summary spans");
assert!(
err.to_string().contains("Reject"),
"error must name the blocking outcome: {err}"
);
assert_eq!(
count_memories(&pool),
0,
"no memory row may be written when summary spans fail validation"
);
}
#[test]
fn insert_candidate_with_v2_fields_refuses_authority_cache_uplift() {
let pool = test_pool();
let repo = MemoryRepo::new(&pool);
let uplifted_span = SummarySpan {
byte_start: 0,
byte_end: u32::try_from(summary_claim().len()).unwrap(),
derived_from_event_ids: vec![source_event_id()],
max_source_authority: SourceAuthority::User,
};
let memory = candidate();
let salience = fresh_salience();
let span_contribution =
summary_span_proof_contribution(&memory, std::slice::from_ref(&uplifted_span), |_| {
SourceAuthority::Derived
});
assert_eq!(span_contribution.outcome, PolicyOutcome::Reject);
let decision = compose_policy_outcomes(
vec![
span_contribution,
cross_session_salience_contribution(&salience),
],
None,
);
let err = repo
.insert_candidate_with_v2_fields(
&memory,
std::slice::from_ref(&uplifted_span),
&salience,
&decision,
)
.expect_err("Reject policy must fail closed for authority uplift");
assert!(err.to_string().contains("Reject"));
assert_eq!(count_memories(&pool), 0);
}
#[test]
fn insert_candidate_with_v2_fields_refuses_prepopulated_cross_session_use_count() {
let pool = test_pool();
let repo = MemoryRepo::new(&pool);
let mut salience = fresh_salience();
salience.cross_session_use_count = 5;
let memory = candidate();
let spans = vec![valid_span()];
let salience_contribution = cross_session_salience_contribution(&salience);
assert_eq!(salience_contribution.outcome, PolicyOutcome::Reject);
let decision = compose_policy_outcomes(
vec![
summary_span_proof_contribution(&memory, &spans, |_| SourceAuthority::Derived),
salience_contribution,
],
None,
);
let err = repo
.insert_candidate_with_v2_fields(&memory, &spans, &salience, &decision)
.expect_err("Reject policy must fail closed for pre-populated salience");
assert!(
err.to_string().contains("Reject"),
"error must name the blocking outcome: {err}"
);
assert_eq!(count_memories(&pool), 0);
}
#[test]
fn insert_candidate_with_v2_fields_refuses_prepopulated_last_validation_at() {
let pool = test_pool();
let repo = MemoryRepo::new(&pool);
let mut salience = fresh_salience();
salience.last_validation_at = Some(at(0));
let memory = candidate();
let spans = vec![valid_span()];
let decision = compose_policy_outcomes(
vec![
summary_span_proof_contribution(&memory, &spans, |_| SourceAuthority::Derived),
cross_session_salience_contribution(&salience),
],
None,
);
let err = repo
.insert_candidate_with_v2_fields(&memory, &spans, &salience, &decision)
.expect_err("Reject policy must fail closed for minted validation freshness");
assert!(err.to_string().contains("Reject"));
assert_eq!(count_memories(&pool), 0);
}
#[test]
fn insert_candidate_with_v2_fields_refuses_prepopulated_blessed_until() {
let pool = test_pool();
let repo = MemoryRepo::new(&pool);
let mut salience = fresh_salience();
salience.blessed_until = Some(at(7));
let memory = candidate();
let spans = vec![valid_span()];
let decision = compose_policy_outcomes(
vec![
summary_span_proof_contribution(&memory, &spans, |_| SourceAuthority::Derived),
cross_session_salience_contribution(&salience),
],
None,
);
let err = repo
.insert_candidate_with_v2_fields(&memory, &spans, &salience, &decision)
.expect_err("Reject policy must fail closed for minted bless window");
assert!(err.to_string().contains("Reject"));
assert_eq!(count_memories(&pool), 0);
}
#[test]
fn insert_candidate_with_v2_fields_refuses_missing_summary_span_contributor() {
let pool = test_pool();
let repo = MemoryRepo::new(&pool);
let memory = candidate();
let spans = vec![valid_span()];
let salience = fresh_salience();
let decision = compose_policy_outcomes(
vec![PolicyContribution::new(
V2_CROSS_SESSION_SALIENCE_RULE_ID,
PolicyOutcome::Allow,
"salience matches candidate-row invariants",
)
.unwrap()],
None,
);
let err = repo
.insert_candidate_with_v2_fields(&memory, &spans, &salience, &decision)
.expect_err("missing summary span contributor must fail closed");
assert!(
err.to_string().contains(V2_SUMMARY_SPAN_PROOF_RULE_ID),
"error must name the missing contributor: {err}"
);
assert_eq!(count_memories(&pool), 0);
}
#[test]
fn insert_candidate_with_v2_fields_refuses_missing_cross_session_salience_contributor() {
let pool = test_pool();
let repo = MemoryRepo::new(&pool);
let memory = candidate();
let spans = vec![valid_span()];
let salience = fresh_salience();
let decision = compose_policy_outcomes(
vec![PolicyContribution::new(
V2_SUMMARY_SPAN_PROOF_RULE_ID,
PolicyOutcome::Allow,
"summary spans validated",
)
.unwrap()],
None,
);
let err = repo
.insert_candidate_with_v2_fields(&memory, &spans, &salience, &decision)
.expect_err("missing cross-session salience contributor must fail closed");
assert!(
err.to_string().contains(V2_CROSS_SESSION_SALIENCE_RULE_ID),
"error must name the missing contributor: {err}"
);
assert_eq!(count_memories(&pool), 0);
}
#[test]
fn insert_candidate_with_v2_fields_refuses_stale_allow_after_invariant_regression() {
let pool = test_pool();
let repo = MemoryRepo::new(&pool);
let mut salience = fresh_salience();
salience.validation_epoch = 9;
let memory = candidate();
let spans = vec![valid_span()];
let stale_allow_decision = insert_candidate_v2_policy_decision_test_allow();
assert_eq!(stale_allow_decision.final_outcome, PolicyOutcome::Allow);
let err = repo
.insert_candidate_with_v2_fields(&memory, &spans, &salience, &stale_allow_decision)
.expect_err("repo must independently re-validate ADR 0017 invariants");
assert!(
err.to_string().contains("validation_epoch"),
"error must name the regressed invariant: {err}"
);
assert_eq!(count_memories(&pool), 0);
}
#[test]
fn insert_candidate_with_v2_fields_accepts_valid_spans_and_fresh_salience() {
let pool = test_pool();
let repo = MemoryRepo::new(&pool);
let memory = candidate();
let spans = vec![valid_span()];
let salience = fresh_salience();
let span_contribution =
summary_span_proof_contribution(&memory, &spans, |_| SourceAuthority::Derived);
assert_eq!(span_contribution.outcome, PolicyOutcome::Allow);
let salience_contribution = cross_session_salience_contribution(&salience);
assert_eq!(salience_contribution.outcome, PolicyOutcome::Allow);
let decision = compose_policy_outcomes(vec![span_contribution, salience_contribution], None);
assert_eq!(decision.final_outcome, PolicyOutcome::Allow);
repo.insert_candidate_with_v2_fields(&memory, &spans, &salience, &decision)
.expect("Allow decision with valid v2 fields must be accepted");
assert_eq!(count_memories(&pool), 1);
let (cross_session_use_count, validation_epoch): (u32, u32) = pool
.query_row(
"SELECT cross_session_use_count, validation_epoch
FROM memories WHERE id = ?1;",
[memory_id().to_string()],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.expect("read v2 salience columns");
assert_eq!(cross_session_use_count, 0);
assert_eq!(validation_epoch, 0);
}