fix(kyberforge): resolve PR #144 review and audit round 1
- factory-audit: no-op hooks, ./ after interpreters, split-quote and
spaced ${PLUGIN_ROOT} paths, camelCase events in Claude-targeted flat
files, case-insensitive routing stems, and non-string YAML keys are
now caught; input: forms and prompt boundary clauses align with
primitive-author; bats 347 -> 367
- primitive-author: routing forms, quoting guidance, install exit on
hidden Unicode, argument-hint exception
- forge: drop duplicated gotcha, fit description and body budgets (#143)
- skill-author: primitive-author boundary, Claude-only env vars
- hook: exit unless CLAUDE_PROJECT_DIR is set, so Copilot/Codex never
run apm update; ADR-0019 correction, ADR-0025 amendment, docs fixes
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:
@@ -57,7 +57,7 @@ teardown() {
|
||||
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
|
||||
assert_failure 1
|
||||
assert_output --partial "is not directly in a .apm/instructions/ directory"
|
||||
refute_output --partial "counterpart"
|
||||
}
|
||||
@@ -77,49 +77,79 @@ teardown() {
|
||||
@test "hook: invalid JSON is a FAIL" {
|
||||
write_hook hooks.json '{"hooks": {'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure
|
||||
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
|
||||
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
|
||||
assert_failure 1
|
||||
assert_output --partial "naked settings-slice shape"
|
||||
}
|
||||
|
||||
@test "hook: an all-lowercase event is a FAIL" {
|
||||
write_hook hooks.json '{"hooks":{"stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure
|
||||
assert_failure 1
|
||||
assert_output --partial "event 'stop' is all-lowercase"
|
||||
}
|
||||
|
||||
@test "hook: camelCase userPromptSubmit in a Claude-shaped file is a FAIL; 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
|
||||
assert_failure 1
|
||||
assert_output --partial "event 'userPromptSubmit' is camelCase"
|
||||
refute_output --partial "event 'sessionStart'"
|
||||
}
|
||||
|
||||
@test "hook: a flat Copilot-shaped file may use camelCase events" {
|
||||
@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 "camelCase"
|
||||
}
|
||||
|
||||
@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' is camelCase and the package's apm.yml targets 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' is camelCase"
|
||||
}
|
||||
|
||||
@test "hook: targets are read from the nearest apm.yml walking up, 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 "is camelCase"
|
||||
}
|
||||
|
||||
@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
|
||||
assert_failure 1
|
||||
assert_output --partial "script '.apm/hooks/scripts/missing.sh' does not exist"
|
||||
}
|
||||
|
||||
@@ -127,7 +157,7 @@ teardown() {
|
||||
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
|
||||
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"}]}]}}'
|
||||
@@ -139,7 +169,7 @@ teardown() {
|
||||
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
|
||||
assert_failure 1
|
||||
assert_output --partial "resolves outside the package"
|
||||
}
|
||||
|
||||
@@ -157,11 +187,26 @@ teardown() {
|
||||
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
|
||||
assert_failure 1
|
||||
assert_output --partial "is a symlink"
|
||||
}
|
||||
|
||||
@@ -176,7 +221,7 @@ teardown() {
|
||||
@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
|
||||
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}]}]}}'
|
||||
@@ -188,7 +233,7 @@ teardown() {
|
||||
@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
|
||||
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}]}]}}'
|
||||
@@ -200,10 +245,99 @@ teardown() {
|
||||
@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
|
||||
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"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Instructions
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -220,7 +354,7 @@ applyTo: "**/*.py"' 'Use type hints on public functions.'
|
||||
@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
|
||||
assert_failure 1
|
||||
assert_output --partial "'description' is missing or empty"
|
||||
}
|
||||
|
||||
@@ -228,14 +362,14 @@ applyTo: "**/*.py"' 'Use type hints on public functions.'
|
||||
write_instruction python 'description: x
|
||||
applyTo: "**/*.py"' ''
|
||||
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
|
||||
assert_failure
|
||||
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
|
||||
assert_failure 1
|
||||
assert_output --partial "frontmatter is not valid YAML"
|
||||
}
|
||||
|
||||
@@ -261,7 +395,7 @@ name: python' 'body'
|
||||
write_instruction empty 'description: x
|
||||
applyTo: ""' 'body'
|
||||
run bash "$SCRIPT" "$PKG/.apm/instructions/empty.instructions.md"
|
||||
assert_failure
|
||||
assert_failure 1
|
||||
assert_output --partial "applyTo is present but empty"
|
||||
refute_output --partial "SUGGESTION no applyTo"
|
||||
}
|
||||
@@ -270,10 +404,21 @@ applyTo: ""' 'body'
|
||||
write_instruction broken 'description: x
|
||||
applyTo: "**/*.{py"' 'body'
|
||||
run bash "$SCRIPT" "$PKG/.apm/instructions/broken.instructions.md"
|
||||
assert_failure
|
||||
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'
|
||||
@@ -287,7 +432,7 @@ applyTo: "**/*.py, **/*.{pyi,pyx}"' 'body'
|
||||
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
|
||||
assert_failure 1
|
||||
assert_output --partial "also exists at the package root"
|
||||
}
|
||||
|
||||
@@ -296,7 +441,7 @@ applyTo: "**/*.py"' 'body'
|
||||
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
|
||||
assert_failure 1
|
||||
assert_output --partial "is a hardlink"
|
||||
}
|
||||
|
||||
@@ -317,7 +462,7 @@ input:
|
||||
@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
|
||||
assert_failure 1
|
||||
assert_output --partial "'description' is missing or empty"
|
||||
}
|
||||
|
||||
@@ -327,23 +472,25 @@ input:
|
||||
- name: pr_number
|
||||
description: The PR' 'Review ${input:pr_number}.'
|
||||
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
||||
assert_failure
|
||||
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]' 'Review.'
|
||||
input:
|
||||
- 1pr: "The PR"' 'Review.'
|
||||
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
||||
assert_failure
|
||||
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]' 'Review ${input:branch}.'
|
||||
input:
|
||||
- pr_number: "The PR"' 'Review ${input:branch}.'
|
||||
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
|
||||
assert_failure
|
||||
assert_failure 1
|
||||
assert_output --partial "input: does not declare 'branch'"
|
||||
assert_output --partial "input 'pr_number' is declared but the body never uses"
|
||||
}
|
||||
@@ -351,7 +498,7 @@ input: [pr_number]' 'Review ${input:branch}.'
|
||||
@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
|
||||
assert_failure 1
|
||||
assert_output --partial "but no input: is declared"
|
||||
}
|
||||
|
||||
@@ -369,10 +516,58 @@ allowedTools: Bash" 'Review the PR.'
|
||||
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]' 'Review ${input:pr_number}.'
|
||||
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:"
|
||||
@@ -392,6 +587,34 @@ $(printf 'line\n%.0s' $(seq 1 80))
|
||||
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
|
||||
assert_failure 1
|
||||
assert_output --partial "is a hardlink"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user