a-agent 0.1.0

Fast, terminal-native coding agent with progressive context
Documentation
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,
};
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 project_skill_overrides_global_skill_metadata() {
    let temp = tempdir().unwrap();
    let global = temp.path().join("global");
    let project = temp.path().join("repo/.a/skills");
    fs::create_dir_all(global.join("review")).unwrap();
    fs::create_dir_all(project.join("review")).unwrap();
    fs::write(
        global.join("review/SKILL.md"),
        "---\nname: review\ndescription: global\n---\nbody",
    )
    .unwrap();
    fs::write(
        project.join("review/SKILL.md"),
        "---\nname: review\ndescription: project\n---\nbody",
    )
    .unwrap();

    let skills = discover_skills(&global, &project).unwrap();
    assert_eq!(skills.len(), 1);
    assert_eq!(skills[0].description, "project");
}

#[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/.a/skills/review/SKILL.md").to_path_buf(),
        }],
        targeted_files: vec![Path::new("/repo/src/a.rs").to_path_buf()],
        platform: "linux".into(),
        shell: "/bin/fish".into(),
    });
    assert!(prompt.contains("Run tests."));
    assert!(prompt.contains("Review code."));
    assert!(prompt.contains("/repo/.a/skills/review/SKILL.md"));
    assert!(prompt.contains("Targeted files:\n- /repo/src/a.rs"));
    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"));
}