use rusqlite::{params, Connection};
fn migrated_conn() -> Connection {
let conn = Connection::open_in_memory().unwrap();
crate::migrate::run_migrations(&conn).unwrap();
crate::retrieval::vector::ensure_vec_table(&conn).unwrap();
conn
}
fn fts_matches(conn: &Connection, term: &str) -> i64 {
conn.query_row(
"SELECT COUNT(*) FROM memories_fts WHERE memories_fts MATCH ?1",
params![term],
|row| row.get(0),
)
.unwrap()
}
fn assert_fts_integrity_clean(conn: &Connection) {
conn.execute(
"INSERT INTO memories_fts(memories_fts, rank) VALUES ('integrity-check', 1)",
[],
)
.expect("memories_fts must stay consistent with the memories content table");
}
#[test]
fn v083_defers_historical_backlog_preserves_ready_rows_and_corrects_credit_pricing() {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
for migration in crate::migrate::MIGRATIONS
.iter()
.filter(|migration| migration.version <= 82)
{
conn.execute_batch(migration.sql).unwrap();
}
conn.execute(
"INSERT INTO memories (id, project, title, content, memory_type,
created_at_epoch, updated_at_epoch, status)
VALUES (1, 'proj', 'Deferred', 'historical body', 'decision', 1, 1, 'active'),
(2, 'proj', 'Ready', 'enriched body', 'decision', 1, 1, 'active')",
[],
)
.unwrap();
conn.execute(
"UPDATE memories SET
search_context = 'context: existing enrichment',
search_context_enrichment_version = 1,
search_context_security_policy_version = 1,
search_context_source_hash = 'existing-source',
search_context_fallback_source_hash = 'existing-source'
WHERE id = 2",
[],
)
.unwrap();
conn.execute(
"INSERT INTO ai_usage_events
(created_at, created_at_epoch, project, operation, executor, model,
input_tokens, output_tokens, total_tokens, estimated_cost_usd, pricing_source)
VALUES ('2026-08-10T11:03:09Z', 1786359789, 'proj',
'retrieval_enrichment', 'codex-cli', 'gpt-5.6-luna',
1000, 100, 1100, 12.5, 'remem_static'),
('2026-08-10T11:04:09Z', 1786359849, 'proj',
'retrieval_enrichment', 'codex-cli', 'gpt-5.6-luna',
1000, 100, 1100, 9.0, 'env_override')",
[],
)
.unwrap();
let migration = crate::migrate::MIGRATIONS
.iter()
.find(|migration| migration.version == 83)
.unwrap();
conn.execute_batch(migration.sql).unwrap();
let states: Vec<(i64, String)> = conn
.prepare("SELECT id, search_context_enrichment_state FROM memories ORDER BY id")
.unwrap()
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
.unwrap()
.map(Result::unwrap)
.collect();
assert_eq!(
states,
vec![(1, "deferred".to_string()), (2, "ready".to_string())]
);
conn.execute(
"INSERT INTO memories (id, project, title, content, memory_type,
created_at_epoch, updated_at_epoch, status)
VALUES (3, 'proj', 'New', 'new body', 'decision', 2, 2, 'active')",
[],
)
.unwrap();
let new_state: String = conn
.query_row(
"SELECT search_context_enrichment_state FROM memories WHERE id = 3",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(new_state, "pending");
conn.execute(
"UPDATE memories SET content = 'changed historical body' WHERE id = 1",
[],
)
.unwrap();
let changed_state: String = conn
.query_row(
"SELECT search_context_enrichment_state FROM memories WHERE id = 1",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(changed_state, "pending");
let usage: Vec<(f64, String)> = conn
.prepare("SELECT estimated_cost_usd, pricing_source FROM ai_usage_events ORDER BY id")
.unwrap()
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
.unwrap()
.map(Result::unwrap)
.collect();
assert_eq!(usage[0], (0.0, "unknown_pricing".to_string()));
assert_eq!(usage[1], (9.0, "env_override".to_string()));
}
#[test]
fn migration_adds_pending_defaults_and_ready_singleton() {
let conn = migrated_conn();
conn.execute(
"INSERT INTO memories (id, project, title, content, memory_type,
created_at_epoch, updated_at_epoch, status)
VALUES (1, 'proj', 'T', 'body', 'decision', 1, 1, 'active')",
[],
)
.unwrap();
let (version, policy, attempt, failures): (i64, i64, i64, i64) = conn
.query_row(
"SELECT search_context_enrichment_version, search_context_security_policy_version,
search_context_enrichment_attempt, search_context_failure_count
FROM memories WHERE id = 1",
[],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
)
.unwrap();
assert_eq!((version, policy, attempt, failures), (0, 0, 0, 0));
let (floor, epoch, target, state): (i64, i64, i64, String) = conn
.query_row(
"SELECT min_security_policy_version, compatibility_epoch,
target_security_policy_version, convergence_state
FROM retrieval_enrichment_compatibility WHERE id = 1",
[],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
)
.unwrap();
assert_eq!((floor, epoch, target, state.as_str()), (1, 1, 1, "ready"));
}
#[test]
fn raw_canonical_update_invalidates_enrichment_and_vectors() {
let conn = migrated_conn();
conn.execute(
"INSERT INTO memories (id, project, title, content, memory_type,
created_at_epoch, updated_at_epoch, status)
VALUES (1, 'proj', 'T', 'old canonical body', 'decision', 1, 1, 'active')",
[],
)
.unwrap();
conn.execute(
"UPDATE memories SET
search_context = 'context: recall helper\nkeywords: zebrafishterm',
search_context_enrichment_version = 1,
search_context_security_policy_version = 1,
search_context_source_hash = 'srchash',
search_context_fallback_source_hash = 'srchash',
search_context_index_hash = 'idxhash'
WHERE id = 1",
[],
)
.unwrap();
conn.execute(
"INSERT INTO memory_embeddings
(memory_id, embedding, dimensions, model, content_hash, updated_at_epoch)
VALUES (1, x'0000803f', 1, 'test-model', 'idxhash', 1)",
[],
)
.unwrap();
assert_eq!(fts_matches(&conn, "zebrafishterm"), 1);
conn.execute(
"UPDATE memories SET content = 'new canonical wording' WHERE id = 1",
[],
)
.unwrap();
let (search_context, version, policy, source, fallback, index, vectors): (
String,
i64,
i64,
Option<String>,
Option<String>,
Option<String>,
i64,
) = conn
.query_row(
"SELECT search_context, search_context_enrichment_version,
search_context_security_policy_version, search_context_source_hash,
search_context_fallback_source_hash, search_context_index_hash,
(SELECT COUNT(*) FROM memory_embeddings WHERE memory_id = 1)
FROM memories WHERE id = 1",
[],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
row.get(5)?,
row.get(6)?,
))
},
)
.unwrap();
assert_eq!(
search_context, "",
"empty fallback must be persisted on the row"
);
assert_eq!(version, 0);
assert_eq!(policy, 1, "policy resets to the DB floor");
assert!(source.is_none() && fallback.is_none() && index.is_none());
assert_eq!(vectors, 0, "stale vector must be removed immediately");
conn.execute(
"UPDATE memories SET access_count = access_count + 1 WHERE id = 1",
[],
)
.unwrap();
assert_eq!(
fts_matches(&conn, "zebrafishterm"),
0,
"old term must stay dead"
);
assert_eq!(
fts_matches(&conn, "canonical wording"),
1,
"new canonical term must hit"
);
assert_fts_integrity_clean(&conn);
}
#[test]
fn production_writer_same_statement_reset_keeps_deterministic_fallback() {
let conn = migrated_conn();
let id = crate::memory::insert_memory(
&conn,
None,
"proj",
Some("gh850-writer"),
"Cache fix",
"Issue: cache timeout. Resolved by invalidating stale entries.",
"bugfix",
None,
)
.unwrap();
conn.execute(
"UPDATE memories SET
search_context_enrichment_state = 'ready',
search_context_enrichment_version = 1,
search_context_security_policy_version = 1,
search_context_source_hash = search_context_fallback_source_hash
WHERE id = ?1",
[id],
)
.unwrap();
let updated = crate::memory::insert_memory(
&conn,
None,
"proj",
Some("gh850-writer"),
"Cache fix",
"Issue: cache timeout. Resolved by rotating cache keys instead.",
"bugfix",
None,
)
.unwrap();
assert_eq!(id, updated, "same topic key must update the same row");
let (search_context, fallback, state): (String, Option<String>, String) = conn
.query_row(
"SELECT search_context, search_context_fallback_source_hash,
search_context_enrichment_state
FROM memories WHERE id = ?1",
[id],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.unwrap();
assert!(
search_context.contains("rotating cache keys"),
"deterministic hints must survive the production update: {search_context:?}"
);
let expected = crate::memory::retrieval_enrichment::enrichment_source_hash(
"Cache fix",
"Issue: cache timeout. Resolved by rotating cache keys instead.",
"bugfix",
Some("gh850-writer"),
None,
);
assert_eq!(fallback.as_deref(), Some(expected.as_str()));
assert_eq!(state, "pending");
assert_fts_integrity_clean(&conn);
}
#[test]
fn v072_is_named_stably() {
let migration = super::types::MIGRATIONS
.iter()
.find(|migration| migration.version == 72)
.expect("v072 migration is missing");
assert_eq!(migration.name, "memory_retrieval_enrichment");
}