nils-agent-docs 0.7.3

CLI crate for nils-agent-docs in the nils-cli workspace.
Documentation
use std::fs;
use std::path::Path;

use agent_docs::commands::baseline::check_builtin_baseline;
use agent_docs::config::CONFIG_FILE_NAME;
use agent_docs::env::ResolvedRoots;
use agent_docs::model::{BaselineTarget, DocumentSource, DocumentStatus, OutputFormat, Scope};
use agent_docs::output::render_baseline;
use agent_docs::run_with_args;
use serde_json::Value;
use tempfile::TempDir;

fn write_text(path: &Path, body: &str) {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).expect("failed to create parent directory");
    }
    fs::write(path, body).expect("failed to write fixture file");
}

fn write_markdown(path: &Path) {
    write_text(path, "# fixture\n");
}

fn roots(home: &TempDir, project: &TempDir) -> ResolvedRoots {
    ResolvedRoots {
        agent_home: home.path().to_path_buf(),
        project_path: project.path().to_path_buf(),
        is_linked_worktree: false,
        git_common_dir: None,
        primary_worktree_path: None,
    }
}

#[test]
fn baseline_check_target_home_reports_three_home_items() {
    let home = TempDir::new().expect("failed to create home tempdir");
    let project = TempDir::new().expect("failed to create project tempdir");
    write_markdown(&home.path().join("AGENTS.md"));

    let report = check_builtin_baseline(BaselineTarget::Home, &roots(&home, &project), false)
        .expect("baseline check should succeed");
    assert_eq!(report.items.len(), 3);
    assert!(report.items.iter().all(|item| item.scope == Scope::Home));
}

#[test]
fn baseline_check_target_project_reports_present_and_missing_in_text_output() {
    let home = TempDir::new().expect("failed to create home tempdir");
    let project = TempDir::new().expect("failed to create project tempdir");

    write_markdown(&project.path().join("AGENTS.md"));

    let report = check_builtin_baseline(BaselineTarget::Project, &roots(&home, &project), false)
        .expect("baseline check should succeed");
    assert_eq!(report.items.len(), 2);
    assert_eq!(report.missing_required, 1);
    assert_eq!(report.missing_optional, 0);
    assert_eq!(
        report.suggested_actions,
        vec!["agent-docs scaffold-baseline --missing-only --target project".to_string()]
    );
    assert_eq!(report.items[0].status, DocumentStatus::Present);
    assert_eq!(report.items[1].status, DocumentStatus::Missing);

    let text =
        render_baseline(OutputFormat::Text, &report).expect("failed to render baseline text");
    assert!(text.contains("[project] startup policy"));
    assert!(text.contains("[project] project-dev"));
    assert!(text.contains(" present "));
    assert!(text.contains(" missing "));
}

#[test]
fn baseline_check_target_all_json_contains_required_fields_and_actions() {
    let home = TempDir::new().expect("failed to create home tempdir");
    let project = TempDir::new().expect("failed to create project tempdir");

    write_markdown(&home.path().join("AGENTS.override.md"));
    write_markdown(&home.path().join("CLI_TOOLS.md"));
    write_markdown(&project.path().join("AGENTS.md"));

    let report = check_builtin_baseline(BaselineTarget::All, &roots(&home, &project), false)
        .expect("baseline check should succeed");
    assert_eq!(report.items.len(), 5);
    assert_eq!(report.missing_required, 2);
    assert_eq!(report.missing_optional, 0);
    assert_eq!(
        report.suggested_actions,
        vec![
            "agent-docs scaffold-baseline --missing-only --target home".to_string(),
            "agent-docs scaffold-baseline --missing-only --target project".to_string(),
        ]
    );
    assert_eq!(report.items[0].path, home.path().join("AGENTS.override.md"));

    let json =
        render_baseline(OutputFormat::Json, &report).expect("failed to render baseline json");
    let value: Value = serde_json::from_str(&json).expect("failed to parse baseline json");
    assert_eq!(value["target"], "all");
    assert_eq!(value["missing_required"], 2);
    assert_eq!(value["missing_optional"], 0);
    assert_eq!(
        value["suggested_actions"],
        serde_json::json!([
            "agent-docs scaffold-baseline --missing-only --target home",
            "agent-docs scaffold-baseline --missing-only --target project"
        ])
    );
}

#[test]
fn baseline_check_strict_mode_returns_non_zero_when_required_docs_missing() {
    let home = TempDir::new().expect("failed to create home tempdir");
    let project = TempDir::new().expect("failed to create project tempdir");
    write_markdown(&project.path().join("AGENTS.md"));

    let exit_missing = run_with_args([
        "agent-docs",
        "--agent-home",
        home.path().to_str().expect("home path should be utf-8"),
        "--project-path",
        project
            .path()
            .to_str()
            .expect("project path should be utf-8"),
        "baseline",
        "--check",
        "--target",
        "project",
        "--strict",
    ]);
    assert_eq!(exit_missing, 1);

    write_markdown(&project.path().join("DEVELOPMENT.md"));
    let exit_complete = run_with_args([
        "agent-docs",
        "--agent-home",
        home.path().to_str().expect("home path should be utf-8"),
        "--project-path",
        project
            .path()
            .to_str()
            .expect("project path should be utf-8"),
        "baseline",
        "--check",
        "--target",
        "project",
        "--strict",
    ]);
    assert_eq!(exit_complete, 0);
}

#[test]
fn baseline_check_includes_required_extension_docs_as_missing_or_present() {
    let home = TempDir::new().expect("failed to create home tempdir");
    let project = TempDir::new().expect("failed to create project tempdir");
    write_markdown(&project.path().join("AGENTS.md"));
    write_markdown(&project.path().join("DEVELOPMENT.md"));
    write_text(
        &project.path().join(CONFIG_FILE_NAME),
        r#"
[[document]]
context = "project-dev"
scope = "project"
path = "BINARY_DEPENDENCIES.md"
required = true
when = "always"

[[document]]
context = "project-dev"
scope = "project"
path = "OPTIONAL_GUIDE.md"
required = false
when = "always"
"#,
    );

    let missing_report =
        check_builtin_baseline(BaselineTarget::Project, &roots(&home, &project), false)
            .expect("baseline check should succeed");
    assert_eq!(missing_report.items.len(), 3);
    assert_eq!(missing_report.missing_required, 1);
    let extension_missing = missing_report
        .items
        .iter()
        .find(|item| {
            item.path == project.path().join("BINARY_DEPENDENCIES.md")
                && item.source == DocumentSource::ExtensionProject
        })
        .expect("required extension item should be included");
    assert_eq!(extension_missing.status, DocumentStatus::Missing);

    write_markdown(&project.path().join("BINARY_DEPENDENCIES.md"));
    let present_report =
        check_builtin_baseline(BaselineTarget::Project, &roots(&home, &project), false)
            .expect("baseline check should succeed");
    assert_eq!(present_report.items.len(), 3);
    assert_eq!(present_report.missing_required, 0);
    let extension_present = present_report
        .items
        .iter()
        .find(|item| {
            item.path == project.path().join("BINARY_DEPENDENCIES.md")
                && item.source == DocumentSource::ExtensionProject
        })
        .expect("required extension item should be included");
    assert_eq!(extension_present.status, DocumentStatus::Present);
}

#[test]
fn baseline_check_strict_mode_fails_on_missing_required_extension_doc() {
    let home = TempDir::new().expect("failed to create home tempdir");
    let project = TempDir::new().expect("failed to create project tempdir");
    write_markdown(&project.path().join("AGENTS.md"));
    write_markdown(&project.path().join("DEVELOPMENT.md"));
    write_text(
        &project.path().join(CONFIG_FILE_NAME),
        r#"
[[document]]
context = "project-dev"
scope = "project"
path = "BINARY_DEPENDENCIES.md"
required = true
when = "always"
"#,
    );

    let exit_missing_extension = run_with_args([
        "agent-docs",
        "--agent-home",
        home.path().to_str().expect("home path should be utf-8"),
        "--project-path",
        project
            .path()
            .to_str()
            .expect("project path should be utf-8"),
        "baseline",
        "--check",
        "--target",
        "project",
        "--strict",
    ]);
    assert_eq!(exit_missing_extension, 1);

    write_markdown(&project.path().join("BINARY_DEPENDENCIES.md"));
    let exit_complete = run_with_args([
        "agent-docs",
        "--agent-home",
        home.path().to_str().expect("home path should be utf-8"),
        "--project-path",
        project
            .path()
            .to_str()
            .expect("project path should be utf-8"),
        "baseline",
        "--check",
        "--target",
        "project",
        "--strict",
    ]);
    assert_eq!(exit_complete, 0);
}

#[test]
fn baseline_check_returns_config_error_on_malformed_toml() {
    let home = TempDir::new().expect("failed to create home tempdir");
    let project = TempDir::new().expect("failed to create project tempdir");
    write_markdown(&project.path().join("AGENTS.md"));
    write_markdown(&project.path().join("DEVELOPMENT.md"));
    write_text(
        &project.path().join(CONFIG_FILE_NAME),
        r#"
[[document
context = "project-dev"
scope = "project"
path = "BINARY_DEPENDENCIES.md"
required = true
when = "always"
"#,
    );

    let exit_code = run_with_args([
        "agent-docs",
        "--agent-home",
        home.path().to_str().expect("home path should be utf-8"),
        "--project-path",
        project
            .path()
            .to_str()
            .expect("project path should be utf-8"),
        "baseline",
        "--check",
        "--target",
        "project",
    ]);

    assert_eq!(exit_code, 3);
}

#[test]
fn baseline_check_same_key_project_override_keeps_first_seen_extension_position() {
    let home = TempDir::new().expect("failed to create home tempdir");
    let project = TempDir::new().expect("failed to create project tempdir");
    write_markdown(&project.path().join("AGENTS.md"));
    write_markdown(&project.path().join("DEVELOPMENT.md"));
    write_markdown(&project.path().join("EXTRA_ALPHA.md"));
    write_markdown(&project.path().join("EXTRA_BETA.md"));

    write_text(
        &home.path().join(CONFIG_FILE_NAME),
        r#"
[[document]]
context = "project-dev"
scope = "project"
path = "EXTRA_ALPHA.md"
required = true
when = "always"
notes = "home-alpha"

[[document]]
context = "project-dev"
scope = "project"
path = "EXTRA_BETA.md"
required = true
when = "always"
notes = "home-beta"
"#,
    );
    write_text(
        &project.path().join(CONFIG_FILE_NAME),
        r#"
[[document]]
context = "project-dev"
scope = "project"
path = "EXTRA_ALPHA.md"
required = true
when = "always"
notes = "project-alpha"
"#,
    );

    let report = check_builtin_baseline(BaselineTarget::Project, &roots(&home, &project), false)
        .expect("baseline check should succeed");
    let extension_items: Vec<_> = report
        .items
        .iter()
        .filter(|item| {
            matches!(
                item.source,
                DocumentSource::ExtensionHome | DocumentSource::ExtensionProject
            )
        })
        .collect();

    assert_eq!(extension_items.len(), 2);
    assert_eq!(
        extension_items[0].path,
        project.path().join("EXTRA_ALPHA.md"),
        "first-seen extension key position should remain stable after override"
    );
    assert_eq!(extension_items[0].source, DocumentSource::ExtensionProject);
    assert!(extension_items[0].why.contains("project-alpha"));
    assert_eq!(
        extension_items[1].path,
        project.path().join("EXTRA_BETA.md")
    );
    assert_eq!(extension_items[1].source, DocumentSource::ExtensionHome);
    assert!(extension_items[1].why.contains("home-beta"));
}