All 8 test categories from the task plan, plus a JobRepo subset:
migration — tests/migration.rs: fresh DB after run_migrations
exposes every required §5 table + index.
unit (copy) — tests/asset_writer.rs: copy mode writes file with
mode 0o644 + correct bytes.
unit (ref) — tests/asset_writer.rs: reference mode does not write
file; row records source path.
unit (cs) — tests/asset_writer.rs: tampered checksum returns a
Conflict-flavoured anyhow error.
unit (idem) — tests/idempotency.rs: same put_document twice → 1 row,
doc_version 1→2; tags re-derived.
unit (rb) — tests/idempotency.rs: put_blocks with FK violation
rolls back; pre-existing rows unchanged.
contract — tests/contract_roundtrip.rs: drives kb-parse-md +
kb-normalize + kb-chunk on
fixtures/markdown/code-and-table.md, persists, then
reloads via DocumentStore::get_document /
get_chunk and asserts byte-equal round-trip.
snapshot — tests/ingest_report_snapshot.rs +
snapshots/ingest_report.snapshot.json: pin the wire
JSON form of kb_core::IngestReport for an inline
fixture run.
jobs — tests/jobs.rs: create → progress → finish flow;
error message round-trip; list filters on status/kind.
Drops the unused `serde` direct dep from Cargo.toml; serde_json brings
its own. Dev-deps confirmed via `cargo tree -p kb-store-sqlite --depth 1`
to live only in the dev tree.
84 lines
2.4 KiB
Rust
84 lines
2.4 KiB
Rust
//! Migration test: a fresh DB, after `run_migrations`, exposes every
|
||
//! table and index P1 needs (per §5.1–§5.7).
|
||
|
||
use kb_store_sqlite::SqliteStore;
|
||
|
||
mod common;
|
||
|
||
#[test]
|
||
fn fresh_db_has_all_p1_tables_and_indexes() {
|
||
let env = common::TestEnv::new();
|
||
let store = SqliteStore::open(&env.config()).expect("open");
|
||
store.run_migrations().expect("run migrations");
|
||
|
||
// Pull the list of user tables from sqlite_master.
|
||
let tables: Vec<String> = env.with_conn(|c| {
|
||
let mut stmt = c.prepare(
|
||
"SELECT name FROM sqlite_master
|
||
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
||
ORDER BY name",
|
||
)?;
|
||
let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
|
||
let tables: rusqlite::Result<Vec<String>> = rows.collect();
|
||
tables
|
||
});
|
||
|
||
let required = [
|
||
"answers",
|
||
"assets",
|
||
"blocks",
|
||
"chunks",
|
||
"document_tags",
|
||
"documents",
|
||
"embedding_records",
|
||
"eval_query_results",
|
||
"eval_runs",
|
||
"ingest_runs",
|
||
"jobs",
|
||
// refinery's own bookkeeping table (`refinery_schema_history`)
|
||
// also lands here; we don't pin it but it's expected.
|
||
"migrations",
|
||
"schema_meta",
|
||
];
|
||
for t in required {
|
||
assert!(
|
||
tables.iter().any(|n| n == t),
|
||
"table `{t}` missing; got {tables:?}"
|
||
);
|
||
}
|
||
|
||
// Pin the documented indexes (subset that matters for hot paths).
|
||
let indexes: Vec<String> = env.with_conn(|c| {
|
||
let mut stmt = c.prepare(
|
||
"SELECT name FROM sqlite_master
|
||
WHERE type = 'index' AND name NOT LIKE 'sqlite_%'
|
||
ORDER BY name",
|
||
)?;
|
||
let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
|
||
let idx: rusqlite::Result<Vec<String>> = rows.collect();
|
||
idx
|
||
});
|
||
for i in [
|
||
"idx_assets_workspace_path",
|
||
"idx_assets_media_type",
|
||
"idx_docs_workspace_path",
|
||
"idx_docs_lang",
|
||
"idx_docs_source_type",
|
||
"idx_document_tags_tag",
|
||
"idx_blocks_doc_id",
|
||
"idx_chunks_doc_id",
|
||
"idx_chunks_chunker_version",
|
||
"idx_embed_chunk",
|
||
"idx_embed_model",
|
||
"idx_jobs_status",
|
||
"idx_jobs_kind",
|
||
"idx_answers_created_at",
|
||
"idx_answers_grounded",
|
||
] {
|
||
assert!(
|
||
indexes.iter().any(|n| n == i),
|
||
"index `{i}` missing; got {indexes:?}"
|
||
);
|
||
}
|
||
}
|