use fathomdb_embedder_api::{Embedder, EmbedderError, EmbedderIdentity, Vector};
use fathomdb_engine::{
DenseReadiness, Engine, InitialState, PreparedWrite, ProjectionRole, ProjectionSpec,
ProjectionVector, SourceId,
};
use fathomdb_schema::{SCHEMA_VERSION, SQLITE_SUFFIX};
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tempfile::TempDir;
#[derive(Clone, Debug)]
struct CountingEmbedder {
identity: EmbedderIdentity,
calls: Arc<AtomicUsize>,
}
impl CountingEmbedder {
fn new() -> Self {
Self {
identity: EmbedderIdentity::new("deterministic", "rev-a", 384),
calls: Arc::new(AtomicUsize::new(0)),
}
}
}
impl Embedder for CountingEmbedder {
fn identity(&self) -> EmbedderIdentity {
self.identity.clone()
}
fn embed(&self, _text: &str) -> Result<Vector, EmbedderError> {
self.calls.fetch_add(1, Ordering::SeqCst);
let mut v = vec![0.0_f32; self.identity.dimension as usize];
v[0] = 1.0;
Ok(v)
}
}
fn db_path(dir: &TempDir, name: &str) -> PathBuf {
dir.path().join(format!("{name}{SQLITE_SUFFIX}"))
}
fn roles(rs: &[ProjectionRole]) -> BTreeSet<ProjectionRole> {
rs.iter().copied().collect()
}
fn vector_spec(name: &str) -> ProjectionSpec {
ProjectionSpec {
name: name.to_string(),
roles: roles(&[ProjectionRole::Searchable]),
fts: None,
vector: Some(ProjectionVector { embedder: None, dense_readiness: None }),
source: None,
}
}
fn filterable_only_spec(name: &str) -> ProjectionSpec {
ProjectionSpec {
name: name.to_string(),
roles: roles(&[ProjectionRole::Filterable]),
fts: None,
vector: None,
source: None,
}
}
fn node(kind: &str, logical_id: &str, body_json: &str) -> PreparedWrite {
PreparedWrite::Node {
kind: kind.to_string(),
body: body_json.to_string(),
source_id: SourceId::new("test:fixture").expect("source id"),
logical_id: Some(logical_id.to_string()),
state: InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
}
}
fn ro(path: &Path) -> rusqlite::Connection {
rusqlite::Connection::open_with_flags(
path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_URI,
)
.expect("open read-only")
}
fn active_cursor(conn: &rusqlite::Connection, logical_id: &str) -> i64 {
conn.query_row(
"SELECT write_cursor FROM canonical_nodes
WHERE logical_id = ?1 AND superseded_at IS NULL",
[logical_id],
|r| r.get::<_, i64>(0),
)
.expect("active cursor")
}
fn vector_row_exists(conn: &rusqlite::Connection, cursor: i64) -> bool {
conn.query_row(
"SELECT COUNT(*) FROM _fathomdb_vector_rows WHERE write_cursor = ?1",
[cursor],
|r| r.get::<_, i64>(0),
)
.expect("vector row probe")
> 0
}
fn vector_kind_registered(conn: &rusqlite::Connection, kind: &str) -> bool {
conn.query_row("SELECT COUNT(*) FROM _fathomdb_vector_kinds WHERE kind = ?1", [kind], |r| {
r.get::<_, i64>(0)
})
.expect("vector kind probe")
> 0
}
fn projection_failure_rows(conn: &rusqlite::Connection) -> i64 {
conn.query_row(
"SELECT COUNT(*) FROM operational_mutations
WHERE collection_name = 'projection_failures'",
[],
|r| r.get::<_, i64>(0),
)
.unwrap_or(0)
}
fn readiness(engine: &Engine, name: &str) -> Option<DenseReadiness> {
engine
.read_projections()
.expect("read_projections")
.into_iter()
.find(|s| s.name == name)
.and_then(|s| s.vector)
.and_then(|v| v.dense_readiness)
}
fn owned(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
fn write_mixed_corpus(engine: &Engine) {
engine.write(&[node("invoice", "I1", r#"{"summary":"payable in 30 days"}"#)]).expect("I1");
engine.write(&[node("doc", "N1", r#"{"summary":"a dense meaning"}"#)]).expect("N1");
engine.write(&[node("entity", "E1", r#"{"summary":"Alice, a person"}"#)]).expect("E1");
engine.write(&[node("entity", "E2", r#"{"summary":"Bob, a person"}"#)]).expect("E2");
engine.write(&[node("invoice", "I2", r#"{"summary":"paid"}"#)]).expect("I2");
}
#[test]
fn an_uncommittable_kind_is_reported_not_silently_dropped() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "tc67_report");
let embedder = CountingEmbedder::new();
let calls = Arc::clone(&embedder.calls);
let opened = Engine::open_with_embedder_for_test(&path, Arc::new(embedder)).expect("open");
let engine = &opened.engine;
write_mixed_corpus(engine);
engine.drain(30_000).expect("baseline drain");
let delta = engine.configure_projections(&[vector_spec("summary")], &[]).expect(
"declaring a vector projection over unsupported kinds is legal — REPORT, not error",
);
assert_eq!(
delta.vector_unsupported_kinds,
owned(&["entity", "invoice"]),
"TC-67: the kinds `resolve_source_type` cannot map are dropped by \
`enqueue_declared_vector_backfill`'s `kind_is_vector_committable` filter with no \
enrolment, no error and nothing recorded. They must be REPORTED — by KIND, sorted and \
de-duplicated — so a caller can tell `deferred`-because-transient from \
will-never-be-embedded"
);
assert!(
!delta.vector_unsupported_kinds.contains(&"doc".to_string()),
"the report must not name a kind the vector writer CAN commit — that would be a false \
permanent-unsupported claim about a kind that is about to be embedded"
);
assert_eq!(
delta.deferred,
owned(&["summary"]),
"the `searchable→vector` sub-target still defers by name — unchanged by TC-67; the report \
is a SEPARATE axis, not a replacement"
);
engine.drain(30_000).expect("drain the declared backfill");
let conn = ro(&path);
assert!(vector_kind_registered(&conn, "doc"), "the commit-able kind still gets its dense arm");
assert!(vector_row_exists(&conn, active_cursor(&conn, "N1")), "…and its vector is at rest");
assert_eq!(calls.load(Ordering::SeqCst), 1, "exactly the one commit-able row was embedded");
for (kind, lid) in [("invoice", "I1"), ("entity", "E1")] {
assert!(
!vector_kind_registered(&conn, kind),
"TC-67 REPORTS the exclusion; it must not lift it. Enrolling `{kind}` would wedge the \
projection worker forever (Slice 20c fix-2)"
);
assert!(!vector_row_exists(&conn, active_cursor(&conn, lid)), "…and no vector at rest");
}
assert_eq!(
projection_failure_rows(&conn),
0,
"a kind with no dense arm is not a FAILURE — reporting it must not start polluting the \
failure audit"
);
assert_eq!(SCHEMA_VERSION, 26, "Slice 19 adds canonical FTS-hydration join indexes");
opened.engine.close().unwrap();
}
#[test]
fn readiness_semantics_are_unchanged_by_the_report() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "tc67_readiness_unchanged");
let embedder = CountingEmbedder::new();
let opened = Engine::open_with_embedder_for_test(&path, Arc::new(embedder)).expect("open");
let engine = &opened.engine;
engine.write(&[node("invoice", "I1", r#"{"summary":"payable in 30 days"}"#)]).expect("I1");
engine.write(&[node("entity", "E1", r#"{"summary":"Alice, a person"}"#)]).expect("E1");
engine.drain(30_000).expect("baseline drain");
let delta = engine.configure_projections(&[vector_spec("summary")], &[]).expect("configure");
assert_eq!(
delta.vector_unsupported_kinds,
owned(&["entity", "invoice"]),
"fixture: both kinds are reported"
);
engine.drain(30_000).expect(
"`drain` must not wait on kinds that will never be embedded — the report is information, \
not outstanding work",
);
assert_eq!(
readiness(engine, "summary"),
Some(DenseReadiness::Ready),
"READINESS SEMANTICS ARE UNCHANGED (plan §3 R-20-VC DoD): an un-enrolled kind is not \
outstanding work, so a corpus made ENTIRELY of unsupported kinds is `ready`, not \
`embedding`. Reporting the kinds must not turn the report into a readiness state"
);
engine.write(&[node("entity", "E2", r#"{"summary":"Bob, a person"}"#)]).expect("E2");
engine.drain(30_000).expect("drain after a further unsupported write");
assert_eq!(
readiness(engine, "summary"),
Some(DenseReadiness::Ready),
"writing MORE rows of an unsupported kind still leaves nothing outstanding"
);
let conn = ro(&path);
assert_eq!(projection_failure_rows(&conn), 0, "…and still no failure-audit noise");
opened.engine.close().unwrap();
}
#[test]
fn a_corpus_of_only_supported_kinds_reports_an_empty_list_not_absent() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "tc67_empty_report");
let embedder = CountingEmbedder::new();
let opened = Engine::open_with_embedder_for_test(&path, Arc::new(embedder)).expect("open");
let engine = &opened.engine;
engine.write(&[node("doc", "N1", r#"{"summary":"a dense meaning"}"#)]).expect("N1");
engine.write(&[node("note", "N2", r#"{"summary":"a second supported kind"}"#)]).expect("N2");
engine.drain(30_000).expect("baseline drain");
let delta = engine.configure_projections(&[vector_spec("summary")], &[]).expect("configure");
assert_eq!(
delta.vector_unsupported_kinds,
Vec::<String>::new(),
"with every kind commit-able the report is EMPTY — present and readable, never absent"
);
assert_eq!(delta.deferred, owned(&["summary"]), "the vector sub-target still defers by name");
let dir2 = TempDir::new().unwrap();
let path2 = db_path(&dir2, "tc67_empty_corpus");
let opened2 = Engine::open_with_embedder_for_test(&path2, Arc::new(CountingEmbedder::new()))
.expect("open");
let delta2 =
opened2.engine.configure_projections(&[vector_spec("summary")], &[]).expect("configure");
assert_eq!(
delta2.vector_unsupported_kinds,
Vec::<String>::new(),
"an empty corpus has no unsupported kinds — empty report, no error"
);
engine.drain(30_000).expect("drain");
opened.engine.close().unwrap();
opened2.engine.close().unwrap();
}
#[test]
fn the_report_is_the_same_without_an_embedder_and_is_not_the_deferral() {
let dir = TempDir::new().unwrap();
let path_absent = db_path(&dir, "tc67_no_embedder");
let absent = Engine::open(path_absent.clone()).expect("open without an embedder");
write_mixed_corpus(&absent.engine);
absent.engine.drain(30_000).expect("baseline drain");
let delta_absent =
absent.engine.configure_projections(&[vector_spec("summary")], &[]).expect("configure");
let path_live = db_path(&dir, "tc67_live_embedder");
let live = Engine::open_with_embedder_for_test(&path_live, Arc::new(CountingEmbedder::new()))
.expect("open with an embedder");
write_mixed_corpus(&live.engine);
live.engine.drain(30_000).expect("baseline drain");
let delta_live =
live.engine.configure_projections(&[vector_spec("summary")], &[]).expect("configure");
assert_eq!(
delta_absent.vector_unsupported_kinds,
owned(&["entity", "invoice"]),
"TC-67: a session with NO embedder must still report the permanent fact. \
`resolve_source_type`'s vocabulary is static, so `entity`/`invoice` will never be \
embedded by any embedder in any session — computing the report inside the \
`dense_arm_live` gate would hide exactly the information a graceful-absent caller needs"
);
assert_eq!(
delta_absent.vector_unsupported_kinds, delta_live.vector_unsupported_kinds,
"the report is embedder-INDEPENDENT: identical with and without a live embedder"
);
assert!(
!delta_absent.vector_unsupported_kinds.contains(&"doc".to_string()),
"and it must NOT claim the commit-able `doc` is permanently unsupported just because this \
session cannot embed anything — that is the deferral, a different (transient) fact"
);
assert_eq!(delta_absent.deferred, delta_live.deferred);
assert_eq!(delta_absent.deferred, owned(&["summary"]));
absent.engine.close().unwrap();
live.engine.drain(30_000).expect("drain");
live.engine.close().unwrap();
}
#[test]
fn the_report_is_state_not_diff_so_an_idempotent_reapply_still_carries_it() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "tc67_state_not_diff");
let embedder = CountingEmbedder::new();
let opened = Engine::open_with_embedder_for_test(&path, Arc::new(embedder)).expect("open");
let engine = &opened.engine;
engine.write(&[node("doc", "N1", r#"{"summary":"a dense meaning"}"#)]).expect("N1");
engine.write(&[node("invoice", "I1", r#"{"summary":"payable in 30 days"}"#)]).expect("I1");
engine.drain(30_000).expect("baseline drain");
let spec = vector_spec("summary");
let first =
engine.configure_projections(std::slice::from_ref(&spec), &[]).expect("first declaration");
assert_eq!(first.vector_unsupported_kinds, owned(&["invoice"]), "fixture: reported once");
engine.drain(30_000).expect("drain the backfill");
let again = engine
.configure_projections(std::slice::from_ref(&spec), &[])
.expect("idempotent re-apply");
assert!(again.unchanged, "re-registering the same spec is still a no-op (Slice 15d keystone)");
assert!(again.built.is_empty() && again.dropped.is_empty() && again.deferred.is_empty());
assert_eq!(
again.vector_unsupported_kinds,
owned(&["invoice"]),
"TC-67: the report is a STATE report, not a diff. A no-op re-apply must still carry it — \
that is the documented refresh path for the declare-time residual, and it must not be \
suppressed by `unchanged`"
);
engine.write(&[node("entity", "E1", r#"{"summary":"Alice, a person"}"#)]).expect("E1");
engine.drain(30_000).expect("drain the late write");
assert_eq!(
first.vector_unsupported_kinds,
owned(&["invoice"]),
"RESIDUAL: the delta the caller already holds is a snapshot; it does not learn about \
`entity`, written after the declaration"
);
let refreshed =
engine.configure_projections(std::slice::from_ref(&spec), &[]).expect("refresh re-apply");
assert!(refreshed.unchanged, "the refresh costs nothing — it is still a no-op");
assert_eq!(
refreshed.vector_unsupported_kinds,
owned(&["entity", "invoice"]),
"…and the no-op re-apply is the refresh: it reports the corpus as it stands NOW"
);
opened.engine.close().unwrap();
}
#[test]
fn no_vector_declaration_means_no_report() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "tc67_scoped_to_dense_arm");
let embedder = CountingEmbedder::new();
let opened = Engine::open_with_embedder_for_test(&path, Arc::new(embedder)).expect("open");
let engine = &opened.engine;
write_mixed_corpus(engine);
engine.drain(30_000).expect("baseline drain");
let delta = engine
.configure_projections(&[filterable_only_spec("summary")], &[])
.expect("filterable-only configure");
assert_eq!(
delta.vector_unsupported_kinds,
Vec::<String>::new(),
"no dense arm is declared, so there is nothing for an unsupported kind to be unsupported \
FOR — reporting here would be noise on every non-vector call"
);
let declared = engine
.configure_projections(&[vector_spec("meaning")], &[])
.expect("declare the dense arm");
assert_eq!(
declared.vector_unsupported_kinds,
owned(&["entity", "invoice"]),
"fixture: with a dense arm declared the report is populated"
);
engine.drain(30_000).expect("drain");
let dropped =
engine.configure_projections(&[], &["meaning".to_string()]).expect("drop the dense arm");
assert!(dropped.dropped.contains(&"meaning".to_string()), "fixture: the drop is reported");
assert_eq!(
dropped.vector_unsupported_kinds,
Vec::<String>::new(),
"once the last `searchable→vector` declaration is gone the report goes quiet with it"
);
opened.engine.close().unwrap();
}
#[test]
fn the_read_configure_round_trip_still_holds() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "tc67_round_trip");
let embedder = CountingEmbedder::new();
let opened = Engine::open_with_embedder_for_test(&path, Arc::new(embedder)).expect("open");
let engine = &opened.engine;
write_mixed_corpus(engine);
engine.drain(30_000).expect("baseline drain");
engine.configure_projections(&[vector_spec("summary")], &[]).expect("declare");
engine.drain(30_000).expect("drain the backfill");
let back = engine.read_projections().expect("read_projections");
assert_eq!(back.len(), 1);
assert_eq!(
back[0].vector.as_ref().and_then(|v| v.dense_readiness),
Some(DenseReadiness::Ready),
"readiness is `ready` — unchanged by TC-67, even with unsupported kinds present"
);
let round_tripped =
engine.configure_projections(&back, &[]).expect("read.projections output re-applies");
assert!(
round_tripped.unchanged,
"the shipped read→configure round-trip is still a no-op — TC-67 adds an OUTPUT-only field \
to the delta, and `configure_projections` takes specs, never a delta, so there is no \
inbound direction it could break"
);
assert_eq!(
round_tripped.vector_unsupported_kinds,
owned(&["entity", "invoice"]),
"…and the no-op still carries the report"
);
opened.engine.close().unwrap();
}