crtx 0.1.1

CLI for the Cortex supervisory memory substrate.
//! CLI integration tests for the receiver-side `axiom_execution_trust.
//! tool_provenance.source_commit` freshness gate.
//!
//! Closes the receiver-side gap surfaced by the 2026-05-13 live exchange
//! against axiom packet SHA `9a15d281` (transcript at
//! `docs/transcripts/AXIOM_LIVE_EXCHANGE_2026-05-13/SUMMARY.md`): the
//! `stale-pai-axiom-sha` fixture was admitted instead of refused. These
//! tests verify the closure end-to-end through the `cortex memory
//! admit-axiom` CLI surface:
//!
//! 1. The unmodified valid fixture (whose `source_commit` is the
//!    Cortex-side test-fixture placeholder, on the default accept-list)
//!    still admits as a candidate.
//! 2. A fixture identical to (1) except for a `source_commit` value
//!    not on the receiver-side accept-list refuses with the stable
//!    invariant `axiom_execution_trust.tool_provenance.source_commit.stale`.
//! 3. The env-var override
//!    `CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS` REPLACES the default
//!    accept-list — setting it to the stale SHA admits, omitting it
//!    refuses.
//!
//! These tests intentionally cover only the freshness gate. The
//! upstream structural validation (envelope schema, required fields,
//! `tool_provenance.source_commit.invalid_format`, etc.) is covered
//! by `cli_pai_axiom_candidate_admission.rs` and unit tests in
//! `cortex-core::axiom_trust`.

use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

use serde_json::Value;

const STALE_SOURCE_COMMIT: &str = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
const DEFAULT_FIXTURE_SOURCE_COMMIT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

fn cortex_bin() -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_cortex"))
}

fn fixture_dir() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join("pai-axiom")
}

fn temp_dir(label: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock after unix epoch")
        .as_nanos();
    let dir = std::env::temp_dir().join(format!(
        "cortex-cli-axiom-freshness-{label}-{}-{nanos}",
        std::process::id()
    ));
    std::fs::create_dir_all(&dir).expect("create temp dir");
    dir
}

/// Build a `--cortex-context-trust` / `--axiom-execution-trust` /
/// `--authority-feedback-loop` invocation against a temp directory, with
/// the supplied axiom-execution-trust payload written into `tmp`.
fn run_admit_axiom_with_payload(
    tmp: &Path,
    axiom_execution_trust_json: &Value,
    env: &[(&str, &str)],
) -> std::process::Output {
    let ctx_path = fixture_dir().join("valid-cortex-context-trust.json");
    let loop_path = fixture_dir().join("valid-authority-feedback-loop.json");
    let exec_path = tmp.join("axiom-execution-trust.json");

    let exec_serialized = serde_json::to_string_pretty(axiom_execution_trust_json)
        .expect("axiom_execution_trust JSON is serializable");
    std::fs::write(&exec_path, exec_serialized).expect("write axiom-execution-trust fixture");

    let mut cmd = Command::new(cortex_bin());
    cmd.current_dir(tmp);
    cmd.env_remove("RUST_LOG");
    cmd.env_remove("CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS");
    cmd.env("XDG_DATA_HOME", tmp.join("xdg"));
    cmd.env("HOME", tmp);
    for (k, v) in env {
        cmd.env(k, v);
    }
    cmd.args([
        "memory",
        "admit-axiom",
        "--cortex-context-trust",
        ctx_path.to_str().expect("ctx path utf8"),
        "--axiom-execution-trust",
        exec_path.to_str().expect("exec path utf8"),
        "--authority-feedback-loop",
        loop_path.to_str().expect("loop path utf8"),
        "--lifecycle",
        "candidate_only",
    ]);
    cmd.output().expect("spawn cortex memory admit-axiom")
}

/// Load the `valid-axiom-execution-trust.json` fixture and override its
/// `tool_provenance.source_commit` to `new_source_commit`.
fn payload_with_source_commit(new_source_commit: &str) -> Value {
    let raw = std::fs::read_to_string(fixture_dir().join("valid-axiom-execution-trust.json"))
        .expect("read valid-axiom-execution-trust fixture");
    let mut value: Value = serde_json::from_str(&raw).expect("parse fixture JSON");
    value["axiom_execution_trust"]["tool_provenance"]["source_commit"] =
        Value::String(new_source_commit.to_string());
    value
}

#[test]
fn admit_axiom_admits_payload_with_default_fixture_source_commit() {
    let tmp = temp_dir("admits-default");
    let payload = payload_with_source_commit(DEFAULT_FIXTURE_SOURCE_COMMIT);
    let out = run_admit_axiom_with_payload(&tmp, &payload, &[]);

    let stdout = String::from_utf8_lossy(&out.stdout);
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        out.status.success(),
        "default-fixture source_commit must admit; exit={:?}\nstdout: {stdout}\nstderr: {stderr}",
        out.status.code()
    );
    assert!(
        stderr.contains("decision=admit_candidate"),
        "default-fixture source_commit must admit as candidate; stderr: {stderr}"
    );

    std::fs::remove_dir_all(&tmp).ok();
}

#[test]
fn admit_axiom_refuses_unknown_source_commit_with_stable_invariant() {
    let tmp = temp_dir("refuses-stale");
    let payload = payload_with_source_commit(STALE_SOURCE_COMMIT);
    let out = run_admit_axiom_with_payload(&tmp, &payload, &[]);

    let stdout = String::from_utf8_lossy(&out.stdout);
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        !out.status.success(),
        "unknown source_commit must refuse; got exit={:?}\nstdout: {stdout}\nstderr: {stderr}",
        out.status.code()
    );
    assert!(
        stderr.contains("axiom_execution_trust.tool_provenance.source_commit.stale"),
        "stderr must surface the stable freshness invariant; got: {stderr}"
    );
    assert!(
        stderr.contains(STALE_SOURCE_COMMIT),
        "stderr must echo the offending source_commit so operators can diagnose; got: {stderr}"
    );

    std::fs::remove_dir_all(&tmp).ok();
}

#[test]
fn admit_axiom_env_var_override_replaces_default_accept_list() {
    // With the env var set to ONLY the stale SHA, the payload that
    // carried the stale SHA must now admit (env var REPLACES default),
    // and the previously-admitting default fixture SHA must now refuse.
    let tmp_admit = temp_dir("env-replaces-admits");
    let stale_payload = payload_with_source_commit(STALE_SOURCE_COMMIT);
    let stale_with_env_out = run_admit_axiom_with_payload(
        &tmp_admit,
        &stale_payload,
        &[("CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS", STALE_SOURCE_COMMIT)],
    );
    let stale_stderr = String::from_utf8_lossy(&stale_with_env_out.stderr);
    assert!(
        stale_with_env_out.status.success(),
        "env-var override must admit the listed SHA; exit={:?}\nstderr: {stale_stderr}",
        stale_with_env_out.status.code()
    );
    assert!(
        stale_stderr.contains("decision=admit_candidate"),
        "env-var override must admit as candidate; stderr: {stale_stderr}"
    );

    let tmp_refuse = temp_dir("env-replaces-refuses");
    let default_payload = payload_with_source_commit(DEFAULT_FIXTURE_SOURCE_COMMIT);
    let default_with_env_out = run_admit_axiom_with_payload(
        &tmp_refuse,
        &default_payload,
        &[("CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS", STALE_SOURCE_COMMIT)],
    );
    let default_stderr = String::from_utf8_lossy(&default_with_env_out.stderr);
    assert!(
        !default_with_env_out.status.success(),
        "env-var override must REPLACE (not extend) the default; the default fixture SHA must now refuse; got exit={:?}\nstderr: {default_stderr}",
        default_with_env_out.status.code()
    );
    assert!(
        default_stderr.contains("axiom_execution_trust.tool_provenance.source_commit.stale"),
        "default fixture SHA must refuse with the freshness invariant when env-var override excludes it; got: {default_stderr}"
    );

    std::fs::remove_dir_all(&tmp_admit).ok();
    std::fs::remove_dir_all(&tmp_refuse).ok();
}

#[test]
fn admit_axiom_empty_env_var_refuses_everything() {
    // Per docs: setting the env var to the empty string yields an empty
    // accept-list, so the freshness gate refuses every source_commit
    // (including the default fixture SHA).
    let tmp = temp_dir("env-empty");
    let payload = payload_with_source_commit(DEFAULT_FIXTURE_SOURCE_COMMIT);
    let out = run_admit_axiom_with_payload(
        &tmp,
        &payload,
        &[("CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS", "")],
    );
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        !out.status.success(),
        "empty env var must yield empty accept-list; got exit={:?}\nstderr: {stderr}",
        out.status.code()
    );
    assert!(
        stderr.contains("axiom_execution_trust.tool_provenance.source_commit.stale"),
        "empty env var must refuse with the freshness invariant; got: {stderr}"
    );

    std::fs::remove_dir_all(&tmp).ok();
}