claude-ketchup 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +544 -0
- package/bin/cli.ts +6 -0
- package/bin/postinstall.ts +5 -0
- package/bin/preuninstall.ts +5 -0
- package/commands/ketchup.md +107 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +7 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/bin/postinstall.d.ts +3 -0
- package/dist/bin/postinstall.d.ts.map +1 -0
- package/dist/bin/postinstall.js +6 -0
- package/dist/bin/postinstall.js.map +1 -0
- package/dist/bin/preuninstall.d.ts +3 -0
- package/dist/bin/preuninstall.d.ts.map +1 -0
- package/dist/bin/preuninstall.js +6 -0
- package/dist/bin/preuninstall.js.map +1 -0
- package/dist/scripts/pre-tool-use.d.ts +3 -0
- package/dist/scripts/pre-tool-use.d.ts.map +1 -0
- package/dist/scripts/pre-tool-use.js +43 -0
- package/dist/scripts/pre-tool-use.js.map +1 -0
- package/dist/scripts/session-start.d.ts +3 -0
- package/dist/scripts/session-start.d.ts.map +1 -0
- package/dist/scripts/session-start.js +42 -0
- package/dist/scripts/session-start.js.map +1 -0
- package/dist/scripts/user-prompt-submit.d.ts +3 -0
- package/dist/scripts/user-prompt-submit.d.ts.map +1 -0
- package/dist/scripts/user-prompt-submit.js +43 -0
- package/dist/scripts/user-prompt-submit.js.map +1 -0
- package/dist/src/clean-logs.d.ts +6 -0
- package/dist/src/clean-logs.d.ts.map +1 -0
- package/dist/src/clean-logs.js +38 -0
- package/dist/src/clean-logs.js.map +1 -0
- package/dist/src/clean-logs.test.d.ts +2 -0
- package/dist/src/clean-logs.test.d.ts.map +1 -0
- package/dist/src/clean-logs.test.js +101 -0
- package/dist/src/clean-logs.test.js.map +1 -0
- package/dist/src/cli/cli.d.ts +3 -0
- package/dist/src/cli/cli.d.ts.map +1 -0
- package/dist/src/cli/cli.js +24 -0
- package/dist/src/cli/cli.js.map +1 -0
- package/dist/src/cli/cli.test.d.ts +2 -0
- package/dist/src/cli/cli.test.d.ts.map +1 -0
- package/dist/src/cli/cli.test.js +20 -0
- package/dist/src/cli/cli.test.js.map +1 -0
- package/dist/src/cli/doctor.d.ts +7 -0
- package/dist/src/cli/doctor.d.ts.map +1 -0
- package/dist/src/cli/doctor.js +52 -0
- package/dist/src/cli/doctor.js.map +1 -0
- package/dist/src/cli/doctor.test.d.ts +2 -0
- package/dist/src/cli/doctor.test.d.ts.map +1 -0
- package/dist/src/cli/doctor.test.js +77 -0
- package/dist/src/cli/doctor.test.js.map +1 -0
- package/dist/src/cli/repair.d.ts +7 -0
- package/dist/src/cli/repair.d.ts.map +1 -0
- package/dist/src/cli/repair.js +67 -0
- package/dist/src/cli/repair.js.map +1 -0
- package/dist/src/cli/repair.test.d.ts +2 -0
- package/dist/src/cli/repair.test.d.ts.map +1 -0
- package/dist/src/cli/repair.test.js +72 -0
- package/dist/src/cli/repair.test.js.map +1 -0
- package/dist/src/cli/skills.d.ts +11 -0
- package/dist/src/cli/skills.d.ts.map +1 -0
- package/dist/src/cli/skills.js +53 -0
- package/dist/src/cli/skills.js.map +1 -0
- package/dist/src/cli/skills.test.d.ts +2 -0
- package/dist/src/cli/skills.test.d.ts.map +1 -0
- package/dist/src/cli/skills.test.js +89 -0
- package/dist/src/cli/skills.test.js.map +1 -0
- package/dist/src/cli/status.d.ts +10 -0
- package/dist/src/cli/status.d.ts.map +1 -0
- package/dist/src/cli/status.js +63 -0
- package/dist/src/cli/status.js.map +1 -0
- package/dist/src/cli/status.test.d.ts +2 -0
- package/dist/src/cli/status.test.d.ts.map +1 -0
- package/dist/src/cli/status.test.js +70 -0
- package/dist/src/cli/status.test.js.map +1 -0
- package/dist/src/clue-collector.d.ts +23 -0
- package/dist/src/clue-collector.d.ts.map +1 -0
- package/dist/src/clue-collector.js +226 -0
- package/dist/src/clue-collector.js.map +1 -0
- package/dist/src/clue-collector.test.d.ts +2 -0
- package/dist/src/clue-collector.test.d.ts.map +1 -0
- package/dist/src/clue-collector.test.js +213 -0
- package/dist/src/clue-collector.test.js.map +1 -0
- package/dist/src/debug-logger.d.ts +2 -0
- package/dist/src/debug-logger.d.ts.map +1 -0
- package/dist/src/debug-logger.js +23 -0
- package/dist/src/debug-logger.js.map +1 -0
- package/dist/src/debug-logger.test.d.ts +2 -0
- package/dist/src/debug-logger.test.d.ts.map +1 -0
- package/dist/src/debug-logger.test.js +63 -0
- package/dist/src/debug-logger.test.js.map +1 -0
- package/dist/src/deny-list.d.ts +3 -0
- package/dist/src/deny-list.d.ts.map +1 -0
- package/dist/src/deny-list.js +62 -0
- package/dist/src/deny-list.js.map +1 -0
- package/dist/src/deny-list.test.d.ts +2 -0
- package/dist/src/deny-list.test.d.ts.map +1 -0
- package/dist/src/deny-list.test.js +93 -0
- package/dist/src/deny-list.test.js.map +1 -0
- package/dist/src/e2e.test.d.ts +2 -0
- package/dist/src/e2e.test.d.ts.map +1 -0
- package/dist/src/e2e.test.js +88 -0
- package/dist/src/e2e.test.js.map +1 -0
- package/dist/src/gitignore-manager.d.ts +2 -0
- package/dist/src/gitignore-manager.d.ts.map +1 -0
- package/dist/src/gitignore-manager.js +45 -0
- package/dist/src/gitignore-manager.js.map +1 -0
- package/dist/src/gitignore-manager.test.d.ts +2 -0
- package/dist/src/gitignore-manager.test.d.ts.map +1 -0
- package/dist/src/gitignore-manager.test.js +70 -0
- package/dist/src/gitignore-manager.test.js.map +1 -0
- package/dist/src/hook-state.d.ts +43 -0
- package/dist/src/hook-state.d.ts.map +1 -0
- package/dist/src/hook-state.js +124 -0
- package/dist/src/hook-state.js.map +1 -0
- package/dist/src/hook-state.test.d.ts +2 -0
- package/dist/src/hook-state.test.d.ts.map +1 -0
- package/dist/src/hook-state.test.js +190 -0
- package/dist/src/hook-state.test.js.map +1 -0
- package/dist/src/hooks/auto-continue.d.ts +9 -0
- package/dist/src/hooks/auto-continue.d.ts.map +1 -0
- package/dist/src/hooks/auto-continue.js +56 -0
- package/dist/src/hooks/auto-continue.js.map +1 -0
- package/dist/src/hooks/auto-continue.test.d.ts +2 -0
- package/dist/src/hooks/auto-continue.test.d.ts.map +1 -0
- package/dist/src/hooks/auto-continue.test.js +141 -0
- package/dist/src/hooks/auto-continue.test.js.map +1 -0
- package/dist/src/hooks/pre-tool-use.d.ts +8 -0
- package/dist/src/hooks/pre-tool-use.d.ts.map +1 -0
- package/dist/src/hooks/pre-tool-use.js +19 -0
- package/dist/src/hooks/pre-tool-use.js.map +1 -0
- package/dist/src/hooks/pre-tool-use.test.d.ts +2 -0
- package/dist/src/hooks/pre-tool-use.test.d.ts.map +1 -0
- package/dist/src/hooks/pre-tool-use.test.js +84 -0
- package/dist/src/hooks/pre-tool-use.test.js.map +1 -0
- package/dist/src/hooks/session-start.d.ts +6 -0
- package/dist/src/hooks/session-start.d.ts.map +1 -0
- package/dist/src/hooks/session-start.js +49 -0
- package/dist/src/hooks/session-start.js.map +1 -0
- package/dist/src/hooks/session-start.test.d.ts +2 -0
- package/dist/src/hooks/session-start.test.d.ts.map +1 -0
- package/dist/src/hooks/session-start.test.js +96 -0
- package/dist/src/hooks/session-start.test.js.map +1 -0
- package/dist/src/hooks/user-prompt-submit.d.ts +6 -0
- package/dist/src/hooks/user-prompt-submit.d.ts.map +1 -0
- package/dist/src/hooks/user-prompt-submit.js +54 -0
- package/dist/src/hooks/user-prompt-submit.js.map +1 -0
- package/dist/src/hooks/user-prompt-submit.test.d.ts +2 -0
- package/dist/src/hooks/user-prompt-submit.test.d.ts.map +1 -0
- package/dist/src/hooks/user-prompt-submit.test.js +92 -0
- package/dist/src/hooks/user-prompt-submit.test.js.map +1 -0
- package/dist/src/hooks/validate-commit.d.ts +12 -0
- package/dist/src/hooks/validate-commit.d.ts.map +1 -0
- package/dist/src/hooks/validate-commit.js +58 -0
- package/dist/src/hooks/validate-commit.js.map +1 -0
- package/dist/src/hooks/validate-commit.test.d.ts +2 -0
- package/dist/src/hooks/validate-commit.test.d.ts.map +1 -0
- package/dist/src/hooks/validate-commit.test.js +150 -0
- package/dist/src/hooks/validate-commit.test.js.map +1 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/linker.d.ts +6 -0
- package/dist/src/linker.d.ts.map +1 -0
- package/dist/src/linker.js +78 -0
- package/dist/src/linker.js.map +1 -0
- package/dist/src/linker.test.d.ts +2 -0
- package/dist/src/linker.test.d.ts.map +1 -0
- package/dist/src/linker.test.js +192 -0
- package/dist/src/linker.test.js.map +1 -0
- package/dist/src/logger.d.ts +21 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +117 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/logger.test.d.ts +2 -0
- package/dist/src/logger.test.d.ts.map +1 -0
- package/dist/src/logger.test.js +159 -0
- package/dist/src/logger.test.js.map +1 -0
- package/dist/src/postinstall.d.ts +7 -0
- package/dist/src/postinstall.d.ts.map +1 -0
- package/dist/src/postinstall.js +81 -0
- package/dist/src/postinstall.js.map +1 -0
- package/dist/src/postinstall.test.d.ts +2 -0
- package/dist/src/postinstall.test.d.ts.map +1 -0
- package/dist/src/postinstall.test.js +125 -0
- package/dist/src/postinstall.test.js.map +1 -0
- package/dist/src/preuninstall.d.ts +2 -0
- package/dist/src/preuninstall.d.ts.map +1 -0
- package/dist/src/preuninstall.js +62 -0
- package/dist/src/preuninstall.js.map +1 -0
- package/dist/src/preuninstall.test.d.ts +2 -0
- package/dist/src/preuninstall.test.d.ts.map +1 -0
- package/dist/src/preuninstall.test.js +97 -0
- package/dist/src/preuninstall.test.js.map +1 -0
- package/dist/src/root-finder.d.ts +2 -0
- package/dist/src/root-finder.d.ts.map +1 -0
- package/dist/src/root-finder.js +71 -0
- package/dist/src/root-finder.js.map +1 -0
- package/dist/src/root-finder.test.d.ts +2 -0
- package/dist/src/root-finder.test.d.ts.map +1 -0
- package/dist/src/root-finder.test.js +111 -0
- package/dist/src/root-finder.test.js.map +1 -0
- package/dist/src/settings-merger.d.ts +2 -0
- package/dist/src/settings-merger.d.ts.map +1 -0
- package/dist/src/settings-merger.js +136 -0
- package/dist/src/settings-merger.js.map +1 -0
- package/dist/src/settings-merger.test.d.ts +2 -0
- package/dist/src/settings-merger.test.d.ts.map +1 -0
- package/dist/src/settings-merger.test.js +387 -0
- package/dist/src/settings-merger.test.js.map +1 -0
- package/dist/src/skills-loader.d.ts +14 -0
- package/dist/src/skills-loader.d.ts.map +1 -0
- package/dist/src/skills-loader.js +90 -0
- package/dist/src/skills-loader.js.map +1 -0
- package/dist/src/skills-loader.test.d.ts +2 -0
- package/dist/src/skills-loader.test.d.ts.map +1 -0
- package/dist/src/skills-loader.test.js +222 -0
- package/dist/src/skills-loader.test.js.map +1 -0
- package/dist/src/state-manager.d.ts +5 -0
- package/dist/src/state-manager.d.ts.map +1 -0
- package/dist/src/state-manager.js +55 -0
- package/dist/src/state-manager.js.map +1 -0
- package/dist/src/state-manager.test.d.ts +2 -0
- package/dist/src/state-manager.test.d.ts.map +1 -0
- package/dist/src/state-manager.test.js +85 -0
- package/dist/src/state-manager.test.js.map +1 -0
- package/dist/src/subagent-classifier.d.ts +4 -0
- package/dist/src/subagent-classifier.d.ts.map +1 -0
- package/dist/src/subagent-classifier.js +53 -0
- package/dist/src/subagent-classifier.js.map +1 -0
- package/dist/src/subagent-classifier.test.d.ts +2 -0
- package/dist/src/subagent-classifier.test.d.ts.map +1 -0
- package/dist/src/subagent-classifier.test.js +88 -0
- package/dist/src/subagent-classifier.test.js.map +1 -0
- package/package.json +59 -0
- package/scripts/pre-tool-use.ts +10 -0
- package/scripts/session-start.ts +9 -0
- package/scripts/tail-logs.sh +17 -0
- package/scripts/test-hooks.sh +910 -0
- package/scripts/user-prompt-submit.ts +10 -0
- package/skills/ketchup.enforced.md +23 -0
- package/templates/settings.json +57 -0
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
6
|
+
TEMP_BASE=$(mktemp -d)
|
|
7
|
+
trap "rm -rf $TEMP_BASE" EXIT
|
|
8
|
+
|
|
9
|
+
RED='\033[0;31m'
|
|
10
|
+
GREEN='\033[0;32m'
|
|
11
|
+
YELLOW='\033[0;33m'
|
|
12
|
+
NC='\033[0m'
|
|
13
|
+
|
|
14
|
+
PASSED=0
|
|
15
|
+
FAILED=0
|
|
16
|
+
|
|
17
|
+
pass() { echo -e "${GREEN}✓${NC} $1"; ((PASSED++)) || true; }
|
|
18
|
+
fail() { echo -e "${RED}✗${NC} $1: $2"; ((FAILED++)) || true; }
|
|
19
|
+
skip() { echo -e "${YELLOW}○${NC} $1 (skipped)"; }
|
|
20
|
+
|
|
21
|
+
#-----------------------------------------------------------
|
|
22
|
+
# Test: auto-continue respects mode=off (no transcript needed)
|
|
23
|
+
#-----------------------------------------------------------
|
|
24
|
+
test_autocontinue_mode_off() {
|
|
25
|
+
local name="auto-continue respects mode=off"
|
|
26
|
+
|
|
27
|
+
# Backup current state
|
|
28
|
+
local state_file="$PROJECT_ROOT/.claude.hooks.json"
|
|
29
|
+
local backup=""
|
|
30
|
+
if [[ -f "$state_file" ]]; then
|
|
31
|
+
backup=$(cat "$state_file")
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Set mode to off
|
|
35
|
+
echo '{"autoContinue":{"mode":"off"}}' > "$state_file"
|
|
36
|
+
|
|
37
|
+
local input='{"session_id":"test-off","permission_mode":"code"}'
|
|
38
|
+
|
|
39
|
+
cd "$PROJECT_ROOT"
|
|
40
|
+
local output
|
|
41
|
+
output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null) || true
|
|
42
|
+
|
|
43
|
+
# Restore state
|
|
44
|
+
if [[ -n "$backup" ]]; then
|
|
45
|
+
echo "$backup" > "$state_file"
|
|
46
|
+
else
|
|
47
|
+
rm -f "$state_file"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if echo "$output" | grep -q '"decision":"block"'; then
|
|
51
|
+
fail "$name" "should not block when mode=off"
|
|
52
|
+
else
|
|
53
|
+
pass "$name"
|
|
54
|
+
fi
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#-----------------------------------------------------------
|
|
58
|
+
# Test: auto-continue skips plan permission mode
|
|
59
|
+
#-----------------------------------------------------------
|
|
60
|
+
test_autocontinue_skips_plan_mode() {
|
|
61
|
+
local name="auto-continue skips plan permission mode"
|
|
62
|
+
|
|
63
|
+
# Backup current state
|
|
64
|
+
local state_file="$PROJECT_ROOT/.claude.hooks.json"
|
|
65
|
+
local backup=""
|
|
66
|
+
if [[ -f "$state_file" ]]; then
|
|
67
|
+
backup=$(cat "$state_file")
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Set mode to smart (but plan mode should be skipped)
|
|
71
|
+
echo '{"autoContinue":{"mode":"smart"}}' > "$state_file"
|
|
72
|
+
|
|
73
|
+
# permission_mode="plan" should be skipped by default
|
|
74
|
+
local input='{"session_id":"test-plan","permission_mode":"plan"}'
|
|
75
|
+
|
|
76
|
+
cd "$PROJECT_ROOT"
|
|
77
|
+
local output
|
|
78
|
+
output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null) || true
|
|
79
|
+
|
|
80
|
+
# Restore state
|
|
81
|
+
if [[ -n "$backup" ]]; then
|
|
82
|
+
echo "$backup" > "$state_file"
|
|
83
|
+
else
|
|
84
|
+
rm -f "$state_file"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
if echo "$output" | grep -q '"decision":"block"'; then
|
|
88
|
+
fail "$name" "should skip plan mode"
|
|
89
|
+
else
|
|
90
|
+
pass "$name"
|
|
91
|
+
fi
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#-----------------------------------------------------------
|
|
95
|
+
# Test: non-stop mode blocks and counts iterations
|
|
96
|
+
#-----------------------------------------------------------
|
|
97
|
+
test_autocontinue_nonstop_iterations() {
|
|
98
|
+
local name="auto-continue non-stop mode counts iterations"
|
|
99
|
+
|
|
100
|
+
# Backup current state
|
|
101
|
+
local state_file="$PROJECT_ROOT/.claude.hooks.json"
|
|
102
|
+
local backup=""
|
|
103
|
+
if [[ -f "$state_file" ]]; then
|
|
104
|
+
backup=$(cat "$state_file")
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# Set non-stop mode with limit of 2
|
|
108
|
+
echo '{"autoContinue":{"mode":"non-stop","maxIterations":2,"iteration":0}}' > "$state_file"
|
|
109
|
+
|
|
110
|
+
local input='{"session_id":"test-nonstop","permission_mode":"code"}'
|
|
111
|
+
|
|
112
|
+
cd "$PROJECT_ROOT"
|
|
113
|
+
|
|
114
|
+
# First call: should block and increment
|
|
115
|
+
local output1
|
|
116
|
+
output1=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null)
|
|
117
|
+
|
|
118
|
+
if ! echo "$output1" | grep -q '"decision":"block"'; then
|
|
119
|
+
# Restore state
|
|
120
|
+
if [[ -n "$backup" ]]; then
|
|
121
|
+
echo "$backup" > "$state_file"
|
|
122
|
+
else
|
|
123
|
+
rm -f "$state_file"
|
|
124
|
+
fi
|
|
125
|
+
fail "$name" "iteration 1 should block"
|
|
126
|
+
return
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# Second call: should block again
|
|
130
|
+
local output2
|
|
131
|
+
output2=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null)
|
|
132
|
+
|
|
133
|
+
if ! echo "$output2" | grep -q '"decision":"block"'; then
|
|
134
|
+
# Restore state
|
|
135
|
+
if [[ -n "$backup" ]]; then
|
|
136
|
+
echo "$backup" > "$state_file"
|
|
137
|
+
else
|
|
138
|
+
rm -f "$state_file"
|
|
139
|
+
fi
|
|
140
|
+
fail "$name" "iteration 2 should block"
|
|
141
|
+
return
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# Third call: should allow (maxIterations reached)
|
|
145
|
+
local output3
|
|
146
|
+
output3=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null) || true
|
|
147
|
+
|
|
148
|
+
# Restore state
|
|
149
|
+
if [[ -n "$backup" ]]; then
|
|
150
|
+
echo "$backup" > "$state_file"
|
|
151
|
+
else
|
|
152
|
+
rm -f "$state_file"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
if echo "$output3" | grep -q '"decision":"block"'; then
|
|
156
|
+
fail "$name" "iteration 3 should allow (limit reached)"
|
|
157
|
+
else
|
|
158
|
+
pass "$name"
|
|
159
|
+
fi
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#-----------------------------------------------------------
|
|
163
|
+
# Test: deny-list denies paths on deny list
|
|
164
|
+
#-----------------------------------------------------------
|
|
165
|
+
test_denylist_denies() {
|
|
166
|
+
local name="deny-list denies paths on deny list"
|
|
167
|
+
|
|
168
|
+
# Backup state and deny list
|
|
169
|
+
local state_file="$PROJECT_ROOT/.claude.hooks.json"
|
|
170
|
+
local state_backup=""
|
|
171
|
+
if [[ -f "$state_file" ]]; then
|
|
172
|
+
state_backup=$(cat "$state_file")
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
local deny_file="$PROJECT_ROOT/.claude/scripts/deny-list.txt"
|
|
176
|
+
local deny_backup=""
|
|
177
|
+
if [[ -f "$deny_file" ]]; then
|
|
178
|
+
deny_backup=$(cat "$deny_file")
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# Enable deny list in state and add test pattern
|
|
182
|
+
echo '{"denyList":{"enabled":true}}' > "$state_file"
|
|
183
|
+
echo "test-secret.txt" > "$deny_file"
|
|
184
|
+
|
|
185
|
+
# Simulate PreToolUse input for Edit tool
|
|
186
|
+
local input='{"tool_name":"Edit","tool_input":{"file_path":"test-secret.txt"}}'
|
|
187
|
+
|
|
188
|
+
cd "$PROJECT_ROOT"
|
|
189
|
+
local output
|
|
190
|
+
output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/deny-list.ts" 2>/dev/null) || true
|
|
191
|
+
|
|
192
|
+
# Restore state and deny list
|
|
193
|
+
if [[ -n "$state_backup" ]]; then
|
|
194
|
+
echo "$state_backup" > "$state_file"
|
|
195
|
+
else
|
|
196
|
+
rm -f "$state_file"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
if [[ -n "$deny_backup" ]]; then
|
|
200
|
+
echo "$deny_backup" > "$deny_file"
|
|
201
|
+
else
|
|
202
|
+
rm -f "$deny_file"
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
if echo "$output" | grep -q '"permissionDecision":"deny"'; then
|
|
206
|
+
pass "$name"
|
|
207
|
+
else
|
|
208
|
+
fail "$name" "should deny access to test-secret.txt, got: $output"
|
|
209
|
+
fi
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#-----------------------------------------------------------
|
|
213
|
+
# Test: deny-list allows non-denied paths
|
|
214
|
+
#-----------------------------------------------------------
|
|
215
|
+
test_denylist_allows_normal() {
|
|
216
|
+
local name="deny-list allows normal paths"
|
|
217
|
+
|
|
218
|
+
# Backup state and deny list
|
|
219
|
+
local state_file="$PROJECT_ROOT/.claude.hooks.json"
|
|
220
|
+
local state_backup=""
|
|
221
|
+
if [[ -f "$state_file" ]]; then
|
|
222
|
+
state_backup=$(cat "$state_file")
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
local deny_file="$PROJECT_ROOT/.claude/scripts/deny-list.txt"
|
|
226
|
+
local deny_backup=""
|
|
227
|
+
if [[ -f "$deny_file" ]]; then
|
|
228
|
+
deny_backup=$(cat "$deny_file")
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
# Enable deny list but only deny something-else.txt
|
|
232
|
+
echo '{"denyList":{"enabled":true}}' > "$state_file"
|
|
233
|
+
echo "something-else.txt" > "$deny_file"
|
|
234
|
+
|
|
235
|
+
local input='{"tool_name":"Edit","tool_input":{"file_path":"normal-file.txt"}}'
|
|
236
|
+
|
|
237
|
+
cd "$PROJECT_ROOT"
|
|
238
|
+
local output
|
|
239
|
+
output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/deny-list.ts" 2>/dev/null) || true
|
|
240
|
+
|
|
241
|
+
# Restore state and deny list
|
|
242
|
+
if [[ -n "$state_backup" ]]; then
|
|
243
|
+
echo "$state_backup" > "$state_file"
|
|
244
|
+
else
|
|
245
|
+
rm -f "$state_file"
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
if [[ -n "$deny_backup" ]]; then
|
|
249
|
+
echo "$deny_backup" > "$deny_file"
|
|
250
|
+
else
|
|
251
|
+
rm -f "$deny_file"
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
if echo "$output" | grep -q '"permissionDecision":"deny"'; then
|
|
255
|
+
fail "$name" "should not deny normal-file.txt"
|
|
256
|
+
else
|
|
257
|
+
pass "$name"
|
|
258
|
+
fi
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#-----------------------------------------------------------
|
|
262
|
+
# Test: prompt-reminder returns context
|
|
263
|
+
#-----------------------------------------------------------
|
|
264
|
+
test_prompt_reminder_returns_context() {
|
|
265
|
+
local name="prompt-reminder returns context"
|
|
266
|
+
|
|
267
|
+
local input='{"prompt":"help me code"}'
|
|
268
|
+
|
|
269
|
+
cd "$PROJECT_ROOT"
|
|
270
|
+
local output
|
|
271
|
+
output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/prompt-reminder.ts" 2>/dev/null) || true
|
|
272
|
+
|
|
273
|
+
# Should contain some kind of response (not empty)
|
|
274
|
+
if [[ -n "$output" ]]; then
|
|
275
|
+
pass "$name"
|
|
276
|
+
else
|
|
277
|
+
fail "$name" "should return some context"
|
|
278
|
+
fi
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#-----------------------------------------------------------
|
|
282
|
+
# Test: sub-agent inherits deny-list (permission_mode doesn't bypass)
|
|
283
|
+
#-----------------------------------------------------------
|
|
284
|
+
test_subagent_inherits_denylist() {
|
|
285
|
+
local name="sub-agent inherits deny-list protection"
|
|
286
|
+
|
|
287
|
+
# Backup state and deny list
|
|
288
|
+
local state_file="$PROJECT_ROOT/.claude.hooks.json"
|
|
289
|
+
local state_backup=""
|
|
290
|
+
if [[ -f "$state_file" ]]; then
|
|
291
|
+
state_backup=$(cat "$state_file")
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
local deny_file="$PROJECT_ROOT/.claude/scripts/deny-list.txt"
|
|
295
|
+
local deny_backup=""
|
|
296
|
+
if [[ -f "$deny_file" ]]; then
|
|
297
|
+
deny_backup=$(cat "$deny_file")
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
# Enable deny list and add test pattern
|
|
301
|
+
echo '{"denyList":{"enabled":true}}' > "$state_file"
|
|
302
|
+
echo "protected-file.txt" > "$deny_file"
|
|
303
|
+
|
|
304
|
+
# Simulate PreToolUse from a sub-agent (with permission_mode that might be different)
|
|
305
|
+
# Sub-agents should STILL be blocked by deny-list regardless of permission_mode
|
|
306
|
+
local input='{"tool_name":"Edit","tool_input":{"file_path":"protected-file.txt"},"permission_mode":"plan"}'
|
|
307
|
+
|
|
308
|
+
cd "$PROJECT_ROOT"
|
|
309
|
+
local output
|
|
310
|
+
output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/deny-list.ts" 2>/dev/null) || true
|
|
311
|
+
|
|
312
|
+
# Restore state and deny list
|
|
313
|
+
if [[ -n "$state_backup" ]]; then
|
|
314
|
+
echo "$state_backup" > "$state_file"
|
|
315
|
+
else
|
|
316
|
+
rm -f "$state_file"
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
if [[ -n "$deny_backup" ]]; then
|
|
320
|
+
echo "$deny_backup" > "$deny_file"
|
|
321
|
+
else
|
|
322
|
+
rm -f "$deny_file"
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
if echo "$output" | grep -q '"permissionDecision":"deny"'; then
|
|
326
|
+
pass "$name"
|
|
327
|
+
else
|
|
328
|
+
fail "$name" "sub-agent should still be blocked by deny-list"
|
|
329
|
+
fi
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#-----------------------------------------------------------
|
|
333
|
+
# Test: sub-agent state modifications persist
|
|
334
|
+
#-----------------------------------------------------------
|
|
335
|
+
test_subagent_state_persists() {
|
|
336
|
+
local name="sub-agent state modifications persist"
|
|
337
|
+
|
|
338
|
+
# Backup current state
|
|
339
|
+
local state_file="$PROJECT_ROOT/.claude.hooks.json"
|
|
340
|
+
local backup=""
|
|
341
|
+
if [[ -f "$state_file" ]]; then
|
|
342
|
+
backup=$(cat "$state_file")
|
|
343
|
+
fi
|
|
344
|
+
|
|
345
|
+
# Set initial state with iteration=5
|
|
346
|
+
echo '{"autoContinue":{"mode":"non-stop","maxIterations":10,"iteration":5}}' > "$state_file"
|
|
347
|
+
|
|
348
|
+
# Simulate a sub-agent call that would increment iteration
|
|
349
|
+
# (non-stop mode increments on each block)
|
|
350
|
+
local input='{"session_id":"subagent-test","permission_mode":"code"}'
|
|
351
|
+
|
|
352
|
+
cd "$PROJECT_ROOT"
|
|
353
|
+
echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null || true
|
|
354
|
+
|
|
355
|
+
# Read back the state - iteration should now be 6
|
|
356
|
+
local new_state
|
|
357
|
+
new_state=$(cat "$state_file")
|
|
358
|
+
|
|
359
|
+
# Restore original state
|
|
360
|
+
if [[ -n "$backup" ]]; then
|
|
361
|
+
echo "$backup" > "$state_file"
|
|
362
|
+
else
|
|
363
|
+
rm -f "$state_file"
|
|
364
|
+
fi
|
|
365
|
+
|
|
366
|
+
if echo "$new_state" | grep -q '"iteration": 6'; then
|
|
367
|
+
pass "$name"
|
|
368
|
+
else
|
|
369
|
+
fail "$name" "state should persist iteration increment, got: $new_state"
|
|
370
|
+
fi
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#-----------------------------------------------------------
|
|
374
|
+
# Test: sub-agent respects skipModes configuration
|
|
375
|
+
#-----------------------------------------------------------
|
|
376
|
+
test_subagent_respects_skipmodes() {
|
|
377
|
+
local name="sub-agent respects custom skipModes"
|
|
378
|
+
|
|
379
|
+
# Backup current state
|
|
380
|
+
local state_file="$PROJECT_ROOT/.claude.hooks.json"
|
|
381
|
+
local backup=""
|
|
382
|
+
if [[ -f "$state_file" ]]; then
|
|
383
|
+
backup=$(cat "$state_file")
|
|
384
|
+
fi
|
|
385
|
+
|
|
386
|
+
# Set mode to non-stop but add "explore" to skipModes
|
|
387
|
+
# This simulates skipping auto-continue for explore sub-agents
|
|
388
|
+
echo '{"autoContinue":{"mode":"non-stop","skipModes":["plan","explore"]}}' > "$state_file"
|
|
389
|
+
|
|
390
|
+
# Simulate a sub-agent with permission_mode="explore"
|
|
391
|
+
local input='{"session_id":"explore-agent","permission_mode":"explore"}'
|
|
392
|
+
|
|
393
|
+
cd "$PROJECT_ROOT"
|
|
394
|
+
local output
|
|
395
|
+
output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null) || true
|
|
396
|
+
|
|
397
|
+
# Restore state
|
|
398
|
+
if [[ -n "$backup" ]]; then
|
|
399
|
+
echo "$backup" > "$state_file"
|
|
400
|
+
else
|
|
401
|
+
rm -f "$state_file"
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
# Should NOT block because "explore" is in skipModes
|
|
405
|
+
if echo "$output" | grep -q '"decision":"block"'; then
|
|
406
|
+
fail "$name" "should skip auto-continue for explore permission_mode"
|
|
407
|
+
else
|
|
408
|
+
pass "$name"
|
|
409
|
+
fi
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
#-----------------------------------------------------------
|
|
413
|
+
# Test: sub-agent non-skipped mode still blocks
|
|
414
|
+
#-----------------------------------------------------------
|
|
415
|
+
test_subagent_nonskipped_blocks() {
|
|
416
|
+
local name="sub-agent non-skipped mode blocks correctly"
|
|
417
|
+
|
|
418
|
+
# Backup current state
|
|
419
|
+
local state_file="$PROJECT_ROOT/.claude.hooks.json"
|
|
420
|
+
local backup=""
|
|
421
|
+
if [[ -f "$state_file" ]]; then
|
|
422
|
+
backup=$(cat "$state_file")
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
# Set mode to non-stop with only "plan" in skipModes
|
|
426
|
+
echo '{"autoContinue":{"mode":"non-stop","maxIterations":10,"skipModes":["plan"]}}' > "$state_file"
|
|
427
|
+
|
|
428
|
+
# Simulate a sub-agent with permission_mode="code" (NOT in skipModes)
|
|
429
|
+
local input='{"session_id":"code-agent","permission_mode":"code"}'
|
|
430
|
+
|
|
431
|
+
cd "$PROJECT_ROOT"
|
|
432
|
+
local output
|
|
433
|
+
output=$(echo "$input" | npx tsx "$PROJECT_ROOT/.claude/scripts/auto-continue.ts" 2>/dev/null) || true
|
|
434
|
+
|
|
435
|
+
# Restore state
|
|
436
|
+
if [[ -n "$backup" ]]; then
|
|
437
|
+
echo "$backup" > "$state_file"
|
|
438
|
+
else
|
|
439
|
+
rm -f "$state_file"
|
|
440
|
+
fi
|
|
441
|
+
|
|
442
|
+
# SHOULD block because "code" is not in skipModes
|
|
443
|
+
if echo "$output" | grep -q '"decision":"block"'; then
|
|
444
|
+
pass "$name"
|
|
445
|
+
else
|
|
446
|
+
fail "$name" "should block for non-skipped permission_mode"
|
|
447
|
+
fi
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
#-----------------------------------------------------------
|
|
451
|
+
# Test: subagent-classifier classifies explore tasks
|
|
452
|
+
#-----------------------------------------------------------
|
|
453
|
+
test_subagent_classifier_explore() {
|
|
454
|
+
local name="subagent-classifier classifies explore tasks"
|
|
455
|
+
|
|
456
|
+
cd "$PROJECT_ROOT"
|
|
457
|
+
|
|
458
|
+
# Create temp test file with absolute import
|
|
459
|
+
local test_file="$PROJECT_ROOT/scripts/_test-classify-explore.ts"
|
|
460
|
+
cat > "$test_file" << 'EOF'
|
|
461
|
+
import { classifySubagent } from '../src/subagent-classifier.js';
|
|
462
|
+
console.log(classifySubagent('Search for auth implementation'));
|
|
463
|
+
EOF
|
|
464
|
+
|
|
465
|
+
local output
|
|
466
|
+
output=$(npx tsx "$test_file" 2>/dev/null) || true
|
|
467
|
+
rm -f "$test_file"
|
|
468
|
+
|
|
469
|
+
if [[ "$output" == "explore" ]]; then
|
|
470
|
+
pass "$name"
|
|
471
|
+
else
|
|
472
|
+
fail "$name" "expected 'explore', got '$output'"
|
|
473
|
+
fi
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
#-----------------------------------------------------------
|
|
477
|
+
# Test: subagent-classifier classifies work tasks
|
|
478
|
+
#-----------------------------------------------------------
|
|
479
|
+
test_subagent_classifier_work() {
|
|
480
|
+
local name="subagent-classifier classifies work tasks"
|
|
481
|
+
|
|
482
|
+
cd "$PROJECT_ROOT"
|
|
483
|
+
|
|
484
|
+
# Create temp test file with absolute import
|
|
485
|
+
local test_file="$PROJECT_ROOT/scripts/_test-classify-work.ts"
|
|
486
|
+
cat > "$test_file" << 'EOF'
|
|
487
|
+
import { classifySubagent } from '../src/subagent-classifier.js';
|
|
488
|
+
console.log(classifySubagent('Implement the new feature'));
|
|
489
|
+
EOF
|
|
490
|
+
|
|
491
|
+
local output
|
|
492
|
+
output=$(npx tsx "$test_file" 2>/dev/null) || true
|
|
493
|
+
rm -f "$test_file"
|
|
494
|
+
|
|
495
|
+
if [[ "$output" == "work" ]]; then
|
|
496
|
+
pass "$name"
|
|
497
|
+
else
|
|
498
|
+
fail "$name" "expected 'work', got '$output'"
|
|
499
|
+
fi
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
#-----------------------------------------------------------
|
|
503
|
+
# Test: shouldValidateCommit respects subagentHooks state
|
|
504
|
+
#-----------------------------------------------------------
|
|
505
|
+
test_shouldvalidatecommit_respects_state() {
|
|
506
|
+
local name="shouldValidateCommit respects subagentHooks state"
|
|
507
|
+
|
|
508
|
+
cd "$PROJECT_ROOT"
|
|
509
|
+
|
|
510
|
+
# Create temp test file with absolute import
|
|
511
|
+
local test_file="$PROJECT_ROOT/scripts/_test-should-validate.ts"
|
|
512
|
+
cat > "$test_file" << 'EOF'
|
|
513
|
+
import { shouldValidateCommit } from '../src/hooks/validate-commit.js';
|
|
514
|
+
const state = { validateCommitOnExplore: false, validateCommitOnWork: true, validateCommitOnUnknown: true };
|
|
515
|
+
console.log(shouldValidateCommit('explore', state) ? 'true' : 'false');
|
|
516
|
+
console.log(shouldValidateCommit('work', state) ? 'true' : 'false');
|
|
517
|
+
EOF
|
|
518
|
+
|
|
519
|
+
local output
|
|
520
|
+
output=$(npx tsx "$test_file" 2>/dev/null) || true
|
|
521
|
+
rm -f "$test_file"
|
|
522
|
+
|
|
523
|
+
local expected=$'false\ntrue'
|
|
524
|
+
if [[ "$output" == "$expected" ]]; then
|
|
525
|
+
pass "$name"
|
|
526
|
+
else
|
|
527
|
+
fail "$name" "expected 'false\\ntrue', got '$output'"
|
|
528
|
+
fi
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
#-----------------------------------------------------------
|
|
532
|
+
# Test: extractTaskDescription parses Task invocations
|
|
533
|
+
#-----------------------------------------------------------
|
|
534
|
+
test_extract_task_description() {
|
|
535
|
+
local name="extractTaskDescription parses Task invocations"
|
|
536
|
+
|
|
537
|
+
cd "$PROJECT_ROOT"
|
|
538
|
+
|
|
539
|
+
# Create temp test file with absolute import
|
|
540
|
+
local test_file="$PROJECT_ROOT/scripts/_test-extract.ts"
|
|
541
|
+
cat > "$test_file" << 'EOF'
|
|
542
|
+
import { extractTaskDescription } from '../src/subagent-classifier.js';
|
|
543
|
+
const transcript = '<invoke name="Task"><parameter name="description">Search for files</parameter></invoke>';
|
|
544
|
+
console.log(extractTaskDescription(transcript) || 'undefined');
|
|
545
|
+
EOF
|
|
546
|
+
|
|
547
|
+
local output
|
|
548
|
+
output=$(npx tsx "$test_file" 2>/dev/null) || true
|
|
549
|
+
rm -f "$test_file"
|
|
550
|
+
|
|
551
|
+
if [[ "$output" == "Search for files" ]]; then
|
|
552
|
+
pass "$name"
|
|
553
|
+
else
|
|
554
|
+
fail "$name" "expected 'Search for files', got '$output'"
|
|
555
|
+
fi
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
#-----------------------------------------------------------
|
|
559
|
+
# Test: full integration - explore task skips validation
|
|
560
|
+
#-----------------------------------------------------------
|
|
561
|
+
test_integration_explore_skips_validation() {
|
|
562
|
+
local name="integration: explore task skips validation by default"
|
|
563
|
+
|
|
564
|
+
cd "$PROJECT_ROOT"
|
|
565
|
+
|
|
566
|
+
# Create integration test file
|
|
567
|
+
local test_file="$PROJECT_ROOT/scripts/_test-integration-explore.ts"
|
|
568
|
+
cat > "$test_file" << 'EOF'
|
|
569
|
+
import { classifySubagent, extractTaskDescription } from '../src/subagent-classifier.js';
|
|
570
|
+
import { shouldValidateCommit } from '../src/hooks/validate-commit.js';
|
|
571
|
+
import { DEFAULT_HOOK_STATE } from '../src/hook-state.js';
|
|
572
|
+
|
|
573
|
+
// Simulate a transcript with an explore task
|
|
574
|
+
const transcript = '<invoke name="Task"><parameter name="description">Search for auth implementation</parameter></invoke>';
|
|
575
|
+
|
|
576
|
+
// Extract and classify
|
|
577
|
+
const description = extractTaskDescription(transcript);
|
|
578
|
+
const subagentType = classifySubagent(description || '');
|
|
579
|
+
|
|
580
|
+
// Check if validation should run with default state
|
|
581
|
+
const shouldValidate = shouldValidateCommit(subagentType, DEFAULT_HOOK_STATE.subagentHooks);
|
|
582
|
+
|
|
583
|
+
console.log(`description: ${description}`);
|
|
584
|
+
console.log(`type: ${subagentType}`);
|
|
585
|
+
console.log(`shouldValidate: ${shouldValidate}`);
|
|
586
|
+
EOF
|
|
587
|
+
|
|
588
|
+
local output
|
|
589
|
+
output=$(npx tsx "$test_file" 2>/dev/null) || true
|
|
590
|
+
rm -f "$test_file"
|
|
591
|
+
|
|
592
|
+
if echo "$output" | grep -q "type: explore" && echo "$output" | grep -q "shouldValidate: false"; then
|
|
593
|
+
pass "$name"
|
|
594
|
+
else
|
|
595
|
+
fail "$name" "explore task should skip validation, got: $output"
|
|
596
|
+
fi
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
#-----------------------------------------------------------
|
|
600
|
+
# Test: full integration - work task runs validation
|
|
601
|
+
#-----------------------------------------------------------
|
|
602
|
+
test_integration_work_runs_validation() {
|
|
603
|
+
local name="integration: work task runs validation by default"
|
|
604
|
+
|
|
605
|
+
cd "$PROJECT_ROOT"
|
|
606
|
+
|
|
607
|
+
# Create integration test file
|
|
608
|
+
local test_file="$PROJECT_ROOT/scripts/_test-integration-work.ts"
|
|
609
|
+
cat > "$test_file" << 'EOF'
|
|
610
|
+
import { classifySubagent, extractTaskDescription } from '../src/subagent-classifier.js';
|
|
611
|
+
import { shouldValidateCommit } from '../src/hooks/validate-commit.js';
|
|
612
|
+
import { DEFAULT_HOOK_STATE } from '../src/hook-state.js';
|
|
613
|
+
|
|
614
|
+
// Simulate a transcript with a work task
|
|
615
|
+
const transcript = '<invoke name="Task"><parameter name="description">Implement the new feature</parameter></invoke>';
|
|
616
|
+
|
|
617
|
+
// Extract and classify
|
|
618
|
+
const description = extractTaskDescription(transcript);
|
|
619
|
+
const subagentType = classifySubagent(description || '');
|
|
620
|
+
|
|
621
|
+
// Check if validation should run with default state
|
|
622
|
+
const shouldValidate = shouldValidateCommit(subagentType, DEFAULT_HOOK_STATE.subagentHooks);
|
|
623
|
+
|
|
624
|
+
console.log(`description: ${description}`);
|
|
625
|
+
console.log(`type: ${subagentType}`);
|
|
626
|
+
console.log(`shouldValidate: ${shouldValidate}`);
|
|
627
|
+
EOF
|
|
628
|
+
|
|
629
|
+
local output
|
|
630
|
+
output=$(npx tsx "$test_file" 2>/dev/null) || true
|
|
631
|
+
rm -f "$test_file"
|
|
632
|
+
|
|
633
|
+
if echo "$output" | grep -q "type: work" && echo "$output" | grep -q "shouldValidate: true"; then
|
|
634
|
+
pass "$name"
|
|
635
|
+
else
|
|
636
|
+
fail "$name" "work task should run validation, got: $output"
|
|
637
|
+
fi
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
#-----------------------------------------------------------
|
|
641
|
+
# Test: full integration - unknown task runs validation (safe default)
|
|
642
|
+
#-----------------------------------------------------------
|
|
643
|
+
test_integration_unknown_runs_validation() {
|
|
644
|
+
local name="integration: unknown task runs validation (safe default)"
|
|
645
|
+
|
|
646
|
+
cd "$PROJECT_ROOT"
|
|
647
|
+
|
|
648
|
+
# Create integration test file
|
|
649
|
+
local test_file="$PROJECT_ROOT/scripts/_test-integration-unknown.ts"
|
|
650
|
+
cat > "$test_file" << 'EOF'
|
|
651
|
+
import { classifySubagent, extractTaskDescription } from '../src/subagent-classifier.js';
|
|
652
|
+
import { shouldValidateCommit } from '../src/hooks/validate-commit.js';
|
|
653
|
+
import { DEFAULT_HOOK_STATE } from '../src/hook-state.js';
|
|
654
|
+
|
|
655
|
+
// Simulate a transcript with an ambiguous task
|
|
656
|
+
const transcript = '<invoke name="Task"><parameter name="description">Process the data</parameter></invoke>';
|
|
657
|
+
|
|
658
|
+
// Extract and classify
|
|
659
|
+
const description = extractTaskDescription(transcript);
|
|
660
|
+
const subagentType = classifySubagent(description || '');
|
|
661
|
+
|
|
662
|
+
// Check if validation should run with default state
|
|
663
|
+
const shouldValidate = shouldValidateCommit(subagentType, DEFAULT_HOOK_STATE.subagentHooks);
|
|
664
|
+
|
|
665
|
+
console.log(`description: ${description}`);
|
|
666
|
+
console.log(`type: ${subagentType}`);
|
|
667
|
+
console.log(`shouldValidate: ${shouldValidate}`);
|
|
668
|
+
EOF
|
|
669
|
+
|
|
670
|
+
local output
|
|
671
|
+
output=$(npx tsx "$test_file" 2>/dev/null) || true
|
|
672
|
+
rm -f "$test_file"
|
|
673
|
+
|
|
674
|
+
if echo "$output" | grep -q "type: unknown" && echo "$output" | grep -q "shouldValidate: true"; then
|
|
675
|
+
pass "$name"
|
|
676
|
+
else
|
|
677
|
+
fail "$name" "unknown task should run validation (safe default), got: $output"
|
|
678
|
+
fi
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
#-----------------------------------------------------------
|
|
682
|
+
# Test: state file controls subagent validation behavior
|
|
683
|
+
#-----------------------------------------------------------
|
|
684
|
+
test_integration_state_controls_behavior() {
|
|
685
|
+
local name="integration: state file controls subagent validation behavior"
|
|
686
|
+
|
|
687
|
+
# Backup current state
|
|
688
|
+
local state_file="$PROJECT_ROOT/.claude.hooks.json"
|
|
689
|
+
local backup=""
|
|
690
|
+
if [[ -f "$state_file" ]]; then
|
|
691
|
+
backup=$(cat "$state_file")
|
|
692
|
+
fi
|
|
693
|
+
|
|
694
|
+
# Set custom state: enable validation for explore, disable for work
|
|
695
|
+
echo '{"subagentHooks":{"validateCommitOnExplore":true,"validateCommitOnWork":false,"validateCommitOnUnknown":true}}' > "$state_file"
|
|
696
|
+
|
|
697
|
+
cd "$PROJECT_ROOT"
|
|
698
|
+
|
|
699
|
+
# Create integration test file
|
|
700
|
+
local test_file="$PROJECT_ROOT/scripts/_test-integration-state.ts"
|
|
701
|
+
cat > "$test_file" << 'EOF'
|
|
702
|
+
import { classifySubagent } from '../src/subagent-classifier.js';
|
|
703
|
+
import { shouldValidateCommit } from '../src/hooks/validate-commit.js';
|
|
704
|
+
import { createHookState } from '../src/hook-state.js';
|
|
705
|
+
|
|
706
|
+
const hookState = createHookState(process.cwd());
|
|
707
|
+
const state = hookState.read();
|
|
708
|
+
|
|
709
|
+
// Test explore
|
|
710
|
+
const exploreType = classifySubagent('Search for files');
|
|
711
|
+
const exploreValidate = shouldValidateCommit(exploreType, state.subagentHooks);
|
|
712
|
+
|
|
713
|
+
// Test work
|
|
714
|
+
const workType = classifySubagent('Implement feature');
|
|
715
|
+
const workValidate = shouldValidateCommit(workType, state.subagentHooks);
|
|
716
|
+
|
|
717
|
+
console.log(`explore: ${exploreValidate}`);
|
|
718
|
+
console.log(`work: ${workValidate}`);
|
|
719
|
+
EOF
|
|
720
|
+
|
|
721
|
+
local output
|
|
722
|
+
output=$(npx tsx "$test_file" 2>/dev/null) || true
|
|
723
|
+
rm -f "$test_file"
|
|
724
|
+
|
|
725
|
+
# Restore state
|
|
726
|
+
if [[ -n "$backup" ]]; then
|
|
727
|
+
echo "$backup" > "$state_file"
|
|
728
|
+
else
|
|
729
|
+
rm -f "$state_file"
|
|
730
|
+
fi
|
|
731
|
+
|
|
732
|
+
# With our custom state: explore=true, work=false (opposite of default)
|
|
733
|
+
if echo "$output" | grep -q "explore: true" && echo "$output" | grep -q "work: false"; then
|
|
734
|
+
pass "$name"
|
|
735
|
+
else
|
|
736
|
+
fail "$name" "state file should control behavior, got: $output"
|
|
737
|
+
fi
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
#-----------------------------------------------------------
|
|
741
|
+
# Test: classifier handles all explore patterns
|
|
742
|
+
#-----------------------------------------------------------
|
|
743
|
+
test_classifier_all_explore_patterns() {
|
|
744
|
+
local name="classifier handles all explore patterns"
|
|
745
|
+
|
|
746
|
+
cd "$PROJECT_ROOT"
|
|
747
|
+
|
|
748
|
+
local test_file="$PROJECT_ROOT/scripts/_test-all-explore.ts"
|
|
749
|
+
cat > "$test_file" << 'EOF'
|
|
750
|
+
import { classifySubagent } from '../src/subagent-classifier.js';
|
|
751
|
+
|
|
752
|
+
const patterns = [
|
|
753
|
+
'Search for files matching pattern',
|
|
754
|
+
'Find the implementation',
|
|
755
|
+
'Understand how it works',
|
|
756
|
+
'Investigate the error',
|
|
757
|
+
'Analyze the codebase',
|
|
758
|
+
'Look for usages',
|
|
759
|
+
'Research existing patterns',
|
|
760
|
+
'Explore the architecture',
|
|
761
|
+
'Discover dependencies',
|
|
762
|
+
'Locate the config file',
|
|
763
|
+
];
|
|
764
|
+
|
|
765
|
+
let allExplore = true;
|
|
766
|
+
for (const p of patterns) {
|
|
767
|
+
const result = classifySubagent(p);
|
|
768
|
+
if (result !== 'explore') {
|
|
769
|
+
console.log(`FAIL: "${p}" classified as ${result}`);
|
|
770
|
+
allExplore = false;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
console.log(allExplore ? 'ALL_EXPLORE' : 'SOME_FAILED');
|
|
774
|
+
EOF
|
|
775
|
+
|
|
776
|
+
local output
|
|
777
|
+
output=$(npx tsx "$test_file" 2>/dev/null) || true
|
|
778
|
+
rm -f "$test_file"
|
|
779
|
+
|
|
780
|
+
if [[ "$output" == "ALL_EXPLORE" ]]; then
|
|
781
|
+
pass "$name"
|
|
782
|
+
else
|
|
783
|
+
fail "$name" "not all explore patterns classified correctly: $output"
|
|
784
|
+
fi
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
#-----------------------------------------------------------
|
|
788
|
+
# Test: classifier handles all work patterns
|
|
789
|
+
#-----------------------------------------------------------
|
|
790
|
+
test_classifier_all_work_patterns() {
|
|
791
|
+
local name="classifier handles all work patterns"
|
|
792
|
+
|
|
793
|
+
cd "$PROJECT_ROOT"
|
|
794
|
+
|
|
795
|
+
local test_file="$PROJECT_ROOT/scripts/_test-all-work.ts"
|
|
796
|
+
cat > "$test_file" << 'EOF'
|
|
797
|
+
import { classifySubagent } from '../src/subagent-classifier.js';
|
|
798
|
+
|
|
799
|
+
const patterns = [
|
|
800
|
+
'Implement the new feature',
|
|
801
|
+
'Create a user form',
|
|
802
|
+
'Write tests for login',
|
|
803
|
+
'Fix the bug in parser',
|
|
804
|
+
'Refactor the database layer',
|
|
805
|
+
'Update the configuration',
|
|
806
|
+
'Add error handling',
|
|
807
|
+
'Build the API endpoint',
|
|
808
|
+
'Modify the schema',
|
|
809
|
+
'Change the default value',
|
|
810
|
+
'Remove unused code',
|
|
811
|
+
'Delete the deprecated file',
|
|
812
|
+
];
|
|
813
|
+
|
|
814
|
+
let allWork = true;
|
|
815
|
+
for (const p of patterns) {
|
|
816
|
+
const result = classifySubagent(p);
|
|
817
|
+
if (result !== 'work') {
|
|
818
|
+
console.log(`FAIL: "${p}" classified as ${result}`);
|
|
819
|
+
allWork = false;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
console.log(allWork ? 'ALL_WORK' : 'SOME_FAILED');
|
|
823
|
+
EOF
|
|
824
|
+
|
|
825
|
+
local output
|
|
826
|
+
output=$(npx tsx "$test_file" 2>/dev/null) || true
|
|
827
|
+
rm -f "$test_file"
|
|
828
|
+
|
|
829
|
+
if [[ "$output" == "ALL_WORK" ]]; then
|
|
830
|
+
pass "$name"
|
|
831
|
+
else
|
|
832
|
+
fail "$name" "not all work patterns classified correctly: $output"
|
|
833
|
+
fi
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
#-----------------------------------------------------------
|
|
837
|
+
# Test: DEFAULT_HOOK_STATE has correct subagentHooks defaults
|
|
838
|
+
#-----------------------------------------------------------
|
|
839
|
+
test_default_state_subagent_hooks() {
|
|
840
|
+
local name="DEFAULT_HOOK_STATE has correct subagentHooks defaults"
|
|
841
|
+
|
|
842
|
+
cd "$PROJECT_ROOT"
|
|
843
|
+
|
|
844
|
+
local test_file="$PROJECT_ROOT/scripts/_test-default-state.ts"
|
|
845
|
+
cat > "$test_file" << 'EOF'
|
|
846
|
+
import { DEFAULT_HOOK_STATE } from '../src/hook-state.js';
|
|
847
|
+
|
|
848
|
+
const sh = DEFAULT_HOOK_STATE.subagentHooks;
|
|
849
|
+
const correct =
|
|
850
|
+
sh.validateCommitOnExplore === false &&
|
|
851
|
+
sh.validateCommitOnWork === true &&
|
|
852
|
+
sh.validateCommitOnUnknown === true;
|
|
853
|
+
|
|
854
|
+
console.log(correct ? 'CORRECT' : `WRONG: explore=${sh.validateCommitOnExplore}, work=${sh.validateCommitOnWork}, unknown=${sh.validateCommitOnUnknown}`);
|
|
855
|
+
EOF
|
|
856
|
+
|
|
857
|
+
local output
|
|
858
|
+
output=$(npx tsx "$test_file" 2>/dev/null) || true
|
|
859
|
+
rm -f "$test_file"
|
|
860
|
+
|
|
861
|
+
if [[ "$output" == "CORRECT" ]]; then
|
|
862
|
+
pass "$name"
|
|
863
|
+
else
|
|
864
|
+
fail "$name" "$output"
|
|
865
|
+
fi
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
#-----------------------------------------------------------
|
|
869
|
+
# Run all tests
|
|
870
|
+
#-----------------------------------------------------------
|
|
871
|
+
echo "Running hook E2E tests..."
|
|
872
|
+
echo ""
|
|
873
|
+
|
|
874
|
+
echo "=== Basic Hook Tests ==="
|
|
875
|
+
test_autocontinue_mode_off
|
|
876
|
+
test_autocontinue_skips_plan_mode
|
|
877
|
+
test_autocontinue_nonstop_iterations
|
|
878
|
+
test_denylist_denies
|
|
879
|
+
test_denylist_allows_normal
|
|
880
|
+
test_prompt_reminder_returns_context
|
|
881
|
+
|
|
882
|
+
echo ""
|
|
883
|
+
echo "=== Sub-Agent Hook Tests ==="
|
|
884
|
+
test_subagent_inherits_denylist
|
|
885
|
+
test_subagent_state_persists
|
|
886
|
+
test_subagent_respects_skipmodes
|
|
887
|
+
test_subagent_nonskipped_blocks
|
|
888
|
+
|
|
889
|
+
echo ""
|
|
890
|
+
echo "=== Sub-Agent Classification Tests ==="
|
|
891
|
+
test_subagent_classifier_explore
|
|
892
|
+
test_subagent_classifier_work
|
|
893
|
+
test_shouldvalidatecommit_respects_state
|
|
894
|
+
test_extract_task_description
|
|
895
|
+
|
|
896
|
+
echo ""
|
|
897
|
+
echo "=== Classification Integration Tests ==="
|
|
898
|
+
test_integration_explore_skips_validation
|
|
899
|
+
test_integration_work_runs_validation
|
|
900
|
+
test_integration_unknown_runs_validation
|
|
901
|
+
test_integration_state_controls_behavior
|
|
902
|
+
test_classifier_all_explore_patterns
|
|
903
|
+
test_classifier_all_work_patterns
|
|
904
|
+
test_default_state_subagent_hooks
|
|
905
|
+
|
|
906
|
+
echo ""
|
|
907
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
908
|
+
echo "Results: $PASSED passed, $FAILED failed"
|
|
909
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
910
|
+
exit $((FAILED > 0 ? 1 : 0))
|