#!/usr/bin/env bats setup() { REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../../../../" && pwd)" load "$REPO_ROOT/tests/test_helper/bats-support/load" load "$REPO_ROOT/tests/test_helper/bats-assert/load" SCRIPT="$(cd "$BATS_TEST_DIRNAME/../scripts" && pwd)/new-agent.sh" ROOT="$(mktemp -d)" } teardown() { rm -rf "$ROOT" } # --------------------------------------------------------------------------- # Help # --------------------------------------------------------------------------- @test "--help exits 0" { run bash "$SCRIPT" --help assert_success assert_output --partial "Usage:" } # --------------------------------------------------------------------------- # Plugin/APM scope (type:-bearing apm.yml at root) # --------------------------------------------------------------------------- @test "plugin/APM scope: creates single agent file in .apm/agents/" { printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml" run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ -f "$ROOT/.apm/agents/my-agent.agent.md" ] } @test "plugin/APM scope: does not create the old dual-file pair" { printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml" run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ ! -f "$ROOT/agents/my-agent.md" ] assert [ ! -f "$ROOT/agents/my-agent.agent.md" ] } @test "plugin/APM scope: creates .apm/agents/ directory if missing" { printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml" run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ -d "$ROOT/.apm/agents" ] } @test "plugin/APM scope: creates sources.md at package root" { printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml" run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ -f "$ROOT/sources.md" ] } @test "plugin/APM scope: no-op if agent file already exists" { printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml" mkdir -p "$ROOT/.apm/agents" echo "existing" > "$ROOT/.apm/agents/my-agent.agent.md" run bash "$SCRIPT" my-agent "$ROOT" assert_success run grep "existing" "$ROOT/.apm/agents/my-agent.agent.md" assert_success } @test "plugin/APM scope: frontmatter has no tools/isolation/maxTurns/effort/memory/permissionMode keys" { printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml" bash "$SCRIPT" my-agent "$ROOT" file="$ROOT/.apm/agents/my-agent.agent.md" fm="$(sed -n '/^---$/,/^---$/p' "$file")" # Column-0 key lines only — comment bodies in the template are indented, # so this anchor naturally excludes commented-out example fields. run grep -E '^(tools|isolation|maxTurns|effort|memory|permissionMode|disallowedTools|skills|color|initialPrompt|background|hooks|mcpServers):' <<< "$fm" assert_failure } @test "plugin/APM scope: frontmatter carries only name, description, model, source_keys fields" { printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml" bash "$SCRIPT" my-agent "$ROOT" file="$ROOT/.apm/agents/my-agent.agent.md" fm="$(sed -n '/^---$/,/^---$/p' "$file")" keys="$(grep -oE '^[a-zA-Z][a-zA-Z0-9_-]*:' <<< "$fm" | sed 's/:$//' | sort -u)" for key in $keys; do if [[ "$key" != "name" && "$key" != "description" && "$key" != "model" && "$key" != "source_keys" ]]; then fail "unexpected frontmatter key: $key" fi done } @test "plugin/APM scope: sources.md contributing-files template mentions the single-file path" { printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml" bash "$SCRIPT" my-agent "$ROOT" run grep ".apm/agents/.agent.md" "$ROOT/sources.md" assert_success } @test "plugin/APM scope: template AGENT_NAME substituted" { printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml" bash "$SCRIPT" my-agent "$ROOT" run grep "my-agent" "$ROOT/.apm/agents/my-agent.agent.md" assert_success } @test "plugin/APM scope: walk-up finds apm.yml at an ancestor directory, not just root arg" { printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml" mkdir -p "$ROOT/nested/subdir" run bash "$SCRIPT" my-agent "$ROOT/nested/subdir" assert_success assert [ -f "$ROOT/.apm/agents/my-agent.agent.md" ] } @test "plugin/APM scope: apm.yml without type: is skipped (marketplace-only manifest)" { printf 'name: my-marketplace\n' > "$ROOT/apm.yml" mkdir -p "$ROOT/.git" run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ ! -f "$ROOT/.apm/agents/my-agent.agent.md" ] assert [ -f "$ROOT/.claude/agents/my-agent.md" ] } @test "plugin/APM scope: type:-less apm.yml at leaf falls through to a type:-bearing apm.yml higher up" { printf 'name: outer-package\ntype: skill\n' > "$ROOT/apm.yml" mkdir -p "$ROOT/inner" printf 'name: inner-marketplace\n' > "$ROOT/inner/apm.yml" run bash "$SCRIPT" my-agent "$ROOT/inner" assert_success assert [ -f "$ROOT/.apm/agents/my-agent.agent.md" ] } @test "plugin/APM scope: matched-quote type value ('skill') is recognized" { printf 'name: my-package\ntype: "skill"\n' > "$ROOT/apm.yml" run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ -f "$ROOT/.apm/agents/my-agent.agent.md" ] } @test "plugin/APM scope: mismatched-quote type value is rejected, falls through to project scope" { mkdir -p "$ROOT/.git" printf "name: my-package\ntype: \"skill'\n" > "$ROOT/apm.yml" run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ ! -f "$ROOT/.apm/agents/my-agent.agent.md" ] assert [ -f "$ROOT/.claude/agents/my-agent.md" ] } @test "plugin/APM scope: type: line is recognized even without a trailing newline on the file" { printf 'name: my-package\ntype: skill' > "$ROOT/apm.yml" run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ -f "$ROOT/.apm/agents/my-agent.agent.md" ] } # --------------------------------------------------------------------------- # Old plugin.json marker is no longer recognized (full switch, no dual-mode) # --------------------------------------------------------------------------- @test "bare plugin.json (no apm.yml) is no longer detected as plugin scope — falls through to project scope" { touch "$ROOT/plugin.json" run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ ! -f "$ROOT/agents/my-agent.md" ] assert [ ! -f "$ROOT/agents/my-agent.agent.md" ] assert [ ! -f "$ROOT/.apm/agents/my-agent.agent.md" ] assert [ -f "$ROOT/.claude/agents/my-agent.md" ] assert [ -f "$ROOT/.github/agents/my-agent.agent.md" ] } # --------------------------------------------------------------------------- # Non-plugin (project) scope # --------------------------------------------------------------------------- @test "non-plugin scope: creates claude code file in .claude/agents/" { run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ -f "$ROOT/.claude/agents/my-agent.md" ] } @test "non-plugin scope: creates copilot file in .github/agents/" { run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ -f "$ROOT/.github/agents/my-agent.agent.md" ] } @test "non-plugin scope: creates .claude/agents/ directory if missing" { run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ -d "$ROOT/.claude/agents" ] } @test "non-plugin scope: creates .github/agents/ directory if missing" { run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ -d "$ROOT/.github/agents" ] } @test "non-plugin scope: no sources.md created" { run bash "$SCRIPT" my-agent "$ROOT" assert_success assert [ ! -f "$ROOT/.claude/agents/sources.md" ] assert [ ! -f "$ROOT/.github/agents/sources.md" ] } @test "non-plugin scope: no-op if claude code file already exists" { mkdir -p "$ROOT/.claude/agents" echo "existing" > "$ROOT/.claude/agents/my-agent.md" run bash "$SCRIPT" my-agent "$ROOT" assert_success run grep "existing" "$ROOT/.claude/agents/my-agent.md" assert_success } @test "project scope: detected via .git present above root, output still relative to root arg" { mkdir -p "$ROOT/repo/.git" mkdir -p "$ROOT/repo/pkg" run bash "$SCRIPT" my-agent "$ROOT/repo/pkg" assert_success assert [ -f "$ROOT/repo/pkg/.claude/agents/my-agent.md" ] assert [ -f "$ROOT/repo/pkg/.github/agents/my-agent.agent.md" ] } # --------------------------------------------------------------------------- # User scope # --------------------------------------------------------------------------- @test "user scope: root exactly \$HOME creates files under ~/.claude and ~/.copilot" { FAKE_HOME="$(mktemp -d)" run env HOME="$FAKE_HOME" bash "$SCRIPT" my-agent "~" assert_success assert [ -f "$FAKE_HOME/.claude/agents/my-agent.md" ] assert [ -f "$FAKE_HOME/.copilot/agents/my-agent.agent.md" ] rm -rf "$FAKE_HOME" } @test "user scope: \$HOME being a dotfiles .git repo does not shadow user scope" { FAKE_HOME="$(mktemp -d)" mkdir "$FAKE_HOME/.git" run env HOME="$FAKE_HOME" bash "$SCRIPT" my-agent "~" assert_success assert [ -f "$FAKE_HOME/.claude/agents/my-agent.md" ] assert [ -f "$FAKE_HOME/.copilot/agents/my-agent.agent.md" ] refute [ -d "$FAKE_HOME/.github" ] rm -rf "$FAKE_HOME" } @test "user scope is checked directly at \$HOME, no walk-up: a marker-less subdir under \$HOME resolves to project scope, not user scope" { FAKE_HOME="$(mktemp -d)" mkdir -p "$FAKE_HOME/scratch/testdir" run env HOME="$FAKE_HOME" bash "$SCRIPT" my-agent "$FAKE_HOME/scratch/testdir" assert_success assert [ -f "$FAKE_HOME/scratch/testdir/.claude/agents/my-agent.md" ] assert [ -f "$FAKE_HOME/scratch/testdir/.github/agents/my-agent.agent.md" ] refute [ -f "$FAKE_HOME/.claude/agents/my-agent.md" ] refute [ -f "$FAKE_HOME/.copilot/agents/my-agent.agent.md" ] rm -rf "$FAKE_HOME" } # --------------------------------------------------------------------------- # Name validation # --------------------------------------------------------------------------- @test "fails when no arguments given" { run bash "$SCRIPT" assert_failure } @test "fails when agent name contains uppercase" { run bash "$SCRIPT" MyAgent "$ROOT" assert_failure } @test "fails when agent name has consecutive hyphens" { run bash "$SCRIPT" my--agent "$ROOT" assert_failure } @test "fails when agent name has a leading hyphen" { run bash "$SCRIPT" -my-agent "$ROOT" assert_failure } @test "fails when agent name has a trailing hyphen" { run bash "$SCRIPT" my-agent- "$ROOT" assert_failure } @test "agent name with numbers is valid" { run bash "$SCRIPT" agent-v2 "$ROOT" assert_success } # --------------------------------------------------------------------------- # Root validation # --------------------------------------------------------------------------- @test "fails when root directory does not exist" { run bash "$SCRIPT" my-agent "/nonexistent/path" assert_failure }