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
| 1 | import { describe, expect, test, tier } from 'claude-code/testing' |
| 2 | |
| 3 | import Hooks from '../hooks' |
| 4 | import Fixtures from './fixtures' |
| 5 | |
| 6 | tier('prepend') |
| 7 | |
| 8 | describe('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 | }) |