pub mod replay;
pub mod storage;
pub use storage::*;
#[cfg(test)]
mod tests {
use super::*;
use crate::db;
use rusqlite::{Connection, params};
fn test_db() -> Connection {
db::open(std::path::Path::new(":memory:")).unwrap()
}
fn chat_corpus() -> String {
let mut s = String::new();
let header = "[system] You are an assistant operating on the alphaonedev/ai-memory-mcp codebase. Always cite tool ids in your replies. Always cite tool ids in your replies.\n";
let user = "[user] What did we decide about the v0.7 attested-cortex epic last sprint? Please include the full transcript of the relevant meeting.\n";
let assistant = "[assistant] Per the v0.7 epic doc, the attested-cortex track adds a memory_transcripts table backed by zstd-3 compressed BLOBs. The decision was logged in the meeting transcript on 2026-04-12 at 14:33 UTC.\n";
let tool_call = "[tool_call name=\"memory_recall\" args={\"query\":\"v0.7 attested cortex\",\"limit\":10,\"namespace\":\"team/eng/memory\"}]\n";
let tool_result = "[tool_result name=\"memory_recall\" ok=true count=10 latency_ms=42]\n";
for _ in 0..16 {
s.push_str(header);
s.push_str(user);
s.push_str(assistant);
s.push_str(tool_call);
s.push_str(tool_result);
}
debug_assert!(s.len() >= 5_000, "corpus too small: {}", s.len());
s
}
#[test]
fn migration_is_idempotent() {
let p = tempfile::NamedTempFile::new().unwrap();
let path = p.path().to_path_buf();
let _ = db::open(&path).unwrap();
let conn = db::open(&path).unwrap();
let cnt: i64 = conn
.query_row("SELECT count(*) FROM memory_transcripts", [], |r| r.get(0))
.unwrap();
assert_eq!(cnt, 0);
}
#[test]
fn round_trip_returns_original_content() {
let conn = test_db();
let body = chat_corpus();
let handle = store(&conn, "team/eng", &body, None).unwrap();
let got = fetch(&conn, &handle.id).unwrap();
assert_eq!(got.as_deref(), Some(body.as_str()));
}
#[test]
fn fetch_missing_id_returns_none() {
let conn = test_db();
let got = fetch(&conn, "not-a-real-uuid").unwrap();
assert!(got.is_none());
}
#[test]
fn compression_ratio_at_least_5x_on_chat_corpus() {
let conn = test_db();
let body = chat_corpus();
let handle = store(&conn, "team/eng", &body, None).unwrap();
let ratio = handle.original_size as f64 / handle.compressed_size as f64;
assert!(
ratio >= 5.0,
"expected >=5x zstd-3 ratio on chat-shaped text, got {ratio:.2}x \
(orig={} compressed={})",
handle.original_size,
handle.compressed_size,
);
assert_eq!(handle.original_size, body.len() as i64);
}
#[test]
fn namespace_created_index_exists() {
let conn = test_db();
let mut stmt = conn
.prepare("PRAGMA index_list('memory_transcripts')")
.unwrap();
let names: Vec<String> = stmt
.query_map([], |r| r.get::<_, String>(1))
.unwrap()
.map(std::result::Result::unwrap)
.collect();
assert!(
names
.iter()
.any(|n| n == "idx_memory_transcripts_namespace_created"),
"expected idx_memory_transcripts_namespace_created in {names:?}"
);
}
fn insert_test_memory(conn: &Connection, id: &str) {
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"INSERT INTO memories (
id, tier, namespace, title, content, created_at, updated_at
) VALUES (?1, 'short', 'team/eng', ?2, 'body', ?3, ?3)",
params![id, format!("title-{id}"), now],
)
.unwrap();
}
#[test]
fn i2_link_then_transcripts_for_memory_round_trip() {
let conn = test_db();
insert_test_memory(&conn, "mem-1");
let t = store(&conn, "team/eng", "abcdefghij", None).unwrap();
link_transcript(&conn, "mem-1", &t.id, Some(2), Some(7)).unwrap();
let got = transcripts_for_memory(&conn, "mem-1").unwrap();
assert_eq!(
got,
vec![TranscriptLink {
memory_id: "mem-1".into(),
transcript_id: t.id.clone(),
span_start: Some(2),
span_end: Some(7),
}],
);
}
#[test]
fn i2_memories_for_transcript_returns_all_linked_memories() {
let conn = test_db();
insert_test_memory(&conn, "mem-a");
insert_test_memory(&conn, "mem-b");
insert_test_memory(&conn, "mem-c");
let t = store(&conn, "team/eng", "shared transcript body", None).unwrap();
link_transcript(&conn, "mem-a", &t.id, None, None).unwrap();
link_transcript(&conn, "mem-b", &t.id, Some(0), Some(10)).unwrap();
link_transcript(&conn, "mem-c", &t.id, Some(11), Some(22)).unwrap();
let got = memories_for_transcript(&conn, &t.id).unwrap();
let ids: Vec<&str> = got.iter().map(|l| l.memory_id.as_str()).collect();
assert_eq!(ids, vec!["mem-a", "mem-b", "mem-c"]);
}
#[test]
fn i2_null_spans_round_trip_as_none() {
let conn = test_db();
insert_test_memory(&conn, "mem-null");
let t = store(&conn, "team/eng", "body", None).unwrap();
link_transcript(&conn, "mem-null", &t.id, None, None).unwrap();
let got = transcripts_for_memory(&conn, "mem-null").unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0].span_start, None);
assert_eq!(got[0].span_end, None);
}
#[test]
fn i2_delete_memory_cascades_to_links() {
let conn = test_db();
insert_test_memory(&conn, "mem-doomed");
insert_test_memory(&conn, "mem-survives");
let t = store(&conn, "team/eng", "body", None).unwrap();
link_transcript(&conn, "mem-doomed", &t.id, None, None).unwrap();
link_transcript(&conn, "mem-survives", &t.id, None, None).unwrap();
assert_eq!(memories_for_transcript(&conn, &t.id).unwrap().len(), 2);
conn.execute("DELETE FROM memories WHERE id = ?1", params!["mem-doomed"])
.unwrap();
let remaining = memories_for_transcript(&conn, &t.id).unwrap();
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].memory_id, "mem-survives");
}
#[test]
fn i2_delete_transcript_cascades_to_links() {
let conn = test_db();
insert_test_memory(&conn, "mem-x");
let t = store(&conn, "team/eng", "ephemeral", None).unwrap();
link_transcript(&conn, "mem-x", &t.id, None, None).unwrap();
assert_eq!(transcripts_for_memory(&conn, "mem-x").unwrap().len(), 1);
conn.execute(
"DELETE FROM memory_transcripts WHERE id = ?1",
params![t.id],
)
.unwrap();
assert!(transcripts_for_memory(&conn, "mem-x").unwrap().is_empty());
}
#[test]
fn i2_migration_is_idempotent() {
let p = tempfile::NamedTempFile::new().unwrap();
let path = p.path().to_path_buf();
let _ = db::open(&path).unwrap();
let conn = db::open(&path).unwrap();
let cnt: i64 = conn
.query_row("SELECT count(*) FROM memory_transcript_links", [], |r| {
r.get(0)
})
.unwrap();
assert_eq!(cnt, 0);
}
#[test]
fn i2_join_table_indexes_exist() {
let conn = test_db();
let mut stmt = conn
.prepare("PRAGMA index_list('memory_transcript_links')")
.unwrap();
let names: Vec<String> = stmt
.query_map([], |r| r.get::<_, String>(1))
.unwrap()
.map(std::result::Result::unwrap)
.collect();
for expected in ["idx_mtl_transcript", "idx_mtl_memory"] {
assert!(
names.iter().any(|n| n == expected),
"expected {expected} in {names:?}"
);
}
}
#[test]
fn purge_expired_only_removes_past_due_rows() {
let conn = test_db();
let expired = store(
&conn,
"team/eng",
"expired body",
Some(chrono::Duration::seconds(-crate::SECS_PER_HOUR)),
)
.unwrap();
let live = store(
&conn,
"team/eng",
"live body",
Some(chrono::Duration::seconds(crate::SECS_PER_HOUR)),
)
.unwrap();
let immortal = store(&conn, "team/eng", "immortal body", None).unwrap();
let n = purge_expired(&conn).unwrap();
assert_eq!(n, 1, "exactly the past-due row should be deleted");
assert!(fetch(&conn, &expired.id).unwrap().is_none());
assert_eq!(
fetch(&conn, &live.id).unwrap().as_deref(),
Some("live body"),
);
assert_eq!(
fetch(&conn, &immortal.id).unwrap().as_deref(),
Some("immortal body"),
);
}
use crate::config::{TranscriptNamespaceConfig, TranscriptsConfig};
use std::collections::HashMap;
fn backdate_created(conn: &Connection, id: &str, secs: i64) -> String {
let stamp = (chrono::Utc::now() - chrono::Duration::seconds(secs)).to_rfc3339();
conn.execute(
"UPDATE memory_transcripts SET created_at = ?1 WHERE id = ?2",
params![stamp, id],
)
.unwrap();
stamp
}
fn backdate_archived(conn: &Connection, id: &str, secs: i64) -> String {
let stamp = (chrono::Utc::now() - chrono::Duration::seconds(secs)).to_rfc3339();
conn.execute(
"UPDATE memory_transcripts SET archived_at = ?1 WHERE id = ?2",
params![stamp, id],
)
.unwrap();
stamp
}
fn fast_cfg() -> TranscriptsConfig {
TranscriptsConfig {
default_ttl_secs: Some(crate::SECS_PER_HOUR),
archive_grace_secs: Some(crate::SECS_PER_HOUR),
namespaces: None,
max_decompressed_bytes: None,
}
}
fn archived_at(conn: &Connection, id: &str) -> Option<String> {
conn.query_row(
"SELECT archived_at FROM memory_transcripts WHERE id = ?1",
params![id],
|r| r.get::<_, Option<String>>(0),
)
.unwrap()
}
fn row_exists(conn: &Connection, id: &str) -> bool {
let n: i64 = conn
.query_row(
"SELECT COUNT(*) FROM memory_transcripts WHERE id = ?1",
params![id],
|r| r.get(0),
)
.unwrap();
n > 0
}
#[test]
fn i3_unlinked_aged_transcript_is_archived() {
let conn = test_db();
let cfg = fast_cfg();
let t = store(&conn, "team/eng", "old body", None).unwrap();
backdate_created(&conn, &t.id, 7200);
let report = sweep_transcript_lifecycle(&conn, &cfg).unwrap();
assert_eq!(report.archived, 1, "phase 1 must archive the aged row");
assert_eq!(
report.pruned, 0,
"phase 2 must not fire on a freshly archived row"
);
assert!(
archived_at(&conn, &t.id).is_some(),
"archived_at must be set after phase 1",
);
}
#[test]
fn i3_archived_past_grace_is_pruned_with_cascade() {
let conn = test_db();
let cfg = fast_cfg();
insert_test_memory(&conn, "mem-cascade");
let t = store(&conn, "team/eng", "to be pruned", None).unwrap();
link_transcript(&conn, "mem-cascade", &t.id, None, None).unwrap();
backdate_archived(&conn, &t.id, 7200);
let report = sweep_transcript_lifecycle(&conn, &cfg).unwrap();
assert_eq!(report.pruned, 1, "phase 2 must hard-DELETE the row");
assert!(!row_exists(&conn, &t.id), "transcript row gone");
assert!(
transcripts_for_memory(&conn, "mem-cascade")
.unwrap()
.is_empty(),
"ON DELETE CASCADE must clear the I2 join row",
);
}
#[test]
fn i3_live_linked_memory_keeps_transcript_alive() {
let conn = test_db();
let cfg = fast_cfg();
insert_test_memory(&conn, "mem-immortal");
let t = store(&conn, "team/eng", "still wanted", None).unwrap();
link_transcript(&conn, "mem-immortal", &t.id, None, None).unwrap();
backdate_created(&conn, &t.id, 7200);
let report = sweep_transcript_lifecycle(&conn, &cfg).unwrap();
assert_eq!(report.archived, 0);
assert!(
archived_at(&conn, &t.id).is_none(),
"live linked memory must keep archived_at NULL",
);
}
#[test]
fn i3_all_linked_memories_expired_then_transcript_is_archived() {
let conn = test_db();
let cfg = fast_cfg();
insert_test_memory(&conn, "mem-expired");
let past = (chrono::Utc::now() - chrono::Duration::seconds(60)).to_rfc3339();
conn.execute(
"UPDATE memories SET expires_at = ?1 WHERE id = 'mem-expired'",
params![past],
)
.unwrap();
let t = store(&conn, "team/eng", "no longer needed", None).unwrap();
link_transcript(&conn, "mem-expired", &t.id, None, None).unwrap();
backdate_created(&conn, &t.id, 7200);
let report = sweep_transcript_lifecycle(&conn, &cfg).unwrap();
assert_eq!(report.archived, 1);
assert!(archived_at(&conn, &t.id).is_some());
}
#[test]
fn i3_per_namespace_override_extends_ttl_beyond_global_default() {
let conn = test_db();
let mut ns_table = HashMap::new();
ns_table.insert(
"team/audit".to_string(),
TranscriptNamespaceConfig {
default_ttl_secs: Some(crate::SECS_PER_DAY),
archive_grace_secs: None,
auto_extract: None,
},
);
let cfg = TranscriptsConfig {
default_ttl_secs: Some(crate::SECS_PER_HOUR),
archive_grace_secs: Some(crate::SECS_PER_HOUR),
namespaces: Some(ns_table),
max_decompressed_bytes: None,
};
let eng = store(&conn, "team/eng", "eng body", None).unwrap();
backdate_created(&conn, &eng.id, 7200);
let audit = store(&conn, "team/audit", "audit body", None).unwrap();
backdate_created(&conn, &audit.id, 7200);
let report = sweep_transcript_lifecycle(&conn, &cfg).unwrap();
assert_eq!(report.archived, 1, "only team/eng is past the resolved TTL");
assert!(archived_at(&conn, &eng.id).is_some());
assert!(
archived_at(&conn, &audit.id).is_none(),
"team/audit override (1d) keeps the audit row live",
);
}
#[test]
fn i3_prefix_pattern_override_matches_child_namespaces() {
let conn = test_db();
let mut ns_table = HashMap::new();
ns_table.insert(
"ephemeral/*".to_string(),
TranscriptNamespaceConfig {
default_ttl_secs: Some(60),
archive_grace_secs: Some(60),
auto_extract: None,
},
);
let cfg = TranscriptsConfig {
default_ttl_secs: Some(crate::SECS_PER_DAY * 30), archive_grace_secs: Some(crate::SECS_PER_WEEK),
namespaces: Some(ns_table),
max_decompressed_bytes: None,
};
let t = store(&conn, "ephemeral/scratch", "scratch", None).unwrap();
backdate_created(&conn, &t.id, 300);
let report = sweep_transcript_lifecycle(&conn, &cfg).unwrap();
assert_eq!(
report.archived, 1,
"prefix pattern must apply to ephemeral/scratch"
);
}
#[test]
fn i3_archived_within_grace_is_not_pruned() {
let conn = test_db();
let cfg = fast_cfg();
let t = store(&conn, "team/eng", "still in grace", None).unwrap();
backdate_archived(&conn, &t.id, 1800);
let report = sweep_transcript_lifecycle(&conn, &cfg).unwrap();
assert_eq!(report.pruned, 0);
assert!(row_exists(&conn, &t.id));
}
#[test]
fn i3_archive_then_prune_in_two_sweeps() {
let conn = test_db();
let cfg = fast_cfg();
let t = store(&conn, "team/eng", "lifecycle e2e", None).unwrap();
backdate_created(&conn, &t.id, 7200);
let r1 = sweep_transcript_lifecycle(&conn, &cfg).unwrap();
assert_eq!(r1.archived, 1);
assert_eq!(r1.pruned, 0);
backdate_archived(&conn, &t.id, 7200);
let r2 = sweep_transcript_lifecycle(&conn, &cfg).unwrap();
assert_eq!(r2.archived, 0);
assert_eq!(r2.pruned, 1);
assert!(!row_exists(&conn, &t.id));
}
#[test]
fn i3_idempotent_phase1_does_not_restamp_archived_rows() {
let conn = test_db();
let cfg = fast_cfg();
let t = store(&conn, "team/eng", "already archived", None).unwrap();
backdate_created(&conn, &t.id, 7200);
let _ = sweep_transcript_lifecycle(&conn, &cfg).unwrap();
let first_stamp = archived_at(&conn, &t.id).unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
let r2 = sweep_transcript_lifecycle(&conn, &cfg).unwrap();
assert_eq!(r2.archived, 0, "no row should be re-archived");
let second_stamp = archived_at(&conn, &t.id).unwrap();
assert_eq!(
first_stamp, second_stamp,
"archived_at must be preserved across sweeps",
);
}
}