crtx 0.1.1

CLI for the Cortex supervisory memory substrate.
//! CLI tests for `cortex migrate seed-drill-operator`.
//!
//! The surface is **drill-only** and hard-gated behind the
//! `CORTEX_DRILL_ALLOW_SEED_OPERATOR=1` environment variable. These tests
//! prove (1) the gate refuses without the env var, (2) the gate allows
//! the call with the env var, (3) successful seeding registers a row in
//! the durable `authority_key_timeline` so a subsequent migrate-v2
//! revalidation passes.
//!
//! Doctrine: ADR 0026 (policy lattice) + the v1-to-v2-drill CI gap
//! captured at `docs/issues/cortex-followup-ci-v1-to-v2-drill-operator-
//! key-unprovisioned.md`. The drill harness needs this surface so the
//! cutover's temporal-authority revalidation has a key to validate.

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

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

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

fn run_with_env(data_dir: &Path, env: &[(&str, &str)], args: &[&str]) -> std::process::Output {
    let mut cmd = Command::new(cortex_bin());
    cmd.env_remove("RUST_LOG");
    cmd.env("XDG_DATA_HOME", data_dir);
    cmd.env("HOME", data_dir);
    cmd.env_remove("CORTEX_DRILL_ALLOW_SEED_OPERATOR");
    for (k, v) in env {
        cmd.env(k, v);
    }
    cmd.args(args);
    cmd.output().expect("spawn cortex")
}

fn run(data_dir: &Path, args: &[&str]) -> std::process::Output {
    run_with_env(data_dir, &[], args)
}

fn assert_exit(out: &std::process::Output, expected: i32, context: &str) {
    let code = out.status.code().expect("process exited via signal");
    assert_eq!(
        code,
        expected,
        "{context}: expected exit {expected}, got {code}\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr),
    );
}

fn init_store(data_dir: &Path) {
    let init = run(data_dir, &["init"]);
    assert_exit(&init, 0, "cortex init");
}

#[test]
fn seed_drill_operator_refuses_without_gate_env_var() {
    let dir = temp_data_dir("gate-refuses");
    init_store(&dir);

    let out = run(
        &dir,
        &[
            "migrate",
            "seed-drill-operator",
            "--operator-key-id",
            "drill-test-operator",
        ],
    );
    assert_exit(&out, 7, "seed-drill-operator without gate"); // 7 = Exit::PreconditionUnmet

    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        stderr.contains("migrate.seed_drill_operator.gate_not_set"),
        "stderr should surface the stable gate invariant; got: {stderr}",
    );
    assert!(
        stderr.contains("CORTEX_DRILL_ALLOW_SEED_OPERATOR=1"),
        "stderr should name the env var; got: {stderr}",
    );
    assert!(
        stderr.contains("no state was changed"),
        "stderr should declare no-state-change; got: {stderr}",
    );

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

#[test]
fn seed_drill_operator_refuses_with_wrong_gate_value() {
    let dir = temp_data_dir("gate-wrong-value");
    init_store(&dir);

    let out = run_with_env(
        &dir,
        &[("CORTEX_DRILL_ALLOW_SEED_OPERATOR", "yes")],
        &[
            "migrate",
            "seed-drill-operator",
            "--operator-key-id",
            "drill-test-operator",
        ],
    );
    assert_exit(&out, 7, "seed-drill-operator with wrong gate value");

    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        stderr.contains("migrate.seed_drill_operator.gate_not_set"),
        "stderr should surface the stable gate invariant; got: {stderr}",
    );

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

#[test]
fn seed_drill_operator_refuses_empty_key_id() {
    let dir = temp_data_dir("empty-key");
    init_store(&dir);

    let out = run_with_env(
        &dir,
        &[("CORTEX_DRILL_ALLOW_SEED_OPERATOR", "1")],
        &["migrate", "seed-drill-operator", "--operator-key-id", "   "],
    );
    assert_exit(&out, 2, "seed-drill-operator empty key"); // 2 = Exit::Usage

    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        stderr.contains("--operator-key-id must not be empty"),
        "stderr should explain empty key refusal; got: {stderr}",
    );

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

#[test]
fn seed_drill_operator_succeeds_with_gate_and_reports_fields() {
    let dir = temp_data_dir("happy-path");
    init_store(&dir);

    let out = run_with_env(
        &dir,
        &[("CORTEX_DRILL_ALLOW_SEED_OPERATOR", "1")],
        &[
            "migrate",
            "seed-drill-operator",
            "--operator-key-id",
            "drill-test-operator",
        ],
    );
    assert_exit(&out, 0, "seed-drill-operator happy path");

    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        stdout.contains("cortex migrate seed-drill-operator: ok"),
        "stdout should announce ok; got: {stdout}",
    );
    assert!(
        stdout.contains("operator_key_id=drill-test-operator"),
        "stdout should echo operator key id; got: {stdout}",
    );
    assert!(
        stdout.contains("principal_id=drill-test-operator-principal"),
        "stdout should derive default principal id; got: {stdout}",
    );
    assert!(
        stdout.contains("trust_tier=operator"),
        "stdout should report operator trust tier; got: {stdout}",
    );
    assert!(
        stdout.contains("key_state=active"),
        "stdout should report active key state; got: {stdout}",
    );
    assert!(
        stdout.contains("effective_at=1970-01-01T00:00:00+00:00"),
        "stdout should report epoch-backdated effective_at so revalidation \
         passes for any signed_at; got: {stdout}",
    );

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

#[test]
fn seed_drill_operator_accepts_explicit_principal_id() {
    let dir = temp_data_dir("explicit-principal");
    init_store(&dir);

    let out = run_with_env(
        &dir,
        &[("CORTEX_DRILL_ALLOW_SEED_OPERATOR", "1")],
        &[
            "migrate",
            "seed-drill-operator",
            "--operator-key-id",
            "drill-test-operator",
            "--principal-id",
            "custom-drill-principal",
        ],
    );
    assert_exit(&out, 0, "seed-drill-operator explicit principal");

    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        stdout.contains("principal_id=custom-drill-principal"),
        "stdout should use explicit principal id; got: {stdout}",
    );

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