use std::path::Path;
use std::sync::Arc;
use fathomdb_embedder_api::{Embedder, EmbedderError, EmbedderIdentity, Vector};
use fathomdb_engine::{
Engine, EngineError, InitialState, LifecycleState, OpenedEngine, PreparedWrite, ReadView,
};
use fathomdb_schema::SQLITE_SUFFIX;
use tempfile::TempDir;
#[derive(Clone, Debug)]
struct DetEmbedder;
impl Embedder for DetEmbedder {
fn identity(&self) -> EmbedderIdentity {
EmbedderIdentity::new("det", "rev-a", 8)
}
fn embed(&self, text: &str) -> Result<Vector, EmbedderError> {
let mut v = vec![0.0_f32; 8];
let mut h: u64 = 0xcbf29ce4_84222325;
for &b in text.as_bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x0100_0000_01b3);
}
for k in 0..4 {
let coord = ((h >> (k * 8)) as usize) % 8;
v[coord] += 0.5_f32;
}
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-6);
for x in &mut v {
*x /= norm;
}
Ok(v)
}
}
fn open(name: &str) -> (TempDir, OpenedEngine) {
let dir = TempDir::new().unwrap();
let path = dir.path().join(format!("{name}{SQLITE_SUFFIX}"));
let opened = Engine::open_with_embedder_for_test(&path, Arc::new(DetEmbedder)).expect("open");
opened.engine.configure_vector_kind_for_test("doc").expect("configure vector kind");
(dir, opened)
}
fn node(body: &str, logical_id: &str) -> PreparedWrite {
PreparedWrite::Node {
kind: "doc".to_string(),
body: body.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some(logical_id.to_string()),
state: InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
}
}
fn edge(logical_id: &str, from: &str, to: &str, body: &str) -> PreparedWrite {
PreparedWrite::Edge {
kind: "link".to_string(),
from: from.to_string(),
to: to.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some(logical_id.to_string()),
body: Some(body.to_string()),
t_valid: None,
t_invalid: None,
confidence: Some(0.9),
extractor_model_id: None,
temporal_fallback: None,
}
}
fn read_state_reason(path: &Path, logical_id: &str) -> Option<(String, Option<String>)> {
let conn = rusqlite::Connection::open_with_flags(
path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_URI,
)
.expect("open read-only");
conn.query_row(
"SELECT state, reason FROM canonical_nodes \
WHERE logical_id = ?1 AND superseded_at IS NULL",
[logical_id],
|r| Ok((r.get::<_, String>(0)?, r.get::<_, Option<String>>(1)?)),
)
.ok()
}
fn count(conn: &rusqlite::Connection, sql: &str) -> i64 {
conn.query_row(sql, [], |r| r.get(0)).expect("count query")
}
#[test]
fn transition_legal_moves_and_reason_semantics() {
let (dir, opened) = open("tr_legal");
let path = dir.path().join(format!("tr_legal{SQLITE_SUFFIX}"));
let engine = &opened.engine;
engine
.write(&[PreparedWrite::Node {
kind: "doc".to_string(),
body: "quarantined body".to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some("p1".to_string()),
state: InitialState::Pending,
reason: Some("awaiting-review".to_string()),
valid_from: None,
valid_until: None,
}])
.expect("write pending");
assert_eq!(read_state_reason(&path, "p1").unwrap().0, "pending");
engine.transition("p1", LifecycleState::Active, None).expect("promote");
assert_eq!(
read_state_reason(&path, "p1"),
Some(("active".to_string(), None)),
"promote → active clears reason to NULL"
);
engine
.write(&[PreparedWrite::Node {
kind: "doc".to_string(),
body: "spam body".to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some("p2".to_string()),
state: InitialState::Pending,
reason: None,
valid_from: None,
valid_until: None,
}])
.expect("write pending 2");
engine
.transition("p2", LifecycleState::Deleted, Some("rejected-spam".to_string()))
.expect("reject");
assert_eq!(
read_state_reason(&path, "p2"),
Some(("deleted".to_string(), Some("rejected-spam".to_string()))),
"reject → deleted sets the supplied reason"
);
engine.write(&[node("live body", "a1")]).expect("write active");
engine
.transition("a1", LifecycleState::Deleted, Some("user-deleted".to_string()))
.expect("soft-delete");
assert_eq!(
read_state_reason(&path, "a1"),
Some(("deleted".to_string(), Some("user-deleted".to_string())))
);
engine.transition("a1", LifecycleState::Active, None).expect("undelete");
assert_eq!(
read_state_reason(&path, "a1"),
Some(("active".to_string(), None)),
"undelete → active clears reason"
);
}
#[test]
fn illegal_transitions_return_typed_error_with_legal_targets() {
let (_dir, opened) = open("tr_illegal");
let engine = &opened.engine;
engine.write(&[node("body", "a1")]).expect("write");
engine
.write(&[PreparedWrite::Node {
kind: "doc".to_string(),
body: "quarantined body".to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some("pend".to_string()),
state: InitialState::Pending,
reason: None,
valid_from: None,
valid_until: None,
}])
.expect("write pending");
let err = engine.transition("pend", LifecycleState::Purged, None).unwrap_err();
assert_eq!(
err,
EngineError::IllegalTransition {
from_state: LifecycleState::Pending,
to_state: LifecycleState::Purged,
legal: vec![LifecycleState::Active, LifecycleState::Deleted],
}
);
let err = engine.transition("a1", LifecycleState::Purged, None).unwrap_err();
assert_eq!(
err,
EngineError::IllegalTransition {
from_state: LifecycleState::Active,
to_state: LifecycleState::Purged,
legal: vec![LifecycleState::Deleted],
}
);
assert!(matches!(
engine.transition("a1", LifecycleState::Active, None).unwrap_err(),
EngineError::IllegalTransition { from_state: LifecycleState::Active, .. }
));
assert!(matches!(
engine.transition("a1", LifecycleState::Pending, None).unwrap_err(),
EngineError::IllegalTransition { to_state: LifecycleState::Pending, .. }
));
engine.transition("a1", LifecycleState::Deleted, None).expect("soft-delete");
let err = engine.transition("a1", LifecycleState::Purged, None).unwrap_err();
assert_eq!(
err,
EngineError::IllegalTransition {
from_state: LifecycleState::Deleted,
to_state: LifecycleState::Purged,
legal: vec![LifecycleState::Active],
}
);
assert!(matches!(
engine.transition("a1", LifecycleState::Deleted, None).unwrap_err(),
EngineError::IllegalTransition { from_state: LifecycleState::Deleted, .. }
));
let err = engine.transition("ghost", LifecycleState::Active, None).unwrap_err();
assert_eq!(
err,
EngineError::IllegalTransition {
from_state: LifecycleState::Purged,
to_state: LifecycleState::Active,
legal: vec![],
}
);
}
#[test]
fn non_logical_ids_are_refused() {
let (_dir, opened) = open("addr");
let engine = &opened.engine;
engine.write(&[node("body", "a1")]).expect("write");
for bad in ["h:deadbeef", "p:7"] {
assert!(
matches!(
engine.transition(bad, LifecycleState::Deleted, None).unwrap_err(),
EngineError::NotLifecycleAddressable { .. }
),
"transition({bad}) must refuse a non-logical id"
);
assert!(
matches!(engine.purge(bad).unwrap_err(), EngineError::NotLifecycleAddressable { .. }),
"purge({bad}) must refuse a non-logical id"
);
}
engine.transition("l:a1", LifecycleState::Deleted, Some("via-prefix".to_string())).expect("l:");
}
#[test]
fn soft_delete_excludes_from_default_reads_and_undelete_restores() {
let (_dir, opened) = open("soft_delete");
let engine = &opened.engine;
engine.write(&[node("zephyrunique payload", "a1")]).expect("write");
engine.drain(15_000).expect("drain");
assert!(engine.read_get("a1", &ReadView::default()).expect("get").is_some());
engine.transition("a1", LifecycleState::Deleted, Some("x".to_string())).expect("soft-delete");
assert!(
engine.read_get("a1", &ReadView::default()).expect("get").is_none(),
"deleted node absent from read.get"
);
let hits = engine.search("zephyrunique").expect("search");
assert!(
!hits.results.iter().any(|h| h.body.contains("zephyrunique payload")),
"deleted node excluded from default search"
);
engine.transition("a1", LifecycleState::Active, None).expect("undelete");
assert!(
engine.read_get("a1", &ReadView::default()).expect("get").is_some(),
"undelete restores read.get visibility"
);
}
#[test]
fn purge_requires_deleted_first_and_is_idempotent() {
let (_dir, opened) = open("pg_precond");
let engine = &opened.engine;
engine.write(&[node("body", "a1")]).expect("write");
let err = engine.purge("a1").unwrap_err();
assert_eq!(
err,
EngineError::IllegalTransition {
from_state: LifecycleState::Active,
to_state: LifecycleState::Purged,
legal: vec![LifecycleState::Deleted],
}
);
engine.transition("a1", LifecycleState::Deleted, None).expect("soft-delete");
engine.purge("a1").expect("purge from deleted");
engine.purge("a1").expect("idempotent re-purge");
engine.purge("never-existed").expect("idempotent absent purge");
}
#[test]
fn purge_erases_all_row_owned_targets_and_cascades_edges() {
let (dir, opened) = open("pg_sweep");
let path = dir.path().join(format!("pg_sweep{SQLITE_SUFFIX}"));
let engine = &opened.engine;
let receipt = engine
.write(&[node("alpha purge-target body", "a"), node("beta survivor body", "b")])
.expect("write nodes");
let cursor_a = receipt.row_cursors[0] as i64;
let cursor_b = receipt.row_cursors[1] as i64;
let edge_receipt =
engine.write(&[edge("e-ab", "a", "b", "alpha relates to beta")]).expect("write edge");
let cursor_e = edge_receipt.row_cursors[0] as i64;
engine.drain(15_000).expect("drain");
let conn = rusqlite::Connection::open_with_flags(
&path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_URI,
)
.expect("read-only conn");
assert!(
count(&conn, &format!("SELECT COUNT(*) FROM search_index WHERE write_cursor = {cursor_a}"))
> 0,
"precondition: node A must have a search_index row before purge"
);
assert!(
count(&conn, &format!("SELECT COUNT(*) FROM vector_default WHERE rowid = {cursor_a}")) > 0,
"precondition: node A must have a vector_default row before purge"
);
let vector_kinds_before = count(&conn, "SELECT COUNT(*) FROM _fathomdb_vector_kinds");
let projection_state_before = count(&conn, "SELECT COUNT(*) FROM _fathomdb_projection_state");
drop(conn);
engine.transition("a", LifecycleState::Deleted, None).expect("soft-delete");
engine.purge("a").expect("purge");
let conn = rusqlite::Connection::open_with_flags(
&path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_URI,
)
.expect("read-only conn 2");
assert_eq!(count(&conn, "SELECT COUNT(*) FROM canonical_nodes WHERE logical_id = 'a'"), 0);
assert_eq!(
count(&conn, "SELECT COUNT(*) FROM canonical_edges WHERE from_id = 'a' OR to_id = 'a'"),
0,
"edges touching the purged node are cascade-removed (no stubs)"
);
for cursor in [cursor_a, cursor_e] {
for table in ["search_index", "search_index_edges", "search_index_v2"] {
assert_eq!(
count(
&conn,
&format!("SELECT COUNT(*) FROM {table} WHERE write_cursor = {cursor}")
),
0,
"{table} must have no row for cursor {cursor} after purge"
);
}
assert_eq!(
count(&conn, &format!("SELECT COUNT(*) FROM vector_default WHERE rowid = {cursor}")),
0,
"vector_default must have no row for cursor {cursor} after purge"
);
assert_eq!(
count(
&conn,
&format!(
"SELECT COUNT(*) FROM _fathomdb_vector_rows WHERE write_cursor = {cursor}"
)
),
0
);
assert_eq!(
count(
&conn,
&format!(
"SELECT COUNT(*) FROM _fathomdb_projection_terminal WHERE write_cursor = {cursor}"
)
),
0
);
}
assert_eq!(count(&conn, "SELECT COUNT(*) FROM canonical_nodes WHERE logical_id = 'b'"), 1);
assert!(
count(&conn, &format!("SELECT COUNT(*) FROM vector_default WHERE rowid = {cursor_b}")) > 0,
"the survivor node's vector row is untouched"
);
assert_eq!(
count(&conn, "SELECT COUNT(*) FROM _fathomdb_vector_kinds"),
vector_kinds_before,
"_fathomdb_vector_kinds is a kind registry, not a purge target"
);
assert_eq!(
count(&conn, "SELECT COUNT(*) FROM _fathomdb_projection_state"),
projection_state_before,
"_fathomdb_projection_state is global high-water state, not a purge target"
);
}
#[test]
fn secure_delete_is_enabled_on_open() {
let (_dir, opened) = open("secure_delete");
assert!(
opened.engine.secure_delete_enabled_for_test().expect("pragma read"),
"PRAGMA secure_delete must be ON on the writer connection at open"
);
}
#[cfg(debug_assertions)]
#[test]
fn secure_delete_is_enabled_on_reader_pool_and_runtime_connections() {
let (_dir, opened) = open("secure_delete_all");
assert!(
opened.engine.reader_secure_delete_enabled_for_test().expect("reader pragma read"),
"PRAGMA secure_delete must be ON on EVERY reader-pool connection at open"
);
assert!(
opened.engine.runtime_secure_delete_enabled_for_test().expect("runtime pragma read"),
"PRAGMA secure_delete must be ON on the projection/runtime connection at open"
);
}