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 parse_envelope(stdout: &[u8]) -> serde_json::Value {
let text = std::str::from_utf8(stdout).expect("stdout is utf-8");
serde_json::from_str(text).unwrap_or_else(|err| {
panic!("expected --json stdout to parse as JSON envelope; err={err}; stdout={text}")
})
}
fn assert_envelope_shape(value: &serde_json::Value, command: &str, exit_code: i32) {
assert_eq!(
value["command"].as_str(),
Some(command),
"command field mismatch"
);
assert_eq!(
value["exit_code"].as_i64(),
Some(i64::from(exit_code)),
"exit_code field mismatch in envelope: {value}"
);
assert!(value["outcome"].is_string(), "outcome field must be string");
assert!(
value.get("report").is_some(),
"report field must be present"
);
}
#[test]
fn init_json_envelope_carries_data_layout() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
let out = run(&[
"--json",
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(0));
let envelope = parse_envelope(&out.stdout);
assert_envelope_shape(&envelope, "cortex.init", 0);
assert_eq!(envelope["outcome"], "ok");
assert_eq!(envelope["report"]["created_db"], true);
assert_eq!(envelope["report"]["created_event_log"], true);
assert!(envelope["report"]["schema_version"].is_number());
assert!(envelope["correlation_id"].is_string());
}
#[test]
fn init_json_envelope_reports_idempotent_second_run() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
let args: Vec<&str> = vec![
"--json",
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
];
let first = run(&args);
assert_eq!(first.status.code(), Some(0));
let second = run(&args);
assert_eq!(second.status.code(), Some(0));
let envelope = parse_envelope(&second.stdout);
assert_envelope_shape(&envelope, "cortex.init", 0);
assert_eq!(envelope["report"]["created_db"], false);
assert_eq!(envelope["report"]["created_event_log"], false);
}
#[test]
fn doctor_without_strict_emits_usage_envelope() {
let out = run(&["--json", "doctor"]);
assert_eq!(out.status.code(), Some(2));
let envelope = parse_envelope(&out.stdout);
assert_envelope_shape(&envelope, "cortex.doctor", 2);
assert_eq!(envelope["outcome"], "usage");
assert_eq!(envelope["report"]["status"], "usage");
}
#[test]
fn audit_verify_clean_chain_emits_envelope_with_chain_ok() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
let init = run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]);
assert_eq!(init.status.code(), Some(0));
let out = run(&[
"--json",
"audit",
"verify",
"--event-log",
log.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(0));
let envelope = parse_envelope(&out.stdout);
assert_envelope_shape(&envelope, "cortex.audit.verify", 0);
assert_eq!(envelope["outcome"], "ok");
assert_eq!(envelope["report"]["chain_ok"], true);
assert!(envelope["report"]["rows_scanned"].is_number());
}
#[test]
fn audit_verify_byte_corruption_emits_chain_corruption_outcome() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
assert_eq!(
run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let session = fixtures_dir().join("session-minimal.json");
let ingest = run(&[
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]);
assert_eq!(ingest.status.code(), Some(0));
std::fs::write(&log, b"{not valid json\n").unwrap();
let out = run(&[
"--json",
"audit",
"verify",
"--event-log",
log.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(6));
let envelope = parse_envelope(&out.stdout);
assert_envelope_shape(&envelope, "cortex.audit.verify", 6);
assert_eq!(envelope["outcome"], "chain_corruption");
}
#[test]
fn audit_anchor_local_only_envelope_carries_forbidden_uses() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
assert_eq!(
run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let session = fixtures_dir().join("session-minimal.json");
let ingest = run(&[
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]);
assert_eq!(ingest.status.code(), Some(0));
let out = run(&[
"--json",
"audit",
"anchor",
"--event-log",
log.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(0));
let envelope = parse_envelope(&out.stdout);
assert_envelope_shape(&envelope, "cortex.audit.anchor", 0);
assert_eq!(envelope["report"]["sink"], "local-only");
assert_eq!(envelope["report"]["anchor_authority"], "local_only");
let forbidden = envelope["report"]["forbidden_uses"]
.as_array()
.expect("forbidden_uses must be present for local-only sink");
assert!(
forbidden.iter().any(|v| v == "compliance_evidence"),
"local-only anchor must not be laundered as compliance evidence: {envelope}"
);
assert!(envelope["report"]["anchor_text"].is_string());
assert!(envelope["report"]["event_count"].is_number());
}
#[test]
fn release_readiness_envelope_includes_policy_outcome_reject() {
let out = run(&["--json", "release", "readiness"]);
assert_eq!(out.status.code(), Some(7));
let envelope = parse_envelope(&out.stdout);
assert_envelope_shape(&envelope, "cortex.release.readiness", 7);
assert_eq!(envelope["outcome"], "precondition_unmet");
assert!(envelope["policy_outcome"].is_object());
let forbidden = envelope["report"]["forbidden_uses"]
.as_array()
.expect("release readiness must carry forbidden_uses");
assert!(forbidden.iter().any(|v| v == "compliance_evidence"));
assert!(forbidden.iter().any(|v| v == "release_readiness_artifact"));
}
#[test]
fn compliance_evidence_envelope_includes_policy_outcome_reject() {
let out = run(&["--json", "compliance", "evidence"]);
assert_eq!(out.status.code(), Some(7));
let envelope = parse_envelope(&out.stdout);
assert_envelope_shape(&envelope, "cortex.compliance.evidence", 7);
assert_eq!(envelope["outcome"], "precondition_unmet");
assert!(envelope["policy_outcome"].is_object());
let forbidden = envelope["report"]["forbidden_uses"]
.as_array()
.expect("compliance evidence must carry forbidden_uses");
assert!(forbidden
.iter()
.any(|v| v == "compliance_evidence_artifact"));
}
#[test]
fn init_without_json_emits_prose_not_envelope() {
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_eq!(out.status.code(), Some(0));
let text = std::str::from_utf8(&out.stdout).unwrap();
assert!(
text.contains("cortex init:"),
"expected prose output, got {text}"
);
let parsed = serde_json::from_str::<serde_json::Value>(text);
assert!(
parsed.is_err(),
"prose output should NOT parse as JSON; got {parsed:?}"
);
}
#[test]
fn ingest_json_envelope_reports_appended_count() {
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_eq!(
run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let out = run(&[
"--json",
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(0));
let envelope = parse_envelope(&out.stdout);
assert_envelope_shape(&envelope, "cortex.ingest", 0);
assert_eq!(envelope["outcome"], "ok");
assert!(envelope["report"]["appended_count"].is_number());
assert!(envelope["report"]["skipped_count"].is_number());
assert!(envelope["report"]["chain_head"].is_string());
}
#[test]
fn audit_verify_envelope_includes_allow_policy_outcome_on_clean_chain() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
assert_eq!(
run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let session = fixtures_dir().join("session-minimal.json");
assert_eq!(
run(&[
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let out = run(&[
"--json",
"audit",
"verify",
"--event-log",
log.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(0));
let envelope = parse_envelope(&out.stdout);
assert_envelope_shape(&envelope, "cortex.audit.verify", 0);
let policy = envelope["policy_outcome"]
.as_object()
.expect("audit verify envelope must surface policy_outcome");
assert_eq!(policy["final_outcome"], "allow");
let contributing = policy["contributing"]
.as_array()
.expect("policy_outcome.contributing must be an array");
assert!(
contributing
.iter()
.any(|contribution| { contribution["rule_id"] == "audit.verify.hash_chain_closure" }),
"audit verify must compose audit.verify.hash_chain_closure: {envelope}"
);
assert_eq!(
envelope["report"]["policy_outcome"]["final_outcome"],
"allow"
);
}
#[test]
fn audit_verify_envelope_emits_reject_policy_when_chain_breaks() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
assert_eq!(
run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let session = fixtures_dir().join("session-minimal.json");
assert_eq!(
run(&[
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
std::fs::write(&log, b"{not valid json\n").unwrap();
let out = run(&[
"--json",
"audit",
"verify",
"--event-log",
log.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(6));
let envelope = parse_envelope(&out.stdout);
let policy = envelope["policy_outcome"]
.as_object()
.expect("audit verify policy_outcome must be present on chain corruption");
assert_eq!(policy["final_outcome"], "reject");
let contributing = policy["contributing"]
.as_array()
.expect("policy_outcome.contributing must be an array");
assert!(
contributing.iter().any(|contribution| {
contribution["rule_id"] == "audit.verify.hash_chain_closure"
&& contribution["outcome"] == "reject"
}),
"broken JSONL must cause audit.verify.hash_chain_closure=Reject: {envelope}"
);
}
#[test]
fn audit_verify_envelope_carries_typed_proof_closure_report_on_clean_chain() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
assert_eq!(
run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let session = fixtures_dir().join("session-minimal.json");
assert_eq!(
run(&[
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let out = run(&[
"--json",
"audit",
"verify",
"--event-log",
log.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(0));
let envelope = parse_envelope(&out.stdout);
let proof_closure = &envelope["report"]["proof_closure"];
assert!(
proof_closure.is_object(),
"audit verify envelope must surface report.proof_closure on clean chain: {envelope}"
);
assert_eq!(
proof_closure["proof_state"], "full_chain_verified",
"clean chain must compose to full_chain_verified: {envelope}"
);
}
#[test]
fn audit_verify_envelope_proof_closure_is_broken_on_chain_corruption() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
assert_eq!(
run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let session = fixtures_dir().join("session-minimal.json");
assert_eq!(
run(&[
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
std::fs::write(&log, b"{not valid json\n").unwrap();
let out = run(&[
"--json",
"audit",
"verify",
"--event-log",
log.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(6));
let envelope = parse_envelope(&out.stdout);
let proof_closure = &envelope["report"]["proof_closure"];
assert!(
proof_closure.is_object(),
"audit verify envelope must surface report.proof_closure on chain corruption: {envelope}"
);
assert_eq!(
proof_closure["proof_state"], "broken",
"chain corruption (hash-chain Reject) must collapse fold to broken: {envelope}"
);
let failing = proof_closure["failing_edges"]
.as_array()
.expect("broken proof closure must carry failing_edges");
assert!(
failing.iter().any(|edge| edge["kind"] == "hash_chain"),
"broken fold must list the failing hash_chain edge: {envelope}"
);
}
#[test]
fn audit_anchor_local_only_envelope_surfaces_allow_policy_outcome() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
assert_eq!(
run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let session = fixtures_dir().join("session-minimal.json");
assert_eq!(
run(&[
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let out = run(&[
"--json",
"audit",
"anchor",
"--event-log",
log.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(0));
let envelope = parse_envelope(&out.stdout);
assert_envelope_shape(&envelope, "cortex.audit.anchor", 0);
let policy = envelope["policy_outcome"]
.as_object()
.expect("audit anchor envelope must surface policy_outcome");
assert_eq!(policy["final_outcome"], "warn");
let contributing = policy["contributing"]
.as_array()
.expect("policy_outcome.contributing must be an array");
let discarded = policy["discarded"]
.as_array()
.expect("policy_outcome.discarded must be an array");
let all_contributions = contributing.iter().chain(discarded.iter());
let all_contributions_vec: Vec<_> = all_contributions.collect();
assert!(
all_contributions_vec.iter().any(|contribution| {
contribution["rule_id"] == "audit.anchor.sink_authority"
&& contribution["outcome"] == "allow"
}),
"local-only anchor must observe audit.anchor.sink_authority=Allow: {envelope}"
);
assert!(
contributing.iter().any(|contribution| {
contribution["rule_id"] == "audit.anchor.temporal_authority"
&& contribution["outcome"] == "warn"
}),
"local-only anchor over unsigned ledger must compose audit.anchor.temporal_authority=Warn: {envelope}"
);
assert_eq!(
envelope["report"]["policy_outcome"]["final_outcome"],
"warn"
);
}
#[test]
fn audit_anchor_rekor_sink_offline_envelope_emits_reject_policy() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
assert_eq!(
run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let session = fixtures_dir().join("session-minimal.json");
assert_eq!(
run(&[
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let out = run(&[
"--json",
"audit",
"anchor",
"--event-log",
log.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--sink",
"rekor",
"--offline",
]);
assert_eq!(out.status.code(), Some(7));
let envelope = parse_envelope(&out.stdout);
assert_envelope_shape(&envelope, "cortex.audit.anchor", 7);
let policy = envelope["policy_outcome"]
.as_object()
.expect("rekor sink envelope must surface policy_outcome");
assert_eq!(policy["final_outcome"], "reject");
let contributing = policy["contributing"]
.as_array()
.expect("policy_outcome.contributing must be an array");
assert!(
contributing.iter().any(|contribution| {
contribution["rule_id"] == "audit.anchor.sink_authority"
&& contribution["outcome"] == "reject"
&& contribution["reason"]
.as_str()
.is_some_and(|reason| reason.contains("audit.anchor.rekor.submit_failed"))
}),
"expected sink_authority=Reject with rekor submit_failed reason: {envelope}"
);
}
#[test]
fn audit_anchor_external_unconfigured_envelope_emits_reject_policy() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
assert_eq!(
run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let session = fixtures_dir().join("session-minimal.json");
assert_eq!(
run(&[
"ingest",
session.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let out = run(&[
"--json",
"audit",
"anchor",
"--event-log",
log.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--sink",
"external-append-only",
]);
assert_eq!(out.status.code(), Some(7));
let envelope = parse_envelope(&out.stdout);
assert_envelope_shape(&envelope, "cortex.audit.anchor", 7);
let policy = envelope["policy_outcome"]
.as_object()
.expect("external-unconfigured anchor envelope must surface policy_outcome");
assert_eq!(policy["final_outcome"], "reject");
}
#[test]
fn audit_export_default_surface_envelope_includes_policy_outcome() {
let tmp = tempfile::tempdir().unwrap();
let db = tmp.path().join("cortex.db");
let log = tmp.path().join("events.jsonl");
assert_eq!(
run(&[
"init",
"--db",
db.to_str().unwrap(),
"--event-log",
log.to_str().unwrap(),
])
.status
.code(),
Some(0)
);
let out = run(&[
"--json",
"audit",
"export",
"--event-log",
log.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(0));
let envelope = parse_envelope(&out.stdout);
assert_envelope_shape(&envelope, "cortex.audit.export", 0);
let policy = envelope["policy_outcome"]
.as_object()
.expect("audit export envelope must surface policy_outcome");
assert_eq!(policy["final_outcome"], "quarantine");
let contributing_rule_ids: Vec<String> = policy["contributing"]
.as_array()
.expect("policy_outcome.contributing must be an array")
.iter()
.filter_map(|c| c["rule_id"].as_str().map(String::from))
.collect();
assert!(
contributing_rule_ids
.iter()
.any(|r| r == "audit.export.proof_closure"),
"default audit export must include audit.export.proof_closure: {envelope}"
);
let discarded_rule_ids: Vec<String> = policy["discarded"]
.as_array()
.expect("policy_outcome.discarded must be an array")
.iter()
.filter_map(|c| c["rule_id"].as_str().map(String::from))
.collect();
let all_rule_ids: std::collections::HashSet<_> = contributing_rule_ids
.iter()
.chain(discarded_rule_ids.iter())
.map(String::as_str)
.collect();
for rule_id in [
"audit.export.development_ledger",
"audit.export.signed_local_class",
"audit.export.proof_closure",
] {
assert!(
all_rule_ids.contains(rule_id),
"default audit export must compose `{rule_id}`: {envelope}"
);
}
assert_eq!(envelope["report"]["policy_outcome"], "quarantine");
}
#[allow(dead_code)]
fn _fixtures_helper(_path: &Path) {}