#!/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-skill.sh" DEST="$(mktemp -d)" } teardown() { rm -rf "$DEST" } # --------------------------------------------------------------------------- # Passing cases # --------------------------------------------------------------------------- @test "--help exits 0" { run bash "$SCRIPT" --help assert_success assert_output --partial "Usage:" } @test "creates scaffold directory at destination" { run bash "$SCRIPT" my-tool "$DEST" assert_success assert [ -d "$DEST/my-tool" ] } @test "scaffold contains SKILL.md" { bash "$SCRIPT" my-tool "$DEST" assert [ -f "$DEST/my-tool/SKILL.md" ] } @test "scaffold contains scripts/, references/, assets/, tests/ directories" { bash "$SCRIPT" my-tool "$DEST" assert [ -d "$DEST/my-tool/scripts" ] assert [ -d "$DEST/my-tool/references" ] assert [ -d "$DEST/my-tool/assets" ] assert [ -d "$DEST/my-tool/tests" ] } @test "substitutes skill name in SKILL.md" { bash "$SCRIPT" my-tool "$DEST" run grep "my-tool" "$DEST/my-tool/SKILL.md" assert_success } @test "substitutes skill name in tests/README.md" { bash "$SCRIPT" my-tool "$DEST" run grep "my-tool" "$DEST/my-tool/tests/README.md" assert_success } @test "skill name with numbers is valid" { run bash "$SCRIPT" my-tool-2 "$DEST" assert_success assert [ -d "$DEST/my-tool-2" ] } @test "next-steps output references /factory-audit not validate.sh" { run bash "$SCRIPT" my-tool "$DEST" assert_output --partial "/factory-audit" refute_output --partial "validate.sh" } # --------------------------------------------------------------------------- # Failing cases # --------------------------------------------------------------------------- @test "fails when no arguments given" { run bash "$SCRIPT" assert_failure } @test "fails when skill name contains uppercase" { run bash "$SCRIPT" MyTool "$DEST" assert_failure } @test "fails when skill name has consecutive hyphens" { run bash "$SCRIPT" my--tool "$DEST" assert_failure } @test "fails when skill name has a leading hyphen" { run bash "$SCRIPT" -my-tool "$DEST" assert_failure } @test "fails when skill name has a trailing hyphen" { run bash "$SCRIPT" my-tool- "$DEST" assert_failure } @test "fails when destination directory does not exist" { run bash "$SCRIPT" my-tool "/nonexistent/path" assert_failure } @test "exits 0 when target already exists (no-op)" { mkdir -p "$DEST/my-tool" run bash "$SCRIPT" my-tool "$DEST" assert_success assert_output --partial "nothing to do" } @test "no SKILL_NAME placeholder remains anywhere in a fresh scaffold" { bash "$SCRIPT" my-tool "$DEST" run grep -r "SKILL_NAME" "$DEST/my-tool" assert_failure } # Lists every entry in $1 other than my-tool and the fixture copies, so a # staging directory of any name (dotted or not) left behind is caught. leftovers() { local entry name for entry in "$1"/* "$1"/.[!.]* "$1"/..?*; do [[ -e "$entry" ]] || continue name=${entry##*/} case "$name" in my-tool|bin|skill.orig) ;; *) printf '%s\n' "$name" ;; esac done } # Puts a `sed` on PATH that fails without writing output, so a test can # inject a failure into the substitution step. stub_failing_sed() { mkdir -p "$DEST/bin" printf '#!/usr/bin/env bash\nexit 1\n' > "$DEST/bin/sed" chmod +x "$DEST/bin/sed" } @test "leaves no staging directory behind after a successful run" { bash "$SCRIPT" my-tool "$DEST" run leftovers "$DEST" assert_output "" } @test "stages the build in a dot-prefixed directory that cannot pass for a skill" { # A SIGKILL skips the cleanup trap, so the staging directory's name is what # keeps a leftover from looking like a skill under .apm/skills/. mkdir -p "$DEST/bin" real_sed="$(command -v sed)" printf '#!/usr/bin/env bash\nprintf "%%s\\n" "${@: -1}" >> "%s/sed.log"\nexec "%s" "$@"\n' \ "$DEST/bin" "$real_sed" > "$DEST/bin/sed" chmod +x "$DEST/bin/sed" PATH="$DEST/bin:$PATH" run bash "$SCRIPT" my-tool "$DEST" assert_success run grep -c . "$DEST/bin/sed.log" refute_output "0" run grep -vE "^$DEST/\.[^/]+/" "$DEST/bin/sed.log" assert_output "" } @test "scaffold directory gets umask-default permissions, not mktemp's 0700" { bash "$SCRIPT" my-tool "$DEST" mkdir "$DEST/reference-dir" assert_equal "$(ls -ld "$DEST/my-tool" | cut -c1-10)" \ "$(ls -ld "$DEST/reference-dir" | cut -c1-10)" rmdir "$DEST/reference-dir" } @test "fresh scaffold: a failing sed aborts non-zero with no target and no staging left" { stub_failing_sed PATH="$DEST/bin:$PATH" run bash "$SCRIPT" my-tool "$DEST" assert_failure assert [ ! -e "$DEST/my-tool" ] run leftovers "$DEST" assert_output "" } @test "repair: a failing sed aborts non-zero and leaves the original file unchanged" { cp -r "$BATS_TEST_DIRNAME/../assets/templates" "$DEST/my-tool" cp "$DEST/my-tool/SKILL.md" "$DEST/skill.orig" stub_failing_sed PATH="$DEST/bin:$PATH" run bash "$SCRIPT" my-tool "$DEST" assert_failure refute_output --partial "Repaired" run cmp "$DEST/skill.orig" "$DEST/my-tool/SKILL.md" assert_success run find "$DEST/my-tool" -name '*.tmp' assert_output "" } @test "a target that appears after the existence check is not nested into" { # A sed wrapper creates the target mid-build, standing in for a concurrent # run winning the race between the check and the final rename. mkdir -p "$DEST/bin" real_sed="$(command -v sed)" printf '#!/usr/bin/env bash\nmkdir -p "%s/my-tool"\nexec "%s" "$@"\n' \ "$DEST" "$real_sed" > "$DEST/bin/sed" chmod +x "$DEST/bin/sed" PATH="$DEST/bin:$PATH" run bash "$SCRIPT" my-tool "$DEST" assert_failure run ls -A "$DEST/my-tool" assert_output "" run leftovers "$DEST" assert_output "" } @test "retry repairs a half-built scaffold that still carries SKILL_NAME" { # Simulate an earlier run that copied the templates but died before the # name substitution: the retry must finish the job, not no-op. cp -r "$BATS_TEST_DIRNAME/../assets/templates" "$DEST/my-tool" run bash "$SCRIPT" my-tool "$DEST" assert_success assert_output --partial "Repaired partial scaffold" assert_output --partial "only the name placeholder" run grep -r "SKILL_NAME" "$DEST/my-tool" assert_failure run grep -E '^name: my-tool$' "$DEST/my-tool/SKILL.md" assert_success } @test "a complete skill whose text mentions SKILL_NAME stays a true no-op" { # Only the template's exact marker lines mark a half-built scaffold. A # finished skill that merely mentions the token must not be rewritten. mkdir -p "$DEST/my-tool/tests" printf -- '---\nname: my-tool\n---\n\nUse `__SKILL_NAME_PLACEHOLDER__` here.\n' \ > "$DEST/my-tool/SKILL.md" printf 'Set SKILL_NAME before running.\n' > "$DEST/my-tool/tests/README.md" cp "$DEST/my-tool/SKILL.md" "$DEST/skill.orig" cp "$DEST/my-tool/tests/README.md" "$DEST/readme.orig" run bash "$SCRIPT" my-tool "$DEST" assert_success assert_output --partial "nothing to do" refute_output --partial "Repaired" run cmp "$DEST/skill.orig" "$DEST/my-tool/SKILL.md" assert_success run cmp "$DEST/readme.orig" "$DEST/my-tool/tests/README.md" assert_success } # --------------------------------------------------------------------------- # Mode detection: package vs standalone # --------------------------------------------------------------------------- @test "standalone mode is chosen when no apm.yml or .git is found above path" { run bash "$SCRIPT" my-tool "$DEST" assert_success assert_output --partial "Mode: standalone" assert [ -d "$DEST/my-tool" ] } @test "package mode: writes into /.apm/skills/, not under the given subdir" { mkdir -p "$DEST/pkg/sub/deep" cat > "$DEST/pkg/apm.yml" <<'EOF' name: my-pkg version: 1.0.0 type: skill EOF run bash "$SCRIPT" my-tool "$DEST/pkg/sub/deep" assert_success assert_output --partial "Mode: package" assert [ -d "$DEST/pkg/.apm/skills/my-tool" ] assert [ ! -d "$DEST/pkg/sub/deep/my-tool" ] } @test "package mode scaffold contains SKILL.md with name substituted" { mkdir -p "$DEST/pkg" cat > "$DEST/pkg/apm.yml" <<'EOF' name: my-pkg version: 1.0.0 type: hybrid EOF bash "$SCRIPT" my-tool "$DEST/pkg" assert [ -f "$DEST/pkg/.apm/skills/my-tool/SKILL.md" ] run grep "my-tool" "$DEST/pkg/.apm/skills/my-tool/SKILL.md" assert_success } @test "scaffold emits a live metadata.version seeded at 0.1.0 (ADR-0022)" { # The scaffold must clear .pre-commit-config.yaml's `skill-size-check` hook # on its first commit: a commented-out metadata block ships a skill with no # version and is blocked. Assert the field is live, not a comment. bash "$SCRIPT" my-tool "$DEST" run grep -E '^metadata:$' "$DEST/my-tool/SKILL.md" assert_success run grep -E '^ version: "[0-9]+\.[0-9]+\.[0-9]+"$' "$DEST/my-tool/SKILL.md" assert_success assert_output --partial '0.1.0' } @test "walk-up skips a type-less apm.yml (marketplace-only) and finds a real package root further up" { mkdir -p "$DEST/mid/sub" cat > "$DEST/apm.yml" <<'EOF' name: root-pkg version: 1.0.0 type: skill EOF cat > "$DEST/mid/apm.yml" <<'EOF' marketplace: owner: acme packages: [] EOF run bash "$SCRIPT" my-tool "$DEST/mid/sub" assert_success assert_output --partial "Mode: package" assert [ -d "$DEST/.apm/skills/my-tool" ] assert [ ! -d "$DEST/mid/.apm" ] } @test "walk-up stops at .git boundary and uses standalone mode at the given path" { mkdir -p "$DEST/repo/.git" mkdir -p "$DEST/repo/sub" run bash "$SCRIPT" my-tool "$DEST/repo/sub" assert_success assert_output --partial "Mode: standalone" assert [ -d "$DEST/repo/sub/my-tool" ] assert [ ! -d "$DEST/repo/my-tool" ] } @test "package mode: matched-quote type value ('skill') is recognized" { mkdir -p "$DEST/pkg" printf 'name: my-pkg\ntype: "skill"\n' > "$DEST/pkg/apm.yml" run bash "$SCRIPT" my-tool "$DEST/pkg" assert_success assert_output --partial "Mode: package" } @test "mismatched-quote type value is rejected, falls through to standalone mode" { printf "name: my-pkg\ntype: \"skill'\n" > "$DEST/apm.yml" run bash "$SCRIPT" my-tool "$DEST" assert_success assert_output --partial "Mode: standalone" } @test "type: line is recognized even without a trailing newline on apm.yml" { printf 'name: my-pkg\ntype: skill' > "$DEST/apm.yml" run bash "$SCRIPT" my-tool "$DEST" assert_success assert_output --partial "Mode: package" }