-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathunified_routes_test.go
More file actions
138 lines (130 loc) · 4.17 KB
/
Copy pathunified_routes_test.go
File metadata and controls
138 lines (130 loc) · 4.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package agenthooks_test
import (
"encoding/json"
"os"
"strings"
"testing"
"github.com/Checkmarx/ast-cx-hooks"
)
// TestNewUnifiedRoutes drives the WhenSubagentIdle, AfterToolFailure, and BeforeFileRead
// handlers end-to-end through Dispatch. Windsurf's blocking path uses os.Exit(2) (ProcessE)
// and is therefore exercised only on the allow path here.
func TestNewUnifiedRoutes(t *testing.T) {
cases := []struct {
name string
route string
register func()
stdin string
wantStdout []string
}{
{
name: "claude-subagent-stop interrupt blocks",
route: "claude-subagent-stop",
register: func() {
agenthooks.WhenSubagentIdle(func(e agenthooks.AgentIdleEvent) agenthooks.IdleVerdict {
if e.Agent != agenthooks.AgentClaude {
t.Fatalf("agent: %q", e.Agent)
}
if e.IsLooping() {
return agenthooks.Resume()
}
return agenthooks.Interrupt("finish the subtask")
})
},
stdin: `{
"session_id":"s-1","cwd":"/repo","hook_event_name":"SubagentStop",
"agent_id":"a-1","agent_type":"Explore","stop_hook_active":false
}`,
wantStdout: []string{`"decision":"block"`, `"reason":"finish the subtask"`},
},
{
name: "claude-subagent-stop loop break",
route: "claude-subagent-stop",
register: func() {
agenthooks.WhenSubagentIdle(func(e agenthooks.AgentIdleEvent) agenthooks.IdleVerdict {
if e.IsLooping() {
return agenthooks.Resume()
}
return agenthooks.Interrupt("should not fire")
})
},
stdin: `{"session_id":"s-2","hook_event_name":"SubagentStop","stop_hook_active":true}`,
wantStdout: []string{},
},
{
name: "cursor-before-read-file deny",
route: "cursor-before-read-file",
register: func() {
agenthooks.BeforeFileRead(func(e agenthooks.FileReadEvent) agenthooks.FileReadVerdict {
if e.Agent != agenthooks.AgentCursor {
t.Fatalf("agent: %q", e.Agent)
}
if strings.HasSuffix(e.FilePath, ".env") {
return agenthooks.DenyRead("no secrets")
}
return agenthooks.AllowRead()
})
},
stdin: `{"conversation_id":"c-1","file_path":"/repo/.env","content":"SECRET=1"}`,
wantStdout: []string{`"permission":"deny"`, `"user_message":"no secrets"`},
},
{
name: "claude-post-tool-use-failure reject",
route: "claude-post-tool-use-failure",
register: func() {
agenthooks.AfterToolFailure(func(e agenthooks.ToolFailureEvent) agenthooks.ToolFailureVerdict {
if e.Error == "" {
t.Fatal("expected error message")
}
return agenthooks.RejectAfterFailure("retry with smaller input")
})
},
stdin: `{
"session_id":"s-3","tool_name":"Bash","tool_input":{"command":"x"},
"error":"exit status 1","tool_use_id":"tu-3"
}`,
// Per the Claude Code docs, PostToolUseFailure cannot block — its only
// control surface is additionalContext. The unified Reject verdict therefore
// surfaces as injected context, not a decision:block.
wantStdout: []string{`"additionalContext":"retry with smaller input"`, `"hookEventName":"PostToolUseFailure"`},
},
{
name: "windsurf-pre-read-code allow",
route: "windsurf-pre-read-code",
register: func() {
agenthooks.BeforeFileRead(func(e agenthooks.FileReadEvent) agenthooks.FileReadVerdict {
if e.Agent != agenthooks.AgentWindsurf || e.FilePath != "/repo/ok.go" {
t.Fatalf("unexpected event: %+v", e)
}
return agenthooks.AllowRead()
})
},
stdin: `{
"agent_action_name":"pre_read_code","trajectory_id":"t-1",
"tool_info":{"file_path":"/repo/ok.go"}
}`,
wantStdout: []string{},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
agenthooks.ClearRoutes()
tc.register()
stdoutBuf := pipeStdio(t, tc.stdin)
origArgs := os.Args
os.Args = []string{"hook", tc.route}
defer func() { os.Args = origArgs }()
agenthooks.Dispatch()
out := stdoutBuf()
var anyJSON map[string]any
if err := json.Unmarshal([]byte(out), &anyJSON); err != nil {
t.Fatalf("stdout is not valid JSON: %q (err=%v)", out, err)
}
for _, want := range tc.wantStdout {
if !strings.Contains(out, want) {
t.Fatalf("stdout missing %q\nfull output: %s", want, out)
}
}
})
}
}