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");
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");
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();
}