#!/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)/validate.sh" TMPDIR="$(mktemp -d)" # Helper: create an APM package root at (apm.yml with a top-level # type: line, marking it a real package manifest — not marketplace-only) # plus a single vendor-neutral agent file at # /.apm/agents/.agent.md. , if given, is # inserted as additional raw frontmatter lines (used to inject fields # under test). make_apm_agent() { local root="$1" local name="$2" local extra_frontmatter="${3:-}" mkdir -p "$root/.apm/agents" cat > "$root/apm.yml" < "$root/.apm/agents/${name}.agent.md" < characters that carries a boundary # clause and names no routing target. ADR-0020's missing-boundary-clause # SUGGESTION fires on any description without one, so a fixture that omits it # is never "otherwise clean" and a test refuting SUGGESTION would be asserting # the boundary check's absence instead of the thing it names. The clause is # paid for out of the measured budget rather than appended to it, because # these tests measure the description LENGTH. "anything else" is not # hyphenated, so no routing target comes with it. desc_of_length() { python3 - "$1" <<'PY' import sys n = int(sys.argv[1]) prefix = 'Use when doing the thing. Do not use for anything else. ' assert n >= len(prefix), 'requested description shorter than the boundary clause' print(prefix + 'x' * (n - len(prefix))) PY } # Helper: same shape as make_apm_agent, but the description is supplied # verbatim — used by the ADR-0020 description-budget tests. make_apm_agent_with_desc() { local root="$1" name="$2" desc="$3" mkdir -p "$root/.apm/agents" cat > "$root/apm.yml" < "$root/.apm/agents/${name}.agent.md" < "$root/.claude/agents/my-agent.md" < "$root/.github/agents/my-agent.agent.md" < "$fake_home/.claude/agents/my-agent.md" < "$fake_home/.copilot/agents/my-agent.agent.md" < "$fake_home/my-agent.md" < "$fake_home/.copilot/agents/my-agent.agent.md" < "$nested/.claude/agents/my-agent.md" < "$nested/.github/agents/my-agent.agent.md" < "$nested/.claude/agents/my-agent.md" < "$fake_home/.copilot/agents/my-agent.agent.md" < "$root/.claude/agents/my-agent.md" < "$root/.github/agents/my-agent.agent.md" < one level below a .git ancestor resolves scope to , not to wherever .git was found (subdirectory of a larger git-tracked tree)" { local repo="$TMPDIR/repo-with-subdir" local root="$repo/subdir" mkdir -p "$repo/.git" "$root/.claude/agents" "$root/.github/agents" cat > "$root/.claude/agents/my-agent.md" < "$root/.github/agents/my-agent.agent.md" < as its root argument, would place the # counterpart at /.github/agents — not at the repo root's # .github/agents, even though .git lives at the repo root one level up. run bash "$SCRIPT" "$root/.claude/agents/my-agent.md" assert_success refute_output --partial "FAIL" refute_output --partial "counterpart" } @test "project scope: a non-conventional path (agent file not directly under a literal 'agents' dir) falls back to the nearest .git boundary instead of two-segments-up arithmetic" { local outer="$TMPDIR/outer-repo" local pkg="$outer/pkgA" mkdir -p "$pkg/.git" "$pkg/.github/agents" "$pkg/extra" # Misplaced file: sits two path segments below $outer (pkgA/extra), which # matches the conventional_root arithmetic by coincidence, but its # immediate parent dir is "extra", not "agents" — conventional_shape is # false, so the fix must fall back to the nearest .git boundary (pkgA), # not trust $outer. cat > "$pkg/extra/my-agent.md" < "$pkg/.github/agents/my-agent.agent.md" < "$root/.claude/agents/my-agent.md" < "$root/.github/agents/my-agent.agent.md" < "$root/.claude/agents/my-agent.md" < "$root/.github/agents/my-agent.agent.md" < "$root/.claude/agents/my-agent.md" < "$root/apm.yml" < "$root/.apm/agents/my-agent.agent.md" < "$root/apm.yml" < "$root/.apm/agents/my-agent.agent.md" < "$root/apm.yml" < "$root/.apm/agents/my-agent.agent.md" < "$root/apm.yml" < "$root/.apm/agents/my-agent.agent.md" < "$root/apm.yml" < "$root/.apm/agents/my-agent.agent.md" < --- You are a test agent. When invoked, do the thing. EOF run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md" assert_failure assert_output --partial "template HTML comments" } @test "fails when body contains unfilled FILL IN: placeholder in a plugin/APM-scope agent file" { local root="$TMPDIR/pkg" mkdir -p "$root/.apm/agents" cat > "$root/apm.yml" < "$root/.apm/agents/my-agent.agent.md" < "$root/apm.yml" < "$root/.apm/agents/nested/apm.yml" < "$root/.apm/agents/nested/deeper/my-agent.agent.md" < "$root/apm.yml" < "$root/.claude/agents/my-agent.md" < "$root/.github/agents/my-agent.agent.md" < "$root/apm.yml" <` line it is 1 character and passes. { echo "---" echo "name: my-agent" echo "description: >" python3 -c "print('\n'.join([' ' + 'x' * 40] * 11))" echo "---" echo "" echo "You are a test agent." } > "$root/.apm/agents/my-agent.agent.md" run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md" assert_failure assert_output --partial "description is 450 chars" } @test "ADR-0020: the description gate applies at project scope too" { local root="$TMPDIR/project" local desc desc="$(python3 -c "print('x' * 401)")" mkdir -p "$root/.git" "$root/.claude/agents" "$root/.github/agents" cat > "$root/.claude/agents/my-agent.md" < "$root/.github/agents/my-agent.agent.md" < "$root/apm.yml" < "$root/.apm/agents/my-agent.agent.md" run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md" assert_success refute_output --partial "FAIL" # A 1,500-word body is 667% of the skill ceiling. Nothing may be said about # it at any tier: not a FAIL, not a SUGGESTION, and not the word-count # wording either tier would use if a gate were quietly added later. refute_output --partial "SUGGESTION" refute_output --partial "1500 words" refute_output --partial "900-word" refute_output --partial "body is" } @test "a bare plugin.json with no apm.yml is no longer plugin scope — falls through to project scope" { local root="$TMPDIR/proj-legacy-plugin-json" mkdir -p "$root/.git" "$root/.claude/agents" "$root/.github/agents" echo '{}' > "$root/plugin.json" # 'hooks' is plugin-silently-ignored only at (old) plugin scope; at # project scope it's a legitimate CC field. If this directory were # mis-detected as plugin scope (old plugin.json-based logic), this would # FAIL with a plugin-silently-ignored-fields finding on 'hooks'. cat > "$root/.claude/agents/my-agent.md" < "$root/.github/agents/my-agent.agent.md" <