#[path = "common/harness.rs"]
mod harness;
use harness::TestHarness;
use macrame::error::DbError;
use macrame::integrity::{audit_current, rebuild_current};
use macrame::schema::migrations;
#[tokio::test]
async fn test_schema_initialization_and_version() {
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();
let version: u32 = conn
.query("PRAGMA user_version", ())
.await
.unwrap()
.next()
.await
.unwrap()
.unwrap()
.get(0)
.unwrap();
assert_eq!(version, macrame::schema::SCHEMA_VERSION);
}
#[tokio::test]
async fn test_trg_links_current_sync() {
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 mut rows = conn
.query(
"SELECT source_id, target_id, edge_type FROM links_current WHERE source_id = 'c1'",
(),
)
.await
.unwrap();
let row = rows.next().await.unwrap().unwrap();
let src: String = row.get(0).unwrap();
let tgt: String = row.get(1).unwrap();
let edge_type: String = row.get(2).unwrap();
assert_eq!(src, "c1");
assert_eq!(tgt, "c2");
assert_eq!(edge_type, "KNOWS");
let log_count: i64 = conn
.query(
"SELECT COUNT(*) FROM transaction_log WHERE table_name = 'links'",
(),
)
.await
.unwrap()
.next()
.await
.unwrap()
.unwrap()
.get(0)
.unwrap();
assert_eq!(log_count, 1);
}
#[tokio::test]
async fn test_trg_links_single_open_violation() {
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 res = conn.execute(
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, weight, properties, recorded_at) \
VALUES ('c1', 'c2', 'KNOWS', '2026-02-01T00:00:00.000000Z', '9999-12-31T23:59:59.999999Z', 1.0, '{}', '2026-02-01T00:00:00.000000Z')",
(),
).await;
assert!(res.is_err());
let err_str = res.err().unwrap().to_string();
assert!(
err_str.contains("open interval"),
"Expected single open interval error, got: {err_str}"
);
}
#[tokio::test]
async fn test_trg_concepts_monotonic_ra_violation() {
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', 'Original Title', '2026-01-01T00:00:00.000000Z', '2026-01-01T12:00:00.000000Z')",
(),
).await.unwrap();
let res = conn.execute(
"UPDATE concepts SET title = 'Updated Title', recorded_at = '2026-01-01T10:00:00.000000Z' WHERE id = 'c1'",
(),
).await;
assert!(res.is_err());
let err_str = res.err().unwrap().to_string();
assert!(
err_str.contains("strictly increasing"),
"Expected monotonic recorded_at error, got: {err_str}"
);
}
#[tokio::test]
async fn test_delete_guard_triggers() {
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', 'Title', '2026-01-01T00:00:00.000000Z', '2026-01-01T00:00:00.000000Z')",
(),
).await.unwrap();
let res = conn
.execute("DELETE FROM concepts WHERE id = 'c1'", ())
.await;
assert!(res.is_err());
let err_str = res.err().unwrap().to_string();
assert!(
err_str.contains("never physically archived"),
"Expected unconditional concepts delete guard, got: {err_str}"
);
conn.execute(
"INSERT INTO concepts (id, title, valid_from, recorded_at) VALUES ('c2', 'Title', '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();
for table in ["links", "transaction_log"] {
let res = conn.execute(&format!("DELETE FROM {table}"), ()).await;
assert!(res.is_err(), "{table} delete should be blocked");
let err_str = res.err().unwrap().to_string();
assert!(
err_str.contains("outside archive session"),
"Expected archive-session guard on {table}, got: {err_str}"
);
}
}
#[tokio::test]
async fn test_archive_session_marker_lifecycle() {
use libsql::TransactionBehavior;
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', 'N', '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', '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', 'KNOWS', '2026-01-01T00:00:00.000000Z', '2026-02-01T00:00:00.000000Z', 1.0, '{}', '2026-01-01T00:00:00.000000Z')",
(),
).await.unwrap();
assert!(conn.execute("DELETE FROM links", ()).await.is_err());
let tx = conn
.transaction_with_behavior(TransactionBehavior::Immediate)
.await
.unwrap();
tx.execute("CREATE TABLE macrame_archive_session (x)", ())
.await
.unwrap();
tx.execute("DELETE FROM links", ()).await.unwrap();
let db2 = libsql::Builder::new_local(&harness.db_path)
.build()
.await
.unwrap();
let conn2 = db2.connect().unwrap();
let seen: i64 = conn2
.query(
"SELECT COUNT(*) FROM sqlite_master WHERE name = 'macrame_archive_session'",
(),
)
.await
.unwrap()
.next()
.await
.unwrap()
.unwrap()
.get(0)
.unwrap();
assert_eq!(
seen, 0,
"uncommitted marker must not be visible to other connections"
);
tx.execute("DROP TABLE macrame_archive_session", ())
.await
.unwrap();
tx.commit().await.unwrap();
let after: i64 = conn
.query(
"SELECT COUNT(*) FROM sqlite_master WHERE name = 'macrame_archive_session'",
(),
)
.await
.unwrap()
.next()
.await
.unwrap()
.unwrap()
.get(0)
.unwrap();
assert_eq!(after, 0);
conn.execute(
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, weight, properties, recorded_at) \
VALUES ('c1', 'c2', 'KNOWS', '2026-03-01T00:00:00.000000Z', '9999-12-31T23:59:59.999999Z', 1.0, '{}', '2026-03-01T00:00:00.000000Z')",
(),
).await.unwrap();
assert!(conn.execute("DELETE FROM links", ()).await.is_err());
}
#[tokio::test]
async fn test_archive_session_marker_rollback_rearms_guard() {
use libsql::TransactionBehavior;
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', 'N', '2026-01-01T00:00:00.000000Z', '2026-01-01T00:00:00.000000Z')",
(),
).await.unwrap();
let tx = conn
.transaction_with_behavior(TransactionBehavior::Immediate)
.await
.unwrap();
tx.execute("CREATE TABLE macrame_archive_session (x)", ())
.await
.unwrap();
tx.rollback().await.unwrap();
let after: i64 = conn
.query(
"SELECT COUNT(*) FROM sqlite_master WHERE name = 'macrame_archive_session'",
(),
)
.await
.unwrap()
.next()
.await
.unwrap()
.unwrap()
.get(0)
.unwrap();
assert_eq!(after, 0, "rollback must discard the marker");
assert!(conn
.execute("DELETE FROM transaction_log", ())
.await
.is_err());
}
#[tokio::test]
async fn test_audit_and_rebuild_current() {
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 initial_drift = audit_current(&conn).await.unwrap();
assert_eq!(initial_drift, 0);
conn.execute(
"INSERT INTO links_current (source_id, target_id, edge_type, valid_from, valid_to, weight, properties, recorded_at) \
VALUES ('c1', 'c2', 'CORRUPT', '2026-01-01T00:00:00.000000Z', '9999-12-31T23:59:59.999999Z', 1.0, '{}', '2026-01-01T00:00:00.000000Z')",
(),
).await.unwrap();
let audit_res = audit_current(&conn).await;
assert!(audit_res.is_err());
if let Err(DbError::CurrentDrift { n }) = audit_res {
assert_eq!(n, 1);
} else {
panic!("Expected CurrentDrift error");
}
let report = rebuild_current(&conn).await.unwrap();
assert_eq!(report.drift_after, 0);
let post_drift = audit_current(&conn).await.unwrap();
assert_eq!(post_drift, 0);
}
#[tokio::test]
async fn test_audit_detects_drift_in_both_directions() {
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();
}
for (etype, ra) in [
("A", "2026-01-01T00:00:00.000000Z"),
("B", "2026-01-02T00:00:00.000000Z"),
] {
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', '9999-12-31T23:59:59.999999Z', 1.0, '{{}}', '{ra}')"),
(),
).await.unwrap();
}
assert_eq!(audit_current(&conn).await.unwrap(), 0);
let drift_count = |res: macrame::Result<usize>| match res {
Err(DbError::CurrentDrift { n }) => n,
other => panic!("expected CurrentDrift, got {other:?}"),
};
conn.execute("DELETE FROM links_current WHERE edge_type = 'A'", ())
.await
.unwrap();
assert_eq!(
drift_count(audit_current(&conn).await),
1,
"missed materialisation must be drift"
);
conn.execute(
"INSERT INTO links_current (source_id, target_id, edge_type, valid_from, valid_to, weight, properties, recorded_at) \
VALUES ('c1', 'c2', 'GHOST', '2026-01-01T00:00:00.000000Z', '9999-12-31T23:59:59.999999Z', 1.0, '{}', '2026-01-01T00:00:00.000000Z')",
(),
).await.unwrap();
assert_eq!(
drift_count(audit_current(&conn).await),
2,
"both directions must be summed"
);
conn.execute("DELETE FROM links_current WHERE edge_type = 'GHOST'", ())
.await
.unwrap();
conn.execute(
"UPDATE links_current SET weight = 99.0 WHERE edge_type = 'B'",
(),
)
.await
.unwrap();
assert_eq!(drift_count(audit_current(&conn).await), 3);
rebuild_current(&conn).await.unwrap();
assert_eq!(
audit_current(&conn).await.unwrap(),
0,
"rebuild must clear both directions"
);
}
#[tokio::test]
async fn test_non_canonical_timestamps_are_rejected_at_write_time() {
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 bad in [
"2026-01-01T00:00:00Z", "2026-01-01T00:00:00.000Z", "2026-01-01T00:00:00.000000", "2026-01-01T00:00:00.000000+01:00", "2026-01-01 00:00:00.000000Z", "not-a-timestamp",
] {
let res = conn
.execute(
"INSERT INTO concepts (id, title, valid_from, recorded_at) VALUES ('x', 'N', ?1, '2026-01-01T00:00:00.000000Z')",
libsql::params![bad],
)
.await;
assert!(res.is_err(), "CHECK must reject valid_from = {bad:?}");
}
conn.execute(
"INSERT INTO concepts (id, title, valid_from, valid_to, recorded_at) \
VALUES ('ok', 'N', '2026-01-01T00:00:00.000000Z', '9999-12-31T23:59:59.999999Z', '2026-01-01T00:00:00.000000Z')",
(),
)
.await
.expect("canonical timestamps must be accepted");
conn.execute(
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, weight, properties, recorded_at) \
VALUES ('ok', 'ok', 'SELF', '2026-01-01T00:00:00Z', '9999-12-31T23:59:59.999999Z', 1.0, '{}', '2026-01-01T00:00:00.000000Z')",
(),
)
.await
.expect_err("links.valid_from must reject second precision");
}
#[tokio::test]
async fn test_valid_time_predicate_matches_edge_at_same_instant() {
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();
let ts = "2026-01-01T00:00:00.000000Z";
for id in ["c1", "c2"] {
conn.execute(
&format!("INSERT INTO concepts (id, title, valid_from, recorded_at) VALUES ('{id}', 'N', '{ts}', '{ts}')"),
(),
).await.unwrap();
}
conn.execute(
&format!("INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, weight, properties, recorded_at) \
VALUES ('c1', 'c2', 'KNOWS', '{ts}', '9999-12-31T23:59:59.999999Z', 1.0, '{{}}', '{ts}')"),
(),
).await.unwrap();
let edges = macrame::temporal::query_as_of_edges(&conn, ts)
.await
.unwrap();
assert_eq!(edges.len(), 1, "edge must be live at its own valid_from");
}
#[tokio::test]
async fn an_archive_leaves_no_drift_although_it_no_longer_checks_itself() {
let harness = TestHarness::new();
let conn = libsql::Builder::new_local(&harness.db_path)
.build()
.await
.unwrap()
.connect()
.unwrap();
migrations::run(&conn).await.unwrap();
let t0 = "2026-01-01T00:00:00.000000Z";
let t1 = "2026-02-01T00:00:00.000000Z";
let t2 = "2026-03-01T00:00:00.000000Z";
let open = "9999-12-31T23:59:59.999999Z";
for id in ["a", "b", "c"] {
conn.execute(
"INSERT INTO concepts (id, title, valid_from, recorded_at) VALUES (?1, 'n', ?2, ?2)",
libsql::params![id, t0],
)
.await
.unwrap();
}
let rows: [(&str, &str, &str, &str, &str); 3] = [
("a", "b", t0, open, t0),
("a", "b", t0, open, t1),
("b", "c", t0, t1, t0),
];
for (src, tgt, vf, vt, rec) in rows {
conn.execute(
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, \
weight, properties, recorded_at) VALUES (?1, ?2, 'KNOWS', ?3, ?4, 1.0, '{}', ?5)",
libsql::params![src, tgt, vf, vt, rec],
)
.await
.unwrap();
}
assert_eq!(
audit_current(&conn).await.unwrap(),
0,
"clean before archive"
);
let cold = harness.temp_dir.path().join("cold.db");
let report = macrame::temporal::archive(&conn, t2, t2, &cold)
.await
.unwrap();
assert!(
report.links_archived > 0,
"the fixture must actually archive something, or this proves nothing"
);
assert_eq!(
audit_current(&conn).await.unwrap(),
0,
"archive left links_current drifted from the projection"
);
}