mod common;
use std::process::Command;
#[test]
fn test_stale_migration_is_backfilled_and_doctor_reports_accurately() {
let (dir, bin) = common::setup_test_project("migration_backfill");
common::run_cmd(&dir, &bin, &["init", "--force", "--task-prefix", "MB"]);
let db_path = dir.join(".git/carryctx/state.sqlite");
assert!(db_path.exists(), "state.sqlite should exist after init");
let rollback_sql = "\
DELETE FROM schema_migrations WHERE version >= 8;\
CREATE TABLE checkpoints_old AS SELECT \
id, project_id, task_id, session_id, worktree_id, agent_id, \
branch, head, dirty, staged_files_json, modified_files_json, \
deleted_files_json, renamed_files_json, untracked_files_json, \
diff_files, diff_insertions, diff_deletions, done_items_json, \
remaining_items_json, blockers_json, risks_json, next_steps_json, \
notes_json, created_at \
FROM checkpoints;\
DROP TABLE checkpoints;\
ALTER TABLE checkpoints_old RENAME TO checkpoints;\
CREATE UNIQUE INDEX checkpoints_id_uq ON checkpoints(id);\
";
let sqlite_status = Command::new("sqlite3")
.arg(&db_path)
.arg(rollback_sql)
.status()
.expect("sqlite3 binary must be on PATH to run this test");
assert!(sqlite_status.success(), "sqlite3 rollback command failed");
let pragma_out = Command::new("sqlite3")
.arg(&db_path)
.arg("PRAGMA table_info(checkpoints);")
.output()
.unwrap();
let pragma = String::from_utf8_lossy(&pragma_out.stdout);
assert!(
!pragma.contains("vcs_backend"),
"test setup must produce a DB without vcs_backend: {pragma}"
);
let doctor = common::run_cmd(&dir, &bin, &["doctor", "--json"]);
let doctor_json: serde_json::Value =
serde_json::from_slice(&doctor.stdout).expect("doctor should print valid JSON");
let schema_check = doctor_json["data"]["checks"]
.as_array()
.unwrap()
.iter()
.find(|c| c["check"] == "database.schema")
.expect("doctor should report a database.schema check");
assert_eq!(
schema_check["status"], "ok",
"schema should be healthy after auto-backfill: {schema_check:?}"
);
let pragma_after = Command::new("sqlite3")
.arg(&db_path)
.arg("PRAGMA table_info(checkpoints);")
.output()
.unwrap();
let pragma_after_str = String::from_utf8_lossy(&pragma_after.stdout);
assert!(
pragma_after_str.contains("vcs_backend"),
"vcs_backend column should exist after doctor auto-backfills pending migrations: {pragma_after_str}"
);
common::run_cmd(
&dir,
&bin,
&[
"agent",
"register",
"--name",
"tester",
"--provider",
"test",
],
);
common::run_cmd(&dir, &bin, &["task", "create", "--title", "stale db task"]);
let checkpoint = common::run_cmd(
&dir,
&bin,
&["checkpoint", "--task", "MB-0001", "--note", "x", "--json"],
);
assert!(
checkpoint.status.success(),
"checkpoint must succeed on a backfilled database: {}",
String::from_utf8_lossy(&checkpoint.stderr)
);
}
#[test]
fn test_0011_backfills_ended_at_for_terminal_sessions() {
let (dir, bin) = common::setup_test_project("migration_0011");
common::run_cmd(&dir, &bin, &["init", "--force", "--task-prefix", "M1"]);
common::run_cmd(
&dir,
&bin,
&[
"agent",
"register",
"--name",
"tester",
"--provider",
"test",
],
);
let start = common::run_cmd(&dir, &bin, &["session", "start"]);
assert!(start.status.success(), "session start failed");
let end = common::run_cmd(&dir, &bin, &["session", "end"]);
assert!(end.status.success(), "session end failed");
let db_path = dir.join(".git/carryctx/state.sqlite");
let stale_sql = "\
DELETE FROM schema_migrations WHERE version >= 11;\
UPDATE sessions SET ended_at = NULL;\
";
let status = Command::new("sqlite3")
.arg(&db_path)
.arg(stale_sql)
.status()
.unwrap();
assert!(status.success(), "sqlite3 failed");
let probe = common::run_cmd(&dir, &bin, &["status"]);
assert!(
probe.status.success(),
"status should trigger migration 0011"
);
let out = Command::new("sqlite3")
.arg(&db_path)
.arg("SELECT ended_at IS NOT NULL FROM sessions;")
.output()
.unwrap();
let val = String::from_utf8_lossy(&out.stdout);
assert_eq!(
val.trim(),
"1",
"migration 0011 must backfill ended_at from last_activity_at"
);
}