nils-agent-docs 0.3.5

CLI crate for nils-agent-docs in the nils-cli workspace.
Documentation
mod common;

use std::fs;
use std::path::Path;

use agent_docs::config::{CONFIG_FILE_NAME, load_scope_config};
use agent_docs::model::{Context, Scope};

#[test]
fn add_full_flow_for_home_and_project_scopes() {
    let workspace = common::FixtureWorkspace::from_fixtures();
    common::write_text(
        &workspace.codex_home.join("TASK_TOOLS_EXTRA.md"),
        "# Fixture: home TASK_TOOLS_EXTRA\n",
    );
    common::write_text(
        &workspace.project_path.join("BINARY_DEPENDENCIES.md"),
        "# Fixture: project BINARY_DEPENDENCIES\n- tree\n- file\n",
    );

    let home_add = common::run_agent_docs_command(
        &workspace,
        &[
            "add",
            "--target",
            "home",
            "--context",
            "task-tools",
            "--scope",
            "home",
            "--path",
            "TASK_TOOLS_EXTRA.md",
            "--required",
            "--notes",
            "home task-tools extension",
        ],
    );
    assert!(
        home_add.success(),
        "add(home) should succeed, got code={} stderr={}",
        home_add.exit_code,
        home_add.stderr
    );
    assert!(
        home_add.stdout.contains("add: target=home action=inserted"),
        "expected add(home) stub output, got:\n{}",
        home_add.stdout
    );
    assert!(
        home_add.stdout.contains(&format!(
            "config={}",
            workspace.codex_home.join(CONFIG_FILE_NAME).display()
        )),
        "add(home) output should include config path, got:\n{}",
        home_add.stdout
    );

    let project_add = common::run_agent_docs_command(
        &workspace,
        &[
            "add",
            "--target",
            "project",
            "--context",
            "project-dev",
            "--scope",
            "project",
            "--path",
            "BINARY_DEPENDENCIES.md",
            "--required",
            "--notes",
            "project binary dependencies extension",
        ],
    );
    assert!(
        project_add.success(),
        "add(project) should succeed, got code={} stderr={}",
        project_add.exit_code,
        project_add.stderr
    );
    assert!(
        project_add
            .stdout
            .contains("add: target=project action=inserted"),
        "expected add(project) stub output, got:\n{}",
        project_add.stdout
    );
    assert!(
        project_add.stdout.contains(&format!(
            "config={}",
            workspace.project_path.join(CONFIG_FILE_NAME).display()
        )),
        "add(project) output should include config path, got:\n{}",
        project_add.stdout
    );

    let home_loaded = load_scope_config(Scope::Home, &workspace.codex_home)
        .expect("load home config")
        .expect("home config should exist");
    let home_entry = home_loaded
        .documents
        .iter()
        .find(|entry| entry.path == Path::new("TASK_TOOLS_EXTRA.md"))
        .expect("home extension entry should exist");
    assert_eq!(home_entry.context, Context::TaskTools);
    assert_eq!(home_entry.scope, Scope::Home);
    assert!(home_entry.required);
    assert_eq!(
        home_entry.notes.as_deref(),
        Some("home task-tools extension")
    );

    let project_loaded = load_scope_config(Scope::Project, &workspace.project_path)
        .expect("load project config")
        .expect("project config should exist");
    let project_entry = project_loaded
        .documents
        .iter()
        .find(|entry| entry.path == Path::new("BINARY_DEPENDENCIES.md"))
        .expect("project extension entry should exist");
    assert_eq!(project_entry.context, Context::ProjectDev);
    assert_eq!(project_entry.scope, Scope::Project);
    assert!(project_entry.required);
    assert_eq!(
        project_entry.notes.as_deref(),
        Some("project binary dependencies extension")
    );

    let task_tools_resolve = common::run_agent_docs_command(
        &workspace,
        &["resolve", "--context", "task-tools", "--format", "text"],
    );
    assert!(
        task_tools_resolve.success(),
        "resolve(task-tools) should succeed, got code={} stderr={}",
        task_tools_resolve.exit_code,
        task_tools_resolve.stderr
    );
    let task_tools_required = common::required_lines(&task_tools_resolve.stdout);
    assert_eq!(
        task_tools_required.len(),
        2,
        "task-tools should include builtin + extension required docs:\n{}",
        task_tools_resolve.stdout
    );
    assert!(
        task_tools_required
            .iter()
            .any(|line| line.contains("CLI_TOOLS.md") && line.contains("source=builtin")),
        "task-tools output should include builtin CLI_TOOLS.md:\n{}",
        task_tools_resolve.stdout
    );
    assert!(
        task_tools_required.iter().any(|line| {
            line.contains("TASK_TOOLS_EXTRA.md")
                && line.contains("source=extension-home")
                && line.contains("status=present")
        }),
        "task-tools output should include extension-home doc:\n{}",
        task_tools_resolve.stdout
    );

    let project_dev_resolve = common::run_agent_docs_command(
        &workspace,
        &["resolve", "--context", "project-dev", "--format", "text"],
    );
    assert!(
        project_dev_resolve.success(),
        "resolve(project-dev) should succeed, got code={} stderr={}",
        project_dev_resolve.exit_code,
        project_dev_resolve.stderr
    );
    let project_dev_required = common::required_lines(&project_dev_resolve.stdout);
    assert_eq!(
        project_dev_required.len(),
        2,
        "project-dev should include builtin + extension required docs:\n{}",
        project_dev_resolve.stdout
    );
    assert!(
        project_dev_required
            .iter()
            .any(|line| line.contains("DEVELOPMENT.md") && line.contains("source=builtin")),
        "project-dev output should include builtin DEVELOPMENT.md:\n{}",
        project_dev_resolve.stdout
    );
    assert!(
        project_dev_required.iter().any(|line| {
            line.contains("BINARY_DEPENDENCIES.md")
                && line.contains("source=extension-project")
                && line.contains("status=present")
        }),
        "project-dev output should include extension-project BINARY_DEPENDENCIES.md:\n{}",
        project_dev_resolve.stdout
    );
}

fn run_home_task_tools_add_update(workspace: &common::FixtureWorkspace) -> common::CliOutput {
    common::run_agent_docs_command(
        workspace,
        &[
            "add",
            "--target",
            "home",
            "--context",
            "task-tools",
            "--scope",
            "home",
            "--path",
            "CLI_TOOLS.md",
            "--required",
            "--notes",
            "after",
        ],
    )
}

fn assert_home_config_matches_golden(workspace: &common::FixtureWorkspace, fixture: &str) {
    let actual = fs::read_to_string(workspace.codex_home.join(CONFIG_FILE_NAME))
        .expect("read updated home config");
    let expected = fs::read_to_string(common::fixture_path(fixture)).expect("read golden fixture");
    assert_eq!(
        actual, expected,
        "home config should match golden fixture: {fixture}"
    );
}

#[test]
fn add_update_preserves_existing_key_order_in_snapshot() {
    let workspace = common::FixtureWorkspace::from_fixtures();
    let config_path = workspace.codex_home.join(CONFIG_FILE_NAME);
    let input = fs::read_to_string(common::fixture_path("add/preserve-key-order.input.toml"))
        .expect("read key-order input fixture");
    common::write_text(&config_path, &input);

    let output = run_home_task_tools_add_update(&workspace);
    assert!(
        output.success(),
        "add update should succeed, got code={} stderr={}",
        output.exit_code,
        output.stderr
    );
    assert!(
        output.stdout.contains("add: target=home action=updated"),
        "expected update output, got:\n{}",
        output.stdout
    );

    assert_home_config_matches_golden(&workspace, "add/preserve-key-order.expected.toml");
}

#[test]
fn add_update_preserves_multisection_comment_style_in_snapshot() {
    let workspace = common::FixtureWorkspace::from_fixtures();
    let config_path = workspace.codex_home.join(CONFIG_FILE_NAME);
    let input = fs::read_to_string(common::fixture_path(
        "add/preserve-multisection-comments.input.toml",
    ))
    .expect("read comments input fixture");
    common::write_text(&config_path, &input);

    let output = run_home_task_tools_add_update(&workspace);
    assert!(
        output.success(),
        "add update should succeed, got code={} stderr={}",
        output.exit_code,
        output.stderr
    );
    assert!(
        output.stdout.contains("add: target=home action=updated"),
        "expected update output, got:\n{}",
        output.stdout
    );

    assert_home_config_matches_golden(
        &workspace,
        "add/preserve-multisection-comments.expected.toml",
    );
}