use std::process::Command;
use fathomdb::Engine;
use fathomdb_cli::exit_code;
use serde_json::Value;
use tempfile::TempDir;
const DOCTOR_VERBS: &[&str] = &[
"check-integrity",
"safe-export",
"verify-embedder",
"trace",
"dump-schema",
"dump-row-counts",
"dump-profile",
"recompute-mean",
];
const RECOVER_FLAGS: &[&str] = &[
"--truncate-wal",
"--rebuild-vec0",
"--rebuild-projections",
"--excise-source",
"--accept-data-loss",
];
fn fathomdb() -> Command {
Command::new(env!("CARGO_BIN_EXE_fathomdb"))
}
#[test]
fn t_040a_every_doctor_verb_help_exits_zero() {
for verb in DOCTOR_VERBS {
let output = fathomdb()
.args(["doctor", verb, "--help"])
.output()
.unwrap_or_else(|e| panic!("failed to spawn fathomdb doctor {verb} --help: {e}"));
assert!(
output.status.success(),
"fathomdb doctor {verb} --help must exit 0; got {:?} stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
}
}
#[test]
fn t_040b_every_doctor_verb_help_has_usage_section() {
for verb in DOCTOR_VERBS {
let output = fathomdb().args(["doctor", verb, "--help"]).output().expect("spawn");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.lines().any(|line| line.starts_with("Usage:")),
"fathomdb doctor {verb} --help must include a `Usage:` line; got:\n{stdout}",
);
}
}
#[test]
fn t_035d_recover_help_exits_zero() {
let output = fathomdb().args(["recover", "--help"]).output().expect("spawn");
assert!(
output.status.success(),
"fathomdb recover --help must exit 0; got {:?} stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
}
#[test]
fn t_058_recover_help_enumerates_canonical_flag_set() {
let output = fathomdb().args(["recover", "--help"]).output().expect("spawn");
assert!(output.status.success(), "recover --help must exit 0");
let stdout = String::from_utf8_lossy(&output.stdout);
for flag in RECOVER_FLAGS {
let count = stdout.matches(flag).count();
assert_eq!(
count, 1,
"recover --help must mention {flag} exactly once; counted {count} in:\n{stdout}",
);
}
}
#[test]
fn t_cli_doctor_rejects_accept_data_loss_binary() {
let output = fathomdb()
.args(["doctor", "check-integrity", "--accept-data-loss"])
.output()
.expect("spawn");
assert!(
!output.status.success(),
"fathomdb doctor check-integrity --accept-data-loss must fail; \
clap should reject the flag (it is owned by `recover`)",
);
}
fn seeded_db() -> (TempDir, std::path::PathBuf) {
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("operator.sqlite");
let opened = Engine::open(path.clone()).expect("engine open");
opened.engine.close().expect("engine close");
drop(opened);
(dir, path)
}
fn doctor_invocation(verb: &str, db_path: &str, out_path: &str) -> Vec<String> {
let mut argv = vec!["doctor".to_string(), verb.to_string()];
match verb {
"safe-export" => {
argv.push(out_path.to_string());
}
"trace" => {
argv.push("--source-ref".to_string());
argv.push("src-test".to_string());
}
"verify-embedder" => {
argv.push("--identity".to_string());
argv.push(
"fathomdb-bge-small-en-v1.5:5c38ec7c405ec4b44b94cc5a9bb96e735b38267a".to_string(),
);
argv.push("--dimension".to_string());
argv.push("384".to_string());
}
_ => {}
}
argv.push(db_path.to_string());
argv
}
#[test]
fn t_035d_recover_refuses_without_accept_data_loss() {
let (dir, db) = seeded_db();
let output = fathomdb()
.args(["recover", "--truncate-wal", db.to_str().unwrap()])
.output()
.expect("spawn");
assert_eq!(
output.status.code(),
Some(exit_code::UNRECOVERABLE),
"recover without --accept-data-loss must exit UNRECOVERABLE; got {:?} stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("refusal stdout must be JSON; err={e} stdout={stdout}"));
let obj = parsed.as_object().expect("expected JSON object");
assert_eq!(obj.get("status").and_then(Value::as_str), Some("refused"));
assert_eq!(obj.get("verb").and_then(Value::as_str), Some("recover"));
assert_eq!(
obj.get("code").and_then(Value::as_str),
Some("E_RECOVER_REQUIRES_ACCEPT_DATA_LOSS"),
);
drop(dir);
}
#[test]
fn t_035d_runtime_doctor_rejects_accept_data_loss() {
let output = fathomdb()
.args(["doctor", "check-integrity", "--accept-data-loss", "/tmp/x.sqlite"])
.output()
.expect("spawn");
assert!(
!output.status.success(),
"doctor must refuse --accept-data-loss; got {:?}",
output.status,
);
}
#[test]
fn t_036_every_doctor_verb_accepts_json_flag_on_binary() {
let (dir, db) = seeded_db();
let out_path = dir.path().join("export.sqlite");
for verb in DOCTOR_VERBS {
let mut argv = doctor_invocation(verb, db.to_str().unwrap(), out_path.to_str().unwrap());
let db_index = argv.len() - 1;
argv.insert(db_index, "--json".to_string());
let output = fathomdb().args(&argv).output().expect("spawn");
let stdout = String::from_utf8_lossy(&output.stdout);
let trimmed = stdout.trim();
assert!(
!trimmed.is_empty(),
"doctor {verb} --json must emit something on stdout; got empty",
);
serde_json::from_str::<Value>(trimmed).unwrap_or_else(|e| {
panic!("doctor {verb} --json stdout must parse as JSON; err={e} stdout={stdout}")
});
}
drop(dir);
}
#[test]
fn t_037_doctor_verbs_emit_single_json_object() {
let (dir, db) = seeded_db();
let out_path = dir.path().join("export.sqlite");
for verb in DOCTOR_VERBS {
let mut argv = doctor_invocation(verb, db.to_str().unwrap(), out_path.to_str().unwrap());
let db_index = argv.len() - 1;
argv.insert(db_index, "--json".to_string());
let output = fathomdb().args(&argv).output().expect("spawn");
let stdout = String::from_utf8_lossy(&output.stdout);
let trimmed = stdout.trim();
let parsed: Value = serde_json::from_str(trimmed).unwrap_or_else(|e| {
panic!("doctor {verb} --json must be parseable; err={e} stdout={stdout}")
});
assert!(
parsed.is_object(),
"doctor {verb} --json must produce a single JSON object; got {parsed:?}",
);
assert_eq!(
trimmed.lines().count(),
trimmed.lines().filter(|l| !l.is_empty()).count(),
"doctor {verb} --json must not contain blank lines (NDJSON guard)",
);
}
drop(dir);
}
#[test]
fn t_pr2b_recompute_mean_happy_path_json_contract() {
let (dir, db) = seeded_db();
let output = fathomdb()
.args(["doctor", "recompute-mean", "--json", db.to_str().unwrap()])
.output()
.expect("spawn");
assert_eq!(
output.status.code(),
Some(exit_code::OK),
"recompute-mean must exit OK; got {:?} stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("recompute-mean --json must be JSON; err={e} stdout={stdout}"));
let obj = parsed.as_object().expect("object");
assert_eq!(obj.get("verb").and_then(Value::as_str), Some("recompute-mean"));
assert_eq!(obj.get("status").and_then(Value::as_str), Some("ok"));
for key in ["dim", "old_doc_count", "doc_count_requantized", "drift_cos_before", "elapsed_ms"] {
assert!(obj.contains_key(key), "recompute-mean --json must carry `{key}`; got {parsed:?}");
}
drop(dir);
}
#[test]
fn t_pr2b_recompute_mean_lock_held_exits_71() {
let dir = TempDir::new().expect("tempdir");
let db = dir.path().join("recompute_locked.sqlite");
let opened = Engine::open(db.clone()).expect("engine open");
let output = fathomdb()
.args(["doctor", "recompute-mean", "--json", db.to_str().unwrap()])
.output()
.expect("spawn");
assert_eq!(
output.status.code(),
Some(exit_code::LOCK_HELD),
"lock-held recompute-mean must exit LOCK_HELD (71); got {:?} stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("lock-held stdout must be JSON; err={e} stdout={stdout}"));
let obj = parsed.as_object().expect("object");
assert_eq!(obj.get("status").and_then(Value::as_str), Some("error"));
assert_eq!(obj.get("code").and_then(Value::as_str), Some("DatabaseLockedError"));
opened.engine.close().expect("close");
drop(opened);
drop(dir);
}
#[test]
fn t_038_pretty_is_human_only_not_machine_schema() {
let (dir, db) = seeded_db();
let output = fathomdb()
.args(["doctor", "check-integrity", "--pretty", db.to_str().unwrap()])
.output()
.expect("spawn");
let stdout = String::from_utf8_lossy(&output.stdout);
if let Ok(parsed) = serde_json::from_str::<Value>(stdout.trim()) {
let obj = parsed.as_object().expect("object");
assert_eq!(
obj.get("verb").and_then(Value::as_str),
Some("check-integrity"),
"--pretty must not introduce a second machine schema; verb discriminator must \
match the one fixed by --json",
);
}
drop(dir);
}
#[test]
fn t_045_lock_held_outcome_maps_to_exit_71() {
let dir = TempDir::new().expect("tempdir");
let db = dir.path().join("locked.sqlite");
let opened = Engine::open(db.clone()).expect("engine open");
let output = fathomdb()
.args(["doctor", "check-integrity", "--json", db.to_str().unwrap()])
.output()
.expect("spawn");
assert_eq!(
output.status.code(),
Some(exit_code::LOCK_HELD),
"lock-held DB must exit LOCK_HELD (71); got {:?} stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("lock-held stdout must be JSON; err={e} stdout={stdout}"));
let obj = parsed.as_object().expect("object");
assert_eq!(obj.get("status").and_then(Value::as_str), Some("error"));
assert_eq!(obj.get("code").and_then(Value::as_str), Some("DatabaseLockedError"));
opened.engine.close().expect("close");
drop(opened);
drop(dir);
}
#[test]
fn t_recover_excise_requires_accept_data_loss_runtime() {
let (dir, db) = seeded_db();
let output = fathomdb()
.args(["recover", "--excise-source", "src-1", db.to_str().unwrap()])
.output()
.expect("spawn");
assert_eq!(output.status.code(), Some(exit_code::UNRECOVERABLE));
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: Value = serde_json::from_str(stdout.trim()).expect("json");
assert_eq!(parsed.get("status").and_then(Value::as_str), Some("refused"));
drop(dir);
}
#[test]
fn t_recover_excise_source_with_accept_data_loss_succeeds() {
let (dir, db) = seeded_db();
let output = fathomdb()
.args(["recover", "--accept-data-loss", "--excise-source", "src-1", db.to_str().unwrap()])
.output()
.expect("spawn");
assert_eq!(
output.status.code(),
Some(exit_code::RECOVERY_ACCEPTED_LOSS),
"recover --excise-source with --accept-data-loss must exit 64; got {:?} stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: Value = serde_json::from_str(stdout.trim()).expect("json");
assert_eq!(parsed.get("verb").and_then(Value::as_str), Some("excise-source"));
drop(dir);
}
#[test]
fn t_safe_export_engine_error_exits_export_failure_66() {
let (dir, db) = seeded_db();
let bad_out = dir.path().join("nonexistent").join("export.sqlite");
let output = fathomdb()
.args(["doctor", "safe-export", bad_out.to_str().unwrap(), db.to_str().unwrap(), "--json"])
.output()
.expect("spawn");
assert_eq!(
output.status.code(),
Some(exit_code::EXPORT_FAILURE),
"stdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
let v: Value = serde_json::from_str(stdout.trim()).expect("one json object");
assert_eq!(v.get("verb").and_then(Value::as_str), Some("safe-export"));
assert_eq!(v.get("status").and_then(Value::as_str), Some("error"));
drop(dir);
}
#[test]
fn t_040a_verify_embedder_cli_emits_match_status_on_matching_input() {
let (dir, db) = seeded_db();
let output = fathomdb()
.args([
"doctor",
"verify-embedder",
"--identity",
"fathomdb-bge-small-en-v1.5:5c38ec7c405ec4b44b94cc5a9bb96e735b38267a",
"--dimension",
"384",
"--json",
db.to_str().unwrap(),
])
.output()
.expect("spawn");
assert_eq!(output.status.code(), Some(exit_code::OK));
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: Value = serde_json::from_str(stdout.trim()).expect("json");
assert_eq!(parsed.get("verb").and_then(Value::as_str), Some("verify-embedder"));
assert_eq!(
parsed.get("stored_identity").and_then(Value::as_str),
Some("fathomdb-bge-small-en-v1.5:5c38ec7c405ec4b44b94cc5a9bb96e735b38267a"),
);
assert_eq!(parsed.get("stored_dimension").and_then(Value::as_u64), Some(384));
assert_eq!(
parsed.get("supplied_identity").and_then(Value::as_str),
Some("fathomdb-bge-small-en-v1.5:5c38ec7c405ec4b44b94cc5a9bb96e735b38267a"),
);
assert_eq!(parsed.get("supplied_dimension").and_then(Value::as_u64), Some(384));
assert_eq!(parsed.get("status").and_then(Value::as_str), Some("match"));
drop(dir);
}
#[test]
fn t_040a_dump_schema_cli_emits_locked_top_level_keys() {
let (dir, db) = seeded_db();
let output = fathomdb()
.args(["doctor", "dump-schema", "--json", db.to_str().unwrap()])
.output()
.expect("spawn");
assert_eq!(output.status.code(), Some(exit_code::OK));
let parsed: Value = serde_json::from_slice(&output.stdout).expect("json");
assert_eq!(parsed.get("verb").and_then(Value::as_str), Some("dump-schema"));
assert!(parsed.get("user_version").and_then(Value::as_u64).is_some(), "user_version missing");
let tables = parsed.get("tables").and_then(Value::as_array).expect("tables array");
assert!(!tables.is_empty(), "tables must be non-empty after open");
assert!(parsed.get("indexes").and_then(Value::as_array).is_some());
drop(dir);
}
#[test]
fn t_040a_dump_row_counts_cli_emits_counts_array() {
let (dir, db) = seeded_db();
let output = fathomdb()
.args(["doctor", "dump-row-counts", "--json", db.to_str().unwrap()])
.output()
.expect("spawn");
assert_eq!(output.status.code(), Some(exit_code::OK));
let parsed: Value = serde_json::from_slice(&output.stdout).expect("json");
assert_eq!(parsed.get("verb").and_then(Value::as_str), Some("dump-row-counts"));
let counts = parsed.get("counts").and_then(Value::as_array).expect("counts");
assert!(!counts.is_empty(), "canonical-table count list must not be empty");
for entry in counts {
assert!(entry.get("name").and_then(Value::as_str).is_some());
assert!(entry.get("rows").and_then(Value::as_u64).is_some());
}
drop(dir);
}
#[test]
fn t_040a_dump_profile_cli_emits_embedder_identity_and_vectorized_kinds() {
let (dir, db) = seeded_db();
let output = fathomdb()
.args(["doctor", "dump-profile", "--json", db.to_str().unwrap()])
.output()
.expect("spawn");
assert_eq!(output.status.code(), Some(exit_code::OK));
let parsed: Value = serde_json::from_slice(&output.stdout).expect("json");
assert_eq!(parsed.get("verb").and_then(Value::as_str), Some("dump-profile"));
assert!(parsed.get("embedder_identity").and_then(Value::as_str).is_some());
assert!(parsed.get("embedder_dimension").and_then(Value::as_u64).is_some());
assert!(parsed.get("vectorized_kinds").and_then(Value::as_array).is_some());
drop(dir);
}
#[test]
fn t_clap_parse_error_on_unknown_root_is_nonzero() {
let output = fathomdb().args(["destroy-everything"]).output().expect("spawn");
assert!(
!output.status.success(),
"unknown root command must exit non-zero; got {:?}",
output.status,
);
}