#[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(macrame::schema::ddl::ABORT_DELETE_GUARD),
"Expected the marker-gated concepts delete guard, got: {err_str}"
);
assert!(
!err_str.contains("never physically archived"),
"The v8 guard body survived into v9 — a re-issued baseline keeps the old \
body and only the rung replaces it (D-126). 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"
);
}
const W2_T0: &str = "2026-01-01T00:00:00.000000Z";
const W2_T1: &str = "2026-02-01T00:00:00.000000Z";
const W2_CUTOFF: &str = "2099-01-01T00:00:00.000000Z";
async fn w2_seeded(path: &std::path::Path) -> macrame::Database {
let db = macrame::Database::open(path).await.unwrap();
for id in ["c1", "c2"] {
db.upsert_concept(macrame::ConceptUpsert::new(id, "N").valid_from(W2_T0))
.await
.unwrap();
}
db.assert_edge(
macrame::graph::EdgeAssertion::new("c1", "c2", "KNOWS")
.valid_from(W2_T0)
.valid_to(W2_T1),
)
.await
.unwrap();
db
}
async fn w2_leak_marker(db: ¯ame::Database) {
db.raw()
.connect()
.unwrap()
.execute("CREATE TABLE macrame_archive_session (x)", ())
.await
.unwrap();
}
async fn w2_open_err(path: &std::path::Path) -> DbError {
match macrame::Database::open(path).await {
Ok(_) => panic!("open must be refused while the marker is present"),
Err(e) => e,
}
}
#[tokio::test]
async fn a_leaked_archive_session_marker_is_refused_at_open() {
let harness = TestHarness::new();
let db = macrame::Database::open(&harness.db_path).await.unwrap();
w2_leak_marker(&db).await;
db.close().await.unwrap();
let err = w2_open_err(&harness.db_path).await;
assert!(
matches!(err, DbError::ArchiveSessionLeaked { ref marker }
if marker == "macrame_archive_session"),
"expected ArchiveSessionLeaked, got {err:?}"
);
}
#[tokio::test]
async fn the_marker_check_names_the_remedy() {
let harness = TestHarness::new();
let db = macrame::Database::open(&harness.db_path).await.unwrap();
w2_leak_marker(&db).await;
db.close().await.unwrap();
let msg = w2_open_err(&harness.db_path).await.to_string();
assert!(
msg.contains("DROP TABLE macrame_archive_session"),
"the message must carry the runnable remedy, got: {msg}"
);
assert!(
msg.contains("audit"),
"the message must say the damage needs auditing, got: {msg}"
);
}
#[tokio::test]
async fn a_normal_archive_leaves_no_marker_and_reopens_clean() {
let harness = TestHarness::new();
let db = w2_seeded(&harness.db_path).await;
let report = db.archive(W2_CUTOFF).await.unwrap();
assert!(
report.links_archived > 0,
"the archive must actually run, or the control proves nothing about a \
path that opened a session"
);
db.close().await.unwrap();
macrame::Database::open(&harness.db_path)
.await
.expect("a database that has completed an archive must still open")
.close()
.await
.unwrap();
}
#[tokio::test]
async fn an_aborted_archive_still_reopens_under_the_marker_check() {
let harness = TestHarness::new();
let cold_path = harness.temp_dir.path().join("test_macrame_archive.db");
{
let cold = libsql::Builder::new_local(&cold_path).build().await.unwrap();
cold.connect()
.unwrap()
.execute("CREATE TABLE links (source_id TEXT)", ())
.await
.unwrap();
}
let db = w2_seeded(&harness.db_path).await;
let err = db
.archive(W2_CUTOFF)
.await
.expect_err("the rigged cold file must abort the session");
let msg = format!("{err}");
assert!(
msg.contains("target_id"),
"the abort must come from the rigged cold table, i.e. from *after* the \
marker was created — got: {msg}"
);
db.close().await.unwrap();
macrame::Database::open(&harness.db_path)
.await
.expect("an aborted archive must not leave a database that refuses to open")
.close()
.await
.unwrap();
}