use std::path::Path;
use tempfile::TempDir;
use crate::{
config::{
ServerConfig,
defaults::{
ASSISTANT_ACCOUNT_ENV_CREDENTIAL_SHAPED, ASSISTANT_ACCOUNT_ENV_NAME_INVALID,
ASSISTANT_ACCOUNT_NAME_DUPLICATE, ASSISTANT_ACCOUNT_NAME_REQUIRED,
ASSISTANT_HARNESS_NAME_DUPLICATE, ASSISTANT_HARNESS_NAME_REQUIRED,
ASSISTANT_HARNESS_NAME_UNKNOWN,
},
},
error::ServerError,
};
type TestResult = Result<(), Box<dyn std::error::Error>>;
const RETIRED_KEYS: &[(&str, &str)] = &[
(
"default_harness",
"[assistant]\ndefault_harness = \"claude-code\"\n",
),
(
"spawn_timeout_ms",
"[assistant]\nspawn_timeout_ms = 30000\n",
),
("turn_timeout_ms", "[assistant]\nturn_timeout_ms = 600000\n"),
("event_buffer", "[assistant]\nevent_buffer = 256\n"),
(
"kind",
"[[assistant.harness]]\nname = \"claude-code\"\nkind = \"acp\"\n",
),
(
"command",
"[[assistant.harness]]\nname = \"claude-code\"\ncommand = \"/opt/acp/claude-code-acp\"\n",
),
(
"args",
"[[assistant.harness]]\nname = \"claude-code\"\nargs = [\"--verbose\"]\n",
),
(
"cwd",
"[[assistant.harness]]\nname = \"claude-code\"\ncwd = \"/srv/work\"\n",
),
(
"env_pass",
"[[assistant.harness]]\nname = \"claude-code\"\nenv_pass = [\"PATH\"]\n",
),
(
"permission",
"[[assistant.harness]]\nname = \"claude-code\"\npermission = \"deny\"\n",
),
(
"exit_grace_ms",
"[[assistant.harness]]\nname = \"claude-code\"\nexit_grace_ms = 10000\n",
),
(
"tool_confinement",
"[[assistant.harness]]\nname = \"claude-code\"\ntool_confinement = \"none\"\n",
),
("tools", "[assistant.tools]\naion = true\n"),
(
"tools",
"[[assistant.tools.mcp_server]]\nname = \"meridian\"\ntransport = \"http\"\nurl = \"http://127.0.0.1:9\"\n",
),
];
fn sandbox() -> std::io::Result<TempDir> {
crate::test_support::private_tempdir()
}
fn load(document: &str, scratch: &Path) -> Result<ServerConfig, ServerError> {
ServerConfig::from_slice_with_home_in(document.as_bytes(), scratch, scratch)
}
fn refusal(document: &str, scratch: &Path) -> Result<String, Box<dyn std::error::Error>> {
match load(document, scratch) {
Ok(_) => Err("expected the configuration to be refused, but it loaded".into()),
Err(ServerError::Config { message }) => Ok(message),
Err(other) => Err(format!("expected a configuration refusal, got: {other}").into()),
}
}
fn assert_refusal(message: &str, prefix: &str, names: &str) {
assert!(
message.starts_with(prefix),
"refusal `{message}` does not carry the stable prefix `{prefix}`"
);
assert!(
message.contains(names),
"refusal `{message}` does not name `{names}`"
);
}
#[test]
fn a_stock_server_with_no_assistant_section_still_serves_the_assistant() -> TestResult {
let scratch = sandbox()?;
let config = load("[namespaces]\ndefault = \"default\"\n", scratch.path())?;
assert!(
config.assistant.is_none(),
"an absent section parsed as present"
);
let (_store, runtime) = config.into_parts();
assert!(
runtime.assistant.harnesses.is_empty(),
"a stock server declares no accounts"
);
assert!(
runtime.assistant.account_names("claude-code").is_empty(),
"a catalogue harness with no declared accounts offers none, which is a complete answer"
);
Ok(())
}
#[test]
fn an_empty_assistant_section_is_a_complete_configuration() -> TestResult {
let scratch = sandbox()?;
let config = load("[assistant]\n", scratch.path())?;
assert!(
config.assistant.is_some(),
"the section was written, so it parsed"
);
let (_store, runtime) = config.into_parts();
assert!(runtime.assistant.harnesses.is_empty());
Ok(())
}
#[test]
fn every_retired_knob_is_refused_by_name() -> TestResult {
let scratch = sandbox()?;
for (key, document) in RETIRED_KEYS {
let message = refusal(document, scratch.path())?;
assert!(
message.contains(key),
"a document carrying the retired key `{key}` must be refused NAMING it, so an \
operator learns the knob is gone rather than believing a value nothing reads is in \
force; got: {message}"
);
}
Ok(())
}
#[test]
fn the_accounts_a_harness_declares_are_resolved_in_order_with_names_on_both_sides() -> TestResult {
let scratch = sandbox()?;
let document = r#"
[[assistant.harness]]
name = "claude-code"
[[assistant.harness.account]]
name = "work"
env = { CLAUDE_CONFIG_DIR = "AION_CLAUDE_WORK_DIR" }
[[assistant.harness.account]]
name = "personal"
env = { CLAUDE_CONFIG_DIR = "AION_CLAUDE_PERSONAL_DIR" }
"#;
let (_store, runtime) = load(document, scratch.path())?.into_parts();
assert_eq!(
runtime.assistant.account_names("claude-code"),
vec!["work".to_owned(), "personal".to_owned()],
"accounts are published in declaration order — the order the console offers them in"
);
let account = runtime
.assistant
.account("claude-code", "work")
.ok_or("the declared account must resolve")?;
assert_eq!(
account.env,
vec![(
"CLAUDE_CONFIG_DIR".to_owned(),
"AION_CLAUDE_WORK_DIR".to_owned()
)],
"the pair is (the name the child gets, the name it is read from)"
);
assert_eq!(
account.source_names(),
vec!["AION_CLAUDE_WORK_DIR".to_owned()],
"the source names are what the spawn's environment declaration is extended with"
);
Ok(())
}
#[test]
fn a_harness_name_the_build_does_not_ship_is_refused_naming_the_catalogue() -> TestResult {
let scratch = sandbox()?;
let message = refusal("[[assistant.harness]]\nname = \"claude\"\n", scratch.path())?;
assert_refusal(&message, ASSISTANT_HARNESS_NAME_UNKNOWN, "claude");
assert!(
message.contains("claude-code"),
"the refusal must list what this build DOES ship, or an operator has to guess the id; \
got: {message}"
);
Ok(())
}
#[test]
fn a_harness_entry_with_no_name_is_refused() -> TestResult {
let scratch = sandbox()?;
let message = refusal("[[assistant.harness]]\n", scratch.path())?;
assert_refusal(
&message,
ASSISTANT_HARNESS_NAME_REQUIRED,
"assistant.harness.name",
);
Ok(())
}
#[test]
fn two_harness_entries_with_one_name_are_refused() -> TestResult {
let scratch = sandbox()?;
let message = refusal(
"[[assistant.harness]]\nname = \"codex\"\n\n[[assistant.harness]]\nname = \"codex\"\n",
scratch.path(),
)?;
assert_refusal(&message, ASSISTANT_HARNESS_NAME_DUPLICATE, "codex");
Ok(())
}
#[test]
fn an_account_with_no_name_is_refused_naming_its_harness() -> TestResult {
let scratch = sandbox()?;
let message = refusal(
"[[assistant.harness]]\nname = \"codex\"\n\n[[assistant.harness.account]]\n",
scratch.path(),
)?;
assert_refusal(&message, ASSISTANT_ACCOUNT_NAME_REQUIRED, "codex");
Ok(())
}
#[test]
fn two_accounts_with_one_name_in_a_harness_are_refused() -> TestResult {
let scratch = sandbox()?;
let message = refusal(
r#"
[[assistant.harness]]
name = "codex"
[[assistant.harness.account]]
name = "work"
[[assistant.harness.account]]
name = "work"
"#,
scratch.path(),
)?;
assert_refusal(&message, ASSISTANT_ACCOUNT_NAME_DUPLICATE, "work");
Ok(())
}
#[test]
fn a_credential_shaped_account_env_name_refuses_under_the_pinned_prefix() -> TestResult {
let scratch = sandbox()?;
let message = refusal(
r#"
[[assistant.harness]]
name = "claude-code"
[[assistant.harness.account]]
name = "work"
env = { ANTHROPIC_API_KEY = "AION_ANTHROPIC_KEY_SOURCE" }
"#,
scratch.path(),
)?;
assert!(
message
.starts_with("assistant.harness.account.env refuses a credential-shaped variable name"),
"the credential refusal's stable prefix changed: {message}"
);
assert_refusal(
&message,
ASSISTANT_ACCOUNT_ENV_CREDENTIAL_SHAPED,
"ANTHROPIC_API_KEY",
);
Ok(())
}
#[test]
fn a_value_written_where_a_source_name_belongs_is_refused_rather_than_read_as_a_variable()
-> TestResult {
let scratch = sandbox()?;
let message = refusal(
r#"
[[assistant.harness]]
name = "claude-code"
[[assistant.harness.account]]
name = "work"
env = { CLAUDE_CONFIG_DIR = "/srv/aion/claude-work" }
"#,
scratch.path(),
)?;
assert_refusal(
&message,
ASSISTANT_ACCOUNT_ENV_NAME_INVALID,
"/srv/aion/claude-work",
);
Ok(())
}
#[test]
fn an_account_env_name_the_child_could_not_carry_is_refused() -> TestResult {
let scratch = sandbox()?;
let message = refusal(
r#"
[[assistant.harness]]
name = "claude-code"
[[assistant.harness.account]]
name = "work"
env = { "1CONFIG" = "AION_SOURCE" }
"#,
scratch.path(),
)?;
assert_refusal(&message, ASSISTANT_ACCOUNT_ENV_NAME_INVALID, "1CONFIG");
Ok(())
}