Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 56 additions & 6 deletions tests/unit/test-pr-creation-log.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,44 @@ else
fail "pr-creation-log: hook should use external script with tool_response schema"
fi

# Select this feature's entry by its command, never by matcher.
# `.hooks.PostToolUse[] | select(.matcher == "Bash")` emits one output per
# matching entry and `jq -e` derives its exit code from the LAST one, so a
# second Bash-matcher PostToolUse hook would silently decide the verdict — and
# for the negated legacy checks it would decide it as a PASS, masking exactly
# the regression they exist to catch. Collect first, then assert on the whole
# set: `length == 1` pins the entry and `all(...)` is the correct collapse for
# a negated condition (`any(...)` would still pass on a regressed first entry).
# Selection walks the PARENT PostToolUse entries (not the inner hooks) so the
# matcher stays visible: this feature must remain bound to matcher "Bash".
_pr_entries='[.hooks.PostToolUse[]?
| select(any(.hooks[]?; (.command? // "") | contains("pr-creation-log/log-pr.sh")))]'

_pr_modern_hook_ok() { # <settings-file>
jq -e "$_pr_entries
| length == 1
and all(.matcher == \"Bash\"
and (.hooks | length == 1)
and all(.hooks[]; .if == \"Bash(gh pr create *)\" and .async == true))" \
"$1" >/dev/null 2>&1
}

_pr_legacy_hook_ok() { # <settings-file>
jq -e "$_pr_entries
| length == 1
and all(.matcher == \"Bash\"
and (.hooks | length == 1)
and all(.hooks[]; (has(\"if\") | not) and (has(\"async\") | not)))" \
"$1" >/dev/null 2>&1
}

build_settings_json \
"$PROJECT_DIR/config/settings-base.json" \
"$PROJECT_DIR/config/permissions.json" \
"$_pr_settings" \
"$_pr_hooks" >/dev/null

if jq -e '.hooks.PostToolUse[] | select(.matcher == "Bash") | .hooks[0].if == "Bash(gh pr create *)"' "$_pr_settings" >/dev/null 2>&1 \
&& jq -e '.hooks.PostToolUse[] | select(.matcher == "Bash") | .hooks[0].async == true' "$_pr_settings" >/dev/null 2>&1; then
if _pr_modern_hook_ok "$_pr_settings"; then
pass "pr-creation-log: merged settings keep if condition and async execution"
else
fail "pr-creation-log: merged settings should keep if condition and async execution"
Expand All @@ -70,8 +100,7 @@ _CLAUDE_SEMVER_CACHE=""
_CLAUDE_SEMVER_CACHE_SET=false
PATH="$_pr_tmp/claude-current-bin:$PATH" build_settings_file "$_pr_supported_settings" >/dev/null

if jq -e '.hooks.PostToolUse[] | select(.matcher == "Bash") | .hooks[0].if == "Bash(gh pr create *)"' "$_pr_supported_settings" >/dev/null 2>&1 \
&& jq -e '.hooks.PostToolUse[] | select(.matcher == "Bash") | .hooks[0].async == true' "$_pr_supported_settings" >/dev/null 2>&1; then
if _pr_modern_hook_ok "$_pr_supported_settings"; then
pass "pr-creation-log: supported Claude Code gets if/async hook"
else
fail "pr-creation-log: supported Claude Code should get if/async hook"
Expand All @@ -90,13 +119,34 @@ _CLAUDE_SEMVER_CACHE=""
_CLAUDE_SEMVER_CACHE_SET=false
PATH="$_pr_tmp/claude-legacy-bin:$PATH" build_settings_file "$_pr_legacy_settings" >/dev/null

if jq -e '.hooks.PostToolUse[] | select(.matcher == "Bash") | .hooks[0] | has("if") | not' "$_pr_legacy_settings" >/dev/null 2>&1 \
&& jq -e '.hooks.PostToolUse[] | select(.matcher == "Bash") | .hooks[0] | has("async") | not' "$_pr_legacy_settings" >/dev/null 2>&1; then
if _pr_legacy_hook_ok "$_pr_legacy_settings"; then
pass "pr-creation-log: legacy Claude Code falls back to legacy hook fragment"
else
fail "pr-creation-log: legacy Claude Code should fall back to legacy hook fragment"
fi

# The arity guard itself. Append a second Bash-matcher PostToolUse entry and
# regress the pr-creation-log one to the modern shape: the legacy assertion
# must now fail. The old `select(.matcher == "Bash") | .hooks[0] | has("if") |
# not` filter returns 0 here (the appended entry is the last output), and so
# does an `any(...)` rewrite — only selecting by command and collapsing with
# `all(...)` reports the regression.
_pr_arity_settings="$_pr_tmp/settings-two-bash.json"
jq '.hooks.PostToolUse = [
{"matcher":"Bash","hooks":[{"type":"command",
"command":"__HOME__/.claude/hooks/pr-creation-log/log-pr.sh",
"if":"Bash(gh pr create *)","async":true}]},
{"matcher":"Bash","hooks":[{"type":"command",
"command":"/somewhere/future-hook.sh"}]}
]' "$_pr_legacy_settings" > "$_pr_arity_settings"

if ! _pr_legacy_hook_ok "$_pr_arity_settings" \
&& _pr_modern_hook_ok "$_pr_arity_settings"; then
pass "pr-creation-log: a second Bash-matcher hook cannot decide the verdict"
else
fail "pr-creation-log: assertions must select this feature's entry by command"
fi

_pr_script="$_pr_tmp/pr-hook.sh"
_pr_input="$_pr_tmp/input.json"
_pr_out="$_pr_tmp/out.json"
Expand Down
Loading