use std::path::{Path, PathBuf};
use std::process::Command;
fn cortex_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_cortex"))
}
fn fixtures_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
}
fn run(args: &[&str]) -> std::process::Output {
Command::new(cortex_bin())
.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),
);
}
#[test]
fn t_1_c_1_init_creates_db_and_event_log() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
let out = run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]);
assert_exit(&out, 0);
assert!(db.exists(), "db file should exist after init");
assert!(log.exists(), "event-log file should exist after init");
}
#[test]
fn t_1_c_1_init_is_idempotent() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
let args: Vec<&str> = vec![
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
];
assert_exit(&run(&args), 0);
let mtime_db_first = mtime(&db);
let mtime_log_first = mtime(&log);
assert_exit(&run(&args), 0);
assert_eq!(mtime(&db), mtime_db_first, "db must not be re-touched");
assert_eq!(
mtime(&log),
mtime_log_first,
"event log must not be re-touched"
);
}
#[test]
fn t_1_c_1_init_bogus_flag_exits_usage() {
let out = run(&["init", "--bogus"]);
assert_exit(&out, 2);
}
#[cfg(unix)]
#[test]
fn t_1_c_1_init_validate_perms_rejects_loose_dir() {
use std::fs;
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("loose");
fs::create_dir(&dir).unwrap();
fs::set_permissions(&dir, fs::Permissions::from_mode(0o755)).unwrap();
let db = dir.join("cortex.db");
let log = dir.join("events.jsonl");
let out = run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
"--validate-perms",
]);
assert_exit(&out, 7);
}
#[test]
fn t_3_d_10_init_rejects_existing_wal_sidecar() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
std::fs::write(tmp.path().join("cortex.db-wal"), b"unverified wal").unwrap();
let out = run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]);
assert_exit(&out, 7);
assert!(
!db.exists(),
"init must not create db after sidecar preflight failure"
);
assert!(
!log.exists(),
"init must not create event log after sidecar preflight failure"
);
}
#[test]
fn t_1_c_2_ingest_then_reingest_is_noop() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
let session = fixtures_dir().join("session-minimal.json");
assert_exit(
&run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]),
0,
);
let first = run(&[
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]);
assert_exit(&first, 0);
let first_log_size = std::fs::metadata(&log).unwrap().len();
let first_stdout = String::from_utf8_lossy(&first.stdout).to_string();
assert!(first_stdout.contains("appended"));
assert!(first_stdout.contains("chain_head"));
let second = run(&[
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]);
assert_exit(&second, 0);
let second_log_size = std::fs::metadata(&log).unwrap().len();
assert_eq!(
first_log_size, second_log_size,
"re-ingest must not grow the JSONL log"
);
let second_stdout = String::from_utf8_lossy(&second.stdout).to_string();
assert!(
second_stdout.contains("appended_count = 0"),
"second ingest stdout: {second_stdout}"
);
}
#[test]
fn t_1_c_3_audit_verify_clean_chain_is_ok() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
let session = fixtures_dir().join("session-minimal.json");
assert_exit(
&run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]),
0,
);
assert_exit(
&run(&[
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]),
0,
);
let out = run(&[
"audit",
"verify",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]);
assert_exit(&out, 0);
}
#[test]
fn t_1_c_3_audit_verify_byte_corruption_returns_chain_corruption() {
use std::io::Write;
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
let session = fixtures_dir().join("session-minimal.json");
assert_exit(
&run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]),
0,
);
assert_exit(
&run(&[
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]),
0,
);
let mut f = std::fs::OpenOptions::new().append(true).open(&log).unwrap();
writeln!(f, "{{not json").unwrap();
drop(f);
let out = run(&[
"audit",
"verify",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]);
assert_exit(&out, 6);
}
#[test]
fn t_1_c_4_reflect_stdout_matches_expected_fixture() {
let out = run(&[
"reflect",
"--trace",
"trc_01ARZ3NDEKTSV4RRFFQ69G5FAW",
"--model",
"replay",
]);
assert_exit(&out, 0);
let expected = std::fs::read(fixtures_dir().join("reflect-expected.json")).unwrap();
assert_eq!(
out.stdout, expected,
"reflect stdout did not match expected byte-for-byte\n--- got ---\n{}\n--- expected ---\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&expected),
);
}
#[test]
fn t_1_c_4_reflect_persists_nothing() {
let tmp = tempfile::tempdir().unwrap();
let before: Vec<_> = std::fs::read_dir(tmp.path()).unwrap().collect();
assert!(before.is_empty(), "tmpdir should start empty");
let out = Command::new(cortex_bin())
.current_dir(tmp.path())
.args([
"reflect",
"--trace",
"trc_01ARZ3NDEKTSV4RRFFQ69G5FAW",
"--model",
"replay",
])
.output()
.expect("spawn cortex");
assert_exit(&out, 0);
let after: Vec<_> = std::fs::read_dir(tmp.path()).unwrap().collect();
assert!(
after.is_empty(),
"reflect must not write to the working directory"
);
}
fn write_replay_fixture_dir(
tmp_root: &Path,
fixture_name: &str,
prompt_hash: &str,
body_text: &str,
) -> PathBuf {
let fixture_path = tmp_root.join(fixture_name);
let body_text_escaped = serde_json::Value::String(body_text.to_string()).to_string();
let fixture_json = format!(
"{{\
\"request_match\":{{\"model\":\"claude-3-5-sonnet-20240620\",\"prompt_hash\":\"{prompt_hash}\"}},\
\"response\":{{\"text\":{body_text_escaped},\"model\":\"claude-3-5-sonnet-20240620\"}}\
}}"
);
std::fs::write(&fixture_path, &fixture_json).unwrap();
let fixture_hash = blake3_hex(fixture_json.as_bytes());
std::fs::write(
tmp_root.join("INDEX.toml"),
format!("[[fixture]]\npath = \"{fixture_name}\"\nblake3 = \"{fixture_hash}\"\n"),
)
.unwrap();
tmp_root.to_path_buf()
}
#[test]
fn t_1_c_4_reflect_quarantines_open_contradiction_and_suppresses_payload() {
let tmp = tempfile::tempdir().unwrap();
let prompt_hash = "b8760ace962fe6210196ba5ef681c479599db7da361225f129a83f21598f4f05";
let body = serde_json::json!({
"trace_id": "trc_01ARZ3NDEKTSV4RRFFQ69G5FQQ",
"episode_candidates": [{
"summary": "Open contradiction demo.",
"source_event_ids": ["evt_01ARZ3NDEKTSV4RRFFQ69G5FAV"],
"domains": ["agents"],
"entities": ["Cortex"],
"candidate_meaning": null,
"confidence": 0.5
}],
"memory_candidates": [{
"memory_type": "strategic",
"claim": "Reflection memory remains candidate-only.",
"source_episode_indexes": [0],
"applies_when": ["reflecting"],
"does_not_apply_when": ["promoting"],
"confidence": 0.8,
"initial_salience": {
"reusability": 0.5,
"consequence": 0.5,
"emotional_charge": 0.0
}
}],
"contradictions": [{"claim": "conflict observed"}],
"doctrine_suggestions": []
})
.to_string();
let fixtures_dir = write_replay_fixture_dir(
tmp.path(),
"cortex-reflect-quarantine.json",
prompt_hash,
&body,
);
let out = Command::new(cortex_bin())
.args([
"reflect",
"--trace",
"trc_01ARZ3NDEKTSV4RRFFQ69G5FQQ",
"--model",
"replay",
"--fixtures-dir",
fixtures_dir.to_str().unwrap(),
])
.output()
.expect("spawn cortex");
assert_exit(&out, 0);
let stdout: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("envelope is valid JSON");
assert_eq!(
stdout["policy_outcome"]["final_outcome"], "quarantine",
"policy outcome must be quarantine for an open contradiction: {stdout}"
);
assert!(
stdout.get("payload").is_none() || stdout["payload"].is_null(),
"quarantine envelope must not include candidate-shaped payload: {stdout}"
);
assert!(
stdout["diagnostic"].is_object(),
"quarantine envelope must include a diagnostic: {stdout}"
);
let contributing = stdout["policy_outcome"]["contributing"]
.as_array()
.expect("contributing must be an array");
let discarded = stdout["policy_outcome"]["discarded"]
.as_array()
.expect("discarded must be an array");
let rule_ids: Vec<&str> = contributing
.iter()
.chain(discarded.iter())
.filter_map(|c| c["rule_id"].as_str())
.collect();
assert!(rule_ids.contains(&"reflect.admission_decision"));
assert!(rule_ids.contains(&"reflect.fixture_integrity"));
assert!(rule_ids.contains(&"reflect.adapter_authority_class"));
}
#[test]
fn t_1_c_4_reflect_rejects_unparseable_payload_with_quarantined_input_exit() {
let tmp = tempfile::tempdir().unwrap();
let prompt_hash = "ac95f33f6ddcaefdb4f11400bbb686d61dd18c09b71f87c83b6a3cfaa81c5ba4";
let body = serde_json::json!({"not_a": "session_reflection"}).to_string();
let fixtures_dir =
write_replay_fixture_dir(tmp.path(), "cortex-reflect-reject.json", prompt_hash, &body);
let out = Command::new(cortex_bin())
.args([
"reflect",
"--trace",
"trc_01ARZ3NDEKTSV4RRFFQ69G5FRR",
"--model",
"replay",
"--fixtures-dir",
fixtures_dir.to_str().unwrap(),
])
.output()
.expect("spawn cortex");
assert_exit(&out, 5);
assert!(
out.stdout.is_empty(),
"reject path must not print candidate-shaped output: {}",
String::from_utf8_lossy(&out.stdout)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("admission rejected"),
"stderr must surface the reject reason: {stderr}"
);
}
#[test]
fn t_1_c_4_reflect_fixture_integrity_failure_exits_quarantined_input() {
let tmp = tempfile::tempdir().unwrap();
let src = fixtures_dir()
.join("replay")
.join("cortex-reflect-trace-demo.json");
let dst_fixture = tmp.path().join("cortex-reflect-trace-demo.json");
std::fs::copy(&src, &dst_fixture).unwrap();
let original_hash = blake3_hex(&std::fs::read(&dst_fixture).unwrap());
std::fs::write(
tmp.path().join("INDEX.toml"),
format!(
"[[fixture]]\npath = \"cortex-reflect-trace-demo.json\"\nblake3 = \"{original_hash}\"\n"
),
)
.unwrap();
let mut bytes = std::fs::read(&dst_fixture).unwrap();
bytes.push(b' ');
std::fs::write(&dst_fixture, bytes).unwrap();
let out = Command::new(cortex_bin())
.args([
"reflect",
"--trace",
"trc_01ARZ3NDEKTSV4RRFFQ69G5FAW",
"--model",
"replay",
"--fixtures-dir",
tmp.path().to_str().unwrap(),
])
.output()
.expect("spawn cortex");
assert_exit(&out, 5);
}
fn blake3_hex(bytes: &[u8]) -> String {
blake3::hash(bytes).to_hex().to_string()
}
fn mtime(p: &Path) -> std::time::SystemTime {
std::fs::metadata(p)
.unwrap()
.modified()
.expect("filesystem supports mtime")
}