- factory-audit: hook events judged per deployed target after apm's rename (Claude/Copilot event sets FAIL, others SUGGESTION); Claude plugin layouts accepted as hook sources; interpreter options and sh -c strings checked; bats 378 -> 386 - primitive-author: Must 4/5 match the audit; reference hand-back points at the right steps; Step 4.2 --target all fallback - skill-author: new-skill.sh repair only on the template marker line, so complete skills stay a no-op; provenance and calibration text - forge: restore "already named" qualifier; drop false HITL claim - apm-workflow: token example uses an env var - docs/hooks.md: the apm-hooks.json sidecar is committed, not ignored Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
839 lines
37 KiB
Bash
839 lines
37 KiB
Bash
#!/usr/bin/env bats
|
|
|
|
# scripts/validate.sh against the three apm primitives with no container of
|
|
# their own: hooks, instructions and prompts. Each rule under test traces to
|
|
# the Authoring checklist in
|
|
# plugins/kyberforge/docs/research/docs/microsoft-apm/<kind>-primitive-schema.md.
|
|
|
|
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)"
|
|
PKG="$TMPDIR/pkg"
|
|
mkdir -p "$PKG/.apm/hooks/scripts" "$PKG/.apm/instructions" "$PKG/.apm/prompts"
|
|
cat > "$PKG/apm.yml" <<EOF
|
|
name: test-package
|
|
version: 0.1.0
|
|
type: hybrid
|
|
EOF
|
|
printf '#!/usr/bin/env bash\nexit 0\n' > "$PKG/.apm/hooks/scripts/check.sh"
|
|
chmod +x "$PKG/.apm/hooks/scripts/check.sh"
|
|
|
|
# Helper: write <content> as hook file <name> under .apm/hooks/.
|
|
write_hook() {
|
|
printf '%s\n' "$2" > "$PKG/.apm/hooks/$1"
|
|
}
|
|
|
|
# Helper: write an instruction <stem> with raw <frontmatter> and <body>.
|
|
write_instruction() {
|
|
printf -- '---\n%s\n---\n\n%s\n' "$2" "$3" > "$PKG/.apm/instructions/$1.instructions.md"
|
|
}
|
|
|
|
# Helper: write a prompt <stem> with raw <frontmatter> and <body>.
|
|
write_prompt() {
|
|
printf -- '---\n%s\n---\n\n%s\n' "$2" "$3" > "$PKG/.apm/prompts/$1.prompt.md"
|
|
}
|
|
}
|
|
|
|
teardown() {
|
|
rm -rf "$TMPDIR"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dispatch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test "dispatch: a .json file outside a hooks/ directory matches no shape (exit 2)" {
|
|
printf '{}\n' > "$PKG/settings.json"
|
|
run bash "$SCRIPT" "$PKG/settings.json"
|
|
assert_equal "$status" 2
|
|
assert_output --partial "matches no auditable shape"
|
|
}
|
|
|
|
@test "dispatch: an *.instructions.md under an agents/ directory takes the instruction flow, not the agent flow" {
|
|
mkdir -p "$PKG/.apm/agents"
|
|
printf -- '---\ndescription: x\n---\n\nbody\n' > "$PKG/.apm/agents/x.instructions.md"
|
|
run bash "$SCRIPT" "$PKG/.apm/agents/x.instructions.md"
|
|
assert_failure 1
|
|
assert_output --partial "is not directly in a .apm/instructions/ directory"
|
|
refute_output --partial "counterpart"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Hooks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test "hook: canonical nested shape with \${PLUGIN_ROOT} passes clean" {
|
|
write_hook hooks.json '{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"${PLUGIN_ROOT}/.apm/hooks/scripts/check.sh","timeout":10}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
refute_output --partial "SUGGESTION"
|
|
}
|
|
|
|
@test "hook: invalid JSON is a FAIL" {
|
|
write_hook hooks.json '{"hooks": {'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "is not valid JSON"
|
|
}
|
|
|
|
@test "hook: an event value that is not a list is a FAIL" {
|
|
write_hook hooks.json '{"hooks":{"PreToolUse":{"hooks":[]}}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "event 'PreToolUse' is not a list"
|
|
}
|
|
|
|
@test "hook: a naked slice with a stray scalar key is a FAIL" {
|
|
write_hook hooks.json '{"description":"x","PreToolUse":[{"hooks":[{"type":"command","command":"true"}]}]}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "naked settings-slice shape"
|
|
}
|
|
|
|
@test "hook: an all-lowercase event no target fires is a FAIL naming the targets" {
|
|
write_hook hooks.json '{"hooks":{"pretooluse":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "event 'pretooluse' never fires on claude, copilot"
|
|
}
|
|
|
|
@test "hook: lowercase stop with no targets: is a FAIL — Claude and Copilot never fire it" {
|
|
write_hook hooks.json '{"hooks":{"stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "event 'stop' never fires on claude, copilot"
|
|
|
|
printf 'name: test-package\nversion: 0.1.0\ntargets: [claude, copilot]\n' > "$PKG/apm.yml"
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "event 'stop' never fires on claude, copilot"
|
|
}
|
|
|
|
@test "hook: lowercase stop is clean for Kiro only (Kiro renames it to Stop)" {
|
|
printf 'name: test-package\nversion: 0.1.0\ntargets: [kiro]\n' > "$PKG/apm.yml"
|
|
write_hook hooks.json '{"hooks":{"stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "event 'stop'"
|
|
}
|
|
|
|
@test "hook: lowercase stop in a Cursor-only package passes, at most a SUGGESTION (no published Cursor event list)" {
|
|
printf 'name: test-package\nversion: 0.1.0\ntargets: [cursor]\n' > "$PKG/apm.yml"
|
|
write_hook hooks.json '{"hooks":{"stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
assert_output --partial "SUGGESTION event 'stop' reaches cursor verbatim"
|
|
}
|
|
|
|
@test "hook: a Windsurf-only snake_case event is a SUGGESTION, not a FAIL" {
|
|
printf 'name: test-package\nversion: 0.1.0\ntargets: [windsurf]\n' > "$PKG/apm.yml"
|
|
write_hook hooks.json '{"hooks":{"pre_run_command":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
assert_output --partial "SUGGESTION event 'pre_run_command' reaches windsurf verbatim"
|
|
}
|
|
|
|
@test "hook: a misspelled PascalCase event targeting Claude is a FAIL" {
|
|
printf 'name: test-package\nversion: 0.1.0\ntargets: [claude]\n' > "$PKG/apm.yml"
|
|
write_hook hooks.json '{"hooks":{"PreToolUSe":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "event 'PreToolUSe' never fires on claude"
|
|
}
|
|
|
|
@test "hook: a real Claude event apm does not rename for Copilot FAILs on Copilot only when Copilot does not fire it" {
|
|
write_hook hooks.json '{"hooks":{"SubagentStop":[{"hooks":[{"type":"command","command":"true"}]}],"PostToolBatch":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
refute_output --partial "event 'SubagentStop'"
|
|
assert_output --partial "event 'PostToolBatch' never fires on copilot"
|
|
}
|
|
|
|
@test "hook: camelCase userPromptSubmit is a FAIL on Claude; mapped sessionStart is not" {
|
|
write_hook hooks.json '{"hooks":{"userPromptSubmit":[{"hooks":[{"type":"command","command":"true"}]}],"sessionStart":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "event 'userPromptSubmit' never fires on claude"
|
|
refute_output --partial "FAIL event 'sessionStart'"
|
|
}
|
|
|
|
@test "hook: a flat Copilot-shaped file may use camelCase events when the package does not target Claude" {
|
|
printf 'targets:\n - copilot\n' >> "$PKG/apm.yml"
|
|
write_hook hooks.json '{"hooks":{"userPromptSubmit":[{"type":"command","bash":"true","timeoutSec":5}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "never fires"
|
|
}
|
|
|
|
@test "hook: camelCase in a flat file is a FAIL when the package targets Claude" {
|
|
printf 'targets: [claude, copilot]\n' >> "$PKG/apm.yml"
|
|
write_hook hooks.json '{"hooks":{"userPromptSubmit":[{"type":"command","bash":"true","timeoutSec":5}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "event 'userPromptSubmit' never fires on claude —"
|
|
}
|
|
|
|
@test "hook: camelCase in a flat file is a FAIL when apm.yml declares no targets (every target)" {
|
|
write_hook hooks.json '{"hooks":{"userPromptSubmit":[{"type":"command","bash":"true","timeoutSec":5}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "event 'userPromptSubmit' never fires on claude"
|
|
}
|
|
|
|
@test "hook: targets are read from the package root's apm.yml, and target: all counts as Claude" {
|
|
mkdir -p "$PKG/sub/hooks"
|
|
printf 'name: sub\nversion: 0.1.0\ntarget: copilot\n' > "$PKG/sub/apm.yml"
|
|
printf '%s\n' '{"hooks":{"userPromptSubmit":[{"type":"command","bash":"true","timeoutSec":5}]}}' > "$PKG/sub/hooks/hooks.json"
|
|
run bash "$SCRIPT" "$PKG/sub/hooks/hooks.json"
|
|
assert_success
|
|
|
|
printf 'name: sub\nversion: 0.1.0\ntarget: all\n' > "$PKG/sub/apm.yml"
|
|
run bash "$SCRIPT" "$PKG/sub/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "never fires on claude"
|
|
}
|
|
|
|
@test "hook: a referenced script that does not exist is a FAIL" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"${PLUGIN_ROOT}/.apm/hooks/scripts/missing.sh"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "script '.apm/hooks/scripts/missing.sh' does not exist"
|
|
}
|
|
|
|
@test "hook: a directly-run script without the executable bit is a FAIL; the same script through an interpreter is not" {
|
|
chmod -x "$PKG/.apm/hooks/scripts/check.sh"
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"./scripts/check.sh"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "is run directly but is not executable"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash ${PLUGIN_ROOT}/.apm/hooks/scripts/check.sh"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
}
|
|
|
|
@test "hook: a script path escaping the package is a FAIL" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"${PLUGIN_ROOT}/../outside.sh"}]}]}}'
|
|
touch "$TMPDIR/outside.sh"
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "resolves outside the package"
|
|
}
|
|
|
|
@test "hook: \${CLAUDE_PLUGIN_ROOT} is a SUGGESTION, not a FAIL" {
|
|
write_hook hooks.json '{"hooks":{"SessionStart":[{"matcher":"startup","hooks":[{"type":"command","command":"${CLAUDE_PLUGIN_ROOT}/.apm/hooks/scripts/check.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
assert_output --partial "SUGGESTION uses \${CLAUDE_PLUGIN_ROOT}"
|
|
}
|
|
|
|
@test "hook: a deprecated filename-routing stem is a SUGGESTION" {
|
|
write_hook claude-hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/claude-hooks.json"
|
|
assert_success
|
|
assert_output --partial "deprecated hook filename routing"
|
|
}
|
|
|
|
@test "hook: routing detection matches apm — case-insensitive, hooks-<target>, prefixed and combined stems" {
|
|
local name
|
|
for name in Claude-Hooks.json HOOKS-Copilot.json x-claude-hooks.json claude-codex-hooks.json; do
|
|
write_hook "$name" '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/$name"
|
|
assert_success
|
|
assert_output --partial "deprecated hook filename routing"
|
|
done
|
|
|
|
write_hook claude-hooks-extra.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/claude-hooks-extra.json"
|
|
assert_success
|
|
refute_output --partial "deprecated hook filename routing"
|
|
}
|
|
|
|
@test "hook: a symlinked hook file is a FAIL" {
|
|
write_hook real.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
ln -s real.json "$PKG/.apm/hooks/link.json"
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/link.json"
|
|
assert_failure 1
|
|
assert_output --partial "is a symlink"
|
|
}
|
|
|
|
@test "hook: a hardlinked hook file is not a FAIL (find_hook_files skips symlinks only)" {
|
|
write_hook real.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
ln "$PKG/.apm/hooks/real.json" "$PKG/.apm/hooks/linked.json"
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/linked.json"
|
|
assert_success
|
|
refute_output --partial "hardlink"
|
|
}
|
|
|
|
@test "hook: an absolute script path is a FAIL; an absolute interpreter path is not" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"/usr/local/bin/check.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "is an absolute path"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"/usr/bin/env true","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "absolute path"
|
|
}
|
|
|
|
@test "hook: a bare relative path to a package script is a FAIL; a bare command is not" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":".apm/hooks/scripts/check.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "is a bare relative path"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"npx some-tool --check","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "bare relative path"
|
|
}
|
|
|
|
@test "hook: a file contributing no entries is a FAIL" {
|
|
write_hook hooks.json '{"hooks":{}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "contributes no hook entries"
|
|
}
|
|
|
|
@test "hook: a file whose every event list is empty is a FAIL" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[],"PreToolUse":[]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "every event list is empty"
|
|
}
|
|
|
|
@test "hook: an entry with no handler is a FAIL, whether nested hooks is empty or absent" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"matcher":"x"}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "event 'Stop' entry 0 has no handler"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "event 'Stop' entry 0 has no handler"
|
|
}
|
|
|
|
@test "hook: a non-command handler type (prompt) counts as a handler" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"prompt","prompt":"check","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "no handler"
|
|
}
|
|
|
|
@test "hook: a ./ script after an interpreter is existence-checked, as apm matches it" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash ./scripts/missing.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "script 'scripts/missing.sh' does not exist"
|
|
|
|
chmod -x "$PKG/.apm/hooks/scripts/check.sh"
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash ./scripts/check.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
}
|
|
|
|
@test "hook: a ../ script path is a FAIL — apm reads it as ./ from the hook directory" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash ../scripts/x.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "starts with ../"
|
|
}
|
|
|
|
@test "hook: the whole-token-quoted \${PLUGIN_ROOT} form passes clean" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"\"${PLUGIN_ROOT}/.apm/hooks/scripts/check.sh\"","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
}
|
|
|
|
@test "hook: a split-quoted \"\${PLUGIN_ROOT}\"/path is a FAIL — apm never rewrites it" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"\"${PLUGIN_ROOT}\"/.apm/hooks/scripts/check.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "splits the quote"
|
|
}
|
|
|
|
@test "hook: a script path with a space is a FAIL, quoted or backslash-escaped" {
|
|
printf '#!/usr/bin/env bash\nexit 0\n' > "$PKG/.apm/hooks/scripts/my hook.sh"
|
|
chmod +x "$PKG/.apm/hooks/scripts/my hook.sh"
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"\"${PLUGIN_ROOT}/.apm/hooks/scripts/my hook.sh\"","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "contains a space"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"${PLUGIN_ROOT}/.apm/hooks/scripts/my\\ hook.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "contains a space"
|
|
}
|
|
|
|
@test "hook: a hook file under a package-root hooks/ directory is audited, scripts resolved from the package root" {
|
|
mkdir -p "$PKG/hooks" "$PKG/scripts"
|
|
printf '#!/usr/bin/env bash\nexit 0\n' > "$PKG/scripts/run.sh"
|
|
chmod +x "$PKG/scripts/run.sh"
|
|
printf '%s\n' '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"${PLUGIN_ROOT}/scripts/run.sh","timeout":5}]}]}}' > "$PKG/hooks/hooks.json"
|
|
run bash "$SCRIPT" "$PKG/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
|
|
printf '%s\n' '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"${PLUGIN_ROOT}/scripts/gone.sh","timeout":5}]}]}}' > "$PKG/hooks/hooks.json"
|
|
run bash "$SCRIPT" "$PKG/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "script 'scripts/gone.sh' does not exist"
|
|
}
|
|
|
|
@test "hook: a ./ argument outside command position is at most a SUGGESTION (npx prettier --check ./src)" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"npx prettier --check ./src","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
assert_output --partial "SUGGESTION argument './src' matches apm's ./ script pattern"
|
|
}
|
|
|
|
@test "hook: a printf escape after a script is not a missing-script FAIL (printf '.\n')" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"\"${PLUGIN_ROOT}/.apm/hooks/scripts/check.sh\" && printf '"'"'.\\n'"'"'","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
refute_output --partial "does not exist"
|
|
}
|
|
|
|
@test "hook: a ../ argument outside command position is a SUGGESTION; in command position a FAIL" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"git -C ../sibling status","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
assert_output --partial "SUGGESTION argument '../sibling'"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"python3 ../tool","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "starts with ../"
|
|
}
|
|
|
|
@test "hook: a non-command-position ./ match is still a FAIL with a script extension or when it is a package directory" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"cat ./gone.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "script 'gone.sh' does not exist in the package"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"npx prettier --check ./scripts","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "script 'scripts' exists in the package but is not a regular file"
|
|
}
|
|
|
|
@test "hook: a quoted \${PLUGIN_ROOT} path followed by arguments is not a spaced path (bash -c \"…/bin/tool --config conf.sh\")" {
|
|
mkdir -p "$PKG/bin"
|
|
printf '#!/usr/bin/env bash\nexit 0\n' > "$PKG/bin/tool"
|
|
chmod +x "$PKG/bin/tool"
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash -c \"${PLUGIN_ROOT}/bin/tool --config conf.sh\"","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
}
|
|
|
|
@test "hook: a bare relative or absolute script after an interpreter is a FAIL; as a later argument it is not" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash scripts/check.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "script 'scripts/check.sh' is a bare relative path"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash /opt/x.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "script '/opt/x.sh' is an absolute path"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"/usr/bin/env python3 /opt/x","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "script '/opt/x' is an absolute path"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"echo scripts/check.sh /opt/x.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
}
|
|
|
|
@test "hook: a hook file under apm's deployed output is a FAIL naming the source location" {
|
|
mkdir -p "$PKG/.github/hooks"
|
|
printf '%s\n' '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"true","timeout":5}]}]}}' > "$PKG/.github/hooks/p.json"
|
|
run bash "$SCRIPT" "$PKG/.github/hooks/p.json"
|
|
assert_failure 1
|
|
assert_output --partial "is apm's deployed output (.github/hooks/)"
|
|
assert_output --partial ".apm/hooks/"
|
|
|
|
mkdir -p "$TMPDIR/loose/hooks"
|
|
printf '%s\n' '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"true","timeout":5}]}]}}' > "$TMPDIR/loose/hooks/x.json"
|
|
run bash "$SCRIPT" "$TMPDIR/loose/hooks/x.json"
|
|
assert_failure 1
|
|
assert_output --partial "is no package source"
|
|
}
|
|
|
|
@test "hook: a Claude-plugin layout (hooks/ beside .claude-plugin/plugin.json, no apm.yml) is package source" {
|
|
mkdir -p "$TMPDIR/cplug/.claude-plugin" "$TMPDIR/cplug/hooks" "$TMPDIR/cplug/scripts"
|
|
printf '{"name":"cplug"}\n' > "$TMPDIR/cplug/.claude-plugin/plugin.json"
|
|
printf '#!/usr/bin/env bash\nexit 0\n' > "$TMPDIR/cplug/scripts/ok.sh"
|
|
chmod +x "$TMPDIR/cplug/scripts/ok.sh"
|
|
printf '%s\n' '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"${PLUGIN_ROOT}/scripts/ok.sh","timeout":5}]}]}}' > "$TMPDIR/cplug/hooks/hooks.json"
|
|
run bash "$SCRIPT" "$TMPDIR/cplug/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
|
|
rm -rf "$TMPDIR/cplug/.claude-plugin"
|
|
printf '{"name":"cplug"}\n' > "$TMPDIR/cplug/plugin.json"
|
|
run bash "$SCRIPT" "$TMPDIR/cplug/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "no package source"
|
|
}
|
|
|
|
@test "hook: interpreter options are skipped to reach the script (bash -e, python3 -u)" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash -e scripts/check.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "script 'scripts/check.sh' is a bare relative path"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"python3 -u /opt/hook.py","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "script '/opt/hook.py' is an absolute path"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash -e ./gone.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "script 'gone.sh' does not exist"
|
|
}
|
|
|
|
@test "hook: sh -c checks the first token of its command string; inline code (python3 -c) is not a script" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"sh -c scripts/check.sh","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "script 'scripts/check.sh' is a bare relative path"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"sh -c \"/opt/x.sh --flag\"","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "script '/opt/x.sh' is an absolute path"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"python3 -c \"import sys; sys.exit(0)\"","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash -e \"${PLUGIN_ROOT}/.apm/hooks/scripts/check.sh\"","timeout":5}]}]}}'
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
}
|
|
|
|
@test "hook: an unedited primitive-author hook template is an unfilled-placeholder FAIL" {
|
|
cp "$REPO_ROOT/plugins/kyberforge/.apm/skills/primitive-author/assets/templates/hook.json.template" "$PKG/.apm/hooks/hooks.json"
|
|
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_failure 1
|
|
assert_output --partial "unfilled template placeholder 'FILL_IN_'"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Instructions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test "instruction: description, applyTo and a body pass clean" {
|
|
write_instruction python 'description: Python style rules
|
|
applyTo: "**/*.py"' 'Use type hints on public functions.'
|
|
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
refute_output --partial "SUGGESTION"
|
|
}
|
|
|
|
@test "instruction: missing description is a FAIL" {
|
|
write_instruction python 'applyTo: "**/*.py"' 'Use type hints.'
|
|
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
|
|
assert_failure 1
|
|
assert_output --partial "'description' is missing or empty"
|
|
}
|
|
|
|
@test "instruction: an empty body is a FAIL" {
|
|
write_instruction python 'description: x
|
|
applyTo: "**/*.py"' ''
|
|
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
|
|
assert_failure 1
|
|
assert_output --partial "body is empty"
|
|
}
|
|
|
|
@test "instruction: invalid frontmatter YAML is a FAIL" {
|
|
write_instruction python 'description: [unclosed' 'body'
|
|
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
|
|
assert_failure 1
|
|
assert_output --partial "frontmatter is not valid YAML"
|
|
}
|
|
|
|
@test "instruction: no applyTo is a SUGGESTION (deliberate always-on), not a FAIL" {
|
|
write_instruction general 'description: General rules' 'Be kind.'
|
|
run bash "$SCRIPT" "$PKG/.apm/instructions/general.instructions.md"
|
|
assert_success
|
|
assert_output --partial "SUGGESTION no applyTo"
|
|
}
|
|
|
|
@test "instruction: a YAML-list applyTo and unread keys are SUGGESTIONs" {
|
|
write_instruction python 'description: x
|
|
applyTo:
|
|
- "**/*.py"
|
|
name: python' 'body'
|
|
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
|
|
assert_success
|
|
assert_output --partial "applyTo is a YAML list"
|
|
assert_output --partial "frontmatter key(s) name"
|
|
}
|
|
|
|
@test "instruction: an applyTo that is present but empty is a FAIL" {
|
|
write_instruction empty 'description: x
|
|
applyTo: ""' 'body'
|
|
run bash "$SCRIPT" "$PKG/.apm/instructions/empty.instructions.md"
|
|
assert_failure 1
|
|
assert_output --partial "applyTo is present but empty"
|
|
refute_output --partial "SUGGESTION no applyTo"
|
|
}
|
|
|
|
@test "instruction: an applyTo glob with unbalanced braces is a FAIL" {
|
|
write_instruction broken 'description: x
|
|
applyTo: "**/*.{py"' 'body'
|
|
run bash "$SCRIPT" "$PKG/.apm/instructions/broken.instructions.md"
|
|
assert_failure 1
|
|
assert_output --partial "unbalanced braces or brackets"
|
|
}
|
|
|
|
@test "instruction: a non-string frontmatter key is a clean SUGGESTION, not a crash" {
|
|
write_instruction python 'description: x
|
|
applyTo: "**/*.py"
|
|
1: a
|
|
zeta: b' 'body'
|
|
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
|
|
assert_success
|
|
refute_output --partial "Traceback"
|
|
assert_output --partial "frontmatter key(s) 1, zeta"
|
|
}
|
|
|
|
@test "instruction: a top-level comma list with a brace group passes clean" {
|
|
write_instruction multi 'description: x
|
|
applyTo: "**/*.py, **/*.{pyi,pyx}"' 'body'
|
|
run bash "$SCRIPT" "$PKG/.apm/instructions/multi.instructions.md"
|
|
assert_success
|
|
refute_output --partial "applyTo"
|
|
}
|
|
|
|
@test "instruction: the same stem at the package root is a FAIL" {
|
|
write_instruction python 'description: x
|
|
applyTo: "**/*.py"' 'body'
|
|
cp "$PKG/.apm/instructions/python.instructions.md" "$PKG/python.instructions.md"
|
|
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
|
|
assert_failure 1
|
|
assert_output --partial "also exists at the package root"
|
|
}
|
|
|
|
@test "instruction: a hardlinked instruction file is a FAIL" {
|
|
write_instruction python 'description: x
|
|
applyTo: "**/*.py"' 'body'
|
|
ln "$PKG/.apm/instructions/python.instructions.md" "$TMPDIR/python.instructions.md"
|
|
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
|
|
assert_failure 1
|
|
assert_output --partial "is a hardlink"
|
|
}
|
|
|
|
@test "instruction: an unedited primitive-author template is an unfilled-placeholder FAIL" {
|
|
cp "$REPO_ROOT/plugins/kyberforge/.apm/skills/primitive-author/assets/templates/name.instructions.md.template" "$PKG/.apm/instructions/name.instructions.md"
|
|
run bash "$SCRIPT" "$PKG/.apm/instructions/name.instructions.md"
|
|
assert_failure 1
|
|
assert_output --partial "unfilled template placeholder 'FILL IN'"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Prompts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test "prompt: declared and used inputs with a plain description pass clean" {
|
|
write_prompt review-pr 'description: Review a pull request with gitea-prs and factory-audit, then summarize.
|
|
input:
|
|
- pr_number: "The PR to review"' 'Review PR ${input:pr_number} with gitea-prs.'
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_success
|
|
refute_output --partial "FAIL"
|
|
refute_output --partial "SUGGESTION"
|
|
}
|
|
|
|
@test "prompt: missing description is a FAIL" {
|
|
write_prompt review-pr 'model: sonnet' 'Review the PR.'
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_failure 1
|
|
assert_output --partial "'description' is missing or empty"
|
|
}
|
|
|
|
@test "prompt: the upstream docs' - name: x / description: form is a FAIL" {
|
|
write_prompt review-pr 'description: Review a PR.
|
|
input:
|
|
- name: pr_number
|
|
description: The PR' 'Review ${input:pr_number}.'
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_failure 1
|
|
assert_output --partial "yields arguments [name, description]"
|
|
}
|
|
|
|
@test "prompt: an invalid input name is a FAIL" {
|
|
write_prompt review-pr 'description: Review a PR.
|
|
input:
|
|
- 1pr: "The PR"' 'Review.'
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_failure 1
|
|
assert_output --partial "input name '1pr' does not match"
|
|
}
|
|
|
|
@test "prompt: an undeclared \${input:x} and an unused input are both FAILs" {
|
|
write_prompt review-pr 'description: Review a PR.
|
|
input:
|
|
- pr_number: "The PR"' 'Review ${input:branch}.'
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_failure 1
|
|
assert_output --partial "input: does not declare 'branch'"
|
|
assert_output --partial "input 'pr_number' is declared but the body never uses"
|
|
}
|
|
|
|
@test "prompt: \${input:x} with no input: declared is a FAIL" {
|
|
write_prompt review-pr 'description: Review a PR.' 'Review ${input:pr_number}.'
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_failure 1
|
|
assert_output --partial "but no input: is declared"
|
|
}
|
|
|
|
@test "prompt: a trigger clause, an over-long description, dropped keys and camelCase aliases are SUGGESTIONs" {
|
|
local long
|
|
long="Use when the user wants a PR reviewed. $(printf 'x%.0s' $(seq 1 240))"
|
|
write_prompt review-pr "description: $long
|
|
mode: agent
|
|
allowedTools: Bash" 'Review the PR.'
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_success
|
|
assert_output --partial "'Use when' trigger clause"
|
|
assert_output --partial "characters (> 250)"
|
|
assert_output --partial "frontmatter key(s) mode are dropped on Claude"
|
|
assert_output --partial "'allowedTools' — use the kebab-case spelling"
|
|
}
|
|
|
|
@test "prompt: input: forms other than the object form are FAILs (primitive-author Must 3)" {
|
|
write_prompt review-pr 'description: Review a PR.
|
|
input: [pr_number]' 'Review ${input:pr_number}.'
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_failure 1
|
|
assert_output --partial "input entry 'pr_number' is a bare name"
|
|
|
|
write_prompt review-pr 'description: Review a PR.
|
|
input: pr_number' 'Review ${input:pr_number}.'
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_failure 1
|
|
assert_output --partial "input: is a bare name"
|
|
|
|
write_prompt review-pr 'description: Review a PR.
|
|
input:
|
|
pr_number: The PR' 'Review ${input:pr_number}.'
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_failure 1
|
|
assert_output --partial "input: is a map"
|
|
}
|
|
|
|
@test "prompt: a Not X -> Y boundary clause in the description is a SUGGESTION" {
|
|
write_prompt review-pr 'description: Review the PR with gitea-prs. Not fixing it -> skill-author.' 'Review the PR with gitea-prs.'
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_success
|
|
assert_output --partial "'Not X -> Y' boundary clause"
|
|
}
|
|
|
|
@test "prompt: a non-string frontmatter key is a clean SUGGESTION, not a crash" {
|
|
write_prompt review-pr 'description: Review a PR.
|
|
1: a
|
|
zeta: b' 'Review the PR.'
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_success
|
|
refute_output --partial "Traceback"
|
|
assert_output --partial "frontmatter key(s) 1, zeta"
|
|
}
|
|
|
|
@test "prompt: a *.prompt.md under an agents/ directory takes the prompt flow, not the agent flow" {
|
|
mkdir -p "$PKG/.apm/agents"
|
|
printf -- '---\ndescription: x\n---\n\nbody\n' > "$PKG/.apm/agents/x.prompt.md"
|
|
run bash "$SCRIPT" "$PKG/.apm/agents/x.prompt.md"
|
|
assert_failure 1
|
|
assert_output --partial "is not directly in a .apm/prompts/ directory"
|
|
refute_output --partial "counterpart"
|
|
}
|
|
|
|
@test "prompt: argument-hint alongside input: is a SUGGESTION" {
|
|
write_prompt review-pr 'description: Review a PR.
|
|
argument-hint: <pr>
|
|
input:
|
|
- pr_number: "The PR"' 'Review ${input:pr_number}.'
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_success
|
|
assert_output --partial "argument-hint is set alongside input:"
|
|
}
|
|
|
|
@test "prompt: no procedure heuristic — a long, stepped body is not a script finding" {
|
|
write_prompt review-pr 'description: Review a PR.' "## Step 1
|
|
$(printf 'line\n%.0s' $(seq 1 80))
|
|
## Gotchas
|
|
- x"
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_success
|
|
refute_output --partial "SUGGESTION"
|
|
}
|
|
|
|
@test "prompt: a hardlinked prompt file is a FAIL" {
|
|
write_prompt review-pr 'description: Review a pull request with gitea-prs.' 'Review the PR with gitea-prs.'
|
|
ln "$PKG/.apm/prompts/review-pr.prompt.md" "$TMPDIR/review-pr.prompt.md"
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_failure 1
|
|
assert_output --partial "is a hardlink"
|
|
}
|
|
|
|
@test "prompt: an unedited primitive-author template is an unfilled-placeholder FAIL" {
|
|
cp "$REPO_ROOT/plugins/kyberforge/.apm/skills/primitive-author/assets/templates/name.prompt.md.template" "$PKG/.apm/prompts/name.prompt.md"
|
|
run bash "$SCRIPT" "$PKG/.apm/prompts/name.prompt.md"
|
|
assert_failure 1
|
|
assert_output --partial "unfilled template placeholder 'FILL IN'"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Never ran (exit 2)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test "exit 2: a PATH with no python3 names the missing dependency in primitive mode" {
|
|
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
|
local emptybin="$TMPDIR/emptybin" cmd bash_bin
|
|
mkdir -p "$emptybin"
|
|
for cmd in cat sed; do
|
|
ln -s "$(command -v "$cmd")" "$emptybin/$cmd"
|
|
done
|
|
bash_bin="$(command -v bash)"
|
|
run env -i PATH="$emptybin" HOME="$HOME" "$bash_bin" "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
|
assert_equal "$status" 2
|
|
assert_output --partial "python3 is required"
|
|
}
|
|
|
|
@test "exit 2: a python3 that cannot import PyYAML names the missing library in primitive mode" {
|
|
write_prompt review-pr 'description: Review a PR.' 'Review the PR.'
|
|
local noyaml="$TMPDIR/noyaml"
|
|
mkdir -p "$noyaml"
|
|
# Shadow PyYAML: PYTHONPATH precedes site-packages, so `import yaml` fails.
|
|
printf 'raise ImportError("PyYAML shadowed by the test")\n' > "$noyaml/yaml.py"
|
|
PYTHONPATH="$noyaml" run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
|
assert_equal "$status" 2
|
|
assert_output --partial "PyYAML is required"
|
|
}
|