use super::*;
fn setup_fact_conn() -> Result<Connection> {
let conn = Connection::open_in_memory()?;
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?;
crate::migrate::run_migrations(&conn)?;
Ok(conn)
}
fn input<'a>(project: &'a str, object: &'a str, valid_from_epoch: i64) -> TemporalFactInput<'a> {
TemporalFactInput {
project,
subject: "deploy-target",
predicate: FactPredicate::AffectsProject,
object,
valid_from_epoch: Some(valid_from_epoch),
valid_to_epoch: None,
learned_at_epoch: Some(valid_from_epoch),
source_memory_id: None,
source_observation_id: None,
source_event_ids: &[],
confidence: 0.9,
supersedes_fact_id: None,
}
}
#[test]
fn supersession_preserves_historical_fact_but_hides_it_from_current() -> Result<()> {
let mut conn = setup_fact_conn()?;
let project = "test-temporal";
let old_id = insert_temporal_fact(&mut conn, &input(project, "staging", 100))?;
let mut replacement = input(project, "production", 200);
replacement.supersedes_fact_id = Some(old_id);
let new_id = insert_temporal_fact(&mut conn, &replacement)?;
let current = list_current_facts(
&conn,
project,
Some("deploy-target"),
Some(FactPredicate::AffectsProject),
)?;
assert_eq!(
current.iter().map(|fact| fact.id).collect::<Vec<_>>(),
vec![new_id]
);
assert_eq!(current[0].object, "production");
let old: TemporalFact = conn.query_row(
"SELECT id, project, subject, predicate, object, valid_from_epoch,
valid_to_epoch, learned_at_epoch, source_memory_id,
source_observation_id, source_event_ids, confidence,
supersedes_fact_id, status
FROM memory_facts WHERE id = ?1",
[old_id],
map_fact_row,
)?;
assert_eq!(old.status, "stale");
assert_eq!(old.valid_to_epoch, Some(200));
let before = list_facts_as_of(
&conn,
project,
150,
Some("deploy-target"),
Some(FactPredicate::AffectsProject),
)?;
assert_eq!(before[0].object, "staging");
let after = list_facts_as_of(
&conn,
project,
250,
Some("deploy-target"),
Some(FactPredicate::AffectsProject),
)?;
assert_eq!(after[0].object, "production");
Ok(())
}
#[test]
fn provenance_links_to_source_memory_and_events() -> Result<()> {
let mut conn = setup_fact_conn()?;
let memory_id = crate::memory::insert_memory(
&conn,
Some("session-a"),
"test-provenance",
Some("verification-command"),
"Verification command",
"The deploy fix was verified with cargo test.",
"bugfix",
None,
)?;
let observation_id = crate::db::insert_observation(
&conn,
"session-a",
"test-provenance",
"bugfix",
Some("Deploy fix"),
None,
Some("The deploy fix was verified with cargo test."),
None,
None,
None,
None,
Some(1),
0,
)?;
let input = TemporalFactInput {
project: "test-provenance",
subject: "deploy-fix",
predicate: FactPredicate::VerifiedBy,
object: "cargo test",
valid_from_epoch: Some(300),
valid_to_epoch: None,
learned_at_epoch: Some(320),
source_memory_id: Some(memory_id),
source_observation_id: Some(observation_id),
source_event_ids: &[11, 12],
confidence: 0.95,
supersedes_fact_id: None,
};
let id = insert_temporal_fact(&mut conn, &input)?;
let facts = list_current_facts(
&conn,
"test-provenance",
Some("deploy-fix"),
Some(FactPredicate::VerifiedBy),
)?;
assert_eq!(facts.len(), 1);
assert_eq!(facts[0].id, id);
assert_eq!(facts[0].source_memory_id, Some(memory_id));
assert_eq!(facts[0].source_observation_id, Some(observation_id));
assert_eq!(facts[0].source_event_ids, vec![11, 12]);
assert_eq!(facts[0].confidence, 0.95);
Ok(())
}
#[test]
fn rejects_invalid_validity_window() -> Result<()> {
let mut conn = setup_fact_conn()?;
let mut input = input("test-invalid", "production", 500);
input.valid_to_epoch = Some(400);
let err = insert_temporal_fact(&mut conn, &input).expect_err("invalid time must fail");
assert!(err.to_string().contains("valid_to_epoch"));
Ok(())
}
#[test]
fn rejects_supersession_cutoff_before_old_valid_from() -> Result<()> {
let mut conn = setup_fact_conn()?;
let project = "test-temporal-cutoff";
let old_id = insert_temporal_fact(&mut conn, &input(project, "staging", 500))?;
let mut replacement = input(project, "production", 400);
replacement.supersedes_fact_id = Some(old_id);
let err = insert_temporal_fact(&mut conn, &replacement)
.expect_err("backdated supersession cutoff should fail before mutation");
assert!(err.to_string().contains("before existing valid_from_epoch"));
let (status, valid_to): (String, Option<i64>) = conn.query_row(
"SELECT status, valid_to_epoch FROM memory_facts WHERE id = ?1",
[old_id],
|row| Ok((row.get(0)?, row.get(1)?)),
)?;
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM memory_facts WHERE project = ?1",
[project],
|row| row.get(0),
)?;
assert_eq!(status, "active");
assert_eq!(valid_to, None);
assert_eq!(count, 1);
Ok(())
}