use std::fs;
use std::path::Path;
use a_agent::config::{Config, ProviderKind};
use a_agent::context::{
ContextInput, SkillMetadata, bound_stdin, build_system_prompt, discover_agents,
discover_skills, parse_skill_metadata, skill_roots,
};
use tempfile::tempdir;
#[test]
fn rejects_legacy_single_provider_configuration() {
let temp = tempdir().unwrap();
let home = temp.path().join("home");
let cwd = temp.path().join("repo");
fs::create_dir_all(home.join(".config/a")).unwrap();
fs::create_dir_all(&cwd).unwrap();
fs::write(
home.join(".config/a/config.toml"),
r#"
[provider]
type = "responses"
model = "old-model"
"#,
)
.unwrap();
let error = Config::load_from(&cwd, &home).unwrap_err().to_string();
assert!(error.contains("legacy [provider]"), "{error}");
}
#[test]
fn resolves_multiple_provider_and_model_profiles_with_effort() {
let temp = tempdir().unwrap();
let home = temp.path().join("home");
let cwd = temp.path().join("repo");
fs::create_dir_all(home.join(".config/a")).unwrap();
fs::create_dir_all(cwd.join(".a")).unwrap();
fs::write(
home.join(".config/a/config.toml"),
r#"
default_model = "fast"
[providers.openai]
type = "responses"
base_url = "https://openai.example/v1"
api_key = "openai-secret"
[providers.claude]
type = "anthropic"
base_url = "https://anthropic.example"
api_key = "claude-secret"
[models.fast]
provider = "openai"
model = "gpt-fast"
effort = "low"
efforts = ["low", "medium", "high"]
context_window = 128000
max_tokens = 4096
[models.deep]
provider = "openai"
model = "gpt-deep"
effort = "high"
efforts = ["medium", "high", "max"]
[models.deep.request]
service_tier = "priority"
[models.claude]
provider = "claude"
model = "claude-deep"
effort = "high"
efforts = ["low", "medium", "high", "max"]
"#,
)
.unwrap();
fs::write(
cwd.join(".a/config.toml"),
"[models.deep]\neffort = \"medium\"\n",
)
.unwrap();
let config = Config::load_from(&cwd, &home).unwrap();
assert_eq!(config.model_names(), ["claude", "deep", "fast"]);
let fast = config.resolve_model(None, None).unwrap();
assert_eq!(fast.name, "fast");
assert_eq!(fast.provider_name, "openai");
assert_eq!(fast.provider.kind, ProviderKind::Responses);
assert_eq!(fast.provider.model, "gpt-fast");
assert_eq!(fast.provider.max_tokens, 4096);
assert_eq!(fast.context_window, Some(128000));
assert_eq!(fast.effort.as_deref(), Some("low"));
assert_eq!(fast.provider.request["reasoning"]["effort"], "low");
let deep = config.resolve_model(Some("deep"), Some("high")).unwrap();
assert_eq!(deep.provider.model, "gpt-deep");
assert_eq!(deep.effort.as_deref(), Some("high"));
assert_eq!(deep.provider.request["reasoning"]["effort"], "high");
assert_eq!(deep.provider.request["service_tier"], "priority");
let claude = config.resolve_model(Some("claude"), Some("max")).unwrap();
assert_eq!(claude.provider.kind, ProviderKind::Anthropic);
assert_eq!(claude.provider.request["output_config"]["effort"], "max");
}
#[test]
fn model_without_effort_does_not_inject_reasoning_configuration() {
let temp = tempdir().unwrap();
let home = temp.path().join("home");
let cwd = temp.path().join("repo");
fs::create_dir_all(home.join(".config/a")).unwrap();
fs::create_dir_all(&cwd).unwrap();
fs::write(
home.join(".config/a/config.toml"),
r#"
default_model = "plain"
[providers.internal]
type = "responses"
api_key = "secret"
[models.plain]
provider = "internal"
model = "provider-model"
"#,
)
.unwrap();
let config = Config::load_from(&cwd, &home).unwrap();
let model = config.resolve_model(None, None).unwrap();
assert_eq!(model.effort, None);
assert!(model.efforts.is_empty());
assert!(!model.provider.request.contains_key("reasoning"));
assert!(config.resolve_model(None, Some("high")).is_err());
}
#[test]
fn context_window_must_leave_room_for_model_output() {
let temp = tempdir().unwrap();
let home = temp.path().join("home");
let cwd = temp.path().join("repo");
fs::create_dir_all(home.join(".config/a")).unwrap();
fs::create_dir_all(&cwd).unwrap();
fs::write(
home.join(".config/a/config.toml"),
r#"
default_model = "invalid"
[providers.test]
type = "responses"
api_key = "secret"
max_tokens = 4096
[models.invalid]
provider = "test"
model = "test-model"
context_window = 4096
"#,
)
.unwrap();
let error = Config::load_from(&cwd, &home).unwrap_err().to_string();
assert!(error.contains("context_window"), "{error}");
assert!(error.contains("max_tokens"), "{error}");
}
#[test]
fn first_run_creates_a_documented_global_config_without_overwriting_it() {
let temp = tempdir().unwrap();
let home = temp.path().join("home");
let path = Config::ensure_user_config(&home).unwrap().unwrap();
assert_eq!(path, home.join(".config/a/config.toml"));
let source = fs::read_to_string(&path).unwrap();
assert!(source.contains("default_model = \"codex\""));
assert!(source.contains("[providers.openai]"));
assert!(source.contains("[models.codex]"));
assert!(source.contains("type = \"responses\""));
assert!(source.contains("api_key_env = \"OPENAI_API_KEY\""));
assert!(source.contains("# api_key = \"sk-...\""));
assert!(source.contains("# base_url = \"https://gateway.example/v1\""));
assert!(source.contains("# [providers.anthropic]"));
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
fs::metadata(&path).unwrap().permissions().mode() & 0o777,
0o600
);
}
fs::write(&path, "# user config\n").unwrap();
assert!(Config::ensure_user_config(&home).unwrap().is_none());
assert_eq!(fs::read_to_string(path).unwrap(), "# user config\n");
}
#[test]
fn stdin_is_bounded_by_bytes_and_keeps_the_tail() {
let result = bound_stdin(b"header\nline one\nimportant failure\n", 20);
assert!(result.starts_with("[stdin truncated; showing last 20 bytes]\n"));
assert!(result.ends_with("important failure\n"));
}
#[test]
fn skill_frontmatter_is_parsed_without_loading_the_body() {
let source = "---\nname: rust-review\ndescription: Review Rust safely.\n---\nSECRET BODY";
let parsed = parse_skill_metadata(source, Path::new("/skills/rust-review/SKILL.md")).unwrap();
assert_eq!(parsed.name, "rust-review");
assert_eq!(parsed.description, "Review Rust safely.");
assert!(!parsed.description.contains("SECRET"));
}
#[test]
fn skill_roots_follow_the_agent_skills_conventions() {
let home = Path::new("/home/user");
let project = Path::new("/repo");
let roots = skill_roots(home, project);
let shown = roots
.iter()
.map(|root| root.display().to_string())
.collect::<Vec<_>>();
assert_eq!(
shown,
vec!["/home/user/.agents/skills", "/repo/.agents/skills"],
"only the shared convention is scanned, user scope first so project wins"
);
}
#[test]
fn project_skill_overrides_user_skill_metadata() {
let temp = tempdir().unwrap();
let home = temp.path().join("home");
let project = temp.path().join("repo");
let user_root = home.join(".agents/skills/review");
let project_root = project.join(".agents/skills/review");
fs::create_dir_all(&user_root).unwrap();
fs::create_dir_all(&project_root).unwrap();
fs::write(
user_root.join("SKILL.md"),
"---\nname: review\ndescription: user\n---\nbody",
)
.unwrap();
fs::write(
project_root.join("SKILL.md"),
"---\nname: review\ndescription: project\n---\nbody",
)
.unwrap();
let skills = discover_skills(&skill_roots(&home, &project)).unwrap();
assert_eq!(skills.len(), 1);
assert_eq!(skills[0].description, "project");
}
#[test]
fn user_and_project_skills_are_merged_and_client_directories_are_ignored() {
let temp = tempdir().unwrap();
let home = temp.path().join("home");
let project = temp.path().join("repo");
for (root, name) in [
(home.join(".agents/skills/shared-user"), "shared-user"),
(
project.join(".agents/skills/shared-project"),
"shared-project",
),
(home.join(".claude/skills/claude-user"), "claude-user"),
(project.join(".a/skills/native-project"), "native-project"),
] {
fs::create_dir_all(&root).unwrap();
fs::write(
root.join("SKILL.md"),
format!("---\nname: {name}\ndescription: {name} description\n---\nbody"),
)
.unwrap();
}
let skills = discover_skills(&skill_roots(&home, &project)).unwrap();
let names = skills
.iter()
.map(|skill| skill.name.as_str())
.collect::<Vec<_>>();
assert_eq!(names, vec!["shared-project", "shared-user"], "{skills:?}");
}
#[test]
fn skill_metadata_follows_the_specification() {
let source = concat!(
"---\n",
"name: pdf-processing\n",
"description: >-\n",
" Extract PDF text, fill forms, merge files.\n",
" Use when handling PDFs.\n",
"license: Apache-2.0\n",
"metadata:\n",
" name: not-the-skill-name\n",
" version: \"1.0\"\n",
"---\n",
"SECRET BODY"
);
let parsed =
parse_skill_metadata(source, Path::new("/skills/pdf-processing/SKILL.md")).unwrap();
assert_eq!(parsed.name, "pdf-processing");
assert_eq!(
parsed.description,
"Extract PDF text, fill forms, merge files. Use when handling PDFs."
);
assert!(!parsed.description.contains("SECRET"));
}
#[test]
fn unquoted_colons_in_a_description_are_tolerated() {
let source = "---\nname: review\ndescription: Use this skill when: the user asks\n---\nbody";
let parsed = parse_skill_metadata(source, Path::new("/skills/review/SKILL.md")).unwrap();
assert_eq!(parsed.description, "Use this skill when: the user asks");
}
#[test]
fn a_skill_without_a_description_is_skipped() {
let temp = tempdir().unwrap();
let root = temp.path().join(".agents/skills");
fs::create_dir_all(root.join("broken")).unwrap();
fs::create_dir_all(root.join("valid")).unwrap();
fs::write(root.join("broken/SKILL.md"), "---\nname: broken\n---\nbody").unwrap();
fs::write(
root.join("valid/SKILL.md"),
"---\nname: valid\ndescription: usable\n---\nbody",
)
.unwrap();
let skills = discover_skills(std::slice::from_ref(&root)).unwrap();
assert_eq!(skills.len(), 1, "{skills:?}");
assert_eq!(skills[0].name, "valid");
}
#[test]
fn agents_discovery_is_ancestry_only_and_broad_to_specific() {
let temp = tempdir().unwrap();
let repo = temp.path().join("repo");
let cwd = repo.join("src/parser");
fs::create_dir_all(&cwd).unwrap();
fs::create_dir(repo.join(".git")).unwrap();
fs::write(repo.join("AGENTS.md"), "repo rules").unwrap();
fs::write(repo.join("src/AGENTS.md"), "src rules").unwrap();
let files = discover_agents(&cwd, None).unwrap();
assert_eq!(
files
.iter()
.map(|item| item.content.as_str())
.collect::<Vec<_>>(),
["repo rules", "src rules"]
);
}
#[test]
fn global_agents_stays_first_even_when_its_path_is_deeper() {
let temp = tempdir().unwrap();
let repo = temp.path().join("repo");
let cwd = repo.join("src");
let global = temp.path().join("very/deep/home/.config/a/AGENTS.md");
fs::create_dir_all(&cwd).unwrap();
fs::create_dir(repo.join(".git")).unwrap();
fs::create_dir_all(global.parent().unwrap()).unwrap();
fs::write(&global, "global rules").unwrap();
fs::write(repo.join("AGENTS.md"), "repo rules").unwrap();
let files = a_agent::context::discover_agents_for_targets(&cwd, Some(&global), &[]).unwrap();
assert_eq!(
files
.iter()
.map(|item| item.content.as_str())
.collect::<Vec<_>>(),
["global rules", "repo rules"]
);
}
#[test]
fn system_prompt_has_active_rules_and_skill_references_only() {
let prompt = build_system_prompt(&ContextInput {
cwd: Path::new("/repo").to_path_buf(),
agents: vec![a_agent::context::AgentsFile {
path: Path::new("/repo/AGENTS.md").to_path_buf(),
content: "Run tests.".into(),
}],
skills: vec![SkillMetadata {
name: "review".into(),
description: "Review code.".into(),
path: Path::new("/repo/.agents/skills/review/SKILL.md").to_path_buf(),
}],
platform: "linux".into(),
shell: "/bin/fish".into(),
});
assert!(prompt.contains("Run tests."));
assert!(prompt.contains("Review code."));
assert!(prompt.contains("/repo/.agents/skills/review/SKILL.md"));
assert!(!prompt.contains("Targeted files"));
assert!(prompt.contains("Relative read and apply_patch paths are resolved from the cwd"));
assert!(prompt.contains("Before any git command, first confirm that .git exists"));
assert!(prompt.contains("Never inspect the workspace merely because it is available"));
assert!(prompt.contains("Rust runtime injects recent records"));
assert!(prompt.contains("integration lacks context injection"));
assert!(!prompt.contains("SECRET BODY"));
}