@namzu/sdk 0.4.3 → 0.4.5

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/dist/bridge/tools/connector/adapter.d.ts +2 -2
  3. package/dist/bus/index.d.ts +3 -1
  4. package/dist/bus/index.d.ts.map +1 -1
  5. package/dist/bus/index.js +18 -11
  6. package/dist/bus/index.js.map +1 -1
  7. package/dist/config/runtime.d.ts +28 -28
  8. package/dist/probe/context.d.ts +8 -0
  9. package/dist/probe/context.d.ts.map +1 -0
  10. package/dist/probe/context.js +7 -0
  11. package/dist/probe/context.js.map +1 -0
  12. package/dist/probe/errors.d.ts +12 -0
  13. package/dist/probe/errors.d.ts.map +1 -0
  14. package/dist/probe/errors.js +21 -0
  15. package/dist/probe/errors.js.map +1 -0
  16. package/dist/probe/index.d.ts +5 -0
  17. package/dist/probe/index.d.ts.map +1 -0
  18. package/dist/probe/index.js +4 -0
  19. package/dist/probe/index.js.map +1 -0
  20. package/dist/probe/registry.d.ts +24 -0
  21. package/dist/probe/registry.d.ts.map +1 -0
  22. package/dist/probe/registry.js +228 -0
  23. package/dist/probe/registry.js.map +1 -0
  24. package/dist/probe/registry.test.d.ts +7 -0
  25. package/dist/probe/registry.test.d.ts.map +1 -0
  26. package/dist/probe/registry.test.js +310 -0
  27. package/dist/probe/registry.test.js.map +1 -0
  28. package/dist/provider/instrumentation.d.ts +9 -0
  29. package/dist/provider/instrumentation.d.ts.map +1 -0
  30. package/dist/provider/instrumentation.js +104 -0
  31. package/dist/provider/instrumentation.js.map +1 -0
  32. package/dist/provider/instrumentation.test.d.ts +2 -0
  33. package/dist/provider/instrumentation.test.d.ts.map +1 -0
  34. package/dist/provider/instrumentation.test.js +152 -0
  35. package/dist/provider/instrumentation.test.js.map +1 -0
  36. package/dist/public-runtime.d.ts +5 -0
  37. package/dist/public-runtime.d.ts.map +1 -1
  38. package/dist/public-runtime.js +8 -0
  39. package/dist/public-runtime.js.map +1 -1
  40. package/dist/public-types.d.ts +3 -0
  41. package/dist/public-types.d.ts.map +1 -1
  42. package/dist/runtime/query/events.d.ts +3 -1
  43. package/dist/runtime/query/events.d.ts.map +1 -1
  44. package/dist/runtime/query/events.js +6 -1
  45. package/dist/runtime/query/events.js.map +1 -1
  46. package/dist/runtime/query/executor.d.ts +3 -1
  47. package/dist/runtime/query/executor.d.ts.map +1 -1
  48. package/dist/runtime/query/executor.js +30 -1
  49. package/dist/runtime/query/executor.js.map +1 -1
  50. package/dist/types/bus/index.d.ts +46 -2
  51. package/dist/types/bus/index.d.ts.map +1 -1
  52. package/dist/types/doctor/check.d.ts +41 -0
  53. package/dist/types/doctor/check.d.ts.map +1 -0
  54. package/dist/types/doctor/check.js +2 -0
  55. package/dist/types/doctor/check.js.map +1 -0
  56. package/dist/types/doctor/index.d.ts +2 -0
  57. package/dist/types/doctor/index.d.ts.map +1 -0
  58. package/dist/types/doctor/index.js +2 -0
  59. package/dist/types/doctor/index.js.map +1 -0
  60. package/dist/types/probe/event-kind.d.ts +6 -0
  61. package/dist/types/probe/event-kind.d.ts.map +1 -0
  62. package/dist/types/probe/event-kind.js +2 -0
  63. package/dist/types/probe/event-kind.js.map +1 -0
  64. package/dist/types/probe/event-of.d.ts +5 -0
  65. package/dist/types/probe/event-of.d.ts.map +1 -0
  66. package/dist/types/probe/event-of.js +2 -0
  67. package/dist/types/probe/event-of.js.map +1 -0
  68. package/dist/types/probe/index.d.ts +4 -0
  69. package/dist/types/probe/index.d.ts.map +1 -0
  70. package/dist/types/probe/index.js +2 -0
  71. package/dist/types/probe/index.js.map +1 -0
  72. package/dist/types/probe/registry.d.ts +27 -0
  73. package/dist/types/probe/registry.d.ts.map +1 -0
  74. package/dist/types/probe/registry.js +2 -0
  75. package/dist/types/probe/registry.js.map +1 -0
  76. package/dist/types/provider/interface.d.ts +10 -0
  77. package/dist/types/provider/interface.d.ts.map +1 -1
  78. package/dist/vault/instrumentation.d.ts +11 -0
  79. package/dist/vault/instrumentation.d.ts.map +1 -0
  80. package/dist/vault/instrumentation.js +32 -0
  81. package/dist/vault/instrumentation.js.map +1 -0
  82. package/dist/vault/instrumentation.test.d.ts +2 -0
  83. package/dist/vault/instrumentation.test.d.ts.map +1 -0
  84. package/dist/vault/instrumentation.test.js +80 -0
  85. package/dist/vault/instrumentation.test.js.map +1 -0
  86. package/package.json +1 -1
  87. package/src/bus/index.ts +21 -10
  88. package/src/probe/context.ts +14 -0
  89. package/src/probe/errors.ts +27 -0
  90. package/src/probe/index.ts +4 -0
  91. package/src/probe/registry.test.ts +480 -0
  92. package/src/probe/registry.ts +276 -0
  93. package/src/provider/instrumentation.test.ts +192 -0
  94. package/src/provider/instrumentation.ts +139 -0
  95. package/src/public-runtime.ts +22 -0
  96. package/src/public-types.ts +3 -0
  97. package/src/runtime/query/events.ts +6 -1
  98. package/src/runtime/query/executor.ts +34 -0
  99. package/src/types/bus/index.ts +54 -2
  100. package/src/types/doctor/check.ts +53 -0
  101. package/src/types/doctor/index.ts +9 -0
  102. package/src/types/probe/event-kind.ts +8 -0
  103. package/src/types/probe/event-of.ts +3 -0
  104. package/src/types/probe/index.ts +11 -0
  105. package/src/types/probe/registry.ts +36 -0
  106. package/src/types/provider/interface.ts +12 -0
  107. package/src/vault/instrumentation.test.ts +98 -0
  108. package/src/vault/instrumentation.ts +56 -0
@@ -0,0 +1,480 @@
1
+ /**
2
+ * Ratified §9 of docs.local/sessions/ses_007-probe-and-doctor/design.md.
3
+ * These tests pin the contract documented there — not an internal
4
+ * implementation detail. If the semantics change, update §9 first.
5
+ */
6
+
7
+ import { describe, expect, it, vi } from 'vitest'
8
+
9
+ import type { Logger } from '../utils/logger.js'
10
+
11
+ import { buildProbeContext } from './context.js'
12
+ import { ProbeNameCollisionError } from './errors.js'
13
+ import { createProbeRegistry } from './registry.js'
14
+
15
+ function makeLogger(): Logger {
16
+ const self = {
17
+ info: vi.fn(),
18
+ warn: vi.fn(),
19
+ error: vi.fn(),
20
+ debug: vi.fn(),
21
+ child: vi.fn(),
22
+ } as unknown as Logger
23
+ ;(self as { child: (ctx: unknown) => Logger }).child = vi.fn(() => self)
24
+ return self
25
+ }
26
+
27
+ describe('ProbeRegistry — typed dispatch', () => {
28
+ it('fires a typed probe when the event kind matches', () => {
29
+ const reg = createProbeRegistry()
30
+ const seen: string[] = []
31
+ reg.on('tool_executing', (event) => {
32
+ seen.push(event.toolName)
33
+ })
34
+ reg.dispatch(
35
+ {
36
+ type: 'tool_executing',
37
+ runId: 'run_1' as never,
38
+ toolName: 'fs.read',
39
+ input: {},
40
+ } as never,
41
+ buildProbeContext(),
42
+ )
43
+ expect(seen).toEqual(['fs.read'])
44
+ })
45
+
46
+ it('does not fire a typed probe for a different event kind', () => {
47
+ const reg = createProbeRegistry()
48
+ const handler = vi.fn()
49
+ reg.on('tool_executing', handler)
50
+ reg.dispatch(
51
+ { type: 'tool_completed', runId: 'r' as never, toolName: 't', result: 'ok' } as never,
52
+ buildProbeContext(),
53
+ )
54
+ expect(handler).not.toHaveBeenCalled()
55
+ })
56
+
57
+ it('supports array-of-kinds registration', () => {
58
+ const reg = createProbeRegistry()
59
+ const seen: string[] = []
60
+ reg.on(['tool_executing', 'tool_completed'], (event) => {
61
+ seen.push(event.type)
62
+ })
63
+ reg.dispatch(
64
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
65
+ buildProbeContext(),
66
+ )
67
+ reg.dispatch(
68
+ { type: 'tool_completed', runId: 'r' as never, toolName: 't', result: 'ok' } as never,
69
+ buildProbeContext(),
70
+ )
71
+ expect(seen).toEqual(['tool_executing', 'tool_completed'])
72
+ })
73
+
74
+ it('applies the where filter before the handler', () => {
75
+ const reg = createProbeRegistry()
76
+ const handler = vi.fn()
77
+ reg.on('tool_executing', handler, {
78
+ where: (event) => event.toolName === 'fs.write',
79
+ })
80
+ reg.dispatch(
81
+ { type: 'tool_executing', runId: 'r' as never, toolName: 'fs.read', input: {} } as never,
82
+ buildProbeContext(),
83
+ )
84
+ expect(handler).not.toHaveBeenCalled()
85
+ reg.dispatch(
86
+ { type: 'tool_executing', runId: 'r' as never, toolName: 'fs.write', input: {} } as never,
87
+ buildProbeContext(),
88
+ )
89
+ expect(handler).toHaveBeenCalledTimes(1)
90
+ })
91
+ })
92
+
93
+ describe('ProbeRegistry — ordering', () => {
94
+ it('fires probes in ascending priority; ties break by registration order', () => {
95
+ const reg = createProbeRegistry()
96
+ const order: string[] = []
97
+ reg.on('tool_executing', () => order.push('a'), { priority: 10, name: 'a' })
98
+ reg.on('tool_executing', () => order.push('b'), { priority: 5, name: 'b' })
99
+ reg.on('tool_executing', () => order.push('c'), { priority: 10, name: 'c' })
100
+ reg.on('tool_executing', () => order.push('d'), { priority: 0, name: 'd' })
101
+ reg.dispatch(
102
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
103
+ buildProbeContext(),
104
+ )
105
+ expect(order).toEqual(['d', 'b', 'a', 'c'])
106
+ })
107
+
108
+ it('typed probes fire BEFORE the between-tier callback; catch-all fires AFTER', () => {
109
+ const reg = createProbeRegistry()
110
+ const order: string[] = []
111
+ reg.on('tool_executing', () => order.push('typed'))
112
+ reg.onAny(() => order.push('any'))
113
+ reg.dispatch(
114
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
115
+ buildProbeContext(),
116
+ () => order.push('between'),
117
+ )
118
+ expect(order).toEqual(['typed', 'between', 'any'])
119
+ })
120
+ })
121
+
122
+ describe('ProbeRegistry — name collision + override', () => {
123
+ it('throws ProbeNameCollisionError on duplicate name without override', () => {
124
+ const reg = createProbeRegistry()
125
+ reg.on('tool_executing', () => {}, { name: 'dup' })
126
+ expect(() => reg.on('tool_executing', () => {}, { name: 'dup' })).toThrow(
127
+ ProbeNameCollisionError,
128
+ )
129
+ })
130
+
131
+ it('allows replacing when override: true', () => {
132
+ const reg = createProbeRegistry()
133
+ const first = vi.fn()
134
+ const second = vi.fn()
135
+ reg.on('tool_executing', first, { name: 'x' })
136
+ reg.on('tool_executing', second, { name: 'x', override: true })
137
+ reg.dispatch(
138
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
139
+ buildProbeContext(),
140
+ )
141
+ expect(first).not.toHaveBeenCalled()
142
+ expect(second).toHaveBeenCalledTimes(1)
143
+ })
144
+
145
+ it('unsubscribes the previous entry on override so the name can be reused', () => {
146
+ const reg = createProbeRegistry()
147
+ reg.on('tool_executing', () => {}, { name: 'x' })
148
+ reg.on('tool_executing', () => {}, { name: 'x', override: true })
149
+ expect(() => reg.on('tool_executing', () => {}, { name: 'x' })).toThrow(ProbeNameCollisionError)
150
+ })
151
+ })
152
+
153
+ describe('ProbeRegistry — throw isolation', () => {
154
+ it('a throwing probe does not suppress later probes', () => {
155
+ const reg = createProbeRegistry()
156
+ reg.setLogger(makeLogger())
157
+ const seen: string[] = []
158
+ reg.on(
159
+ 'tool_executing',
160
+ () => {
161
+ throw new Error('boom')
162
+ },
163
+ { priority: 0, name: 'bad' },
164
+ )
165
+ reg.on('tool_executing', () => seen.push('ran'), { priority: 10, name: 'good' })
166
+ reg.dispatch(
167
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
168
+ buildProbeContext(),
169
+ )
170
+ expect(seen).toEqual(['ran'])
171
+ })
172
+
173
+ it('a throwing probe does not suppress the between-tier callback or the catch-all', () => {
174
+ const reg = createProbeRegistry()
175
+ reg.setLogger(makeLogger())
176
+ const order: string[] = []
177
+ reg.on(
178
+ 'tool_executing',
179
+ () => {
180
+ throw new Error('boom')
181
+ },
182
+ { name: 'bad' },
183
+ )
184
+ reg.onAny(() => order.push('any'))
185
+ reg.dispatch(
186
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
187
+ buildProbeContext(),
188
+ () => order.push('between'),
189
+ )
190
+ expect(order).toEqual(['between', 'any'])
191
+ })
192
+ })
193
+
194
+ describe('ProbeRegistry — frozen event boundary', () => {
195
+ it('freezes the event before fan-out so probes cannot mutate it', () => {
196
+ const reg = createProbeRegistry()
197
+ reg.on('tool_executing', (event) => {
198
+ expect(() => {
199
+ ;(event as { toolName: string }).toolName = 'mutated'
200
+ }).toThrow()
201
+ })
202
+ reg.dispatch(
203
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
204
+ buildProbeContext(),
205
+ )
206
+ })
207
+
208
+ it('later probes see the original event value, not a mutation from an earlier probe', () => {
209
+ const reg = createProbeRegistry()
210
+ reg.setLogger(makeLogger())
211
+ const tampered: string[] = []
212
+ reg.on(
213
+ 'tool_executing',
214
+ (event) => {
215
+ try {
216
+ ;(event as { toolName: string }).toolName = 'mutated'
217
+ } catch {
218
+ // expected — frozen
219
+ }
220
+ },
221
+ { priority: 0 },
222
+ )
223
+ reg.on(
224
+ 'tool_executing',
225
+ (event) => {
226
+ tampered.push(event.toolName)
227
+ },
228
+ { priority: 10 },
229
+ )
230
+ reg.dispatch(
231
+ { type: 'tool_executing', runId: 'r' as never, toolName: 'original', input: {} } as never,
232
+ buildProbeContext(),
233
+ )
234
+ expect(tampered).toEqual(['original'])
235
+ })
236
+ })
237
+
238
+ describe('ProbeRegistry — unsubscribe', () => {
239
+ it('unsub removes the probe from subsequent dispatches', () => {
240
+ const reg = createProbeRegistry()
241
+ const handler = vi.fn()
242
+ const unsub = reg.on('tool_executing', handler)
243
+ reg.dispatch(
244
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
245
+ buildProbeContext(),
246
+ )
247
+ unsub()
248
+ reg.dispatch(
249
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
250
+ buildProbeContext(),
251
+ )
252
+ expect(handler).toHaveBeenCalledTimes(1)
253
+ })
254
+
255
+ it('unsub on an array-kind probe removes it from every kind it was registered for', () => {
256
+ const reg = createProbeRegistry()
257
+ const handler = vi.fn()
258
+ const unsub = reg.on(['tool_executing', 'tool_completed'], handler)
259
+ unsub()
260
+ reg.dispatch(
261
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
262
+ buildProbeContext(),
263
+ )
264
+ reg.dispatch(
265
+ { type: 'tool_completed', runId: 'r' as never, toolName: 't', result: 'ok' } as never,
266
+ buildProbeContext(),
267
+ )
268
+ expect(handler).not.toHaveBeenCalled()
269
+ })
270
+ })
271
+
272
+ describe('ProbeRegistry — catch-all', () => {
273
+ it('onAny receives every event regardless of kind', () => {
274
+ const reg = createProbeRegistry()
275
+ const seen: string[] = []
276
+ reg.onAny((event) => seen.push(event.type))
277
+ reg.dispatch(
278
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
279
+ buildProbeContext(),
280
+ )
281
+ reg.dispatch(
282
+ { type: 'lock_acquired', lockId: 'lock_1' as never, filePath: '/x', owner: 'r' as never },
283
+ buildProbeContext(),
284
+ )
285
+ expect(seen).toEqual(['tool_executing', 'lock_acquired'])
286
+ })
287
+ })
288
+
289
+ describe('ProbeRegistry — ctx.isReplay', () => {
290
+ it('defaults to false when unset', () => {
291
+ const reg = createProbeRegistry()
292
+ let seenReplay: boolean | undefined
293
+ reg.on('tool_executing', (_event, ctx) => {
294
+ seenReplay = ctx.isReplay
295
+ })
296
+ reg.dispatch(
297
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
298
+ buildProbeContext(),
299
+ )
300
+ expect(seenReplay).toBe(false)
301
+ })
302
+
303
+ it('carries isReplay:true when the event is from a replayed run', () => {
304
+ const reg = createProbeRegistry()
305
+ let seenReplay: boolean | undefined
306
+ reg.on('tool_executing', (_event, ctx) => {
307
+ seenReplay = ctx.isReplay
308
+ })
309
+ reg.dispatch(
310
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
311
+ buildProbeContext({ isReplay: true }),
312
+ )
313
+ expect(seenReplay).toBe(true)
314
+ })
315
+ })
316
+
317
+ describe('ProbeRegistry — veto API', () => {
318
+ it('returns allow when no veto handlers registered', () => {
319
+ const reg = createProbeRegistry()
320
+ const outcome = reg.queryVeto(
321
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
322
+ buildProbeContext(),
323
+ )
324
+ expect(outcome.action).toBe('allow')
325
+ expect(outcome.probeName).toBeUndefined()
326
+ })
327
+
328
+ it('returns deny when a veto handler returns "deny"', () => {
329
+ const reg = createProbeRegistry()
330
+ reg.veto('tool_executing', () => 'deny', { name: 'fs-guard' })
331
+ const outcome = reg.queryVeto(
332
+ { type: 'tool_executing', runId: 'r' as never, toolName: 'fs.write', input: {} } as never,
333
+ buildProbeContext(),
334
+ )
335
+ expect(outcome.action).toBe('deny')
336
+ expect(outcome.probeName).toBe('fs-guard')
337
+ })
338
+
339
+ it('returns deny + reason when handler returns { action: "deny", reason }', () => {
340
+ const reg = createProbeRegistry()
341
+ reg.veto('tool_executing', () => ({ action: 'deny', reason: 'outside workspace' }), {
342
+ name: 'sandbox',
343
+ })
344
+ const outcome = reg.queryVeto(
345
+ { type: 'tool_executing', runId: 'r' as never, toolName: 'fs.write', input: {} } as never,
346
+ buildProbeContext(),
347
+ )
348
+ expect(outcome.action).toBe('deny')
349
+ expect(outcome.probeName).toBe('sandbox')
350
+ expect(outcome.reason).toBe('outside workspace')
351
+ })
352
+
353
+ it('first-deny-wins by ascending priority; subsequent veto handlers still run for audit', () => {
354
+ const reg = createProbeRegistry()
355
+ const audit: string[] = []
356
+ reg.veto(
357
+ 'tool_executing',
358
+ () => {
359
+ audit.push('high-priority-allow')
360
+ return 'allow'
361
+ },
362
+ { priority: 0, name: 'p0-allow' },
363
+ )
364
+ reg.veto(
365
+ 'tool_executing',
366
+ () => {
367
+ audit.push('mid-priority-deny')
368
+ return { action: 'deny', reason: 'first' }
369
+ },
370
+ { priority: 5, name: 'p5-deny' },
371
+ )
372
+ reg.veto(
373
+ 'tool_executing',
374
+ () => {
375
+ audit.push('low-priority-deny')
376
+ return { action: 'deny', reason: 'second' }
377
+ },
378
+ { priority: 10, name: 'p10-deny' },
379
+ )
380
+
381
+ const outcome = reg.queryVeto(
382
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
383
+ buildProbeContext(),
384
+ )
385
+ expect(outcome.action).toBe('deny')
386
+ expect(outcome.probeName).toBe('p5-deny')
387
+ expect(outcome.reason).toBe('first')
388
+ expect(audit).toEqual(['high-priority-allow', 'mid-priority-deny', 'low-priority-deny'])
389
+ })
390
+
391
+ it('a throwing veto handler defaults to allow for that probe; aggregate unaffected', () => {
392
+ const reg = createProbeRegistry()
393
+ reg.setLogger(makeLogger())
394
+ const audit: string[] = []
395
+ reg.veto(
396
+ 'tool_executing',
397
+ () => {
398
+ audit.push('throw')
399
+ throw new Error('boom')
400
+ },
401
+ { priority: 0, name: 'bad' },
402
+ )
403
+ reg.veto(
404
+ 'tool_executing',
405
+ () => {
406
+ audit.push('allow')
407
+ return 'allow'
408
+ },
409
+ { priority: 5, name: 'good' },
410
+ )
411
+
412
+ const outcome = reg.queryVeto(
413
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
414
+ buildProbeContext(),
415
+ )
416
+ expect(outcome.action).toBe('allow')
417
+ expect(audit).toEqual(['throw', 'allow'])
418
+ })
419
+
420
+ it('observe-tier dispatch is independent — veto registration does not fire on dispatch', () => {
421
+ const reg = createProbeRegistry()
422
+ const vetoCalls: number[] = []
423
+ reg.veto(
424
+ 'tool_executing',
425
+ () => {
426
+ vetoCalls.push(1)
427
+ return 'deny'
428
+ },
429
+ { name: 'v' },
430
+ )
431
+ reg.dispatch(
432
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
433
+ buildProbeContext(),
434
+ )
435
+ expect(vetoCalls).toEqual([])
436
+ })
437
+
438
+ it('honors where filter on veto registrations', () => {
439
+ const reg = createProbeRegistry()
440
+ reg.veto('tool_executing', () => 'deny', {
441
+ name: 'writes-only',
442
+ where: (event) => event.toolName.startsWith('fs.write'),
443
+ })
444
+
445
+ const allow = reg.queryVeto(
446
+ { type: 'tool_executing', runId: 'r' as never, toolName: 'fs.read', input: {} } as never,
447
+ buildProbeContext(),
448
+ )
449
+ const deny = reg.queryVeto(
450
+ { type: 'tool_executing', runId: 'r' as never, toolName: 'fs.write.x', input: {} } as never,
451
+ buildProbeContext(),
452
+ )
453
+ expect(allow.action).toBe('allow')
454
+ expect(deny.action).toBe('deny')
455
+ })
456
+
457
+ it('unsubscribing a veto handler removes it from subsequent queries', () => {
458
+ const reg = createProbeRegistry()
459
+ const unsub = reg.veto('tool_executing', () => 'deny', { name: 'g' })
460
+ const denied = reg.queryVeto(
461
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
462
+ buildProbeContext(),
463
+ )
464
+ expect(denied.action).toBe('deny')
465
+ unsub()
466
+ const allowed = reg.queryVeto(
467
+ { type: 'tool_executing', runId: 'r' as never, toolName: 't', input: {} } as never,
468
+ buildProbeContext(),
469
+ )
470
+ expect(allowed.action).toBe('allow')
471
+ })
472
+
473
+ it('name collision applies across observe + veto tiers', () => {
474
+ const reg = createProbeRegistry()
475
+ reg.on('tool_executing', () => {}, { name: 'shared' })
476
+ expect(() => reg.veto('tool_executing', () => 'allow', { name: 'shared' })).toThrow(
477
+ ProbeNameCollisionError,
478
+ )
479
+ })
480
+ })