#![cfg(unix)]
mod common;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
fn run_in(cwd: &Path, home: &Path, args: &[&str]) -> (Option<i32>, String) {
let out = Command::new(env!("CARGO_BIN_EXE_agentd"))
.args(args)
.current_dir(cwd)
.env_remove("AGENT_CONFIG")
.env_remove("AGENTD_CONFIG")
.env_remove("XDG_CONFIG_HOME")
.env("HOME", home)
.stdin(Stdio::null())
.stdout(Stdio::null())
.output()
.expect("run");
(
out.status.code(),
String::from_utf8_lossy(&out.stderr).to_string(),
)
}
fn project(tag: &str) -> (PathBuf, PathBuf, PathBuf) {
let root = PathBuf::from(common::unique_path(tag, "d"));
let (home, work) = (root.join("home"), root.join("work"));
std::fs::create_dir_all(home.join(".config/agentd")).unwrap();
std::fs::create_dir_all(&work).unwrap();
(root, home, work)
}
const BASE: &str = "config_version: \"1\"\n\
agent: { name: conv, instruction: conventions }\n\
intelligence: { endpoints: \"mock:final\", model: mock }\n\
store: { kind: memory }\n\
observability: { log_level: info }\n\
lifecycle: { run_until: idle, idle_grace: 1s }\n";
fn wf(name: &str) -> String {
format!(
"name: {name}\nsteps:\n s: {{ kind: manual }}\n \
f: {{ kind: finish, depends_on: [s], status: completed }}\n"
)
}
#[test]
fn the_chain_layers_and_the_most_specific_rung_wins() {
let (root, home, work) = project("chain");
std::fs::write(home.join(".config/agentd/config.yml"), BASE).unwrap();
let (code, log) = run_in(&work, &home, &["--validate-config"]);
assert_eq!(code, Some(0), "{log}");
assert!(
log.contains("config.yml"),
"the user rung should load\n{log}"
);
std::fs::write(work.join("agentd.yml"), "agent: { name: from_project }\n").unwrap();
std::fs::write(
work.join("agentd.local.yml"),
"agent: { name: from_local }\n",
)
.unwrap();
let (code, log) = run_in(&work, &home, &[]);
assert_eq!(code, Some(0), "{log}");
assert!(
log.contains("\"instance\":\"from_local\""),
"the local rung should win\n{log}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn ambiguity_is_per_rung_not_across_the_chain() {
let (root, home, work) = project("ambig");
std::fs::write(work.join("agentd.yml"), BASE).unwrap();
std::fs::write(work.join("agentd.local.yml"), "agent: { name: ok }\n").unwrap();
let (code, log) = run_in(&work, &home, &["--validate-config"]);
assert_eq!(code, Some(0), "project + local must compose\n{log}");
std::fs::write(work.join("agentd.yaml"), BASE).unwrap();
let (code, log) = run_in(&work, &home, &["--validate-config"]);
assert_eq!(code, Some(2), "{log}");
assert!(
log.contains("project config is ambiguous"),
"the refusal should name the rung\n{log}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn an_explicit_config_suppresses_the_whole_chain() {
let (root, home, work) = project("explicit");
std::fs::write(work.join("agentd.yml"), BASE).unwrap();
std::fs::write(
work.join("agentd.local.yml"),
"agent: { name: must_not_win }\n",
)
.unwrap();
let (code, log) = run_in(&work, &home, &["-c", "agentd.yml", "--validate-config"]);
assert_eq!(code, Some(0), "{log}");
assert!(
!log.contains("agentd.local.yml"),
"a named config must not adopt the local rung\n{log}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_workflows_folder_is_adopted_in_filename_order() {
let (root, home, work) = project("wfdir");
std::fs::write(work.join("agentd.yml"), BASE).unwrap();
std::fs::create_dir_all(work.join("workflows")).unwrap();
std::fs::write(work.join("workflows/30-charlie.yaml"), wf("charlie")).unwrap();
std::fs::write(work.join("workflows/10-alpha.yaml"), wf("alpha")).unwrap();
std::fs::write(work.join("workflows/20-bravo.yaml"), wf("bravo")).unwrap();
let (code, log) = run_in(&work, &home, &[]);
assert_eq!(code, Some(0), "{log}");
let order: Vec<&str> = log
.lines()
.filter(|l| l.contains("\"workflow.loaded\""))
.filter_map(|l| {
["alpha", "bravo", "charlie"]
.into_iter()
.find(|n| l.contains(&format!("\"name\":\"{n}\"")))
})
.collect();
assert_eq!(order, ["alpha", "bravo", "charlie"], "{log}");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn an_explicit_workflows_setting_suppresses_the_folder() {
let (root, home, work) = project("wfexplicit");
std::fs::write(work.join("agentd.yml"), format!("{BASE}workflows: []\n")).unwrap();
std::fs::create_dir_all(work.join("workflows")).unwrap();
std::fs::write(work.join("workflows/a.yaml"), wf("should_not_load")).unwrap();
let (code, log) = run_in(&work, &home, &[]);
assert_eq!(code, Some(0), "{log}");
assert!(
!log.contains("should_not_load"),
"an explicit empty list means none\n{log}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn absent_folders_are_silent() {
let (root, home, work) = project("empty");
std::fs::write(work.join("agentd.yml"), BASE).unwrap();
std::fs::create_dir_all(work.join("workflows")).unwrap(); let (code, log) = run_in(&work, &home, &["--validate-config"]);
assert_eq!(
code,
Some(0),
"an empty conventional folder must not fail\n{log}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn subagent_and_context_templates_load_from_their_folders() {
let (root, home, work) = project("tpls");
std::fs::write(work.join("agentd.yml"), BASE).unwrap();
std::fs::create_dir_all(work.join("subagents")).unwrap();
std::fs::create_dir_all(work.join("context")).unwrap();
std::fs::write(
work.join("subagents/reviewer.yaml"),
"instruction: |\n Review what you are given.\nparams:\n target: { type: string }\n",
)
.unwrap();
std::fs::write(work.join("context/minimal.md"), "You are {{instance}}.\n").unwrap();
let (code, log) = run_in(&work, &home, &["--validate-config"]);
assert_eq!(code, Some(0), "{log}");
assert!(
log.contains("subagents.templates are declared but a2a.listen is unset"),
"the subagents folder should have populated templates\n{log}"
);
std::fs::write(work.join("context/minimal.md"), "You are {{nope}}.\n").unwrap();
let (code, log) = run_in(&work, &home, &["--validate-config"]);
assert_eq!(code, Some(2), "{log}");
assert!(
log.contains("context.templates.minimal"),
"the refusal should name the folder-loaded template\n{log}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_skills_folder_is_adopted_without_a_server() {
let (root, home, work) = project("skdir");
std::fs::write(work.join("agentd.yml"), BASE).unwrap();
std::fs::create_dir_all(work.join("skills/runbook")).unwrap();
std::fs::write(
work.join("skills/triage.md"),
"---\nname: triage\ndescription: Triage an issue. Use when: it has no labels\n---\n\nRead it, label it.\n",
)
.unwrap();
std::fs::write(
work.join("skills/runbook/SKILL.md"),
"---\nname: incident\ndescription: Handle an incident\n---\n\nAcknowledge, then mitigate.\n",
)
.unwrap();
std::fs::write(
work.join("skills/deploy.md"),
"# Deploy safely\n\nAlways deploy behind a flag.\n",
)
.unwrap();
let (code, log) = run_in(&work, &home, &[]);
assert_eq!(code, Some(0), "{log}");
assert!(
log.contains("\"server\":\"file\"") && log.contains("\"count\":3"),
"all three layouts should register\n{log}"
);
for name in ["triage", "incident", "deploy"] {
assert!(log.contains(name), "{name} missing\n{log}");
}
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn an_explicit_skills_dir_suppresses_the_convention() {
let (root, home, work) = project("skexplicit");
std::fs::create_dir_all(work.join("skills")).unwrap();
std::fs::create_dir_all(work.join("elsewhere")).unwrap();
std::fs::write(
work.join("skills/ignored.md"),
"# Ignored\n\nnot this one.\n",
)
.unwrap();
std::fs::write(work.join("elsewhere/chosen.md"), "# Chosen\n\nthis one.\n").unwrap();
std::fs::write(
work.join("agentd.yml"),
format!("{BASE}skills: {{ dir: ./elsewhere }}\n"),
)
.unwrap();
let (code, log) = run_in(&work, &home, &[]);
assert_eq!(code, Some(0), "{log}");
assert!(log.contains("chosen"), "{log}");
assert!(
!log.contains("ignored"),
"the convention must not override\n{log}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_config_var_resolves_in_a_workflow_dir_and_file_reference() {
let (root, home, work) = project("wfvar");
std::fs::create_dir_all(work.join("default_wf")).unwrap();
std::fs::create_dir_all(work.join("site_wf")).unwrap();
std::fs::write(work.join("default_wf/a.yaml"), wf("default_extra")).unwrap();
std::fs::write(work.join("site_wf/a.yaml"), wf("site_extra")).unwrap();
std::fs::write(work.join("standalone.yaml"), wf("via_file_var")).unwrap();
let cfg = r#"vars:
wf_dir: ./default_wf
wf_file: ./standalone.yaml
workflows:
- name: from_base
steps:
s: { kind: manual }
f: { kind: finish, depends_on: [s], status: completed }
- dir: "{{config.wf_dir}}"
glob: "*.yaml"
- name: via_file
file: "{{config.wf_file}}"
"#;
std::fs::write(work.join("agentd.yml"), format!("{BASE}{cfg}")).unwrap();
let (code, log) = run_in(&work, &home, &[]);
assert_eq!(code, Some(0), "{log}");
for name in ["from_base", "default_extra", "via_file_var"] {
assert!(
log.contains(&format!("\"name\":\"{name}\"")),
"{name} should have loaded\n{log}"
);
}
std::fs::write(
work.join("agentd.local.yml"),
"vars: { wf_dir: ./site_wf }\n",
)
.unwrap();
let (code, log) = run_in(&work, &home, &[]);
assert_eq!(code, Some(0), "{log}");
assert!(
log.contains("site_extra"),
"the overlay should redirect\n{log}"
);
assert!(
log.contains("from_base") && log.contains("via_file_var"),
"and must not drop what the base declared\n{log}"
);
assert!(
!log.contains("default_extra"),
"the old folder should no longer load\n{log}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn an_undefined_var_in_a_reference_is_reported_once() {
let (root, home, work) = project("wfvarbad");
std::fs::create_dir_all(work.join("wfs")).unwrap();
std::fs::write(work.join("wfs/a.yaml"), wf("never_loads")).unwrap();
std::fs::write(
work.join("agentd.yml"),
format!("{BASE}workflows:\n - dir: \"{{{{config.nope}}}}\"\n"),
)
.unwrap();
let (_code, log) = run_in(&work, &home, &[]);
let hits = log.matches("config.nope is not defined").count();
assert_eq!(hits, 1, "expected exactly one message\n{log}");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn an_overlay_elsewhere_does_not_hide_the_projects_folders() {
let (root, home, work) = project("overlaydir");
std::fs::write(work.join("agentd.yml"), BASE).unwrap();
std::fs::create_dir_all(work.join("workflows")).unwrap();
std::fs::write(work.join("workflows/a.yaml"), wf("from_folder")).unwrap();
let over = root.join("elsewhere");
std::fs::create_dir_all(&over).unwrap();
let over_cfg = over.join("over.yml");
std::fs::write(&over_cfg, "agent: { name: overlaid }\n").unwrap();
let (code, log) = run_in(
&work,
&home,
&["-c", "agentd.yml", "-c", over_cfg.to_str().unwrap()],
);
assert_eq!(code, Some(0), "{log}");
assert!(
log.contains("from_folder"),
"the project's workflows folder should still be found\n{log}"
);
assert!(
log.contains("\"instance\":\"overlaid\""),
"and the overlay still wins on the keys it sets\n{log}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn the_working_directory_is_not_searched_for_a_named_config() {
let (root, home, work) = project("cwdleak");
let proj = work.join("proj");
std::fs::create_dir_all(&proj).unwrap();
std::fs::write(proj.join("agentd.yml"), BASE).unwrap();
std::fs::create_dir_all(work.join("skills")).unwrap();
std::fs::write(
work.join("skills/not-mine.md"),
"# Not mine\n\nbelongs to whatever lives in this directory.\n",
)
.unwrap();
let (code, log) = run_in(&work, &home, &["-c", "proj/agentd.yml"]);
assert_eq!(code, Some(0), "{log}");
assert!(
!log.contains("not-mine"),
"a named config must not adopt the working directory's folders\n{log}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn the_working_directory_is_searched_when_no_config_exists() {
let (root, home, work) = project("cwdonly");
std::fs::create_dir_all(work.join("workflows")).unwrap();
std::fs::write(work.join("workflows/a.yaml"), wf("bare_cwd")).unwrap();
let (_code, log) = run_in(
&work,
&home,
&[
"--intelligence",
"mock:final",
"--model",
"mock",
"--validate-config",
],
);
assert!(
log.contains("config.valid") || log.contains("bare_cwd"),
"a config-less run should still find ./workflows\n{log}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn validate_refuses_every_credential_reference_startup_refuses() {
let (root, home, work) = project("credrefs");
let cases: [(&str, &str); 4] = [
(
"intelligence.token",
"intelligence: { endpoints: \"https://x/v1\", model: m, token: \"{{secret:ABSENT_ONE}}\" }\n",
),
(
"intelligence.headers",
"intelligence: { endpoints: \"https://x/v1\", model: m, headers: { authorization: \"Bearer {{secret:ABSENT_ONE}}\" } }\n",
),
(
"mcp auth.token",
"intelligence: { endpoints: \"https://x/v1\", model: m }\nmcp: { servers: [ { name: s, endpoint: \"https://x/mcp\", auth: { kind: static, token: \"{{secret:ABSENT_ONE}}\" } } ] }\n",
),
(
"principal bearer_ref",
"intelligence: { endpoints: \"https://x/v1\", model: m }\na2a: { listen: \"http://127.0.0.1:8477\", principals: [ { match: { bearer_ref: \"{{secret:ABSENT_ONE}}\" }, role: user } ] }\n",
),
];
for (what, body) in cases {
let cfg = work.join("c.yml");
std::fs::write(
&cfg,
format!("config_version: \"1\"\nagent: {{ name: c, instruction: x }}\nstore: {{ kind: memory }}\n{body}"),
)
.unwrap();
let (code, log) = run_in(&work, &home, &["-c", "c.yml", "--validate-config"]);
assert_eq!(
code,
Some(2),
"{what} should be refused at validate time\n{log}"
);
assert!(
log.contains("ABSENT_ONE") && log.contains("is not set in the environment"),
"{what}: the refusal should name the reference\n{log}"
);
}
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_resolvable_credential_reference_validates() {
let (root, home, work) = project("credok");
std::fs::write(
work.join("c.yml"),
"config_version: \"1\"\nagent: { name: c, instruction: x }\nstore: { kind: memory }\nintelligence: { endpoints: \"https://x/v1\", model: m, token: \"{{secret:PRESENT_ONE}}\" }\n",
)
.unwrap();
let out = std::process::Command::new(env!("CARGO_BIN_EXE_agentd"))
.args(["-c", "c.yml", "--validate-config"])
.current_dir(&work)
.env_remove("AGENT_CONFIG")
.env_remove("AGENTD_CONFIG")
.env("HOME", &home)
.env("PRESENT_ONE", "a-value")
.output()
.expect("run");
assert_eq!(
out.status.code(),
Some(0),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn an_unimplemented_hmac_algo_is_refused_at_validate_time() {
let (root, home, work) = project("hmacalgo");
let cfg = r#"config_version: "1"
agent: { name: c, instruction: x }
store: { kind: memory }
intelligence: { endpoints: "https://x/v1", model: m }
webhooks:
listen: "http://127.0.0.1:8467"
default_auth: { hmac: { secret: "{{secret:HOOKSEC}}", header: X-Signature, algo: sha512, prefix: "sha512=" } }
workflows:
- name: w
steps:
h: { kind: webhook, path: /h, methods: [POST] }
f: { kind: finish, depends_on: [h], status: completed }
"#;
std::fs::write(work.join("c.yml"), cfg).unwrap();
let out = std::process::Command::new(env!("CARGO_BIN_EXE_agentd"))
.args(["-c", "c.yml", "--validate-config"])
.current_dir(&work)
.env_remove("AGENT_CONFIG")
.env_remove("AGENTD_CONFIG")
.env("HOME", &home)
.env("HOOKSEC", "s")
.output()
.expect("run");
let log = String::from_utf8_lossy(&out.stderr);
assert_eq!(out.status.code(), Some(2), "{log}");
assert!(
log.contains("hmac.algo") && log.contains("HMAC-SHA256 only"),
"the refusal should name the field and what IS computed\n{log}"
);
for algo in ["sha256", "SHA256"] {
std::fs::write(work.join("c.yml"), cfg.replace("sha512", algo)).unwrap();
let out = std::process::Command::new(env!("CARGO_BIN_EXE_agentd"))
.args(["-c", "c.yml", "--validate-config"])
.current_dir(&work)
.env_remove("AGENT_CONFIG")
.env_remove("AGENTD_CONFIG")
.env("HOME", &home)
.env("HOOKSEC", "s")
.output()
.expect("run");
assert_eq!(
out.status.code(),
Some(0),
"{algo} should validate\n{}",
String::from_utf8_lossy(&out.stderr)
);
}
let _ = std::fs::remove_dir_all(&root);
}