use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use chrono::{TimeZone, Utc};
use cortex_core::{
Attestor, Event, EventId, EventSource, EventType, InMemoryAttestor, KeyLifecycleState,
TrustTier, SCHEMA_VERSION,
};
use cortex_ledger::{append_policy_decision_test_allow, current_anchor, JsonlLog};
use cortex_store::repo::{AuthorityRepo, KeyTimelineRecord, PrincipalTimelineRecord};
use rusqlite::Connection;
use serde_json::json;
fn cortex_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_cortex"))
}
fn run(args: &[&str]) -> std::process::Output {
let tmp = tempfile::tempdir().expect("isolated cli tempdir");
run_in(tmp.path(), args)
}
fn run_in(cwd: &Path, args: &[&str]) -> std::process::Output {
let data_dir = cwd.join("xdg").join("cortex");
Command::new(cortex_bin())
.current_dir(cwd)
.env("CORTEX_DATA_DIR", &data_dir)
.env("XDG_DATA_HOME", cwd.join("xdg"))
.env("HOME", cwd)
.env("APPDATA", cwd.join("appdata"))
.env("LOCALAPPDATA", cwd.join("localappdata"))
.args(args)
.output()
.expect("spawn cortex")
}
fn assert_exit(out: &std::process::Output, expected: i32) {
let code = out.status.code().expect("process exited via signal");
assert_eq!(
code,
expected,
"expected exit {expected}, got {code}\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}
fn write_json(path: &Path, value: serde_json::Value) {
fs::write(path, serde_json::to_vec_pretty(&value).unwrap()).unwrap();
}
fn blake3_digest(bytes: &[u8]) -> String {
format!("blake3:{}", blake3::hash(bytes).to_hex())
}
fn write_pre_v2_backup(bundle: &Path) -> PathBuf {
fs::create_dir_all(bundle).unwrap();
let sqlite = b"sqlite-backup-bytes";
let jsonl = b"{\"event\":\"backup\"}\n";
fs::write(bundle.join("cortex.db"), sqlite).unwrap();
fs::write(bundle.join("events.jsonl"), jsonl).unwrap();
let manifest = json!({
"kind": "cortex_pre_v2_backup",
"schema_version": 1,
"sqlite_store": "cortex.db",
"jsonl_mirror": "events.jsonl",
"tool_version": "cortex-test",
"backup_timestamp": "2026-05-04T22:00:00Z",
"sqlite_store_size_bytes": sqlite.len(),
"sqlite_store_blake3": blake3_digest(sqlite),
"jsonl_mirror_size_bytes": jsonl.len(),
"jsonl_mirror_blake3": blake3_digest(jsonl),
});
let manifest_path = bundle.join("BACKUP_MANIFEST");
write_json(&manifest_path, manifest);
manifest_path
}
fn fixture_event(seq: u64) -> Event {
Event {
id: EventId::new(),
schema_version: SCHEMA_VERSION,
observed_at: Utc::now(),
recorded_at: Utc::now(),
source: EventSource::User,
event_type: EventType::UserMessage,
trace_id: None,
session_id: None,
domain_tags: vec![],
payload: json!({ "seq": seq }),
payload_hash: String::new(),
prev_event_hash: None,
event_hash: String::new(),
}
}
fn append_fixture_event(log_path: &Path) {
let mut log = JsonlLog::open(log_path).expect("open backup JSONL log");
log.append(fixture_event(1), &append_policy_decision_test_allow())
.expect("append backup JSONL event");
}
fn write_manifest_for_artifacts(
bundle: &Path,
sqlite_source: &Path,
jsonl_source: &Path,
) -> PathBuf {
fs::create_dir_all(bundle).unwrap();
let sqlite = fs::read(sqlite_source).unwrap();
let jsonl = fs::read(jsonl_source).unwrap();
fs::write(bundle.join("cortex.db"), &sqlite).unwrap();
fs::write(bundle.join("events.jsonl"), &jsonl).unwrap();
let manifest = json!({
"kind": "cortex_pre_v2_backup",
"schema_version": 1,
"sqlite_store": "cortex.db",
"jsonl_mirror": "events.jsonl",
"tool_version": "cortex-test",
"backup_timestamp": "2026-05-04T22:00:00Z",
"sqlite_store_size_bytes": sqlite.len(),
"sqlite_store_blake3": blake3_digest(&sqlite),
"jsonl_mirror_size_bytes": jsonl.len(),
"jsonl_mirror_blake3": blake3_digest(&jsonl),
});
let manifest_path = bundle.join("BACKUP_MANIFEST");
write_json(&manifest_path, manifest);
manifest_path
}
fn read_active_artifacts(db: &Path) -> (Vec<u8>, Vec<u8>) {
let jsonl = db.parent().unwrap().join("events.jsonl");
(fs::read(db).unwrap(), fs::read(jsonl).unwrap())
}
fn apply_recovery_dir(db: &Path) -> PathBuf {
db.parent().unwrap().join(".restore-apply-recovery")
}
fn audit_count(db: &Path, operation: &str) -> i64 {
let pool = Connection::open(db).expect("open sqlite db for audit count");
pool.query_row(
"SELECT COUNT(*) FROM audit_records WHERE operation = ?1;",
[operation],
|row| row.get(0),
)
.unwrap()
}
fn audit_source_refs(db: &Path, operation: &str) -> serde_json::Value {
let pool = Connection::open(db).expect("open sqlite db for audit source refs");
let raw: String = pool
.query_row(
"SELECT source_refs_json FROM audit_records WHERE operation = ?1 ORDER BY created_at DESC LIMIT 1;",
[operation],
|row| row.get(0),
)
.unwrap();
serde_json::from_str(&raw).expect("audit source_refs_json parses")
}
fn audit_hashes(db: &Path, operation: &str) -> (Option<String>, String) {
let pool = Connection::open(db).expect("open sqlite db for audit hashes");
pool.query_row(
"SELECT before_hash, after_hash FROM audit_records WHERE operation = ?1 ORDER BY created_at DESC LIMIT 1;",
[operation],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap()
}
fn init_store(cwd: &Path) -> PathBuf {
let init = run_in(cwd, &["init"]);
assert_exit(&init, 0);
String::from_utf8_lossy(&init.stdout)
.lines()
.find(|line| line.starts_with("cortex init: db"))
.and_then(|line| {
line.split_once('=')
.and_then(|(_, rest)| rest.trim().split_once(" ("))
.map(|(path, _)| PathBuf::from(path))
})
.expect("init stdout includes db path")
}
fn apply_store_migrations(db: &Path) -> Connection {
let pool = Connection::open(db).expect("open initialized store");
cortex_store::migrate::apply_pending(&pool).expect("apply migrations");
pool
}
fn restore_attestation_key_fixture(dir: &Path) -> (PathBuf, InMemoryAttestor) {
let path = dir.join("restore-operator-attestation-key.bin");
let seed = [0x11u8; 32];
fs::write(&path, seed).expect("write attestation key fixture");
let attestor = InMemoryAttestor::from_seed(&seed);
(path, attestor)
}
fn seed_operator_authority_for_restore(db: &Path, attestor: &InMemoryAttestor) {
let pool = Connection::open(db).expect("open initialized sqlite db");
cortex_store::migrate::apply_pending(&pool).expect("apply migrations");
let repo = AuthorityRepo::new(&pool);
let effective_at = Utc.with_ymd_and_hms(2026, 1, 1, 12, 0, 1).unwrap();
repo.append_principal_state(
&PrincipalTimelineRecord {
principal_id: "operator-principal".into(),
trust_tier: TrustTier::Operator,
effective_at,
trust_review_due_at: None,
removed_at: None,
audit_ref: None,
},
&cortex_store::repo::authority::principal_state_policy_decision_test_allow(),
)
.expect("append operator trust state");
repo.append_key_state(
&KeyTimelineRecord {
key_id: attestor.key_id().to_string(),
principal_id: "operator-principal".into(),
state: KeyLifecycleState::Active,
effective_at,
reason: None,
audit_ref: None,
},
&cortex_store::repo::authority::key_state_policy_decision_test_allow(),
)
.expect("append active operator key state");
}
fn revoke_operator_authority_for_restore(
db: &Path,
attestor: &InMemoryAttestor,
effective_at: chrono::DateTime<Utc>,
) {
let pool = Connection::open(db).expect("open initialized sqlite db");
cortex_store::migrate::apply_pending(&pool).expect("apply migrations");
AuthorityRepo::new(&pool)
.append_key_state(
&KeyTimelineRecord {
key_id: attestor.key_id().to_string(),
principal_id: "operator-principal".into(),
state: KeyLifecycleState::Revoked,
effective_at,
reason: Some("test revocation".into()),
audit_ref: None,
},
&cortex_store::repo::authority::key_state_policy_decision_test_allow(),
)
.expect("append revoked operator key state");
}
fn insert_active_memory(pool: &Connection, id: &str, claim: &str, score: f64) {
pool.execute(
"INSERT INTO memories (
id, memory_type, status, claim, source_episodes_json, source_events_json,
domains_json, salience_json, confidence, authority, applies_when_json,
does_not_apply_when_json, created_at, updated_at
) VALUES (
?1, 'semantic', 'active', ?2,
'[]', '[\"evt_restore_candidate\"]', '[]', json_object('score', ?3), 0.7, 'verified',
'[]', '[]', '2026-05-04T12:00:00Z', '2026-05-04T12:00:00Z'
);",
rusqlite::params![id, claim, score],
)
.expect("insert active memory");
}
fn base_snapshot(id: &str) -> serde_json::Value {
json!({
"snapshot_id": id,
"schema_version": 2,
"truth_ceiling": {
"runtime_mode": "signed_local_ledger",
"proof_state": "full_chain_verified",
"claim_ceiling": "trusted_local_ledger"
}
})
}
#[test]
fn restore_snapshot_extracts_empty_store_semantics() {
let tmp = tempfile::tempdir().unwrap();
init_store(tmp.path());
let out = run_in(tmp.path(), &["restore", "snapshot"]);
assert_exit(&out, 0);
let snapshot: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(snapshot["snapshot_id"], "store-current");
assert_eq!(snapshot["schema_version"], serde_json::Value::Null);
assert_eq!(snapshot["active_memories"], json!({}));
assert_eq!(snapshot["active_doctrine"], json!({}));
assert_eq!(snapshot["unresolved_contradictions"], json!({}));
}
#[test]
fn restore_snapshot_extracts_candidate_store_read_only() {
let tmp = tempfile::tempdir().unwrap();
let db = init_store(tmp.path());
let pool = apply_store_migrations(&db);
insert_active_memory(
&pool,
"mem_restore_candidate",
"Candidate restore snapshot extraction is read-only.",
0.4,
);
let out = run(&["restore", "snapshot", "--store", db.to_str().unwrap()]);
assert_exit(&out, 0);
let snapshot: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(snapshot["snapshot_id"], "store-current");
assert_eq!(
snapshot["active_memories"]["mem_restore_candidate"]["claim"],
"Candidate restore snapshot extraction is read-only."
);
assert_eq!(
snapshot["active_memories"]["mem_restore_candidate"]["salience"],
40
);
assert_eq!(snapshot["salience_distribution"]["medium"], 1);
}
#[test]
fn restore_semantic_diff_compares_candidate_stores_read_only() {
let tmp = tempfile::tempdir().unwrap();
let current_root = tmp.path().join("current");
let restored_root = tmp.path().join("restored");
fs::create_dir_all(¤t_root).unwrap();
fs::create_dir_all(&restored_root).unwrap();
let current_db = init_store(¤t_root);
let restored_db = init_store(&restored_root);
let current_pool = apply_store_migrations(¤t_db);
apply_store_migrations(&restored_db);
insert_active_memory(
¤t_pool,
"mem_restore_current",
"Direct store semantic diff must see removed active memories.",
0.8,
);
let out = run(&[
"restore",
"semantic-diff",
"--current-store",
current_db.to_str().unwrap(),
"--restored-store",
restored_db.to_str().unwrap(),
]);
assert_exit(&out, 7);
let report: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(report["command"], "restore.semantic_diff");
assert_eq!(report["severity"], "precondition_unmet");
assert_eq!(report["mutated_store"], false);
assert!(
out.stdout
.windows("active_memory_missing".len())
.any(|window| window == b"active_memory_missing"),
"stdout should name removed memory category: {}",
String::from_utf8_lossy(&out.stdout)
);
}
#[test]
fn restore_semantic_diff_clean_snapshots_pass_without_mutation() {
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join("current.json");
let restored = tmp.path().join("restored.json");
write_json(¤t, base_snapshot("snap-a"));
write_json(&restored, base_snapshot("snap-b"));
let out = run(&[
"restore",
"semantic-diff",
"--current",
current.to_str().unwrap(),
"--restored",
restored.to_str().unwrap(),
]);
assert_exit(&out, 0);
let report: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(report["command"], "restore.semantic_diff");
assert_eq!(report["severity"], "clean");
assert_eq!(report["change_count"], 0);
assert_eq!(report["mutated_store"], false);
}
#[test]
fn restore_semantic_diff_blocks_truth_ceiling_downgrade() {
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join("current.json");
let restored = tmp.path().join("restored.json");
write_json(¤t, base_snapshot("snap-current"));
let mut restored_json = base_snapshot("snap-restored");
restored_json["truth_ceiling"]["claim_ceiling"] = json!("advisory");
write_json(&restored, restored_json);
let out = run(&[
"restore",
"semantic-diff",
"--current",
current.to_str().unwrap(),
"--restored",
restored.to_str().unwrap(),
]);
assert_exit(&out, 7);
let report: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(report["severity"], "precondition_unmet");
assert_eq!(report["mutated_store"], false);
assert!(
out.stdout
.windows("truth_ceiling_downgraded".len())
.any(|window| window == b"truth_ceiling_downgraded"),
"stdout should name downgrade category: {}",
String::from_utf8_lossy(&out.stdout)
);
}
#[test]
fn restore_semantic_diff_malformed_snapshot_fails_closed() {
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join("current.json");
let restored = tmp.path().join("restored.json");
fs::write(¤t, b"{not-json").unwrap();
write_json(&restored, base_snapshot("snap-restored"));
let out = run(&[
"restore",
"semantic-diff",
"--current",
current.to_str().unwrap(),
"--restored",
restored.to_str().unwrap(),
]);
assert_exit(&out, 5);
assert!(out.stdout.is_empty());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("failed to parse current snapshot"),
"stderr: {stderr}"
);
}
#[test]
fn restore_verify_backup_accepts_pre_v2_manifest_without_mutation() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let out = run(&[
"restore",
"verify-backup",
"--manifest",
manifest.to_str().unwrap(),
]);
assert_exit(&out, 0);
let report: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(report["command"], "restore.verify_backup");
assert_eq!(report["kind"], "cortex_pre_v2_backup");
assert_eq!(report["schema_version"], 1);
assert_eq!(report["restore_performed"], false);
assert_eq!(report["cutover_performed"], false);
assert_eq!(report["destructive_restore_supported"], false);
assert_eq!(report["mutated_store"], false);
assert_eq!(report["artifacts"][0]["field"], "sqlite_store");
assert_eq!(report["artifacts"][1]["field"], "jsonl_mirror");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("destructive restore/cutover is not implemented"),
"stderr: {stderr}"
);
}
#[test]
fn restore_preflight_reports_semantic_diff_pending_without_candidate_store() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let out = run(&[
"restore",
"preflight",
"--manifest",
manifest.to_str().unwrap(),
]);
assert_exit(&out, 7);
let report: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(report["command"], "restore.preflight");
assert_eq!(report["structural_verification"]["status"], "verified");
assert_eq!(report["semantic_diff"]["required"], true);
assert_eq!(report["semantic_diff"]["executed"], false);
assert_eq!(report["semantic_diff"]["status"], "required_not_executed");
assert_eq!(report["restore_performed"], false);
assert_eq!(report["cutover_performed"], false);
assert_eq!(report["mutated_store"], false);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("no state was changed"), "stderr: {stderr}");
}
#[test]
fn restore_preflight_runs_clean_semantic_diff_against_candidate_store_read_only() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let current_root = tmp.path().join("current");
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(¤t_root).unwrap();
fs::create_dir_all(&candidate_root).unwrap();
let current_db = init_store(¤t_root);
let candidate_db = init_store(&candidate_root);
apply_store_migrations(¤t_db);
apply_store_migrations(&candidate_db);
let out = run(&[
"restore",
"preflight",
"--manifest",
manifest.to_str().unwrap(),
"--current-store",
current_db.to_str().unwrap(),
"--candidate-store",
candidate_db.to_str().unwrap(),
]);
assert_exit(&out, 0);
let report: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(report["command"], "restore.preflight");
assert_eq!(report["structural_verification"]["status"], "verified");
assert_eq!(report["semantic_diff"]["required"], true);
assert_eq!(report["semantic_diff"]["executed"], true);
assert_eq!(report["semantic_diff"]["status"], "clean");
assert_eq!(report["semantic_diff"]["change_count"], 0);
assert_eq!(report["restore_performed"], false);
assert_eq!(report["cutover_performed"], false);
assert_eq!(report["mutated_store"], false);
}
#[test]
fn restore_preflight_production_plan_acquires_releases_temp_lock_without_mutation() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let current_root = tmp.path().join("current");
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(¤t_root).unwrap();
fs::create_dir_all(&candidate_root).unwrap();
let current_db = init_store(¤t_root);
let candidate_db = init_store(&candidate_root);
apply_store_migrations(¤t_db);
apply_store_migrations(&candidate_db);
let before = read_active_artifacts(¤t_db);
let lock_marker = current_db
.parent()
.unwrap()
.join(".cortex-restore-active-store.lock");
assert!(!lock_marker.exists());
let out = run(&[
"restore",
"preflight",
"--manifest",
manifest.to_str().unwrap(),
"--current-store",
current_db.to_str().unwrap(),
"--candidate-store",
candidate_db.to_str().unwrap(),
"--production-active-store-plan",
]);
assert_exit(&out, 7);
let report: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(report["command"], "restore.preflight");
assert_eq!(report["structural_verification"]["status"], "verified");
assert_eq!(report["semantic_diff"]["status"], "clean");
assert_eq!(report["production_active_store_plan"]["requested"], true);
assert_eq!(report["production_active_store_plan"]["status"], "blocked");
assert_eq!(
report["production_active_store_plan"]["mutation_supported"],
false
);
assert_eq!(
report["production_active_store_plan"]["preflight_gates"]["semantic_diff"],
"satisfied"
);
assert_eq!(
report["production_active_store_plan"]["preflight_gates"]["active_store_lock"],
"temp_lock_acquire_release_verified"
);
assert_eq!(
report["production_active_store_plan"]["preflight_gates"]["schema_v2_cutover"],
"blocked"
);
assert_eq!(
report["production_active_store_plan"]["preflight_gates"]["post_restore_verification"],
"blocked"
);
assert_eq!(
report["production_active_store_plan"]["post_restore_verification_gates"]["status"],
"blocked"
);
assert_eq!(
report["production_active_store_plan"]["post_restore_verification_gates"]["anchors"]
["status"],
"missing_external_anchor_authority"
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]["marker_exists"],
false
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]["absence_satisfied"],
true
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]["exclusive_lock_acquired"],
false
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]
["exclusive_lock_acquire_attempted"],
true
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]
["exclusive_lock_acquired_during_probe"],
true
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]["exclusive_lock_released"],
true
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]
["exclusive_lock_acquire_release_verified"],
true
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]["lock_marker"].as_str(),
Some(lock_marker.to_string_lossy().as_ref())
);
assert!(
!lock_marker.exists(),
"lock probe must release the temp-test marker"
);
assert!(
report["production_active_store_plan"]["required_gates"]
.as_array()
.unwrap()
.iter()
.any(|gate| gate["gate"] == "active_store_lock"),
"production plan must name the active-store lock gate: {}",
String::from_utf8_lossy(&out.stdout)
);
assert!(
report["production_active_store_plan"]["required_gates"]
.as_array()
.unwrap()
.iter()
.any(|gate| gate["gate"] == "schema_v2_cutover"),
"production plan must name the schema v2 blocker: {}",
String::from_utf8_lossy(&out.stdout)
);
assert_eq!(report["restore_performed"], false);
assert_eq!(report["cutover_performed"], false);
assert_eq!(report["mutated_store"], false);
assert_eq!(read_active_artifacts(¤t_db), before);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("production active-store restore plan is blocked")
&& stderr.contains("no state was changed"),
"stderr: {stderr}"
);
}
#[test]
fn restore_preflight_production_plan_reports_existing_active_store_lock_marker() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let current_root = tmp.path().join("current");
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(¤t_root).unwrap();
fs::create_dir_all(&candidate_root).unwrap();
let current_db = init_store(¤t_root);
let candidate_db = init_store(&candidate_root);
apply_store_migrations(¤t_db);
apply_store_migrations(&candidate_db);
let lock_marker = tmp.path().join("active-store.lock");
fs::write(&lock_marker, b"held-by-test\n").unwrap();
let before = read_active_artifacts(¤t_db);
let out = run(&[
"restore",
"preflight",
"--manifest",
manifest.to_str().unwrap(),
"--current-store",
current_db.to_str().unwrap(),
"--candidate-store",
candidate_db.to_str().unwrap(),
"--production-active-store-plan",
"--active-store-lock-marker",
lock_marker.to_str().unwrap(),
]);
assert_exit(&out, 7);
let report: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(report["command"], "restore.preflight");
assert_eq!(
report["production_active_store_plan"]["preflight_gates"]["active_store_lock"],
"blocked_existing_lock_marker"
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]["lock_marker"].as_str(),
Some(lock_marker.to_string_lossy().as_ref())
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]["marker_exists"],
true
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]["absence_satisfied"],
false
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]["exclusive_lock_acquired"],
false
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]
["exclusive_lock_acquire_attempted"],
false
);
assert_eq!(
report["production_active_store_plan"]["active_store_lock"]
["exclusive_lock_acquire_release_verified"],
false
);
assert_eq!(report["restore_performed"], false);
assert_eq!(report["cutover_performed"], false);
assert_eq!(report["mutated_store"], false);
assert_eq!(read_active_artifacts(¤t_db), before);
assert_eq!(fs::read(&lock_marker).unwrap(), b"held-by-test\n");
}
#[test]
fn restore_verify_backup_rejects_wrong_manifest_kind() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let mut value: serde_json::Value =
serde_json::from_slice(&fs::read(&manifest).unwrap()).unwrap();
value["kind"] = json!("cortex_untrusted_backup");
write_json(&manifest, value);
let out = run(&[
"restore",
"verify-backup",
"--manifest",
manifest.to_str().unwrap(),
]);
assert_exit(&out, 7);
assert!(out.stdout.is_empty());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("expected cortex_pre_v2_backup") && stderr.contains("no state was changed"),
"stderr: {stderr}"
);
}
#[test]
fn restore_verify_backup_rejects_missing_digest_fields() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let mut value: serde_json::Value =
serde_json::from_slice(&fs::read(&manifest).unwrap()).unwrap();
value.as_object_mut().unwrap().remove("jsonl_mirror_blake3");
write_json(&manifest, value);
let out = run(&[
"restore",
"verify-backup",
"--manifest",
manifest.to_str().unwrap(),
]);
assert_exit(&out, 7);
assert!(out.stdout.is_empty());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("malformed") && stderr.contains("jsonl_mirror_blake3"),
"stderr: {stderr}"
);
}
#[test]
fn restore_verify_backup_rejects_artifact_digest_mismatch() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let jsonl = manifest.parent().unwrap().join("events.jsonl");
let original_len = fs::metadata(&jsonl).unwrap().len() as usize;
fs::write(&jsonl, vec![b'x'; original_len]).unwrap();
let out = run(&[
"restore",
"verify-backup",
"--manifest",
manifest.to_str().unwrap(),
]);
assert_exit(&out, 5);
assert!(out.stdout.is_empty());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("digest mismatch") && stderr.contains("no state was changed"),
"stderr: {stderr}"
);
}
#[test]
fn restore_stage_requires_acknowledgement_without_mutation() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let stage_dir = tmp.path().join("stage");
let out = run(&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
]);
assert_exit(&out, 7);
assert!(out.stdout.is_empty());
assert!(
!stage_dir.exists(),
"stage directory must not be created without acknowledgement"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--acknowledge-destructive-restore")
&& stderr.contains("no state was changed"),
"stderr: {stderr}"
);
}
#[test]
fn restore_stage_rejects_corrupt_backup_before_staging() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let jsonl = manifest.parent().unwrap().join("events.jsonl");
let original_len = fs::metadata(&jsonl).unwrap().len() as usize;
fs::write(&jsonl, vec![b'x'; original_len]).unwrap();
let stage_dir = tmp.path().join("stage");
let out = run(&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--acknowledge-destructive-restore",
]);
assert_exit(&out, 5);
assert!(out.stdout.is_empty());
assert!(
!stage_dir.exists(),
"stage directory must not be created for a corrupt backup"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("digest mismatch") && stderr.contains("no state was changed"),
"stderr: {stderr}"
);
}
#[test]
fn restore_stage_copies_then_audit_verifies_and_semantic_preflights_candidate() {
let tmp = tempfile::tempdir().unwrap();
let current_root = tmp.path().join("current");
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(¤t_root).unwrap();
fs::create_dir_all(&candidate_root).unwrap();
let current_db = init_store(¤t_root);
let candidate_db = init_store(&candidate_root);
apply_store_migrations(¤t_db);
apply_store_migrations(&candidate_db);
let candidate_log = candidate_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&candidate_log);
let anchor = current_anchor(&candidate_log, Utc::now()).unwrap();
let anchor_path = tmp.path().join("RESTORE_ANCHOR");
fs::write(&anchor_path, anchor.to_anchor_text()).unwrap();
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &candidate_db, &candidate_log);
let stage_dir = tmp.path().join("stage");
let out = run(&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--current-store",
current_db.to_str().unwrap(),
"--acknowledge-destructive-restore",
]);
assert_exit(&out, 0);
let report: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(report["command"], "restore.stage");
assert_eq!(report["structural_verification"]["status"], "verified");
assert_eq!(report["audit_verification"]["status"], "verified");
assert_eq!(report["audit_verification"]["rows_scanned"], 1);
assert_eq!(report["semantic_diff"]["executed"], true);
assert_eq!(report["semantic_diff"]["status"], "clean");
assert_eq!(report["destructive_restore_staged"], true);
assert_eq!(report["restore_performed"], false);
assert_eq!(report["cutover_performed"], false);
assert_eq!(report["mutated_store"], false);
assert!(stage_dir.join("cortex.db").is_file());
assert!(stage_dir.join("events.jsonl").is_file());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("active store was not changed"),
"stderr: {stderr}"
);
}
#[test]
fn restore_apply_stage_requires_acknowledgement_without_mutating_active_store() {
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let before = read_active_artifacts(&active_db);
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(&candidate_root).unwrap();
let candidate_db = init_store(&candidate_root);
apply_store_migrations(&candidate_db);
let candidate_log = candidate_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&candidate_log);
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &candidate_db, &candidate_log);
let stage_dir = tmp.path().join("stage");
let stage = run_in(
tmp.path(),
&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--current-store",
active_db.to_str().unwrap(),
"--acknowledge-destructive-restore",
],
);
assert_exit(&stage, 0);
let out = run_in(
tmp.path(),
&[
"restore",
"apply-stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
],
);
assert_exit(&out, 7);
assert!(out.stdout.is_empty());
assert_eq!(read_active_artifacts(&active_db), before);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--acknowledge-active-store-replacement")
&& stderr.contains("--acknowledge-temp-test-data-dir")
&& stderr.contains("active store was not changed"),
"stderr: {stderr}"
);
}
#[test]
fn restore_apply_stage_rejects_corrupt_stage_without_mutating_active_store() {
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let before = read_active_artifacts(&active_db);
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(&candidate_root).unwrap();
let candidate_db = init_store(&candidate_root);
apply_store_migrations(&candidate_db);
let candidate_log = candidate_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&candidate_log);
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &candidate_db, &candidate_log);
let stage_dir = tmp.path().join("stage");
let stage = run_in(
tmp.path(),
&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--current-store",
active_db.to_str().unwrap(),
"--acknowledge-destructive-restore",
],
);
assert_exit(&stage, 0);
let staged_log = stage_dir.join("events.jsonl");
let original_len = fs::metadata(&staged_log).unwrap().len() as usize;
fs::write(&staged_log, vec![b'x'; original_len]).unwrap();
let out = run_in(
tmp.path(),
&[
"restore",
"apply-stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--acknowledge-active-store-replacement",
"--acknowledge-temp-test-data-dir",
],
);
assert_exit(&out, 5);
assert!(out.stdout.is_empty());
assert_eq!(read_active_artifacts(&active_db), before);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("digest mismatch") && stderr.contains("active store was not changed"),
"stderr: {stderr}"
);
}
#[test]
fn restore_apply_stage_fails_closed_when_recovery_evidence_exists() {
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let before = read_active_artifacts(&active_db);
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(&candidate_root).unwrap();
let candidate_db = init_store(&candidate_root);
apply_store_migrations(&candidate_db);
let candidate_log = candidate_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&candidate_log);
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &candidate_db, &candidate_log);
let stage_dir = tmp.path().join("stage");
let stage = run_in(
tmp.path(),
&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--current-store",
active_db.to_str().unwrap(),
"--acknowledge-destructive-restore",
],
);
assert_exit(&stage, 0);
fs::create_dir(apply_recovery_dir(&active_db)).unwrap();
let out = run_in(
tmp.path(),
&[
"restore",
"apply-stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--acknowledge-active-store-replacement",
"--acknowledge-temp-test-data-dir",
],
);
assert_exit(&out, 7);
assert!(out.stdout.is_empty());
assert_eq!(read_active_artifacts(&active_db), before);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("recovery evidence directory")
&& stderr.contains("active store was not changed"),
"stderr: {stderr}"
);
}
#[test]
fn restore_apply_stage_applies_clean_candidate_in_temp_test_data_dir() {
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let before = read_active_artifacts(&active_db);
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(&candidate_root).unwrap();
let candidate_db = init_store(&candidate_root);
apply_store_migrations(&candidate_db);
let candidate_log = candidate_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&candidate_log);
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &candidate_db, &candidate_log);
let stage_dir = tmp.path().join("stage");
let stage = run_in(
tmp.path(),
&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--current-store",
active_db.to_str().unwrap(),
"--acknowledge-destructive-restore",
],
);
assert_exit(&stage, 0);
let out = run_in(
tmp.path(),
&[
"restore",
"apply-stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--acknowledge-active-store-replacement",
"--acknowledge-temp-test-data-dir",
],
);
assert_exit(&out, 0);
let report: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(report["command"], "restore.apply_stage");
assert_eq!(report["structural_verification"]["status"], "verified");
assert_eq!(report["staged_artifacts"], "verified");
assert_eq!(report["audit_verification"]["status"], "verified");
assert_eq!(report["semantic_diff"]["status"], "clean");
assert_eq!(
report["recovery_evidence"]["status"],
"prepared_before_active_replacement"
);
assert_eq!(
report["post_restore_verification"]["manifest_artifacts"]["status"],
"source_verified_active_jsonl_verified_sqlite_payload_semantically_verified"
);
assert_eq!(
report["post_restore_verification"]["manifest_artifacts"]["jsonl_mirror"]["status"],
"active_digest_verified"
);
assert_eq!(
report["post_restore_verification"]["manifest_artifacts"]["sqlite_store"]
["active_exact_digest"],
"not_claimed_final_sqlite_contains_command_audit_row"
);
assert_eq!(
report["post_restore_verification"]["manifest_artifacts"]["sqlite_store"]
["final_active_digest_claimed"],
false
);
assert_eq!(
report["post_restore_verification"]["jsonl_audit"]["status"],
"verified"
);
assert_eq!(
report["post_restore_verification"]["semantic_diff"]["status"],
"clean"
);
assert_eq!(
report["post_restore_verification"]["semantic_diff"]["comparison"],
"staged_candidate_to_restored_active_store"
);
assert_eq!(
report["post_restore_verification"]["anchors"]["status"],
"not_configured"
);
assert_eq!(
report["post_restore_verification"]["anchors"]["single_anchor"],
serde_json::Value::Null
);
assert_eq!(
report["post_restore_verification"]["anchors"]["production_anchor_authority"],
false
);
assert_eq!(
report["post_restore_verification"]["production_eligible"],
false
);
assert_eq!(report["restore_performed"], true);
assert_eq!(report["cutover_performed"], true);
assert_eq!(report["mutated_store"], true);
assert_eq!(report["audit_operation"], "command.restore.apply_stage");
assert_eq!(audit_count(&active_db, "command.restore.apply_stage"), 1);
let audit_refs = audit_source_refs(&active_db, "command.restore.apply_stage");
let (_, after_hash) = audit_hashes(&active_db, "command.restore.apply_stage");
assert_eq!(
after_hash,
audit_refs["sqlite_store_blake3"].as_str().unwrap()
);
assert_eq!(
audit_refs["manifest"].as_str(),
Some(manifest.to_string_lossy().as_ref())
);
assert_eq!(
audit_refs["after_hash_semantics"],
"verified restored SQLite payload before command audit row append; final active SQLite digest is not self-claimed by the row"
);
assert_eq!(audit_refs["scope"], "temp-test");
let recovery_manifest =
PathBuf::from(report["recovery_evidence"]["manifest"].as_str().unwrap());
let active_db_backup = PathBuf::from(
report["recovery_evidence"]["active_db_backup"]
.as_str()
.unwrap(),
);
let active_event_log_backup = PathBuf::from(
report["recovery_evidence"]["active_event_log_backup"]
.as_str()
.unwrap(),
);
assert!(recovery_manifest.is_file());
assert_eq!(fs::read(&active_db_backup).unwrap(), before.0);
assert_eq!(fs::read(&active_event_log_backup).unwrap(), before.1);
let manifest_json: serde_json::Value =
serde_json::from_slice(&fs::read(recovery_manifest).unwrap()).unwrap();
assert_eq!(
manifest_json["kind"],
"cortex_restore_apply_stage_recovery_manifest"
);
assert_eq!(manifest_json["scope"], "temp-test");
assert_eq!(
manifest_json["status"],
"prepared_before_active_replacement"
);
assert_ne!(
fs::read(&active_db).unwrap(),
fs::read(stage_dir.join("cortex.db")).unwrap(),
"active SQLite store should include the persisted apply-stage command audit row"
);
assert_eq!(
fs::read(active_db.parent().unwrap().join("events.jsonl")).unwrap(),
fs::read(stage_dir.join("events.jsonl")).unwrap()
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("active store was mutated"),
"stderr: {stderr}"
);
}
#[test]
fn restore_apply_stage_rolls_back_when_post_restore_anchor_verification_fails() {
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let before = read_active_artifacts(&active_db);
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(&candidate_root).unwrap();
let candidate_db = init_store(&candidate_root);
apply_store_migrations(&candidate_db);
let candidate_log = candidate_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&candidate_log);
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &candidate_db, &candidate_log);
let stage_dir = tmp.path().join("stage");
let stage = run_in(
tmp.path(),
&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--current-store",
active_db.to_str().unwrap(),
"--acknowledge-destructive-restore",
],
);
assert_exit(&stage, 0);
let stale_anchor = current_anchor(&candidate_log, Utc::now()).unwrap();
let stale_anchor_text = stale_anchor.to_anchor_text().replace(
&stale_anchor.chain_head_hash,
"0000000000000000000000000000000000000000000000000000000000000000",
);
let stale_anchor_path = tmp.path().join("STALE_RESTORE_ANCHOR");
fs::write(&stale_anchor_path, stale_anchor_text).unwrap();
let out = run_in(
tmp.path(),
&[
"restore",
"apply-stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--acknowledge-active-store-replacement",
"--acknowledge-temp-test-data-dir",
"--post-restore-anchor",
stale_anchor_path.to_str().unwrap(),
],
);
assert_exit(&out, 3);
assert!(out.stdout.is_empty());
assert_eq!(read_active_artifacts(&active_db), before);
let recovery_dir = apply_recovery_dir(&active_db);
assert!(
recovery_dir.join("RECOVERY_MANIFEST.json").is_file(),
"failed post-restore verification should leave recovery evidence for operator audit"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("post-restore anchor verification failed")
&& stderr.contains("active backups were restored")
&& stderr.contains("pre-apply state"),
"stderr: {stderr}"
);
}
#[test]
fn restore_recover_apply_requires_acknowledgement_without_mutating_active_store() {
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(&candidate_root).unwrap();
let candidate_db = init_store(&candidate_root);
apply_store_migrations(&candidate_db);
let candidate_log = candidate_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&candidate_log);
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &candidate_db, &candidate_log);
let stage_dir = tmp.path().join("stage");
let stage = run_in(
tmp.path(),
&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--current-store",
active_db.to_str().unwrap(),
"--acknowledge-destructive-restore",
],
);
assert_exit(&stage, 0);
let apply = run_in(
tmp.path(),
&[
"restore",
"apply-stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--acknowledge-active-store-replacement",
"--acknowledge-temp-test-data-dir",
],
);
assert_exit(&apply, 0);
let apply_report: serde_json::Value = serde_json::from_slice(&apply.stdout).unwrap();
let recovery_manifest = PathBuf::from(
apply_report["recovery_evidence"]["manifest"]
.as_str()
.unwrap(),
);
let after_apply = read_active_artifacts(&active_db);
let out = run_in(
tmp.path(),
&[
"restore",
"recover-apply",
"--manifest",
recovery_manifest.to_str().unwrap(),
],
);
assert_exit(&out, 7);
assert!(out.stdout.is_empty());
assert_eq!(read_active_artifacts(&active_db), after_apply);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--acknowledge-current-backup-restore")
&& stderr.contains("--acknowledge-temp-test-data-dir")
&& stderr.contains("active store was not changed"),
"stderr: {stderr}"
);
}
#[cfg(unix)]
#[test]
fn restore_recover_apply_rejects_symlinked_active_target_without_writing_through() {
use std::os::unix::fs::symlink;
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(&candidate_root).unwrap();
let candidate_db = init_store(&candidate_root);
apply_store_migrations(&candidate_db);
let candidate_log = candidate_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&candidate_log);
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &candidate_db, &candidate_log);
let stage_dir = tmp.path().join("stage");
let stage = run_in(
tmp.path(),
&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--current-store",
active_db.to_str().unwrap(),
"--acknowledge-destructive-restore",
],
);
assert_exit(&stage, 0);
let apply = run_in(
tmp.path(),
&[
"restore",
"apply-stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--acknowledge-active-store-replacement",
"--acknowledge-temp-test-data-dir",
],
);
assert_exit(&apply, 0);
let apply_report: serde_json::Value = serde_json::from_slice(&apply.stdout).unwrap();
let recovery_manifest = PathBuf::from(
apply_report["recovery_evidence"]["manifest"]
.as_str()
.unwrap(),
);
let outside_target = tmp.path().join("outside-target.db");
fs::write(&outside_target, b"outside target must not be overwritten").unwrap();
fs::remove_file(&active_db).unwrap();
symlink(&outside_target, &active_db).unwrap();
let outside_before = fs::read(&outside_target).unwrap();
let out = run_in(
tmp.path(),
&[
"restore",
"recover-apply",
"--manifest",
recovery_manifest.to_str().unwrap(),
"--acknowledge-current-backup-restore",
"--acknowledge-temp-test-data-dir",
],
);
assert_exit(&out, 7);
assert!(out.stdout.is_empty());
assert_eq!(
fs::read(&outside_target).unwrap(),
outside_before,
"recover-apply must not write through a symlinked active SQLite target"
);
assert!(
fs::symlink_metadata(&active_db)
.unwrap()
.file_type()
.is_symlink(),
"failed recovery must leave the symlink target untouched for operator inspection"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("active SQLite path")
&& stderr.contains("is a symlink")
&& stderr.contains("active store was not changed"),
"stderr: {stderr}"
);
}
#[cfg(unix)]
#[test]
fn restore_recover_apply_rejects_symlinked_active_jsonl_without_writing_through() {
use std::os::unix::fs::symlink;
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let active_jsonl = active_db.parent().unwrap().join("events.jsonl");
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(&candidate_root).unwrap();
let candidate_db = init_store(&candidate_root);
apply_store_migrations(&candidate_db);
let candidate_log = candidate_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&candidate_log);
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &candidate_db, &candidate_log);
let stage_dir = tmp.path().join("stage");
let stage = run_in(
tmp.path(),
&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--current-store",
active_db.to_str().unwrap(),
"--acknowledge-destructive-restore",
],
);
assert_exit(&stage, 0);
let apply = run_in(
tmp.path(),
&[
"restore",
"apply-stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--acknowledge-active-store-replacement",
"--acknowledge-temp-test-data-dir",
],
);
assert_exit(&apply, 0);
let apply_report: serde_json::Value = serde_json::from_slice(&apply.stdout).unwrap();
let recovery_manifest = PathBuf::from(
apply_report["recovery_evidence"]["manifest"]
.as_str()
.unwrap(),
);
let outside_target = tmp.path().join("outside-target.events.jsonl");
fs::write(&outside_target, b"outside target must not be overwritten").unwrap();
fs::remove_file(&active_jsonl).unwrap();
symlink(&outside_target, &active_jsonl).unwrap();
let outside_before = fs::read(&outside_target).unwrap();
let out = run_in(
tmp.path(),
&[
"restore",
"recover-apply",
"--manifest",
recovery_manifest.to_str().unwrap(),
"--acknowledge-current-backup-restore",
"--acknowledge-temp-test-data-dir",
],
);
assert_exit(&out, 7);
assert!(out.stdout.is_empty());
assert_eq!(
fs::read(&outside_target).unwrap(),
outside_before,
"recover-apply must not write through a symlinked active JSONL target"
);
assert!(
fs::symlink_metadata(&active_jsonl)
.unwrap()
.file_type()
.is_symlink(),
"failed recovery must leave the symlink target untouched for operator inspection"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("active JSONL path")
&& stderr.contains("is a symlink")
&& stderr.contains("active store was not changed"),
"stderr: {stderr}"
);
}
#[test]
fn restore_recover_apply_restores_current_backups_from_apply_recovery_manifest() {
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let before = read_active_artifacts(&active_db);
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(&candidate_root).unwrap();
let candidate_db = init_store(&candidate_root);
apply_store_migrations(&candidate_db);
let candidate_log = candidate_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&candidate_log);
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &candidate_db, &candidate_log);
let stage_dir = tmp.path().join("stage");
let stage = run_in(
tmp.path(),
&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--current-store",
active_db.to_str().unwrap(),
"--acknowledge-destructive-restore",
],
);
assert_exit(&stage, 0);
let apply = run_in(
tmp.path(),
&[
"restore",
"apply-stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--acknowledge-active-store-replacement",
"--acknowledge-temp-test-data-dir",
],
);
assert_exit(&apply, 0);
let apply_report: serde_json::Value = serde_json::from_slice(&apply.stdout).unwrap();
let recovery_manifest = PathBuf::from(
apply_report["recovery_evidence"]["manifest"]
.as_str()
.unwrap(),
);
assert_ne!(read_active_artifacts(&active_db), before);
let (attestation_key, attestor) = restore_attestation_key_fixture(tmp.path());
seed_operator_authority_for_restore(&active_db, &attestor);
let out = run_in(
tmp.path(),
&[
"restore",
"recover-apply",
"--manifest",
recovery_manifest.to_str().unwrap(),
"--acknowledge-current-backup-restore",
"--acknowledge-temp-test-data-dir",
"--attestation",
attestation_key.to_str().unwrap(),
],
);
assert_exit(&out, 0);
let report: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(report["command"], "restore.recover_apply");
assert_eq!(report["scope"], "temp-test");
assert_eq!(
report["recovery_status"],
"current_backups_restored_with_command_audit_row"
);
assert_eq!(report["restore_performed"], true);
assert_eq!(report["cutover_performed"], true);
assert_eq!(report["mutated_store"], true);
assert_eq!(report["audit_operation"], "command.restore.recover_apply");
assert_eq!(audit_count(&active_db, "command.restore.recover_apply"), 1);
let audit_refs = audit_source_refs(&active_db, "command.restore.recover_apply");
let (_, after_hash) = audit_hashes(&active_db, "command.restore.recover_apply");
assert_eq!(
after_hash,
audit_refs["active_db_backup_blake3"].as_str().unwrap()
);
assert_eq!(
audit_refs["manifest"].as_str(),
Some(recovery_manifest.to_string_lossy().as_ref())
);
assert_eq!(audit_refs["scope"], "temp-test");
assert_eq!(
audit_refs["after_hash_semantics"],
"verified recovered SQLite backup payload before command audit row append; final active SQLite digest is not self-claimed by the row"
);
assert_ne!(
fs::read(&active_db).unwrap(),
before.0,
"recovered SQLite store should include the persisted recover-apply command audit row"
);
assert_eq!(
fs::read(active_db.parent().unwrap().join("events.jsonl")).unwrap(),
before.1
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("current backups restored") && stderr.contains("active store was mutated"),
"stderr: {stderr}"
);
}
fn production_apply_stage_dir(base: &Path) -> PathBuf {
let stage = base.join("stage");
fs::create_dir_all(&stage).unwrap();
fs::write(stage.join("cortex.db"), b"placeholder-sqlite").unwrap();
fs::write(stage.join("events.jsonl"), b"{}\n").unwrap();
stage
}
fn dummy_path(base: &Path, name: &str) -> PathBuf {
let path = base.join(name);
fs::write(&path, b"placeholder").unwrap();
path
}
#[test]
fn production_restore_rejects_unknown_sink_kind() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let stage = production_apply_stage_dir(tmp.path());
let restore_intent = dummy_path(tmp.path(), "RESTORE_INTENT.json");
let restore_intent_sig = dummy_path(tmp.path(), "RESTORE_INTENT.sig");
let operator_key = dummy_path(tmp.path(), "operator.pubkey");
let anchor_path = dummy_path(tmp.path(), "ANCHOR.json");
let anchor_history_path = dummy_path(tmp.path(), "ANCHOR_HISTORY");
let sink_path = tmp.path().join("sink");
let out = run(&[
"restore",
"apply",
"--production",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage.to_str().unwrap(),
"--restore-intent",
restore_intent.to_str().unwrap(),
"--restore-intent-signature",
restore_intent_sig.to_str().unwrap(),
"--operator-verification-key",
operator_key.to_str().unwrap(),
"--against",
anchor_path.to_str().unwrap(),
"--against-history",
anchor_history_path.to_str().unwrap(),
"--anchor-sink",
"definitely-not-a-real-sink",
"--sink-path",
sink_path.to_str().unwrap(),
"--acknowledge-production-destructive-restore",
"--acknowledge-active-store-replacement",
]);
assert_exit(&out, 7);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("restore.production.sink_kind.unknown"),
"stable invariant token must surface; stderr: {stderr}"
);
assert!(
stderr.contains("active store was not changed"),
"fail-closed clause must surface; stderr: {stderr}"
);
assert!(!sink_path.exists(), "no sink artifact may be written");
}
#[test]
fn production_restore_refuses_legacy_external_append_only_sink_at_parser() {
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let active_jsonl = active_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&active_jsonl);
let active_db_bytes_before = fs::read(&active_db).unwrap();
let active_jsonl_bytes_before = fs::read(&active_jsonl).unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let stage = production_apply_stage_dir(tmp.path());
let restore_intent = dummy_path(tmp.path(), "RESTORE_INTENT.json");
let restore_intent_sig = dummy_path(tmp.path(), "RESTORE_INTENT.sig");
let operator_key = dummy_path(tmp.path(), "operator.pubkey");
let anchor_path = dummy_path(tmp.path(), "ANCHOR.json");
let anchor_history_path = dummy_path(tmp.path(), "ANCHOR_HISTORY");
let sink_path = dummy_path(tmp.path(), "sink");
let sink_bytes_before = fs::read(&sink_path).unwrap();
let out = run_in(
tmp.path(),
&[
"restore",
"apply",
"--production",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage.to_str().unwrap(),
"--restore-intent",
restore_intent.to_str().unwrap(),
"--restore-intent-signature",
restore_intent_sig.to_str().unwrap(),
"--operator-verification-key",
operator_key.to_str().unwrap(),
"--against",
anchor_path.to_str().unwrap(),
"--against-history",
anchor_history_path.to_str().unwrap(),
"--anchor-sink",
"external-append-only",
"--sink-path",
sink_path.to_str().unwrap(),
"--acknowledge-production-destructive-restore",
"--acknowledge-active-store-replacement",
],
);
let code = out.status.code().expect("process exited via signal");
assert_ne!(
code,
0,
"legacy sink kind must refuse; stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
assert_exit(&out, 7);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("restore.production.sink_kind.external_append_only_not_authorized"),
"stable invariant token must surface; stderr: {stderr}"
);
assert!(
stderr.contains("active store was not changed"),
"fail-closed clause must surface; stderr: {stderr}"
);
assert!(
stderr.contains("--anchor-sink `rekor`") || stderr.contains("--anchor-sink rekor"),
"operator guidance to rekor must surface; stderr: {stderr}"
);
assert_eq!(
fs::read(&active_db).unwrap(),
active_db_bytes_before,
"parser-level refusal must not mutate the active SQLite store",
);
assert_eq!(
fs::read(&active_jsonl).unwrap(),
active_jsonl_bytes_before,
"parser-level refusal must not mutate the active JSONL log",
);
assert_eq!(
fs::read(&sink_path).unwrap(),
sink_bytes_before,
"parser-level refusal must not emit a sink witness",
);
}
#[test]
fn production_restore_rekor_sink_rejects_missing_parent_directory() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let stage = production_apply_stage_dir(tmp.path());
let restore_intent = dummy_path(tmp.path(), "RESTORE_INTENT.json");
let restore_intent_sig = dummy_path(tmp.path(), "RESTORE_INTENT.sig");
let operator_key = dummy_path(tmp.path(), "operator.pubkey");
let anchor_path = dummy_path(tmp.path(), "ANCHOR.json");
let anchor_history_path = dummy_path(tmp.path(), "ANCHOR_HISTORY");
let sink_path = tmp.path().join("missing").join("receipt.json");
let out = run(&[
"restore",
"apply",
"--production",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage.to_str().unwrap(),
"--restore-intent",
restore_intent.to_str().unwrap(),
"--restore-intent-signature",
restore_intent_sig.to_str().unwrap(),
"--operator-verification-key",
operator_key.to_str().unwrap(),
"--against",
anchor_path.to_str().unwrap(),
"--against-history",
anchor_history_path.to_str().unwrap(),
"--anchor-sink",
"rekor",
"--sink-path",
sink_path.to_str().unwrap(),
"--acknowledge-production-destructive-restore",
"--acknowledge-active-store-replacement",
]);
assert_exit(&out, 7);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("parent of --sink-path"),
"rekor sink must check parent directory; stderr: {stderr}"
);
assert!(
stderr.contains("active store was not changed"),
"fail-closed clause must surface; stderr: {stderr}"
);
assert!(!sink_path.exists(), "no receipt is written");
}
#[test]
fn production_restore_rekor_sink_rejects_preexisting_sink_path() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let stage = production_apply_stage_dir(tmp.path());
let restore_intent = dummy_path(tmp.path(), "RESTORE_INTENT.json");
let restore_intent_sig = dummy_path(tmp.path(), "RESTORE_INTENT.sig");
let operator_key = dummy_path(tmp.path(), "operator.pubkey");
let anchor_path = dummy_path(tmp.path(), "ANCHOR.json");
let anchor_history_path = dummy_path(tmp.path(), "ANCHOR_HISTORY");
let sink_path = tmp.path().join("preexisting-receipt.json");
fs::write(&sink_path, b"stale-receipt-bytes").unwrap();
let out = run(&[
"restore",
"apply",
"--production",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage.to_str().unwrap(),
"--restore-intent",
restore_intent.to_str().unwrap(),
"--restore-intent-signature",
restore_intent_sig.to_str().unwrap(),
"--operator-verification-key",
operator_key.to_str().unwrap(),
"--against",
anchor_path.to_str().unwrap(),
"--against-history",
anchor_history_path.to_str().unwrap(),
"--anchor-sink",
"rekor",
"--sink-path",
sink_path.to_str().unwrap(),
"--acknowledge-production-destructive-restore",
"--acknowledge-active-store-replacement",
]);
assert_exit(&out, 7);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("refusing to overwrite a prior Rekor receipt"),
"rekor sink must refuse to clobber an existing receipt; stderr: {stderr}"
);
assert_eq!(fs::read(&sink_path).unwrap(), b"stale-receipt-bytes");
}
#[test]
fn production_restore_rekor_sink_does_not_refuse_at_freshness_gate_for_current_snapshot() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let stage = production_apply_stage_dir(tmp.path());
let restore_intent = dummy_path(tmp.path(), "RESTORE_INTENT.json");
let restore_intent_sig = dummy_path(tmp.path(), "RESTORE_INTENT.sig");
let operator_key = dummy_path(tmp.path(), "operator.pubkey");
let anchor_path = dummy_path(tmp.path(), "ANCHOR.json");
let anchor_history_path = dummy_path(tmp.path(), "ANCHOR_HISTORY");
let receipt_dir = tmp.path().join("evidence");
fs::create_dir_all(&receipt_dir).unwrap();
let sink_path = receipt_dir.join("production-restore-rekor-receipt.json");
let out = run(&[
"restore",
"apply",
"--production",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage.to_str().unwrap(),
"--restore-intent",
restore_intent.to_str().unwrap(),
"--restore-intent-signature",
restore_intent_sig.to_str().unwrap(),
"--operator-verification-key",
operator_key.to_str().unwrap(),
"--against",
anchor_path.to_str().unwrap(),
"--against-history",
anchor_history_path.to_str().unwrap(),
"--anchor-sink",
"rekor",
"--sink-path",
sink_path.to_str().unwrap(),
"--acknowledge-production-destructive-restore",
"--acknowledge-active-store-replacement",
]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("restore.production.sink.rekor.trust_root_snapshot_stale"),
"freshness gate must NOT refuse on current embedded snapshot; stderr: {stderr}"
);
assert!(
!stderr.contains("restore.production.sink.rekor.trusted_root_stale"),
"freshness gate must NOT refuse on current embedded snapshot (generic invariant); stderr: {stderr}"
);
assert!(
!sink_path.exists(),
"no receipt is written without a fully validated drill"
);
}
#[test]
fn production_restore_rekor_sink_accepts_writable_sink_path() {
let tmp = tempfile::tempdir().unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let stage = production_apply_stage_dir(tmp.path());
let restore_intent = dummy_path(tmp.path(), "RESTORE_INTENT.json");
let restore_intent_sig = dummy_path(tmp.path(), "RESTORE_INTENT.sig");
let operator_key = dummy_path(tmp.path(), "operator.pubkey");
let anchor_path = dummy_path(tmp.path(), "ANCHOR.json");
let anchor_history_path = dummy_path(tmp.path(), "ANCHOR_HISTORY");
let receipt_dir = tmp.path().join("evidence");
fs::create_dir_all(&receipt_dir).unwrap();
let sink_path = receipt_dir.join("production-restore-rekor-receipt.json");
let out = run(&[
"restore",
"apply",
"--production",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage.to_str().unwrap(),
"--restore-intent",
restore_intent.to_str().unwrap(),
"--restore-intent-signature",
restore_intent_sig.to_str().unwrap(),
"--operator-verification-key",
operator_key.to_str().unwrap(),
"--against",
anchor_path.to_str().unwrap(),
"--against-history",
anchor_history_path.to_str().unwrap(),
"--anchor-sink",
"rekor",
"--sink-path",
sink_path.to_str().unwrap(),
"--acknowledge-production-destructive-restore",
"--acknowledge-active-store-replacement",
]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("restore.production.sink_kind.unknown"),
"rekor must be accepted by the sink-kind whitelist; stderr: {stderr}"
);
assert!(
!stderr.contains("--anchor-sink must be `external-append-only`"),
"back-compat regression: rekor must not fall through to the legacy reject; stderr: {stderr}"
);
assert!(
!stderr.contains("restore.production.sink.rekor.trust_root_snapshot_stale")
&& !stderr.contains("restore.production.sink.rekor.trust_root_cache_stale")
&& !stderr.contains("restore.production.sink.rekor.trusted_root_stale"),
"freshness gate must NOT refuse on current embedded snapshot (Bug J pin); stderr: {stderr}"
);
assert_exit(&out, 5);
assert!(
!sink_path.exists(),
"no receipt is written without a fully validated drill"
);
}
fn principal_binding_derive(key_bytes: &[u8; 32]) -> String {
let digest = blake3::hash(key_bytes);
let hex = digest.to_hex().to_string();
format!("operator:{hex}")
}
fn principal_binding_derive_deployment_id(data_dir: &Path) -> String {
let canon = data_dir
.canonicalize()
.unwrap_or_else(|_| data_dir.to_path_buf());
let digest = blake3::hash(canon.to_string_lossy().as_bytes());
format!("deployment:{}", digest.to_hex())
}
#[test]
fn production_restore_refuses_principal_not_bound_to_verifying_key() {
use ed25519_dalek::{Signer, SigningKey};
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let data_dir = active_db.parent().unwrap().to_path_buf();
let active_jsonl = data_dir.join("events.jsonl");
append_fixture_event(&active_jsonl);
let stage = tmp.path().join("stage");
fs::create_dir_all(&stage).unwrap();
let staged_db = stage.join("cortex.db");
let staged_jsonl = stage.join("events.jsonl");
fs::copy(&active_db, &staged_db).unwrap();
fs::copy(&active_jsonl, &staged_jsonl).unwrap();
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &staged_db, &staged_jsonl);
let signing_key = SigningKey::from_bytes(&[42u8; 32]);
let verifying_key = signing_key.verifying_key();
let key_bytes = verifying_key.to_bytes();
let derived_principal = principal_binding_derive(&key_bytes);
let forged_principal = "operator:trusted-incident-responder";
assert_ne!(
forged_principal, derived_principal,
"test premise: forged principal must differ from derived",
);
let operator_key_path = tmp.path().join("operator.pubkey");
fs::write(&operator_key_path, key_bytes).unwrap();
let deployment_id = principal_binding_derive_deployment_id(&data_dir);
let active_db_canon = active_db
.canonicalize()
.unwrap_or_else(|_| active_db.clone());
let active_jsonl_canon = active_jsonl
.canonicalize()
.unwrap_or_else(|_| active_jsonl.clone());
let manifest_bytes = fs::read(&manifest).unwrap();
let manifest_b3 = blake3_digest(&manifest_bytes);
let staged_db_b3 = blake3_digest(&fs::read(&staged_db).unwrap());
let staged_jsonl_b3 = blake3_digest(&fs::read(&staged_jsonl).unwrap());
let not_before = Utc::now() - chrono::Duration::minutes(5);
let not_after = Utc::now() + chrono::Duration::hours(1);
let payload = json!({
"kind": "cortex_restore_intent",
"schema_version": 1,
"deployment_id": deployment_id,
"active_db_path": active_db_canon,
"active_event_log_path": active_jsonl_canon,
"backup_manifest_blake3": manifest_b3,
"staged_sqlite_blake3": staged_db_b3,
"staged_jsonl_blake3": staged_jsonl_b3,
"operator_principal_id": forged_principal,
"not_before": not_before.to_rfc3339(),
"not_after": not_after.to_rfc3339(),
"p_n_schema_version": 1,
});
let canonical_bytes = serde_json::to_vec(&payload).unwrap();
let restore_intent = tmp.path().join("RESTORE_INTENT.json");
fs::write(&restore_intent, &canonical_bytes).unwrap();
let signature = signing_key.sign(&canonical_bytes);
let restore_intent_sig = tmp.path().join("RESTORE_INTENT.sig");
fs::write(&restore_intent_sig, signature.to_bytes()).unwrap();
let anchor_path = dummy_path(tmp.path(), "ANCHOR.json");
let anchor_history_path = dummy_path(tmp.path(), "ANCHOR_HISTORY");
let sink_path = tmp.path().join("production-restore-rekor-receipt.json");
assert!(
!sink_path.exists(),
"test premise: sink_path must not exist for rekor cutover gate"
);
let out = run_in(
tmp.path(),
&[
"restore",
"apply",
"--production",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage.to_str().unwrap(),
"--restore-intent",
restore_intent.to_str().unwrap(),
"--restore-intent-signature",
restore_intent_sig.to_str().unwrap(),
"--operator-verification-key",
operator_key_path.to_str().unwrap(),
"--against",
anchor_path.to_str().unwrap(),
"--against-history",
anchor_history_path.to_str().unwrap(),
"--anchor-sink",
"rekor",
"--sink-path",
sink_path.to_str().unwrap(),
"--acknowledge-production-destructive-restore",
"--acknowledge-active-store-replacement",
],
);
let code = out.status.code().expect("process exited via signal");
assert_ne!(
code,
0,
"principal-binding fail-closed gate must refuse; stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("restore.intent.operator_principal_id.not_bound_to_verifying_key"),
"stable invariant must surface in stderr; stderr: {stderr}",
);
assert!(
stderr.contains("active store was not changed"),
"fail-closed clause must surface; stderr: {stderr}",
);
assert!(
!sink_path.exists(),
"principal-binding fail-closed must not write a rekor receipt",
);
}
#[test]
fn restore_recover_apply_refuses_when_operator_attestation_missing() {
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(&candidate_root).unwrap();
let candidate_db = init_store(&candidate_root);
apply_store_migrations(&candidate_db);
let candidate_log = candidate_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&candidate_log);
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &candidate_db, &candidate_log);
let stage_dir = tmp.path().join("stage");
let stage = run_in(
tmp.path(),
&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--current-store",
active_db.to_str().unwrap(),
"--acknowledge-destructive-restore",
],
);
assert_exit(&stage, 0);
let apply = run_in(
tmp.path(),
&[
"restore",
"apply-stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--acknowledge-active-store-replacement",
"--acknowledge-temp-test-data-dir",
],
);
assert_exit(&apply, 0);
let apply_report: serde_json::Value = serde_json::from_slice(&apply.stdout).unwrap();
let recovery_manifest = PathBuf::from(
apply_report["recovery_evidence"]["manifest"]
.as_str()
.unwrap(),
);
let out = run_in(
tmp.path(),
&[
"restore",
"recover-apply",
"--manifest",
recovery_manifest.to_str().unwrap(),
"--acknowledge-current-backup-restore",
"--acknowledge-temp-test-data-dir",
],
);
assert_exit(&out, 7);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("restore.recover_apply.operator_temporal_authority.revalidation_failed")
&& stderr.contains("--attestation <PATH> is required"),
"stderr must carry the stable invariant + remediation hint: {stderr}"
);
}
#[test]
fn restore_recover_apply_refuses_when_operator_key_revoked() {
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(&candidate_root).unwrap();
let candidate_db = init_store(&candidate_root);
apply_store_migrations(&candidate_db);
let candidate_log = candidate_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&candidate_log);
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &candidate_db, &candidate_log);
let stage_dir = tmp.path().join("stage");
let stage = run_in(
tmp.path(),
&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--current-store",
active_db.to_str().unwrap(),
"--acknowledge-destructive-restore",
],
);
assert_exit(&stage, 0);
let apply = run_in(
tmp.path(),
&[
"restore",
"apply-stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--acknowledge-active-store-replacement",
"--acknowledge-temp-test-data-dir",
],
);
assert_exit(&apply, 0);
let apply_report: serde_json::Value = serde_json::from_slice(&apply.stdout).unwrap();
let recovery_manifest = PathBuf::from(
apply_report["recovery_evidence"]["manifest"]
.as_str()
.unwrap(),
);
let (attestation_key, attestor) = restore_attestation_key_fixture(tmp.path());
seed_operator_authority_for_restore(&active_db, &attestor);
revoke_operator_authority_for_restore(
&active_db,
&attestor,
Utc.with_ymd_and_hms(2026, 1, 1, 12, 0, 2).unwrap(),
);
let out = run_in(
tmp.path(),
&[
"restore",
"recover-apply",
"--manifest",
recovery_manifest.to_str().unwrap(),
"--acknowledge-current-backup-restore",
"--acknowledge-temp-test-data-dir",
"--attestation",
attestation_key.to_str().unwrap(),
],
);
assert_exit(&out, 7);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("restore.recover_apply.operator_temporal_authority.revalidation_failed"),
"stderr must carry the stable invariant: {stderr}"
);
assert!(
stderr.contains("signed_after_revocation") || stderr.contains("revoked_after_signing"),
"stderr should name the per-reason wire string: {stderr}"
);
}
fn run_with_fixture_env(cwd: &Path, fixture_path: &Path, args: &[&str]) -> std::process::Output {
let data_dir = cwd.join("xdg").join("cortex");
Command::new(cortex_bin())
.current_dir(cwd)
.env("CORTEX_DATA_DIR", &data_dir)
.env("XDG_DATA_HOME", cwd.join("xdg"))
.env("HOME", cwd)
.env("APPDATA", cwd.join("appdata"))
.env("LOCALAPPDATA", cwd.join("localappdata"))
.env("CORTEX_REKOR_FIXTURE_RECEIPT", fixture_path)
.args(args)
.output()
.expect("spawn cortex")
}
#[test]
fn production_restore_refuses_rekor_fixture_receipt_env_before_any_mutation() {
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let active_jsonl = active_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&active_jsonl);
let active_db_bytes_before = fs::read(&active_db).unwrap();
let active_jsonl_bytes_before = fs::read(&active_jsonl).unwrap();
let manifest = write_pre_v2_backup(&tmp.path().join("backup"));
let stage = production_apply_stage_dir(tmp.path());
let restore_intent = dummy_path(tmp.path(), "RESTORE_INTENT.json");
let restore_intent_sig = dummy_path(tmp.path(), "RESTORE_INTENT.sig");
let operator_key = dummy_path(tmp.path(), "operator.pubkey");
let anchor_path = dummy_path(tmp.path(), "ANCHOR.json");
let anchor_history_path = dummy_path(tmp.path(), "ANCHOR_HISTORY");
let sink_path = tmp.path().join("sink");
let fixture_receipt = tmp.path().join("rekor-fixture-receipt.txt");
let out = run_with_fixture_env(
tmp.path(),
&fixture_receipt,
&[
"restore",
"apply",
"--production",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage.to_str().unwrap(),
"--restore-intent",
restore_intent.to_str().unwrap(),
"--restore-intent-signature",
restore_intent_sig.to_str().unwrap(),
"--operator-verification-key",
operator_key.to_str().unwrap(),
"--against",
anchor_path.to_str().unwrap(),
"--against-history",
anchor_history_path.to_str().unwrap(),
"--anchor-sink",
"rekor",
"--sink-path",
sink_path.to_str().unwrap(),
"--acknowledge-production-destructive-restore",
"--acknowledge-active-store-replacement",
],
);
assert_exit(&out, 7);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("restore.production.rekor_fixture_receipt_env_forbidden_in_production"),
"stable invariant must surface; stderr: {stderr}"
);
assert!(
stderr.contains("active store was not changed"),
"fail-closed clause must surface; stderr: {stderr}"
);
assert_eq!(fs::read(&active_db).unwrap(), active_db_bytes_before);
assert_eq!(fs::read(&active_jsonl).unwrap(), active_jsonl_bytes_before);
assert!(!sink_path.exists(), "no sink artifact may be written");
}
#[test]
fn apply_stage_still_honors_rekor_fixture_receipt_env_on_temp_test_path() {
let tmp = tempfile::tempdir().unwrap();
let active_db = init_store(tmp.path());
apply_store_migrations(&active_db);
let candidate_root = tmp.path().join("candidate");
fs::create_dir_all(&candidate_root).unwrap();
let candidate_db = init_store(&candidate_root);
apply_store_migrations(&candidate_db);
let candidate_log = candidate_db.parent().unwrap().join("events.jsonl");
append_fixture_event(&candidate_log);
let manifest =
write_manifest_for_artifacts(&tmp.path().join("backup"), &candidate_db, &candidate_log);
let stage_dir = tmp.path().join("stage");
let stage = run_in(
tmp.path(),
&[
"restore",
"stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--current-store",
active_db.to_str().unwrap(),
"--acknowledge-destructive-restore",
],
);
assert_exit(&stage, 0);
let fixture_receipt = tmp.path().join("rekor-fixture-receipt.txt");
fs::write(&fixture_receipt, b"placeholder-fixture-receipt").unwrap();
let apply = run_with_fixture_env(
tmp.path(),
&fixture_receipt,
&[
"restore",
"apply-stage",
"--manifest",
manifest.to_str().unwrap(),
"--stage-dir",
stage_dir.to_str().unwrap(),
"--acknowledge-active-store-replacement",
"--acknowledge-temp-test-data-dir",
],
);
let stderr = String::from_utf8_lossy(&apply.stderr);
assert!(
!stderr.contains("rekor_fixture_receipt_env_forbidden_in_production"),
"apply-stage temp-test path must not surface the production-only refusal: stderr={stderr}"
);
}