use rusqlite::params;
use crate::db::test_support::ScopedTestDataDir;
use crate::{db, memory};
use super::database::{
check_database, check_pending_queue, check_raw_archive_ingest, check_worker_daemon,
};
use super::health_action::{queue_actions, render_action_block};
use super::report::{run_doctor_with_writer, DoctorOptions};
use super::schema::check_schema_migration;
#[test]
fn check_database_reports_shared_active_memory_count() {
let _test_dir = ScopedTestDataDir::new("doctor-db");
let conn = db::open_db().expect("db should open");
memory::insert_memory(
&conn,
Some("session-1"),
"proj-a",
None,
"active",
"kept",
"decision",
None,
)
.expect("active memory insert should succeed");
let archived_id = memory::insert_memory(
&conn,
Some("session-2"),
"proj-a",
None,
"archived",
"hidden",
"decision",
None,
)
.expect("archived memory insert should succeed");
conn.execute(
"UPDATE memories SET status = 'archived' WHERE id = ?1",
params![archived_id],
)
.expect("archive update should succeed");
let stats = db::query_system_stats(&conn).expect("system stats should load");
drop(conn);
let check = check_database();
assert_eq!(check.icon(), "ok");
assert!(check
.detail
.contains(&format!("{} memories", stats.active_memories)));
}
#[test]
fn health_action_queue_actions_are_empty_when_runtime_is_clear() {
let actions = queue_actions(0, 0, 0, 0);
assert!(actions.is_empty());
assert!(render_action_block(&actions).is_empty());
}
#[test]
fn health_action_queue_actions_render_copy_paste_commands() {
let actions = queue_actions(43, 1, 2, 3);
let text = render_action_block(&actions);
assert!(text.contains("Needs attention:"));
assert!(text.contains("43 failed pending observations"));
assert!(text.contains("inspect: remem pending list-failed --limit 20"));
assert!(text.contains("preview retry: remem pending retry-failed --dry-run"));
assert!(text.contains("1 expired processing pending observation"));
assert!(text.contains("2 failed jobs"));
assert!(text.contains("3 stuck jobs"));
assert!(text.contains("inspect counts: remem status --json"));
assert!(text.contains("recover: remem worker --once"));
}
#[test]
fn check_pending_queue_reports_shared_counts() -> anyhow::Result<()> {
let _test_dir = ScopedTestDataDir::new("doctor-pending");
let conn = db::open_db().expect("db should open");
db::enqueue_pending(
&conn,
"codex-cli",
"session-1",
"proj-a",
"tool",
None,
None,
None,
)
.expect("pending row insert should succeed");
let failed_id = db::enqueue_pending(
&conn,
"codex-cli",
"session-2",
"proj-a",
"tool",
None,
None,
None,
)
.expect("failed row insert should succeed");
conn.execute(
"UPDATE pending_observations SET status = 'failed' WHERE id = ?1",
params![failed_id],
)
.expect("failed status update should succeed");
let job_id = db::enqueue_job(
&conn,
"codex-cli",
db::JobType::Observation,
"proj-a",
Some("session-3"),
"{}",
1,
)
.expect("job insert should succeed");
conn.execute(
"UPDATE jobs SET state = 'processing', lease_expires_epoch = ?2 WHERE id = ?1",
params![job_id, chrono::Utc::now().timestamp() - 1],
)
.expect("job update should succeed");
let failed_job_id = db::enqueue_job(
&conn,
"codex-cli",
db::JobType::Summary,
"proj-a",
Some("session-4"),
"{}",
1,
)?;
conn.execute(
"UPDATE jobs SET state = 'failed' WHERE id = ?1",
params![failed_job_id],
)?;
let stats = db::query_system_stats(&conn).expect("system stats should load");
drop(conn);
let check = check_pending_queue();
assert_eq!(check.icon(), "WARN");
let expected_counts = format!(
"{} ready, {} delayed, {} processing ({} expired), {} failed pending; {} jobs pending, {} processing, {} failed, {} stuck",
stats.ready_pending_observations,
stats.delayed_pending_observations,
stats.processing_pending_observations,
stats.expired_processing_pending_observations,
stats.failed_pending_observations,
stats.pending_jobs,
stats.processing_jobs,
stats.failed_jobs,
stats.stuck_jobs,
);
assert!(check.detail.contains(&expected_counts), "{}", check.detail);
assert!(
check.detail.contains("will auto-recover"),
"{}",
check.detail
);
assert!(
check
.detail
.contains("inspect: `remem pending list-failed --limit 20`"),
"{}",
check.detail
);
assert!(
check
.detail
.contains("preview retry: `remem pending retry-failed --dry-run`"),
"{}",
check.detail
);
assert!(
check
.detail
.contains("inspect counts: `remem status --json`"),
"{}",
check.detail
);
assert!(
check.detail.contains("recover: `remem worker --once`"),
"{}",
check.detail
);
Ok(())
}
#[test]
fn check_raw_archive_ingest_warns_on_recorded_failures() -> anyhow::Result<()> {
let _test_dir = ScopedTestDataDir::new("doctor-raw-ingest");
let conn = db::open_db()?;
conn.execute(
"INSERT INTO raw_ingest_failures
(project, session_id, source, transcript_path, error_kind, error_message,
inserted, duplicates, parse_errors, insert_errors, created_at_epoch)
VALUES ('proj-a', 'session-a', 'transcript', '/bad/path.jsonl',
'read_error', 'read failed', 0, 0, 0, 0, ?1)",
params![chrono::Utc::now().timestamp()],
)?;
drop(conn);
let check = check_raw_archive_ingest();
assert_eq!(check.icon(), "WARN");
assert!(check.detail.contains("1 failure"), "{}", check.detail);
assert!(check.detail.contains("read_error"), "{}", check.detail);
assert!(check.detail.contains("/bad/path.jsonl"), "{}", check.detail);
Ok(())
}
#[test]
fn check_schema_migration_reads_encrypted_database() -> anyhow::Result<()> {
let test_dir = ScopedTestDataDir::new("doctor-encrypted-schema");
std::fs::create_dir_all(&test_dir.path)?;
std::fs::write(test_dir.path.join(".key"), "doctor-schema-key")?;
let conn = db::open_db()?;
drop(conn);
let check = check_schema_migration();
assert_eq!(check.icon(), "ok");
assert!(check.detail.contains("up to date"), "got: {}", check.detail);
Ok(())
}
#[test]
fn check_schema_migration_reports_v022_schema_drift() -> anyhow::Result<()> {
let _test_dir = ScopedTestDataDir::new("doctor-schema-drift");
let conn = db::open_db()?;
conn.execute_batch(
"PRAGMA foreign_keys=OFF;
DROP TABLE memory_state_keys;
PRAGMA foreign_keys=ON;",
)?;
drop(conn);
let check = check_schema_migration();
assert_eq!(check.icon(), "FAIL");
assert!(
check.detail.contains("schema drift"),
"got: {}",
check.detail
);
assert!(
check
.detail
.contains("v022_memory_state_keys marked applied"),
"got: {}",
check.detail
);
assert!(
check.detail.contains("table memory_state_keys"),
"got: {}",
check.detail
);
Ok(())
}
#[test]
fn run_doctor_with_writer_returns_outcome_and_emits_human_lines() {
let _test_dir = ScopedTestDataDir::new("doctor-run-human");
let _ = db::open_db().expect("db should open");
let mut buf: Vec<u8> = Vec::new();
let outcome = run_doctor_with_writer(DoctorOptions::default(), &mut buf)
.expect("run_doctor_with_writer should succeed");
let text = String::from_utf8(buf).expect("output should be utf-8");
assert!(text.contains("system check"));
assert!(text.contains("Database"));
if outcome.fails > 0 {
assert_eq!(outcome.exit_code(), 2);
}
}
#[test]
fn run_doctor_with_writer_emits_parseable_json() {
let _test_dir = ScopedTestDataDir::new("doctor-run-json");
let _ = db::open_db().expect("db should open");
let mut buf: Vec<u8> = Vec::new();
let outcome = run_doctor_with_writer(
DoctorOptions {
json: true,
quiet: false,
},
&mut buf,
)
.expect("run_doctor_with_writer should succeed in json mode");
let text = String::from_utf8(buf).expect("output should be utf-8");
let parsed: serde_json::Value =
serde_json::from_str(text.trim()).expect("output must be a single JSON object");
assert!(parsed["version"].is_string());
assert!(parsed["status"].is_string());
let checks = parsed["checks"].as_array().expect("checks must be array");
assert!(!checks.is_empty(), "doctor should always emit some checks");
assert_eq!(
parsed["fails"].as_u64().unwrap_or(0) as usize,
outcome.fails
);
assert_eq!(
parsed["warns"].as_u64().unwrap_or(0) as usize,
outcome.warns
);
}
#[test]
fn run_doctor_with_writer_quiet_suppresses_output() {
let _test_dir = ScopedTestDataDir::new("doctor-run-quiet");
let _ = db::open_db().expect("db should open");
let mut buf: Vec<u8> = Vec::new();
let _outcome = run_doctor_with_writer(
DoctorOptions {
json: false,
quiet: true,
},
&mut buf,
)
.expect("run_doctor_with_writer should succeed in quiet mode");
assert!(buf.is_empty(), "quiet mode must not write to stdout");
}
#[test]
fn check_worker_daemon_reports_healthy_heartbeat() {
let _test_dir = ScopedTestDataDir::new("doctor-worker-healthy");
let conn = db::open_db().expect("db should open");
let now = chrono::Utc::now().timestamp();
db::upsert_worker_heartbeat(
&conn,
"worker-daemon",
i64::from(std::process::id()),
now - 5,
now - 5,
)
.expect("heartbeat should insert");
drop(conn);
let check = check_worker_daemon();
assert_eq!(check.icon(), "ok");
assert!(check.detail.contains("healthy"));
assert!(check.detail.contains("worker-daemon"));
}
#[test]
fn check_worker_daemon_reports_missing_as_fallback_ok() -> anyhow::Result<()> {
let _test_dir = ScopedTestDataDir::new("doctor-worker-missing");
let conn = db::open_db()?;
drop(conn);
let check = check_worker_daemon();
assert_eq!(check.icon(), "ok");
assert_eq!(
check.detail,
"not running; safe fallback when Stop hooks are installed: `remem worker --once`"
);
Ok(())
}