fix(hooks): reject invalid matcher regexes
This commit is contained in:
@@ -44,17 +44,22 @@ function writeConfig(hooks: unknown, scripts: Record<string, string> = {}): stri
|
||||
return dir
|
||||
}
|
||||
|
||||
async function harness(configDir: string, adapter: MockAdapter): Promise<Context> {
|
||||
return (await harnessWithFiber(configDir, adapter)).ctx
|
||||
async function harness(configDir: string, adapter: MockAdapter, beforeHooks?: (ctx: Context) => void): Promise<Context> {
|
||||
return (await harnessWithFiber(configDir, adapter, beforeHooks)).ctx
|
||||
}
|
||||
|
||||
/** {@link harness}, also exposing the bridge's fiber for tests that dispose it. */
|
||||
async function harnessWithFiber(configDir: string, adapter: MockAdapter): Promise<{ ctx: Context; hooks: Fiber }> {
|
||||
async function harnessWithFiber(
|
||||
configDir: string,
|
||||
adapter: MockAdapter,
|
||||
beforeHooks?: (ctx: Context) => void,
|
||||
): Promise<{ ctx: Context; hooks: Fiber }> {
|
||||
const ctx = new Context()
|
||||
await mountAgentLoopTestDependencies(ctx)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(LocalSubprocessService)
|
||||
await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 })
|
||||
beforeHooks?.(ctx)
|
||||
const hooks = await ctx.plugin(HooksClaude, { configPath: join(configDir, 'hooks.json') })
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
return { ctx, hooks }
|
||||
@@ -360,6 +365,25 @@ describe('hooks-claude bridge — load resilience', () => {
|
||||
expect(adapter.requests).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('an invalid regex matcher is reported and registers no hooks', async () => {
|
||||
const dir = writeConfig({
|
||||
UserPromptSubmit: [{ hooks: [{ type: 'command', command: 'exit 2' }] }],
|
||||
PreToolUse: [{ matcher: '(', hooks: [{ type: 'command', command: 'exit 2' }] }],
|
||||
})
|
||||
const adapter = new MockAdapter([textResponse('fine')])
|
||||
const warn = vi.fn()
|
||||
const ctx = await harness(dir, adapter, (ctx) => { ctx.logger.warn = warn as never })
|
||||
const agent = ctx.agentLoop.create(SessionId('invalid-claude-matcher'), { provider: 'mock', model: 'mock' })
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
expect(adapter.requests).toHaveLength(1)
|
||||
expect(events(agent).some(event => event.type === 'hook/invoked')).toBe(false)
|
||||
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining(
|
||||
'invalid claude regex matcher "(" on event "PreToolUse"',
|
||||
))
|
||||
})
|
||||
|
||||
it('disposing the bridge fiber removes its listeners (HMR safety)', async () => {
|
||||
// A BLOCKING UserPromptSubmit hook: if the listener leaked past dispose it
|
||||
// would veto the prompt (0 model requests) and log a hook/invoked. Build the
|
||||
|
||||
@@ -63,4 +63,10 @@ describe('parseClaudeConfig', () => {
|
||||
const { config } = parseClaudeConfig({ Stop: [{ hooks: [{ type: 'command', command: 's.sh' }] }] })
|
||||
expect('matcher' in config.Stop![0]!).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects an invalid regex matcher with its event name', () => {
|
||||
expect(() => parseClaudeConfig({
|
||||
PreToolUse: [{ matcher: '(', hooks: [{ type: 'command', command: 'x.sh' }] }],
|
||||
})).toThrow('invalid claude regex matcher "(" on event "PreToolUse"')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user