mod common;
use std::path::Path;
use std::process::Command;
fn workdir(tag: &str) -> String {
let dir = common::unique_path(tag, "dir");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create workdir");
dir
}
fn write(dir: &str, name: &str, body: &str) -> String {
let path = Path::new(dir).join(name);
std::fs::write(&path, body).expect("write config");
path.to_string_lossy().into_owned()
}
fn agentd(dir: &str, args: &[&str], env: &[(&str, &str)]) -> (i32, String) {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_agentd"));
cmd.current_dir(dir).args(args);
for (k, _) in std::env::vars() {
if k.starts_with("AGENTD_") || k.starts_with("AGENT_") {
cmd.env_remove(k);
}
}
for (k, v) in env {
cmd.env(k, v);
}
let out = cmd.output().expect("spawn agentd");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
const HEADER_REF: &str = "config_version: \"2\"\nstore: {kind: memory}\n\
intelligence:\n endpoints: [\"https://intel.invalid/v1\"]\n model: m\n\
\x20 headers:\n authorization: \"Bearer {{secret:AGENTD_TEST_ABSENT_INTEL_KEY}}\"\n";
#[test]
fn an_unresolvable_intelligence_header_ref_is_exit_2_and_names_the_ref() {
let dir = workdir("cfg-secret");
let cfg = write(&dir, "intel.yaml", HEADER_REF);
let (code, err) = agentd(&dir, &["--config", &cfg, "--validate-config"], &[]);
assert_eq!(code, 2, "unresolvable secret ref must be exit 2:\n{err}");
assert!(
err.contains("AGENTD_TEST_ABSENT_INTEL_KEY"),
"the message must name the unresolved reference:\n{err}"
);
assert!(
err.contains("intelligence.headers"),
"the message must name the setting:\n{err}"
);
let (code, err) = agentd(&dir, &["--config", &cfg], &[]);
assert_eq!(code, 2, "startup must refuse the same config:\n{err}");
assert!(err.contains("AGENTD_TEST_ABSENT_INTEL_KEY"), "{err}");
let (code, err) = agentd(
&dir,
&["--config", &cfg, "--validate-config"],
&[("AGENTD_TEST_ABSENT_INTEL_KEY", "sk-test")],
);
assert_eq!(code, 0, "a resolvable ref must validate clean:\n{err}");
let _ = std::fs::remove_dir_all(&dir);
}
const TRIFECTA_MCP: &str = "config_version: \"2\"\nstore: {kind: memory}\n\
mcp:\n servers:\n\
\x20 - name: web\n endpoint: https://mcp-web.invalid/mcp\n tags: {\"*\": [untrusted_input]}\n\
\x20 - name: vault\n endpoint: https://mcp-vault.invalid/mcp\n tags: {\"*\": [sensitive, egress]}\n";
#[test]
fn a_discovered_dotfile_cannot_lift_the_trifecta_gate() {
let dir = workdir("cfg-discover");
let dotfile = write(
&dir,
".agentd.yml",
&format!("{TRIFECTA_MCP}security:\n allow_trifecta: true\n"),
);
let (code, err) = agentd(&dir, &["--validate-config"], &[]);
assert_eq!(
code, 2,
"a discovered dotfile must not lift the trifecta gate:\n{err}"
);
assert!(
err.contains("security.allow_trifecta") && err.contains(".agentd.yml"),
"the refusal must name the setting and the file:\n{err}"
);
let plain = workdir("cfg-discover-plain");
write(&plain, ".agentd.yml", TRIFECTA_MCP);
let (code, err) = agentd(&plain, &["--validate-config"], &[]);
assert_eq!(code, 2, "the trifecta grant itself is refused:\n{err}");
assert!(err.contains("lethal-trifecta"), "{err}");
let named = workdir("cfg-named");
let (code, err) = agentd(&named, &["--config", &dotfile, "--validate-config"], &[]);
assert_eq!(
code, 0,
"--config must keep the power discovery does not have:\n{err}"
);
for d in [&dir, &plain, &named] {
let _ = std::fs::remove_dir_all(d);
}
}
#[test]
fn a_multibyte_budget_reset_is_exit_2_not_a_panic() {
let dir = workdir("cfg-reset");
let cfg = write(
&dir,
"budget.yaml",
"config_version: \"2\"\nstore: {kind: memory}\n\
intelligence:\n budget:\n windows:\n - per: day\n tokens: 100\n reset: \"0é:0Z\"\n",
);
let (code, err) = agentd(&dir, &["--config", &cfg, "--validate-config"], &[]);
assert_ne!(code, 101, "a config error must not panic:\n{err}");
assert_eq!(code, 2, "a bad reset value is a config error:\n{err}");
assert!(
err.contains("reset"),
"the message must name the field:\n{err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn the_both_spellings_refusal_applies_only_to_discovery() {
let ambiguous = workdir("cfg-ambiguous");
write(&ambiguous, ".agentd.yml", "config_version: \"2\"\n");
write(&ambiguous, ".agentd.yaml", "config_version: \"2\"\n");
let (code, err) = agentd(&ambiguous, &["--validate-config"], &[]);
assert_eq!(code, 2, "two discovered spellings must refuse:\n{err}");
assert!(err.contains(".agentd.yaml"), "{err}");
let base = workdir("cfg-layer-base");
let over = workdir("cfg-layer-over");
let a = write(
&base,
".agentd.yml",
"config_version: \"2\"\nstore: {kind: memory}\nintelligence:\n model: from-base\n",
);
let b = write(
&over,
".agentd.yaml",
"intelligence:\n model: from-overlay\n",
);
let run = workdir("cfg-layer-run");
let (code, err) = agentd(
&run,
&["--config", &a, "--config", &b, "--validate-config"],
&[],
);
assert_eq!(
code, 0,
"explicitly named files may be spelled anything:\n{err}"
);
assert!(
err.contains("config.valid"),
"the pre-flight must report the layered set as valid:\n{err}"
);
for d in [&ambiguous, &base, &over, &run] {
let _ = std::fs::remove_dir_all(d);
}
}