fix(skill-author): fail loudly when scaffold repair cannot substitute
Why: repair_placeholders ran inside a command substitution, so a failing sed left an emptied file behind and the script still exited 0 reporting success. - write via tmp file and abort on sed or mv failure - re-check the target before moving staging into place - stage in a dot-prefixed mktemp dir so a killed run leaves no fake skill - source apm claims in deployment-modes.md; tag untyped code blocks Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
This commit is contained in:
@@ -113,9 +113,90 @@ teardown() {
|
||||
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 bash -c "ls -d '$DEST'/my-tool.partial.* 2>/dev/null"
|
||||
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 ""
|
||||
}
|
||||
|
||||
@@ -126,6 +207,7 @@ teardown() {
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user