agpm-cli 0.4.9

AGent Package Manager - A Git-based package manager for coding agents
Documentation
//! Integration tests for pattern-based dependency installation.

use anyhow::Result;
use tokio::fs;

use crate::common::{ManifestBuilder, TestProject};

/// Test installing dependencies using glob patterns.
#[tokio::test]
async fn test_pattern_based_installation() -> Result<()> {
    agpm_cli::test_utils::init_test_logging(None);

    let project = TestProject::new().await?;

    // Create mock source repository with multiple agents
    let test_repo = project.create_source_repo("test-repo").await?;

    // Create AI-related agents
    test_repo
        .add_resource("agents/ai", "assistant", "# AI Assistant\n\nAI assistant agent")
        .await?;
    test_repo.add_resource("agents/ai", "analyzer", "# AI Analyzer\n\nAI analyzer agent").await?;
    test_repo
        .add_resource("agents/ai", "generator", "# AI Generator\n\nAI generator agent")
        .await?;

    // Create review-related agents
    test_repo.add_resource("agents", "reviewer", "# Reviewer\n\nCode reviewer agent").await?;
    test_repo
        .add_resource("agents", "review-helper", "# Review Helper\n\nReview helper agent")
        .await?;

    // Create other agents
    test_repo.add_resource("agents", "debugger", "# Debugger\n\nDebugger agent").await?;
    test_repo.add_resource("agents", "tester", "# Tester\n\nTester agent").await?;

    // Commit all files
    test_repo.commit_all("Add multiple agent files")?;
    test_repo.tag_version("v1.0.0")?;

    // Get repo URL as file://
    let repo_url = test_repo.bare_file_url(project.sources_path())?;

    // Create manifest with pattern dependencies (preserving nested structure)
    let manifest = ManifestBuilder::new()
        .add_source("test-repo", &repo_url)
        .add_agent("ai-agents", |d| {
            d.source("test-repo").path("agents/ai/*.md").version("v1.0.0").flatten(false)
        })
        .add_agent("review-agents", |d| {
            d.source("test-repo").path("agents/review*.md").version("v1.0.0").flatten(false)
        })
        .add_agent("all-agents", |d| {
            d.source("test-repo").path("agents/**/*.md").version("v1.0.0").flatten(false)
        })
        .build();

    project.write_manifest(&manifest).await?;

    // Run install command
    let output = project.run_agpm(&["install"])?;
    assert!(output.success);

    // Verify that all AI agents were installed
    // With relative path preservation, subdirectory structure is maintained
    let ai_agents_dir = project.project_path().join(".claude/agents");
    assert!(ai_agents_dir.join("ai/assistant.md").exists(), "AI assistant not installed");
    assert!(ai_agents_dir.join("ai/analyzer.md").exists(), "AI analyzer not installed");
    assert!(ai_agents_dir.join("ai/generator.md").exists(), "AI generator not installed");

    // Verify review agents were installed (no subdirectory)
    assert!(ai_agents_dir.join("reviewer.md").exists(), "Reviewer not installed");
    assert!(ai_agents_dir.join("review-helper.md").exists(), "Review helper not installed");

    // Verify lockfile was created with all resources
    let lockfile_path = project.project_path().join("agpm.lock");
    assert!(lockfile_path.exists(), "Lockfile not created");

    let lockfile_content = fs::read_to_string(&lockfile_path).await?;
    assert!(lockfile_content.contains("assistant"), "Assistant not in lockfile");
    assert!(lockfile_content.contains("analyzer"), "Analyzer not in lockfile");
    assert!(lockfile_content.contains("generator"), "Generator not in lockfile");
    assert!(lockfile_content.contains("reviewer"), "Reviewer not in lockfile");
    assert!(lockfile_content.contains("review-helper"), "Review helper not in lockfile");

    Ok(())
}

/// Test pattern dependencies with custom target directories.
#[tokio::test]
async fn test_pattern_with_custom_target() -> Result<()> {
    agpm_cli::test_utils::init_test_logging(None);

    let project = TestProject::new().await?;
    let test_repo = project.create_source_repo("test-repo").await?;

    // Create snippet files
    test_repo.add_resource("snippets", "util1", "# Utility 1").await?;
    test_repo.add_resource("snippets", "util2", "# Utility 2").await?;
    test_repo.add_resource("snippets", "helper", "# Helper").await?;

    test_repo.commit_all("Add snippets")?;
    test_repo.tag_version("v1.0.0")?;

    // Get repo URL as file://
    let repo_url = test_repo.bare_file_url(project.sources_path())?;

    // Create manifest with custom target
    let manifest = ManifestBuilder::new()
        .add_source("test-repo", &repo_url)
        .add_snippet("utilities", |d| {
            d.source("test-repo")
                .path("snippets/util*.md")
                .version("v1.0.0")
                .target("tools/utilities")
        })
        .build();

    project.write_manifest(&manifest).await?;

    // Run install
    let output = project.run_agpm(&["install"])?;
    assert!(output.success);

    // Verify custom installation path
    // Custom target is relative to default snippets directory (.agpm/snippets/ for agpm tool)
    let custom_dir = project.project_path().join(".agpm/snippets/tools/utilities");
    assert!(custom_dir.join("util1.md").exists(), "util1 not installed to custom path");
    assert!(custom_dir.join("util2.md").exists(), "util2 not installed to custom path");
    assert!(!custom_dir.join("helper.md").exists(), "helper should not be installed");

    Ok(())
}

/// Test pattern dependencies with version constraints.
#[tokio::test]
async fn test_pattern_with_versions() -> Result<()> {
    agpm_cli::test_utils::init_test_logging(None);

    let project = TestProject::new().await?;
    let test_repo = project.create_source_repo("test-repo").await?;

    // Create v1.0.0 agents
    test_repo.add_resource("agents", "agent1", "# Agent 1 v1.0.0").await?;
    test_repo.add_resource("agents", "agent2", "# Agent 2 v1.0.0").await?;
    test_repo.commit_all("Add agents v1.0.0")?;
    test_repo.tag_version("v1.0.0")?;

    // For this test, we'll just use v1.0.0 as testing multiple versions
    // would require more complex git operations

    // Get repo URL as file://
    let repo_url = test_repo.bare_file_url(project.sources_path())?;

    // Create manifest with v1.0.0 pattern dependency
    let manifest = ManifestBuilder::new()
        .add_source("test-repo", &repo_url)
        .add_agent_pattern("v1-agents", "test-repo", "agents/*.md", "v1.0.0")
        .build();

    project.write_manifest(&manifest).await?;

    // Run install
    let output = project.run_agpm(&["install"])?;
    assert!(output.success);

    // Verify v1.0.0 agents were installed
    let agent1_path = project.project_path().join(".claude/agents/agent1.md");
    let agent2_path = project.project_path().join(".claude/agents/agent2.md");
    let agent3_path = project.project_path().join(".claude/agents/agent3.md");

    assert!(agent1_path.exists(), "Agent 1 not installed");
    assert!(agent2_path.exists(), "Agent 2 not installed");
    assert!(!agent3_path.exists(), "Agent 3 should not exist in v1.0.0");

    // Verify content is from v1.0.0
    let agent1_content = fs::read_to_string(&agent1_path).await?;
    assert!(agent1_content.contains("v1.0.0"), "Agent 1 should be v1.0.0");
    assert!(!agent1_content.contains("Updated"), "Agent 1 should not be updated version");

    Ok(())
}

/// Test local filesystem patterns.
#[tokio::test]
async fn test_local_pattern_dependencies() -> Result<()> {
    agpm_cli::test_utils::init_test_logging(None);

    let project = TestProject::new().await?;

    // Create a local directory with resources
    let resources_dir = project.sources_path().join("local_resources");
    let agents_dir = resources_dir.join("agents");
    fs::create_dir_all(&agents_dir).await?;

    fs::write(agents_dir.join("local1.md"), "# Local Agent 1").await?;
    fs::write(agents_dir.join("local2.md"), "# Local Agent 2").await?;
    fs::write(agents_dir.join("local3.md"), "# Local Agent 3").await?;

    // Create manifest with local pattern dependency
    let manifest = ManifestBuilder::new()
        .add_local_agent("local-agents", &format!("{}/agents/local*.md", resources_dir.display()))
        .build();

    project.write_manifest(&manifest).await?;

    // Run install
    let output = project.run_agpm(&["install"])?;

    // Local patterns might not be supported in the same way as remote patterns
    // This test documents the current behavior
    if output.success {
        let agents_installed = project.project_path().join(".claude/agents");
        println!("Checking for installed local agents in: {:?}", agents_installed);
        // Verify if agents were installed
        assert!(
            agents_installed.join("local1.md").exists()
                || agents_installed.join("local2.md").exists()
                || agents_installed.join("local3.md").exists(),
            "At least one local agent should be installed"
        );
    } else {
        // Local patterns might require different handling
        println!("Local pattern installation not yet supported");
    }

    Ok(())
}

/// Test error handling for invalid patterns.
#[tokio::test]
async fn test_invalid_pattern_error() -> Result<()> {
    agpm_cli::test_utils::init_test_logging(None);

    let project = TestProject::new().await?;

    // Create manifest with path traversal pattern - intentionally invalid, keep as-is
    let manifest_content = r#"
[sources]
test-repo = "https://github.com/example/repo.git"

[agents]
unsafe = { source = "test-repo", path = "../../../etc/*.conf", version = "latest" }
"#;

    project.write_manifest(manifest_content).await?;

    // Run validate command
    let output = project.run_agpm(&["validate"])?;

    // Should fail validation due to path traversal
    assert!(!output.success);

    Ok(())
}

/// Test pattern matching performance with many files.
#[tokio::test]
async fn test_pattern_performance() -> Result<()> {
    agpm_cli::test_utils::init_test_logging(None);

    let project = TestProject::new().await?;
    let test_repo = project.create_source_repo("test-repo").await?;

    // Create 100 agent files
    for i in 0..100 {
        let content = format!("# Agent {}\n\nAgent {} description", i, i);
        test_repo.add_resource("agents", &format!("agent{:03}", i), &content).await?;
    }

    test_repo.commit_all("Add 100 agents")?;
    test_repo.tag_version("v1.0.0")?;

    // Get repo URL as file://
    let repo_url = test_repo.bare_file_url(project.sources_path())?;

    // Create manifest
    let manifest = ManifestBuilder::new()
        .add_source("test-repo", &repo_url)
        .add_agent_pattern("all-agents", "test-repo", "agents/*.md", "v1.0.0")
        .build();

    project.write_manifest(&manifest).await?;

    // Measure installation time
    let start = std::time::Instant::now();

    let output = project.run_agpm(&["install"])?;
    assert!(output.success);

    let duration = start.elapsed();

    // Should complete in reasonable time (< 30 seconds for 100 files)
    assert!(duration.as_secs() < 30, "Installation took too long: {:?}", duration);

    // Verify all files were installed
    let lockfile_content = fs::read_to_string(project.project_path().join("agpm.lock")).await?;
    let agent_count = lockfile_content.matches("agent").count();
    assert!(agent_count >= 100, "Not all agents were installed");

    Ok(())
}