spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / mods / sec-default / tests / caught.test.ts
351 lines · 10.5 KBRaw
1import { describe, expect, test, tier } from 'claude-code/testing'
2
3import Hooks from '../hooks'
4import Fixtures from './fixtures'
5
6tier('prepend')
7
8describe('caught', () => {
9 test('the check handler refuses a verdict of theirs unchecked', async () => {
10 const handler = Fixtures.handlersRegistered()('tool.check')
11
12 const loosened = Fixtures.unchecked([
13 Fixtures.linkOf('easy', 'user', Fixtures.ALLOWED),
14 Fixtures.linkOf('engine', 'core', Fixtures.ASKED),
15 ])
16
17 expect([
18 await handler(Fixtures.UNREADABLE, Fixtures.CHECKED, loosened),
19 await handler(Fixtures.THROWING, Fixtures.CHECKED, loosened),
20 await handler(
21 Fixtures.answering(Fixtures.MANAGED_POLICY),
22 Fixtures.CHECKED,
23 loosened,
24 ),
25 ]).toEqual([
26 Hooks.UNCHECKED_DENY,
27 Hooks.UNCHECKED_DENY,
28 Hooks.UNCHECKED_DENY,
29 ])
30 })
31
32 test('under a ceiling, where plugins may override, it asks', async () => {
33 const handler = Fixtures.handlersRegistered()('tool.check')
34
35 const loosened = Fixtures.unchecked([
36 Fixtures.linkOf('easy', 'user', Fixtures.ALLOWED),
37 Fixtures.linkOf('engine', 'core', Fixtures.CAPPED_ASK),
38 ])
39
40 expect([
41 await handler(
42 Fixtures.answering(Fixtures.overridePolicyOf(true)),
43 Fixtures.CAPPED,
44 loosened,
45 ),
46 await handler(Fixtures.UNREADABLE, Fixtures.CAPPED, loosened),
47 ]).toEqual([Hooks.uncheckedCeiling('ask'), Hooks.UNCHECKED_DENY])
48 })
49
50 test('a verdict none of theirs loosened stands with it', async () => {
51 expect(
52 await Fixtures.handlersRegistered()('tool.check')(
53 Fixtures.UNREADABLE,
54 Fixtures.CHECKED,
55 Fixtures.unchecked([
56 Fixtures.linkOf('listening', 'user', Fixtures.ASKED),
57 Fixtures.linkOf('engine', 'core', Fixtures.ASKED),
58 ]),
59 ),
60 ).toEqual(Fixtures.ASKED)
61 })
62
63 test('the register handler refuses a plugin of theirs', async () => {
64 const handler = Fixtures.handlersRegistered()('tool.register')
65 const next = Fixtures.uncalled('beneath', 'past')
66
67 expect([
68 await handler(Fixtures.UNREADABLE, Fixtures.JUDGED, next),
69 await handler(Fixtures.THROWING, Fixtures.JUDGED, next),
70 await handler(
71 Fixtures.answering(Fixtures.ALLOWLIST),
72 Fixtures.JUDGED,
73 next,
74 ),
75 ]).toEqual([
76 { deny: Hooks.TOOL_REGISTER_REFUSAL },
77 { deny: Hooks.TOOL_REGISTER_REFUSAL },
78 { deny: Hooks.TOOL_REGISTER_REFUSAL },
79 ])
80 })
81
82 test('it reads policy itself, each time it is asked', async () => {
83 const handler = Fixtures.handlersRegistered()('tool.register')
84 const next = Fixtures.uncalled('beneath', 'past')
85
86 expect([
87 await handler(
88 Fixtures.answering(Fixtures.ALLOWLIST),
89 Fixtures.JUDGED,
90 next,
91 ),
92 await handler(
93 Fixtures.answering(Fixtures.NO_ALLOWLIST),
94 Fixtures.JUDGED,
95 next,
96 ),
97 ]).toEqual([{ deny: Hooks.TOOL_REGISTER_REFUSAL }, 'beneath'])
98 })
99
100 test('it decides for every other caller as the hook does', async () => {
101 const handler = Fixtures.handlersRegistered()('tool.register')
102
103 expect([
104 await handler(
105 Fixtures.UNREADABLE,
106 Fixtures.JUDGED,
107 Fixtures.uncalled('beneath', 'past', 'prepend'),
108 ),
109 await handler(
110 Fixtures.UNREADABLE,
111 Fixtures.JUDGED,
112 Fixtures.uncalled('beneath', 'past', 'builtin'),
113 ),
114 await handler(
115 Fixtures.answering(Fixtures.NO_ALLOWLIST),
116 Fixtures.JUDGED,
117 Fixtures.uncalled('beneath', 'past'),
118 ),
119 ]).toEqual(['past', 'beneath', 'beneath'])
120 })
121
122 test('the list and log handlers answer past theirs', async () => {
123 const registered = Fixtures.handlersRegistered()
124 const next = Fixtures.uncalled('beneath', 'past')
125
126 expect([
127 await registered('tool.list')(Fixtures.UNREADABLE, Fixtures.JUDGED, next),
128 await registered('tool.list')(Fixtures.THROWING, Fixtures.JUDGED, next),
129 await registered('ui.log')(Fixtures.UNREADABLE, Fixtures.JUDGED, next),
130 await registered('ui.log')(Fixtures.THROWING, Fixtures.JUDGED, next),
131 ]).toEqual(['past', 'past', 'past', 'past'])
132 })
133
134 test('the admission handler refuses, log or no log', async () => {
135 const handler = Fixtures.handlersRegistered()('plugin.register')
136 const next = Fixtures.uncalled('beneath', 'past')
137 const lines: string[] = []
138
139 expect([
140 await handler(Fixtures.UNREADABLE, Fixtures.JUDGED, next),
141 await handler(Fixtures.THROWING, Fixtures.JUDGED, next),
142 await handler(
143 Fixtures.answering(Fixtures.MANAGED_POLICY, line => {
144 lines.push(line)
145 }),
146 Fixtures.JUDGED,
147 next,
148 ),
149 ]).toEqual([
150 { refuse: Hooks.managedModsOnlyRefusal('mine') },
151 { refuse: Hooks.managedModsOnlyRefusal('mine') },
152 { refuse: Hooks.managedModsOnlyRefusal('mine') },
153 ])
154
155 expect(lines).toEqual([Hooks.admissionFailure('mine', { kind: 'throw' })])
156 })
157
158 test('a hook that had called next gets that call back', async () => {
159 const registered = Fixtures.handlersRegistered()
160 const next = Fixtures.replaying('last')
161
162 expect([
163 await registered('tool.register')(
164 Fixtures.UNREADABLE,
165 Fixtures.JUDGED,
166 next,
167 ),
168 await registered('tool.list')(Fixtures.UNREADABLE, Fixtures.JUDGED, next),
169 await registered('ui.log')(Fixtures.UNREADABLE, Fixtures.JUDGED, next),
170 await registered('plugin.register')(
171 Fixtures.UNREADABLE,
172 Fixtures.JUDGED,
173 next,
174 ),
175 ]).toEqual(['last', 'last', 'last', 'last'])
176 })
177
178 test('the classic and settings handlers answer past theirs', async () => {
179 const registered = Fixtures.handlersRegistered()
180 const next = Fixtures.uncalled('beneath', 'past')
181
182 expect(
183 await Promise.all(
184 Fixtures.PASS_OVER_EVENTS.flatMap(event => [
185 registered(event)(Fixtures.UNREADABLE, Fixtures.JUDGED, next),
186 registered(event)(Fixtures.THROWING, Fixtures.JUDGED, next),
187 ]),
188 ),
189 ).toEqual(['past', 'past', 'past', 'past'])
190 })
191
192 test('the subject handlers keep what is theirs from the rest', async () => {
193 const registered = Fixtures.handlersRegistered()
194 const next = Fixtures.uncalled('beneath', 'past')
195
196 expect(
197 await Promise.all(
198 Fixtures.SUBJECT_EVENTS.flatMap(event => [
199 registered(event)(Fixtures.UNREADABLE, Fixtures.ORGS_SUBJECT, next),
200 registered(event)(Fixtures.THROWING, Fixtures.THEIRS_SUBJECT, next),
201 ]),
202 ),
203 ).toEqual([
204 'past',
205 'beneath',
206 'past',
207 'beneath',
208 'past',
209 'beneath',
210 'past',
211 'beneath',
212 ])
213 })
214
215 test('they read the provider as their hooks do, tier by tier', async () => {
216 const registered = Fixtures.handlersRegistered()
217 const next = Fixtures.uncalled('beneath', 'past')
218
219 const providers = [
220 ...Fixtures.ORG_PROVIDERS,
221 ...Fixtures.ODD_PROVIDERS,
222 ...Fixtures.USER_REACHABLE_PROVIDERS,
223 ]
224
225 const answers = [
226 ...Fixtures.ORG_PROVIDERS.map(() => 'past'),
227 ...Fixtures.ODD_PROVIDERS.map(() => 'past'),
228 ...Fixtures.USER_REACHABLE_PROVIDERS.map(() => 'beneath'),
229 ]
230
231 expect(
232 await Promise.all(
233 Fixtures.SUBJECT_EVENTS.flatMap(event =>
234 providers.map(provider =>
235 registered(event)(Fixtures.UNREADABLE, { provider }, next),
236 ),
237 ),
238 ),
239 ).toEqual(Fixtures.SUBJECT_EVENTS.flatMap(() => answers))
240
241 expect(
242 await Promise.all(
243 Fixtures.SUBJECT_EVENTS.flatMap(event =>
244 providers.map(provider =>
245 registered(event, 'hook')(Fixtures.UNREADABLE, { provider }, next),
246 ),
247 ),
248 ),
249 'each hook, asked the same',
250 ).toEqual(Fixtures.SUBJECT_EVENTS.flatMap(() => answers))
251 })
252
253 test('the classic and settings hooks pass over theirs', async () => {
254 const registered = Fixtures.handlersRegistered()
255 const next = Fixtures.uncalled('beneath', 'past')
256
257 expect(
258 await Promise.all(
259 Fixtures.PASS_OVER_EVENTS.map(event =>
260 registered(event, 'hook')(Fixtures.UNREADABLE, Fixtures.JUDGED, next),
261 ),
262 ),
263 ).toEqual(['past', 'past'])
264 })
265
266 test('each of the six that had called next gets that call back', async () => {
267 const registered = Fixtures.handlersRegistered()
268 const next = Fixtures.replaying('last')
269
270 expect(
271 await Promise.all(
272 [...Fixtures.PASS_OVER_EVENTS, ...Fixtures.SUBJECT_EVENTS].map(event =>
273 registered(event)(Fixtures.UNREADABLE, Fixtures.ORGS_SUBJECT, next),
274 ),
275 ),
276 ).toEqual(['last', 'last', 'last', 'last', 'last', 'last'])
277 })
278
279 test('a line the check hook cannot log changes no answer', async () => {
280 const next = Fixtures.rechecked(
281 [
282 Fixtures.linkOf('easy', 'user', Fixtures.ALLOWED),
283 Fixtures.linkOf('engine', 'core', Fixtures.CAPPED_ASK),
284 ],
285 Fixtures.CAPPED_ASK,
286 )
287
288 expect([
289 await Fixtures.handlersRegistered()('tool.check', 'hook')(
290 Fixtures.THROWING,
291 Fixtures.CAPPED,
292 next,
293 ),
294 await Fixtures.handlersRegistered()('tool.check', 'hook')(
295 Fixtures.UNREADABLE,
296 Fixtures.CAPPED,
297 next,
298 ),
299 ]).toEqual([Fixtures.CAPPED_ASK, Fixtures.CAPPED_ASK])
300 })
301
302 test('nor does the line of a deny rule that held', async () => {
303 const next = Fixtures.rechecked(
304 [
305 Fixtures.linkOf('easy', 'user', Fixtures.ALLOWED),
306 Fixtures.linkOf('engine', 'core', Fixtures.RULE_DENY),
307 ],
308 Fixtures.RULE_DENY,
309 )
310
311 expect([
312 await Fixtures.handlersRegistered()('tool.check', 'hook')(
313 Fixtures.THROWING,
314 Fixtures.CHECKED,
315 next,
316 ),
317 await Fixtures.handlersRegistered()('tool.check', 'hook')(
318 Fixtures.UNREADABLE,
319 Fixtures.CHECKED,
320 next,
321 ),
322 ]).toEqual([Fixtures.RULE_DENY, Fixtures.RULE_DENY])
323 })
324
325 test('the list hook starts its two runs together', async () => {
326 const order: string[] = []
327
328 const listed = Hooks.toolsListed(
329 Fixtures.ALLOWLIST,
330 Fixtures.JUDGED,
331 Fixtures.started(text => {
332 order.push(text)
333 }),
334 )
335
336 expect(order, 'both are started before either settles').toEqual([
337 'chain',
338 'past append',
339 ])
340
341 expect((await listed).value).toEqual([...Fixtures.TOOLS])
342 })
343
344 test('a call that throws where it is made is a rejection', async () => {
345 await expect(
346 Hooks.attempted(Fixtures.THROWING.settings.read),
347 ).rejects.toThrow('settings unreadable')
348
349 expect(Hooks.quietly(Fixtures.THROWING.ui.log)).toBe(undefined)
350 })
351})