use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn cortex_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_cortex"))
}
fn run_in(cwd: &Path, args: &[&str]) -> std::process::Output {
run_in_with_env(cwd, args, &[])
}
fn run_in_with_env(
cwd: &Path,
args: &[&str],
extra_env: &[(&str, String)],
) -> std::process::Output {
let mut cmd = Command::new(cortex_bin());
cmd.current_dir(cwd)
.env_remove("CORTEX_CONFIG")
.env_remove("CORTEX_DATA_DIR")
.env_remove("CORTEX_DB_PATH")
.env_remove("CORTEX_EVENT_LOG_PATH")
.env_remove("RUST_LOG")
.env("XDG_CONFIG_HOME", cwd.join("xdg-config"))
.env("XDG_DATA_HOME", cwd.join("xdg"))
.env("HOME", cwd)
.env("APPDATA", cwd.join("AppData").join("Roaming"))
.env("CORTEX_PROFILE", "local")
.env("CORTEX_API_TOKEN", "super-secret-token")
.args(args);
for (key, value) in extra_env {
cmd.env(key, value);
}
cmd.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),
);
}
fn expected_data_dir(cwd: &Path) -> PathBuf {
cwd.join("xdg").join("cortex")
}
fn expected_config_path(cwd: &Path) -> PathBuf {
cwd.join("xdg-config").join("cortex").join("config.toml")
}
fn toml_string(path: &Path) -> String {
format!("{:?}", path.display().to_string())
}
fn write_config(path: &Path, body: &str) {
fs::create_dir_all(path.parent().expect("config has parent")).unwrap();
fs::write(path, body).unwrap();
}
fn field_value(line: &str, key: &str) -> Option<String> {
let cleaned = strip_ansi_codes(line);
let needle = format!("{key}=");
let value = cleaned.split(&needle).nth(1)?;
if let Some(quoted) = value.strip_prefix('"') {
return quoted.split('"').next().map(ToOwned::to_owned);
}
value.split_whitespace().next().map(ToOwned::to_owned)
}
fn strip_ansi_codes(line: &str) -> String {
let mut cleaned = String::with_capacity(line.len());
let mut chars = line.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\u{1b}' && chars.peek() == Some(&'[') {
chars.next();
for code_ch in chars.by_ref() {
if code_ch.is_ascii_alphabetic() {
break;
}
}
continue;
}
cleaned.push(ch);
}
cleaned
}
#[test]
fn print_config_reports_effective_paths_and_redacts_secret_env() {
let tmp = tempfile::tempdir().unwrap();
let data_dir = expected_data_dir(tmp.path());
let out = run_in(tmp.path(), &["--print-config"]);
assert_exit(&out, 0);
assert!(
out.stderr.is_empty(),
"stderr should be empty: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stdout.contains("super-secret-token"),
"stdout must not leak secret env values: {stdout}"
);
let json: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("stdout is valid config JSON");
assert_eq!(json["data_dir"], data_dir.display().to_string());
assert_eq!(
json["db_path"],
data_dir.join("cortex.db").display().to_string()
);
assert_eq!(
json["event_log_path"],
data_dir.join("events.jsonl").display().to_string()
);
assert_eq!(json["env"]["CORTEX_PROFILE"], "local");
assert_eq!(json["env"]["CORTEX_API_TOKEN"], "<redacted>");
}
#[test]
fn command_observability_logs_command_lifecycle_without_secrets() {
let tmp = tempfile::tempdir().unwrap();
let out = run_in_with_env(
tmp.path(),
&["--print-config"],
&[("RUST_LOG", "cortex=info".to_string())],
);
assert_exit(&out, 0);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("super-secret-token"),
"observability stderr must not leak secret env values: {stderr}"
);
let lifecycle_lines: Vec<&str> = stderr
.lines()
.filter(|line| line.contains("command_started") || line.contains("command_completed"))
.collect();
assert_eq!(
lifecycle_lines.len(),
2,
"expected start and completion observability lines, got: {stderr}"
);
let start = lifecycle_lines
.iter()
.find(|line| line.contains("command_started"))
.expect("start line");
let completed = lifecycle_lines
.iter()
.find(|line| line.contains("command_completed"))
.expect("completed line");
for line in [*start, *completed] {
assert_eq!(
field_value(line, "operation").as_deref(),
Some("cli.command")
);
assert_eq!(
field_value(line, "command").as_deref(),
Some("print_config")
);
assert_eq!(
field_value(line, "audit_schema_version").as_deref(),
Some("1")
);
assert_eq!(field_value(line, "proof_state").as_deref(), Some("UNKNOWN"));
assert_eq!(
field_value(line, "authority_class").as_deref(),
Some("diagnostic_only")
);
assert_eq!(
field_value(line, "diagnostic_only").as_deref(),
Some("true")
);
assert!(
field_value(line, "correlation_id")
.as_deref()
.is_some_and(|value| value.starts_with("cmd_")),
"correlation_id should use cmd_ prefix: {line}"
);
}
assert_eq!(
field_value(start, "correlation_id"),
field_value(completed, "correlation_id"),
"start/completed lines should share the same correlation id"
);
assert_eq!(field_value(completed, "exit_code").as_deref(), Some("0"));
assert_eq!(field_value(completed, "status").as_deref(), Some("ok"));
}
#[test]
fn command_observability_does_not_create_persisted_audit_artifacts() {
let tmp = tempfile::tempdir().unwrap();
let data_dir = expected_data_dir(tmp.path());
let out = run_in_with_env(
tmp.path(),
&["--print-config"],
&[("RUST_LOG", "cortex=info".to_string())],
);
assert_exit(&out, 0);
assert!(
!data_dir.join("audit_records.jsonl").exists(),
"diagnostic command tracing must not create audit_records.jsonl"
);
assert!(
!data_dir.join("cortex.sqlite3").exists(),
"diagnostic command tracing must not open or create the SQLite store"
);
assert!(
!data_dir.join("cortex.db").exists(),
"diagnostic command tracing must not create legacy DB artifacts"
);
}
#[test]
fn print_config_loads_default_config_file_layer() {
let tmp = tempfile::tempdir().unwrap();
let config_path = expected_config_path(tmp.path());
let data_dir = tmp.path().join("configured-data");
let db_path = tmp.path().join("configured.db");
let event_log_path = tmp.path().join("configured-events.jsonl");
write_config(
&config_path,
&format!(
"data_dir = {}\ndb_path = {}\nevent_log_path = {}\n",
toml_string(&data_dir),
toml_string(&db_path),
toml_string(&event_log_path),
),
);
let out = run_in(tmp.path(), &["--print-config"]);
assert_exit(&out, 0);
let json: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("stdout is valid config JSON");
assert_eq!(json["data_dir"], data_dir.display().to_string());
assert_eq!(json["db_path"], db_path.display().to_string());
assert_eq!(json["event_log_path"], event_log_path.display().to_string());
}
#[test]
fn print_config_uses_cortex_config_and_env_overrides_file() {
let tmp = tempfile::tempdir().unwrap();
let config_path = tmp.path().join("custom-config.toml");
let file_data_dir = tmp.path().join("file-data");
let file_db_path = tmp.path().join("file.db");
let file_event_log_path = tmp.path().join("file-events.jsonl");
let env_data_dir = tmp.path().join("env-data");
let env_db_path = tmp.path().join("env.db");
write_config(
&config_path,
&format!(
"data_dir = {}\ndb_path = {}\nevent_log_path = {}\n",
toml_string(&file_data_dir),
toml_string(&file_db_path),
toml_string(&file_event_log_path),
),
);
let out = run_in_with_env(
tmp.path(),
&["--print-config"],
&[
("CORTEX_CONFIG", config_path.display().to_string()),
("CORTEX_DATA_DIR", env_data_dir.display().to_string()),
("CORTEX_DB_PATH", env_db_path.display().to_string()),
],
);
assert_exit(&out, 0);
let json: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("stdout is valid config JSON");
assert_eq!(json["data_dir"], env_data_dir.display().to_string());
assert_eq!(json["db_path"], env_db_path.display().to_string());
assert_eq!(
json["event_log_path"],
file_event_log_path.display().to_string()
);
assert_eq!(
json["env"]["CORTEX_CONFIG"],
config_path.display().to_string()
);
}
#[test]
fn print_config_rejects_subcommand_combination() {
let tmp = tempfile::tempdir().unwrap();
let out = run_in(tmp.path(), &["--print-config", "init"]);
assert_exit(&out, 2);
assert!(
String::from_utf8_lossy(&out.stderr).contains("cannot be combined"),
"stderr should explain invalid combination: {}",
String::from_utf8_lossy(&out.stderr)
);
}