use aether_cli::init::{HarnessIntegration, InitError, InitOutcome, InitTarget, Preset, apply_init};
use aether_core::agent_spec::ToolMatcher;
use aether_core::core::Prompt;
use aether_project::{AetherSettings, AgentCatalog, McpSourceSpec, PromptSource};
use llm::catalog::Provider;
use llm::{LlmModel, ReasoningEffort};
use mcp_servers::{CodingMcpArgs, PlanMcpArgs, SkillsMcpArgs, SubAgentsMcpArgs, TasksMcpArgs};
use mcp_utils::client::McpServerConfig;
use std::collections::BTreeMap;
use std::path::Path;
fn load(settings_path: &Path) -> AetherSettings {
let content = std::fs::read_to_string(settings_path).expect("settings.json");
AetherSettings::try_from(content.as_str()).expect("settings parses")
}
fn agent_provider(agent: &aether_project::AgentConfig) -> Provider {
agent.model.parse::<LlmModel>().expect("model parses").provider_enum()
}
fn inline_args(servers: &BTreeMap<String, McpServerConfig>, name: &str) -> Vec<String> {
let McpServerConfig::InMemory(in_memory) = servers.get(name).unwrap_or_else(|| panic!("{name} server")) else {
panic!("expected in-memory {name}");
};
in_memory.args.clone()
}
fn inline_servers(spec: &McpSourceSpec) -> &BTreeMap<String, McpServerConfig> {
let McpSourceSpec::Inline { servers } = spec else {
panic!("expected inline MCPs, got a file spec");
};
servers
}
#[test]
fn writes_user_minimal_preset_for_codex() {
let dir = tempfile::tempdir().unwrap();
let outcome =
apply_init(InitTarget::user(dir.path()), Provider::Codex, Preset::Minimal, &[], false).expect("apply_init");
assert!(matches!(outcome, InitOutcome::Applied { .. }), "{outcome:?}");
assert!(dir.path().join("settings.json").is_file());
assert!(dir.path().join("SYSTEM.md").is_file());
assert!(
!dir.path().join("agents/codebase-explorer/AGENTS.md").exists(),
"minimal should not write explorer assets"
);
let settings = load(&dir.path().join("settings.json"));
assert_eq!(settings.prompts[0].path(), Some("SYSTEM.md"));
assert_eq!(settings.prompts[1], PromptSource::file("${WORKSPACE}/AGENTS.md").optional());
assert_eq!(settings.agents.len(), 1);
let plan = &settings.agents[0];
assert_eq!(plan.name, "Default");
assert_eq!(agent_provider(plan), Provider::Codex);
assert_eq!(plan.reasoning_effort, Some(ReasoningEffort::Xhigh));
assert_minimal_mcp_and_tools(plan);
}
#[test]
fn writes_project_minimal_preset_for_codex() {
let dir = tempfile::tempdir().unwrap();
let outcome =
apply_init(InitTarget::project(dir.path()), Provider::Codex, Preset::Minimal, &[], false).expect("apply_init");
assert!(matches!(outcome, InitOutcome::Applied { .. }), "{outcome:?}");
assert!(dir.path().join(".aether/settings.json").is_file());
assert!(dir.path().join(".aether/SYSTEM.md").is_file());
let settings = load(&dir.path().join(".aether/settings.json"));
assert_eq!(settings.prompts[0].path(), Some(".aether/SYSTEM.md"));
assert_eq!(settings.prompts[1], PromptSource::file("${WORKSPACE}/AGENTS.md").optional());
assert_minimal_mcp_and_tools(&settings.agents[0]);
std::fs::write(dir.path().join("AGENTS.md"), "Project instructions").unwrap();
let catalog = AgentCatalog::from_settings(dir.path(), settings).expect("catalog resolves");
let prompts = &catalog.default_agent().unwrap().prompts;
let prompt_path = match &prompts[0] {
Prompt::File { path, .. } => path,
other => panic!("expected file prompt, got {other:?}"),
};
assert_eq!(prompt_path, &dir.path().join(".aether/SYSTEM.md"));
let agents_prompt_path = match &prompts[1] {
Prompt::File { path, .. } => path,
other => panic!("expected AGENTS.md file prompt, got {other:?}"),
};
assert_eq!(agents_prompt_path, &dir.path().join("AGENTS.md"));
}
#[test]
fn writes_project_batteries_preset_for_anthropic() {
let dir = tempfile::tempdir().unwrap();
let outcome =
apply_init(InitTarget::project(dir.path()), Provider::Anthropic, Preset::BatteriesIncluded, &[], false)
.expect("apply_init");
assert!(matches!(outcome, InitOutcome::Applied { .. }), "{outcome:?}");
assert!(dir.path().join(".aether/agents/codebase-explorer/AGENTS.md").is_file());
let skill = dir.path().join(".aether/skills/aether/SKILL.md");
assert!(skill.is_file(), "batteries-included should write the aether docs skill into a scanned skills dir");
assert!(std::fs::read_to_string(&skill).unwrap().contains("name: aether"));
let settings = load(&dir.path().join(".aether/settings.json"));
let names: Vec<&str> = settings.agents.iter().map(|a| a.name.as_str()).collect();
assert_eq!(names, vec!["Plan", "Build", "Explore"]);
for agent in &settings.agents {
assert_eq!(agent_provider(agent), Provider::Anthropic);
}
assert_eq!(settings.agents[0].reasoning_effort, Some(ReasoningEffort::High));
assert_eq!(settings.agents[1].reasoning_effort, Some(ReasoningEffort::High));
let plan = &settings.agents[0];
assert_read_only_coding_tools(plan);
let plan_servers = inline_servers(&plan.mcps[0]);
let plan_server_names: Vec<&str> = plan_servers.keys().map(String::as_str).collect();
assert_eq!(plan_server_names, vec!["coding", "plan", "skills", "subagents", "survey", "tasks"]);
assert!(!plan.tools.deny.iter().any(|tool| matches!(tool, ToolMatcher::Name(name) if name.starts_with("plan__"))));
let explore = &settings.agents[2];
assert!(explore.agent_invocable);
assert!(!explore.user_invocable);
assert_eq!(explore.prompts[0].path(), Some(".aether/agents/codebase-explorer/AGENTS.md"));
assert_read_only_coding_tools(explore);
let explore_servers = inline_servers(&explore.mcps[0]);
let server_names: Vec<&str> = explore_servers.keys().map(String::as_str).collect();
assert_eq!(server_names, vec!["coding"]);
}
#[test]
fn batteries_included_harness_configurations() {
for case in &harness_cases() {
let dir = tempfile::tempdir().unwrap();
apply_init(
InitTarget::project(dir.path()),
Provider::Anthropic,
Preset::BatteriesIncluded,
&case.harnesses,
false,
)
.unwrap_or_else(|e| panic!("{}: apply_init failed: {e}", case.name));
let settings = load(&dir.path().join(".aether/settings.json"));
assert_eq!(settings.prompts.len(), case.expected_prompt_paths.len(), "{}: prompt count mismatch", case.name);
for (i, expected) in case.expected_prompt_paths.iter().enumerate() {
assert_eq!(settings.prompts[i].path(), Some(*expected), "{}: prompt[{i}] path mismatch", case.name);
}
let plan = &settings.agents[0];
let servers = inline_servers(&plan.mcps[0]);
if !case.skills_fragments.is_empty() {
let args = inline_args(servers, "skills");
assert_contains_all(&args, &case.skills_fragments, &format!("{}: skills args", case.name));
}
if !case.rules_fragments.is_empty() {
let args = inline_args(servers, "coding");
assert_contains_all(&args, &case.rules_fragments, &format!("{}: rules args", case.name));
}
for fragment in &case.excluded_fragments {
let skills = inline_args(servers, "skills");
let coding = inline_args(servers, "coding");
assert_does_not_contain(&skills, fragment, &format!("{}: skills should exclude", case.name));
assert_does_not_contain(&coding, fragment, &format!("{}: rules should exclude", case.name));
}
}
}
#[test]
fn batteries_included_harness_skills_dirs_are_ordered_before_project_skills() {
let dir = tempfile::tempdir().unwrap();
apply_init(
InitTarget::project(dir.path()),
Provider::Anthropic,
Preset::BatteriesIncluded,
&[HarnessIntegration::Agents],
false,
)
.expect("apply_init");
let settings = load(&dir.path().join(".aether/settings.json"));
let plan = &settings.agents[0];
let servers = inline_servers(&plan.mcps[0]);
let skills_args = inline_args(servers, "skills");
let aether_home_pos = skills_args.iter().position(|a| a == "${AETHER_HOME}/skills").unwrap();
let home_agents_pos = skills_args.iter().position(|a| a == "${HOME}/.agents/skills").unwrap();
let workspace_agents_pos = skills_args.iter().position(|a| a == "${WORKSPACE}/.agents/skills").unwrap();
let project_skills_pos = skills_args.iter().position(|a| a == "${WORKSPACE}/.aether/skills").unwrap();
assert!(aether_home_pos < home_agents_pos, "AETHER_HOME skills should come before harness user skills");
assert!(home_agents_pos < workspace_agents_pos, "harness user skills should come before harness project skills");
assert!(
workspace_agents_pos < project_skills_pos,
"harness project skills should come before native project skills"
);
}
#[test]
fn refuses_to_overwrite_existing_user_settings_without_force() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("settings.json"), "{}").unwrap();
let outcome =
apply_init(InitTarget::user(dir.path()), Provider::Codex, Preset::Minimal, &[], false).expect("apply_init");
assert!(matches!(outcome, InitOutcome::AlreadyInitialized { .. }), "{outcome:?}");
assert_eq!(std::fs::read_to_string(dir.path().join("settings.json")).unwrap(), "{}");
}
#[test]
fn refuses_to_overwrite_existing_project_settings_without_force() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".aether")).unwrap();
std::fs::write(dir.path().join(".aether/settings.json"), "{}").unwrap();
let outcome =
apply_init(InitTarget::project(dir.path()), Provider::Codex, Preset::Minimal, &[], false).expect("apply_init");
assert!(matches!(outcome, InitOutcome::AlreadyInitialized { .. }), "{outcome:?}");
assert_eq!(std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap(), "{}");
}
#[test]
fn force_overwrites_selected_target_only() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("settings.json"), "{}").unwrap();
std::fs::create_dir_all(dir.path().join(".aether")).unwrap();
std::fs::write(dir.path().join(".aether/settings.json"), "{}").unwrap();
let outcome =
apply_init(InitTarget::user(dir.path()), Provider::Anthropic, Preset::Minimal, &[], true).expect("apply_init");
assert!(matches!(outcome, InitOutcome::Applied { .. }), "{outcome:?}");
let settings = load(&dir.path().join("settings.json"));
assert_eq!(agent_provider(&settings.agents[0]), Provider::Anthropic);
assert_eq!(std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap(), "{}");
}
#[test]
fn every_inline_mcp_in_init_presets_parses_its_args() {
for preset in [Preset::Minimal, Preset::BatteriesIncluded] {
for harnesses in
[vec![], vec![HarnessIntegration::Claude], vec![HarnessIntegration::Claude, HarnessIntegration::Agents]]
{
let dir = tempfile::tempdir().unwrap();
apply_init(InitTarget::user(dir.path()), Provider::Anthropic, preset, &harnesses, false)
.expect("apply_init");
let settings = load(&dir.path().join("settings.json"));
for agent in &settings.agents {
for mcp in &agent.mcps {
let McpSourceSpec::Inline { servers } = mcp else { continue };
for (name, config) in servers {
let McpServerConfig::InMemory(in_memory) = config else { continue };
let args = in_memory.args.clone();
let parse_result = match name.as_str() {
"coding" => CodingMcpArgs::from_args(args).map(|_| ()),
"skills" => SkillsMcpArgs::from_args(args).map(|_| ()),
"subagents" => SubAgentsMcpArgs::from_args(args).map(|_| ()),
"tasks" => TasksMcpArgs::from_args(args).map(|_| ()),
"plan" => PlanMcpArgs::from_args(args).map(|_| ()),
"survey" => Ok(()),
other => {
panic!("preset references unknown in-memory MCP `{other}`; add a parse check")
}
};
assert!(
parse_result.is_ok(),
"{preset:?} agent `{}` server `{name}` harnesses={harnesses:?} args failed to parse: {parse_result:?}",
agent.name
);
}
}
}
}
}
}
#[test]
fn unsupported_provider_returns_error_without_writing_files() {
let dir = tempfile::tempdir().unwrap();
let err = apply_init(InitTarget::user(dir.path()), Provider::Gemini, Preset::Minimal, &[], false)
.expect_err("Gemini has no preset");
assert!(matches!(err, InitError::UnsupportedProvider { provider: Provider::Gemini, .. }), "{err:?}");
assert!(!dir.path().join("settings.json").exists(), "no settings.json should be written");
assert!(!dir.path().join("SYSTEM.md").exists(), "no SYSTEM.md should be written");
}
fn assert_read_only_coding_tools(agent: &aether_project::AgentConfig) {
assert_eq!(
agent.tools.allow,
vec![
ToolMatcher::read_only(),
ToolMatcher::name("plan__*"),
ToolMatcher::name("skills__*"),
ToolMatcher::name("subagents__*"),
ToolMatcher::name("tasks__*"),
ToolMatcher::name("survey__*"),
]
);
assert!(agent.tools.deny.is_empty());
}
fn assert_minimal_mcp_and_tools(agent: &aether_project::AgentConfig) {
let McpSourceSpec::Inline { servers } = &agent.mcps[0] else { panic!("minimal MCPs should be inline") };
let names: Vec<&str> = servers.keys().map(String::as_str).collect();
assert_eq!(names, vec!["coding", "skills"]);
assert_eq!(agent.tools.allow, vec![ToolMatcher::name("coding__bash"), ToolMatcher::name("skills__*")]);
assert!(agent.tools.deny.is_empty());
}
fn assert_contains_all(args: &[String], expected: &[&str], context: &str) {
for expected_item in expected {
assert!(
args.iter().any(|a| a == expected_item),
"{context}: expected args to contain `{expected_item}`, got: {args:?}"
);
}
}
fn assert_does_not_contain(args: &[String], fragment: &str, context: &str) {
assert!(
!args.iter().any(|a| a.contains(fragment)),
"{context}: expected args not to contain `{fragment}`, got: {args:?}"
);
}
struct HarnessCase {
name: &'static str,
harnesses: Vec<HarnessIntegration>,
expected_prompt_paths: Vec<&'static str>,
skills_fragments: Vec<&'static str>,
rules_fragments: Vec<&'static str>,
excluded_fragments: Vec<&'static str>,
}
fn harness_cases() -> Vec<HarnessCase> {
vec![
HarnessCase {
name: "no harnesses",
harnesses: vec![],
expected_prompt_paths: vec![".aether/SYSTEM.md"],
skills_fragments: vec![],
rules_fragments: vec![],
excluded_fragments: vec![],
},
HarnessCase {
name: "claude only",
harnesses: vec![HarnessIntegration::Claude],
expected_prompt_paths: vec![".aether/SYSTEM.md", "${WORKSPACE}/CLAUDE.md"],
skills_fragments: vec!["--dir", "${HOME}/.claude/skills", "--dir", "${WORKSPACE}/.claude/skills"],
rules_fragments: vec!["--rules-dir", "${HOME}/.claude/rules", "--rules-dir", "${WORKSPACE}/.claude/rules"],
excluded_fragments: vec![".agents/"],
},
HarnessCase {
name: "agents only",
harnesses: vec![HarnessIntegration::Agents],
expected_prompt_paths: vec![".aether/SYSTEM.md", "${WORKSPACE}/AGENTS.md"],
skills_fragments: vec!["--dir", "${HOME}/.agents/skills", "--dir", "${WORKSPACE}/.agents/skills"],
rules_fragments: vec!["--rules-dir", "${HOME}/.agents/rules", "--rules-dir", "${WORKSPACE}/.agents/rules"],
excluded_fragments: vec![".claude/"],
},
HarnessCase {
name: "both harnesses",
harnesses: vec![HarnessIntegration::Claude, HarnessIntegration::Agents],
expected_prompt_paths: vec![".aether/SYSTEM.md", "${WORKSPACE}/CLAUDE.md", "${WORKSPACE}/AGENTS.md"],
skills_fragments: vec![
"--dir",
"${HOME}/.claude/skills",
"--dir",
"${WORKSPACE}/.claude/skills",
"--dir",
"${HOME}/.agents/skills",
"--dir",
"${WORKSPACE}/.agents/skills",
],
rules_fragments: vec![
"--rules-dir",
"${HOME}/.claude/rules",
"--rules-dir",
"${WORKSPACE}/.claude/rules",
"--rules-dir",
"${HOME}/.agents/rules",
"--rules-dir",
"${WORKSPACE}/.agents/rules",
],
excluded_fragments: vec![],
},
]
}