#[path = "common/harness.rs"]
mod harness;
const ARCHIVED_AT: &str = "2026-07-30T12:00:00.000000Z";
use harness::TestHarness;
use macrame::error::DbError;
use macrame::graph::AttributeMode;
use macrame::integrity::audit_current;
use macrame::schema::migrations;
use macrame::temporal::{
archive, hydrate_attributes, load_snapshot, reconstruct, save_snapshot, Interval,
MaterializedState,
};
use std::path::Path;
#[test]
fn test_interval_containment_and_overlap() {
let open_interval = Interval::new("2026-01-01T00:00:00.000000Z", "9999-12-31T23:59:59.999999Z");
assert!(open_interval.is_open());
assert!(open_interval.contains("2026-06-01T00:00:00.000000Z"));
let closed_interval =
Interval::new("2026-01-01T00:00:00.000000Z", "2026-06-01T00:00:00.000000Z");
assert!(!closed_interval.is_open());
assert!(closed_interval.contains("2026-03-01T00:00:00.000000Z"));
assert!(!closed_interval.contains("2026-07-01T00:00:00.000000Z"));
let overlapping = Interval::new("2026-05-01T00:00:00.000000Z", "2026-08-01T00:00:00.000000Z");
let non_overlapping =
Interval::new("2026-07-01T00:00:00.000000Z", "2026-09-01T00:00:00.000000Z");
assert!(closed_interval.overlaps(&overlapping));
assert!(!closed_interval.overlaps(&non_overlapping));
}
#[tokio::test]
async fn test_monday_wednesday_friday_scenario() {
let harness = TestHarness::new();
let db = libsql::Builder::new_local(&harness.db_path)
.build()
.await
.unwrap();
let conn = db.connect().unwrap();
migrations::run(&conn).await.unwrap();
conn.execute(
"INSERT INTO concepts (id, title, content, valid_from, recorded_at) \
VALUES ('c1', 'Monday Title', 'Content', '2026-01-05T00:00:00.000000Z', '2026-01-05T00:00:00.000000Z')",
(),
)
.await
.unwrap();
conn.execute(
"UPDATE concepts SET title = 'Wednesday Title', recorded_at = '2026-01-07T00:00:00.000000Z' WHERE id = 'c1'",
(),
)
.await
.unwrap();
let node_ids = vec!["c1".to_string()];
let tuesday_ts = "2026-01-06T00:00:00.000000Z";
let current_attrs = hydrate_attributes(&conn, &node_ids, tuesday_ts, AttributeMode::Current)
.await
.unwrap();
assert_eq!(current_attrs[0].title, "Wednesday Title");
let at_time_attrs = hydrate_attributes(&conn, &node_ids, tuesday_ts, AttributeMode::AtTime)
.await
.unwrap();
assert_eq!(at_time_attrs[0].title, "Monday Title");
let omit_attrs = hydrate_attributes(&conn, &node_ids, tuesday_ts, AttributeMode::Omit)
.await
.unwrap();
assert!(omit_attrs.is_empty());
}
#[tokio::test]
async fn test_reconstruct_now_equals_live_table() {
let harness = TestHarness::new();
let db = libsql::Builder::new_local(&harness.db_path)
.build()
.await
.unwrap();
let conn = db.connect().unwrap();
migrations::run(&conn).await.unwrap();
conn.execute(
"INSERT INTO concepts (id, title, valid_from, recorded_at) VALUES ('c1', 'Node 1', '2026-01-01T00:00:00.000000Z', '2026-01-01T00:00:00.000000Z')",
(),
)
.await
.unwrap();
conn.execute(
"INSERT INTO concepts (id, title, valid_from, recorded_at) VALUES ('c2', 'Node 2', '2026-01-01T00:00:00.000000Z', '2026-01-01T00:00:00.000000Z')",
(),
)
.await
.unwrap();
conn.execute(
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, weight, properties, recorded_at) \
VALUES ('c1', 'c2', 'KNOWS', '2026-01-01T00:00:00.000000Z', '9999-12-31T23:59:59.999999Z', 1.0, '{}', '2026-01-01T00:00:00.000000Z')",
(),
)
.await
.unwrap();
let state = reconstruct(&conn, "2026-01-10T00:00:00.000000Z", None, None)
.await
.unwrap();
assert!(state.concepts.contains_key("c1"));
assert!(state.concepts.contains_key("c2"));
assert_eq!(state.edges.len(), 1);
assert_eq!(state.edges[0].0, "c1");
assert_eq!(state.edges[0].1, "c2");
}
#[test]
fn test_snapshot_save_load_roundtrip() {
let harness = TestHarness::new();
let snapshots_dir = harness.temp_dir.path().join("snapshots");
let state = MaterializedState {
seq_anchor: 100,
timestamp: "2026-01-01T00:00:00.000000Z".to_string(),
concepts: std::collections::HashMap::new(),
edges: vec![(
"c1".to_string(),
"c2".to_string(),
"KNOWS".to_string(),
"2026-01-01T00:00:00.000000Z".to_string(),
"9999-12-31T23:59:59.999999Z".to_string(),
)],
};
let path = save_snapshot(&snapshots_dir, &state).unwrap();
assert!(path.exists());
let loaded = load_snapshot(&path).unwrap();
assert_eq!(loaded.seq_anchor, 100);
assert_eq!(loaded.edges.len(), 1);
assert_eq!(loaded.edges[0].0, "c1");
}
#[tokio::test]
async fn test_archive_moves_closed_intervals_and_leaves_no_drift() {
let harness = TestHarness::new();
let db = libsql::Builder::new_local(&harness.db_path)
.build()
.await
.unwrap();
let conn = db.connect().unwrap();
migrations::run(&conn).await.unwrap();
for id in ["c1", "c2"] {
conn.execute(
&format!("INSERT INTO concepts (id, title, valid_from, recorded_at) VALUES ('{id}', 'N', '2026-01-01T00:00:00.000000Z', '2026-01-01T00:00:00.000000Z')"),
(),
).await.unwrap();
}
conn.execute(
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, weight, properties, recorded_at) \
VALUES ('c1', 'c2', 'OLD', '2026-01-01T00:00:00.000000Z', '2026-02-01T00:00:00.000000Z', 1.0, '{}', '2026-01-01T00:00:00.000000Z')",
(),
).await.unwrap();
conn.execute(
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, weight, properties, recorded_at) \
VALUES ('c1', 'c2', 'LIVE', '2026-01-01T00:00:00.000000Z', '9999-12-31T23:59:59.999999Z', 1.0, '{}', '2026-01-01T00:00:00.000000Z')",
(),
).await.unwrap();
let archive_path = harness.temp_dir.path().join("test_macrame_archive.db");
let report = archive(
&conn,
"2026-06-01T00:00:00.000000Z",
ARCHIVED_AT,
&archive_path,
)
.await
.expect("archive session should succeed");
assert!(
archive_path.exists(),
"cold database file should be created"
);
assert_eq!(
report.links_archived, 1,
"only the closed interval is archivable"
);
let remaining: i64 = conn
.query("SELECT COUNT(*) FROM links", ())
.await
.unwrap()
.next()
.await
.unwrap()
.unwrap()
.get(0)
.unwrap();
assert_eq!(remaining, 1, "the open interval must stay hot");
let live: i64 = conn
.query(
"SELECT COUNT(*) FROM links_current WHERE edge_type = 'LIVE'",
(),
)
.await
.unwrap()
.next()
.await
.unwrap()
.unwrap()
.get(0)
.unwrap();
assert_eq!(live, 1);
assert_eq!(
audit_current(&conn).await.unwrap(),
0,
"archive must not induce drift"
);
assert!(conn.execute("DELETE FROM links", ()).await.is_err());
archive(
&conn,
"2026-06-01T00:00:00.000000Z",
ARCHIVED_AT,
&archive_path,
)
.await
.expect("second archive session should succeed (cold DB detached)");
}
#[tokio::test]
async fn test_cold_db_reconstruct_missing_archive_error() {
let harness = TestHarness::new();
let db = libsql::Builder::new_local(&harness.db_path)
.build()
.await
.unwrap();
let conn = db.connect().unwrap();
migrations::run(&conn).await.unwrap();
conn.execute(
"INSERT INTO concepts (id, title, valid_from, recorded_at) VALUES ('c1', 'Node 1', '2026-05-01T00:00:00.000000Z', '2026-05-01T00:00:00.000000Z')",
(),
)
.await
.unwrap();
let missing_path = Path::new("non_existent_archive.db");
let res = reconstruct(
&conn,
"2020-01-01T00:00:00.000000Z",
Some(missing_path),
None,
)
.await;
assert!(res.is_err());
if let Err(DbError::ReplayCorrupt { reason, .. }) = res {
assert!(reason.contains("does not exist"));
} else {
panic!("Expected ReplayCorrupt error for missing archive file");
}
}
const VIII_JAN: &str = "2026-01-01T00:00:00.000000Z";
const VIII_FEB: &str = "2026-02-01T00:00:00.000000Z";
const VIII_MAR: &str = "2026-03-01T00:00:00.000000Z";
const VIII_OPEN: &str = "9999-12-31T23:59:59.999999Z";
async fn newest_stamp(conn: &libsql::Connection) -> String {
conn.query("SELECT MAX(recorded_at) FROM transaction_log", ())
.await
.unwrap()
.next()
.await
.unwrap()
.unwrap()
.get(0)
.unwrap()
}
fn at_valid_time(
state: &MaterializedState,
t: &str,
) -> std::collections::BTreeSet<(String, String, String, String, String)> {
state
.edges
.iter()
.filter(|(_, _, _, vf, vt)| vf.as_str() <= t && t < vt.as_str())
.cloned()
.collect()
}
async fn as_of_set(
conn: &libsql::Connection,
t: &str,
) -> std::collections::BTreeSet<(String, String, String, String, String)> {
macrame::temporal::query_as_of_edges(conn, t)
.await
.unwrap()
.into_iter()
.collect()
}
#[tokio::test]
async fn a_retroactive_retirement_makes_as_of_and_reconstruct_diverge() {
let harness = TestHarness::new();
let db = macrame::prelude::Database::open(&harness.db_path)
.await
.unwrap();
for id in ["c1", "c2"] {
db.upsert_concept(macrame::prelude::ConceptUpsert::new(id, "N").valid_from(VIII_JAN))
.await
.unwrap();
}
db.assert_edge(
macrame::prelude::EdgeAssertion::new("c1", "c2", "REL")
.valid_from(VIII_JAN)
.valid_to(VIII_OPEN),
)
.await
.unwrap();
let believed_open = newest_stamp(db.read_conn()).await;
db.retire_edge("c1", "c2", "REL", VIII_JAN, VIII_FEB)
.await
.unwrap();
let believed_closed = newest_stamp(db.read_conn()).await;
assert!(
believed_open < believed_closed,
"the correction must be recorded strictly later, or there is no gap to test"
);
let live = as_of_set(db.read_conn(), VIII_MAR).await;
let then = at_valid_time(&db.reconstruct(&believed_open).await.unwrap(), VIII_MAR);
let now = at_valid_time(&db.reconstruct(&believed_closed).await.unwrap(), VIII_MAR);
assert!(
live.is_empty(),
"current belief has the interval closed in February, so March is outside it: {live:?}"
);
assert_eq!(
then.len(),
1,
"belief at the first stamp had the interval open, so March is inside it: {then:?}"
);
assert_ne!(
live, then,
"as_of and reconstruct agreed across a retroactive retirement; \
Doctrine VIII requires the gap to be visible, not smoothed over"
);
assert_eq!(
live, now,
"once the correction is itself in the past, the two questions have the same answer"
);
db.close().await.unwrap();
}
#[tokio::test]
async fn a_retroactive_assertion_is_invisible_to_the_earlier_belief() {
let harness = TestHarness::new();
let db = macrame::prelude::Database::open(&harness.db_path)
.await
.unwrap();
for id in ["c1", "c2"] {
db.upsert_concept(macrame::prelude::ConceptUpsert::new(id, "N").valid_from(VIII_JAN))
.await
.unwrap();
}
db.assert_edge(
macrame::prelude::EdgeAssertion::new("c1", "c2", "EARLY")
.valid_from(VIII_JAN)
.valid_to(VIII_OPEN),
)
.await
.unwrap();
let before_the_late_news = newest_stamp(db.read_conn()).await;
db.assert_edge(
macrame::prelude::EdgeAssertion::new("c1", "c2", "LATE")
.valid_from(VIII_JAN)
.valid_to(VIII_OPEN),
)
.await
.unwrap();
let live = as_of_set(db.read_conn(), VIII_MAR).await;
let then = at_valid_time(
&db.reconstruct(&before_the_late_news).await.unwrap(),
VIII_MAR,
);
let types = |s: &std::collections::BTreeSet<(String, String, String, String, String)>| {
s.iter()
.map(|e| e.2.clone())
.collect::<std::collections::BTreeSet<_>>()
};
assert!(
types(&live).contains("LATE"),
"current belief must include the retroactively asserted edge: {live:?}"
);
assert!(
!types(&then).contains("LATE"),
"belief at the earlier stamp cannot contain something recorded after it: {then:?}"
);
assert_ne!(
live, then,
"as_of and reconstruct agreed across a retroactive assertion; \
reconstruct is reading current state rather than folding the ledger"
);
assert!(
types(&then).contains("EARLY"),
"the earlier belief must still hold what it did know: {then:?}"
);
db.close().await.unwrap();
}
async fn row_set(
conn: &libsql::Connection,
expr: &str,
table: &str,
) -> std::collections::BTreeSet<String> {
let mut rows = conn
.query(&format!("SELECT {expr} FROM {table}"), ())
.await
.unwrap();
let mut out = std::collections::BTreeSet::new();
while let Some(r) = rows.next().await.unwrap() {
out.insert(r.get::<String>(0).unwrap());
}
out
}
const HOT_LINK_ROW: &str = "source_id||'|'||target_id||'|'||edge_type||'|'||valid_from||'|'||\
valid_to||'|'||weight||'|'||properties||'|'||recorded_at";
const HOT_LOG_ROW: &str = "seq_id||'|'||table_name||'|'||entity_id||'|'||operation||'|'||\
payload||'|'||recorded_at";
const HOT_CURRENT_ROW: &str = "source_id||'|'||target_id||'|'||edge_type||'|'||valid_from||'|'||\
valid_to||'|'||recorded_at";
async fn archivable_fixture(path: &Path) -> (libsql::Database, libsql::Connection) {
let db = libsql::Builder::new_local(path).build().await.unwrap();
let conn = db.connect().unwrap();
migrations::run(&conn).await.unwrap();
for id in ["c1", "c2"] {
conn.execute(
&format!(
"INSERT INTO concepts (id, title, valid_from, recorded_at) \
VALUES ('{id}', 'N', '2026-01-01T00:00:00.000000Z', '2026-01-01T00:00:00.000000Z')"
),
(),
)
.await
.unwrap();
}
for (etype, valid_to) in [
("OLD", "2026-02-01T00:00:00.000000Z"),
("LIVE", "9999-12-31T23:59:59.999999Z"),
] {
conn.execute(
&format!(
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, \
weight, properties, recorded_at) VALUES \
('c1', 'c2', '{etype}', '2026-01-01T00:00:00.000000Z', '{valid_to}', \
1.0, '{{}}', '2026-01-01T00:00:00.000000Z')"
),
(),
)
.await
.unwrap();
}
(db, conn)
}
#[tokio::test]
async fn a_failed_archive_session_leaves_the_hot_database_untouched_and_the_guards_armed() {
let cases: [(&str, &str, &str); 2] = [
(
"links",
"CREATE TABLE links (not_the_right_column TEXT)",
"source_id",
),
(
"transaction_log",
"CREATE TABLE transaction_log (seq_id INTEGER PRIMARY KEY, table_name TEXT, \
entity_id TEXT, operation TEXT, recorded_at TEXT)",
"payload",
),
];
for (broken_table, broken_ddl, missing_column) in cases {
let harness = TestHarness::new();
let (_db, conn) = archivable_fixture(&harness.db_path).await;
let cold = harness
.temp_dir
.path()
.join(format!("incompatible_{broken_table}.db"));
{
let d = libsql::Builder::new_local(&cold).build().await.unwrap();
let c = d.connect().unwrap();
c.execute(broken_ddl, ()).await.unwrap();
}
let before_links = row_set(&conn, HOT_LINK_ROW, "links").await;
let before_log = row_set(&conn, HOT_LOG_ROW, "transaction_log").await;
let before_current = row_set(&conn, HOT_CURRENT_ROW, "links_current").await;
assert!(
!before_links.is_empty(),
"the fixture must have rows to lose"
);
let cutoff = "2026-06-01T00:00:00.000000Z";
let err = archive(&conn, cutoff, ARCHIVED_AT, &cold)
.await
.expect_err(
"the session was supposed to fail on the incompatible cold table; \
if it succeeded this case tests nothing",
)
.to_string();
assert!(
err.contains(missing_column),
"[{broken_table}] expected the failure to be the copy missing {missing_column:?}, \
so the session dies at the phase this case exists to cover; got: {err}"
);
assert_eq!(
row_set(&conn, HOT_LINK_ROW, "links").await,
before_links,
"[{broken_table}] a failed archive session moved or dropped rows from links"
);
assert_eq!(
row_set(&conn, HOT_LOG_ROW, "transaction_log").await,
before_log,
"[{broken_table}] a failed archive session moved or dropped ledger rows"
);
assert_eq!(
row_set(&conn, HOT_CURRENT_ROW, "links_current").await,
before_current,
"[{broken_table}] a failed archive session left the materialization re-derived"
);
assert_eq!(
audit_current(&conn).await.unwrap(),
0,
"[{broken_table}] a failed archive session left links_current drifted from links"
);
let marker: i64 = conn
.query(
"SELECT COUNT(*) FROM sqlite_master \
WHERE type='table' AND name='macrame_archive_session'",
(),
)
.await
.unwrap()
.next()
.await
.unwrap()
.unwrap()
.get(0)
.unwrap();
assert_eq!(
marker, 0,
"[{broken_table}] the archive-session marker outlived the session: the \
delete guards are disarmed and will stay that way"
);
assert!(
conn.execute("DELETE FROM links", ()).await.is_err(),
"[{broken_table}] an ad-hoc DELETE succeeded after a failed archive \
session (Doctrine V)"
);
let good = harness.temp_dir.path().join("good_cold.db");
let report = archive(&conn, cutoff, ARCHIVED_AT, &good)
.await
.expect("a later archive session must still succeed after a failed one");
assert_eq!(
report.links_archived, 1,
"[{broken_table}] the closed interval is still archivable"
);
}
}