mars-agents 0.2.9

Agent package manager for .agents/ directories
Documentation
use super::*;

#[test]
fn skill_not_found_has_requester_and_search_context() {
    let dir = TempDir::new().unwrap();
    let tree_a = dir.path().join("a");
    std::fs::create_dir_all(&tree_a).unwrap();
    write_minimal_package_marker(&tree_a);
    write_agent(&tree_a, "coder", &["missing-skill"]);

    let mut provider = MockProvider::new();
    provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
    provider.add_source("a", tree_a, None);

    let config = make_config(vec![(
        "a",
        git_spec("https://example.com/a.git", Some("v1.0.0")),
    )]);

    let err = resolve(&config, &provider, None, &default_options()).unwrap_err();
    match err {
        MarsError::Resolution(ResolutionError::SkillNotFound {
            skill,
            required_by,
            searched,
        }) => {
            assert_eq!(skill, "missing-skill");
            assert_eq!(required_by, "a/coder");
            assert_eq!(searched, vec!["a".to_string()]);
        }
        other => panic!("expected SkillNotFound, got {other:?}"),
    }
}

#[test]
fn excluded_skill_not_reintroduced_from_frontmatter_reference() {
    let dir = TempDir::new().unwrap();
    let tree = dir.path().join("a");
    std::fs::create_dir_all(&tree).unwrap();
    write_minimal_package_marker(&tree);
    write_agent(&tree, "coder", &["forbidden"]);
    write_skill_with_deps(&tree, "forbidden", &["missing-skill"]);

    let mut provider = MockProvider::new();
    provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
    provider.add_source("a", tree, None);

    let mut dependencies = IndexMap::new();
    dependencies.insert(
        SourceName::from("a"),
        EffectiveDependency {
            name: "a".into(),
            id: SourceId::git(SourceUrl::from("https://example.com/a.git")),
            spec: git_spec("https://example.com/a.git", Some("v1.0.0")),
            subpath: None,
            filter: FilterMode::Exclude(vec!["forbidden".into()]),
            rename: RenameMap::new(),
            is_overridden: false,
            original_git: None,
        },
    );
    let config = EffectiveConfig {
        dependencies,
        settings: Settings::default(),
    };

    let graph = resolve(&config, &provider, None, &default_options()).unwrap();
    assert!(graph.nodes.contains_key("a"));
}

#[test]
fn skill_resolution_prefers_dependency_closure_over_insertion_order() {
    let dir = TempDir::new().unwrap();
    let requester_tree = dir.path().join("requester");
    let sibling_tree = dir.path().join("sibling");
    let dep_tree = dir.path().join("my-dep");
    std::fs::create_dir_all(&requester_tree).unwrap();
    std::fs::create_dir_all(&sibling_tree).unwrap();
    std::fs::create_dir_all(&dep_tree).unwrap();

    write_minimal_package_marker(&requester_tree);
    write_minimal_package_marker(&sibling_tree);
    write_minimal_package_marker(&dep_tree);
    write_agent(&requester_tree, "coder", &["planning"]);
    write_skill(&sibling_tree, "planning");
    write_skill(&dep_tree, "planning");

    let requester_manifest = make_manifest(
        "requester",
        "1.0.0",
        vec![("my-dep", "https://example.com/my-dep.git", ">=1.0.0")],
    );

    let mut provider = MockProvider::new();
    provider.add_versions("https://example.com/requester.git", vec![(1, 0, 0)]);
    provider.add_versions("https://example.com/sibling.git", vec![(1, 0, 0)]);
    provider.add_versions("https://example.com/my-dep.git", vec![(1, 0, 0)]);
    provider.add_source("requester", requester_tree, Some(requester_manifest));
    provider.add_source("sibling", sibling_tree, None);
    provider.add_source("my-dep", dep_tree, None);

    let config = make_config(vec![
        (
            "sibling",
            git_spec("https://example.com/sibling.git", Some("v1.0.0")),
        ),
        (
            "requester",
            git_spec("https://example.com/requester.git", Some("v1.0.0")),
        ),
    ]);

    let graph = resolve(&config, &provider, None, &default_options()).unwrap();

    let planning_include = FilterMode::Include {
        agents: Vec::new(),
        skills: vec![ItemName::from("planning")],
    };
    assert!(
        graph
            .filters
            .get("my-dep")
            .is_some_and(|filters| filters.contains(&planning_include)),
        "expected planning include filter on my-dep; got {:?}",
        graph.filters.get("my-dep")
    );
    assert!(
        !graph
            .filters
            .get("sibling")
            .is_some_and(|filters| filters.contains(&planning_include)),
        "did not expect planning include filter on sibling; got {:?}",
        graph.filters.get("sibling")
    );
}