#[path = "common/harness.rs"]
mod harness;
use std::collections::BTreeSet;
use harness::TestHarness;
use macrame::integrity::{audit_current, rebuild_current};
use macrame::schema::{ddl, migrations};
const TS: &str = "2026-01-01T00:00:00.000000Z";
const SENTINEL: &str = "9999-12-31T23:59:59.999999Z";
async fn seeded(harness: &TestHarness) -> (libsql::Database, libsql::Connection) {
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 ["c0", "c1"] {
conn.execute(
"INSERT INTO concepts (id, title, valid_from, recorded_at) VALUES (?1, 'N', ?2, ?2)",
libsql::params![id, TS],
)
.await
.unwrap();
}
for (etype, vt, ra) in [
("A", SENTINEL, TS),
("B", "2026-03-01T00:00:00.000000Z", TS),
(
"B",
"2026-04-01T00:00:00.000000Z",
"2026-02-01T00:00:00.000000Z",
),
] {
conn.execute(
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, \
weight, properties, recorded_at) VALUES ('c0','c1',?1,?2,?3,1.0,'{}',?4)",
libsql::params![etype, TS, vt, ra],
)
.await
.unwrap();
}
(db, conn)
}
async fn shape(conn: &libsql::Connection, table: &str) -> Vec<(String, String, i64, i64)> {
let mut rows = conn
.query(&format!("PRAGMA table_info({table})"), ())
.await
.unwrap();
let mut out = Vec::new();
while let Some(r) = rows.next().await.unwrap() {
out.push((
r.get(1).unwrap(),
r.get(2).unwrap(),
r.get(3).unwrap(),
r.get(5).unwrap(),
));
}
out
}
#[tokio::test]
async fn the_ledger_tables_have_the_shape_the_contract_freezes() {
let harness = TestHarness::new();
let (_db, conn) = seeded(&harness).await;
assert_eq!(
shape(&conn, "links").await,
vec![
("source_id".into(), "TEXT".into(), 1, 1),
("target_id".into(), "TEXT".into(), 1, 2),
("edge_type".into(), "TEXT".into(), 1, 3),
("valid_from".into(), "TEXT".into(), 1, 4),
("recorded_at".into(), "TEXT".into(), 1, 5),
("valid_to".into(), "TEXT".into(), 1, 0),
("weight".into(), "REAL".into(), 1, 0),
("properties".into(), "TEXT".into(), 1, 0),
],
"links is a frozen ledger table (D-003: the 5-column bitemporal PK)"
);
assert_eq!(
shape(&conn, "concepts").await,
vec![
("id".into(), "TEXT".into(), 0, 1),
("title".into(), "TEXT".into(), 1, 0),
("content".into(), "TEXT".into(), 1, 0),
("embedding_model".into(), "TEXT".into(), 0, 0),
("valid_from".into(), "TEXT".into(), 1, 0),
("valid_to".into(), "TEXT".into(), 1, 0),
("recorded_at".into(), "TEXT".into(), 1, 0),
("retired".into(), "INTEGER".into(), 1, 0),
],
"concepts is a frozen ledger table"
);
assert_eq!(
shape(&conn, "transaction_log").await,
vec![
("seq_id".into(), "INTEGER".into(), 0, 1),
("table_name".into(), "TEXT".into(), 1, 0),
("entity_id".into(), "TEXT".into(), 1, 0),
("operation".into(), "TEXT".into(), 1, 0),
("payload".into(), "TEXT".into(), 1, 0),
("recorded_at".into(), "TEXT".into(), 1, 0),
],
"transaction_log is a frozen ledger table (D-002)"
);
}
#[tokio::test]
async fn an_added_temporal_column_can_still_carry_its_canonical_check() {
let harness = TestHarness::new();
let (_db, conn) = seeded(&harness).await;
conn.execute(
&format!(
"ALTER TABLE links ADD COLUMN asserted_at TEXT NOT NULL DEFAULT '{TS}' \
CHECK (asserted_at GLOB {})",
macrame::util::timestamp::CANONICAL_TS_GLOB
),
(),
)
.await
.expect("ADD COLUMN must accept a column-level CHECK");
let triggers: i64 = conn
.query(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'trigger'",
(),
)
.await
.unwrap()
.next()
.await
.unwrap()
.unwrap()
.get(0)
.unwrap();
assert_eq!(
triggers as usize,
ddl::CREATE_TRIGGERS.len(),
"ADD COLUMN must not disturb the guards"
);
let bad = conn
.execute(
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, weight, \
properties, recorded_at, asserted_at) \
VALUES ('c0','c1','C',?1,?2,1.0,'{}',?1,'2026-01-01T00:00:00Z')",
libsql::params![TS, SENTINEL],
)
.await;
assert!(
bad.is_err(),
"the added column's CHECK must reject non-canonical timestamps"
);
}
#[tokio::test]
async fn dropping_and_rebuilding_the_materialization_loses_nothing() {
let harness = TestHarness::new();
let (_db, conn) = seeded(&harness).await;
let row = "source_id||'|'||target_id||'|'||edge_type||'|'||valid_from||'|'||valid_to\
||'|'||weight||'|'||properties||'|'||recorded_at";
let capture = |c: &libsql::Connection| {
let c = c.clone();
async move {
let mut rows = c
.query(&format!("SELECT {row} FROM links_current"), ())
.await
.unwrap();
let mut out = BTreeSet::new();
while let Some(r) = rows.next().await.unwrap() {
out.insert(r.get::<String>(0).unwrap());
}
out
}
};
let before = capture(&conn).await;
assert!(
!before.is_empty(),
"the fixture must have something to lose"
);
assert_eq!(audit_current(&conn).await.unwrap(), 0);
conn.execute("DROP TABLE links_current", ()).await.unwrap();
conn.execute(ddl::CREATE_LINKS_CURRENT_TABLE, ())
.await
.unwrap();
assert!(capture(&conn).await.is_empty());
let report = rebuild_current(&conn).await.unwrap();
assert_eq!(capture(&conn).await, before, "the rebuild must be lossless");
assert_eq!(report.rows_rebuilt, before.len());
assert_eq!(report.drift_after, 0);
assert_eq!(audit_current(&conn).await.unwrap(), 0);
}
#[tokio::test]
async fn no_guard_is_attached_to_the_disposable_table() {
let harness = TestHarness::new();
let (_db, conn) = seeded(&harness).await;
let mut rows = conn
.query(
"SELECT name FROM sqlite_master WHERE type = 'trigger' AND tbl_name = 'links_current'",
(),
)
.await
.unwrap();
let mut attached = Vec::new();
while let Some(r) = rows.next().await.unwrap() {
attached.push(r.get::<String>(0).unwrap());
}
assert!(
attached.is_empty(),
"triggers on the disposable table would not survive a migration DROP: {attached:?}"
);
}