Merge remote-tracking branch 'origin/master' into codex/trim-ai-prose
# Conflicts: # docs/AGENTS.md # docs/config-catalog.md # packages/bash/bash-sandbox/src/index.ts # packages/bash/bash/src/session-mode.ts # packages/bash/tool-bash/README.md # packages/code-runtime/code-runtime-worker/README.md # packages/compact/compact/src/index.ts # packages/core/agent-core/README.md # packages/hooks/hooks-claude/src/config.ts # packages/hooks/hooks-claude/src/index.ts # packages/hooks/hooks-codex/src/config.ts # packages/hooks/hooks-codex/src/index.ts # packages/llm/llm/README.md # packages/session-persistence/session-persistence-jsonl/README.md # packages/session-persistence/session-persistence/README.md # packages/skill/skill-local/README.md # packages/support/acp-snapshot/README.md # packages/support/invariants/src/index.ts # packages/ui/acp/README.md # packages/ui/jsonrpc-agent/README.md # packages/ui/jsonrpc/README.md # packages/ui/permission/README.md # packages/ui/user-approval/README.md # packages/ui/user-interaction/README.md # packages/web/web-search-deepseek/README.md
This commit is contained in:
@@ -15,10 +15,9 @@ import * as HooksCodex from '@deepseek-ai/dsh-hooks-codex'
|
||||
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
|
||||
|
||||
/**
|
||||
* Full-loop Codex-bridge tests: scripted mock MODEL + REAL loop + REAL bash +
|
||||
* REAL `dsh-hooks-codex` running REAL shell scripts from a temp `hooks.json`.
|
||||
* Codex dialect specifics exercised here: regex matcher (substring), block-only
|
||||
* decisions, the five-event subset.
|
||||
* Full-loop Codex bridge tests with a mock model, the real loop and bash
|
||||
* executor, and shell hooks from a temporary config. Covers regex matching,
|
||||
* block-only decisions, and the five-event subset.
|
||||
*/
|
||||
|
||||
const dirs: string[] = []
|
||||
@@ -90,36 +89,30 @@ describe('hooks-codex bridge', () => {
|
||||
const result = events(agent).find(e => e.type === 'tool/result')
|
||||
expect(result?.type === 'tool/result' && result.data.isError).toBe(true)
|
||||
expect(result?.type === 'tool/result' && result.data.content.some(b => b.type === 'text' && b.text.includes('codex blocked it'))).toBe(true)
|
||||
// recorded under the codex dialect
|
||||
expect(events(agent).some(e => e.type === 'hook/invoked' && e.data.dialect === 'codex' && e.data.point === 'PreToolUse')).toBe(true)
|
||||
})
|
||||
|
||||
it('a Stop hook (exit 2) forces the turn to continue with the reason as steering', async () => {
|
||||
const dir = configDir()
|
||||
// Block exactly ONCE (a marker file), then allow — without a one-shot guard a
|
||||
// hook that always exits 2 would force-continue forever (the deferred
|
||||
// stop_hook_active loop-guard is the real fix; here we self-limit so the test
|
||||
// exercises the continue path without looping).
|
||||
// Block once with a marker; until the loop guard lands, an always-blocking
|
||||
// hook would never let this test finish.
|
||||
const marker = join(dir, 'fired')
|
||||
const cont = script(dir, 'cont.sh', `#!/usr/bin/env bash\nif [ -e "${marker}" ]; then exit 0; fi\ntouch "${marker}"\necho "keep going: address the goal" >&2\nexit 2\n`)
|
||||
writeHooks(dir, { Stop: [{ hooks: [{ type: 'command', command: cont }] }] })
|
||||
|
||||
// Step 1 has no tool calls → would stop; the Stop hook forces step 2.
|
||||
const adapter = new MockAdapter([textResponse('first answer'), textResponse('second answer after goal')])
|
||||
const ctx = await harness(dir, adapter)
|
||||
const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
// The Stop hook's reason became next-step steering → a second model request ran.
|
||||
expect(adapter.requests).toHaveLength(2)
|
||||
expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('keep going: address the goal')
|
||||
})
|
||||
|
||||
it('only the five Codex events are honored — a SubagentStop entry is ignored', async () => {
|
||||
it('only the five bridge-supported Codex events are honored — a SubagentStop entry is ignored', async () => {
|
||||
const dir = configDir()
|
||||
const s = script(dir, 'x.sh', '#!/usr/bin/env bash\nexit 2\n')
|
||||
// SubagentStop is NOT a Codex event; it must be dropped (no crash, no effect).
|
||||
writeHooks(dir, { SubagentStop: [{ hooks: [{ type: 'command', command: s }] }] })
|
||||
|
||||
const adapter = new MockAdapter([textResponse('fine')])
|
||||
@@ -127,7 +120,6 @@ describe('hooks-codex bridge', () => {
|
||||
const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
|
||||
agent.send([{ type: 'text', text: 'go' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
// Ran normally; the unknown event was dropped at parse.
|
||||
expect(adapter.requests).toHaveLength(1)
|
||||
})
|
||||
|
||||
@@ -193,8 +185,8 @@ describe('hooks-codex bridge', () => {
|
||||
// Disposal reaches quiescence only after the aborted run settles and the process is reaped, so
|
||||
// `kill(pid, 0)` must report ESRCH. Untracked fire-and-forget work would remain.
|
||||
expect(() => process.kill(pid, 0)).toThrow()
|
||||
// The aborted run resolves as a non-blocking error (runHook never rejects),
|
||||
// so the drained continuation must NOT have logged a failure.
|
||||
// runHook resolves an aborted run as a non-blocking error, so draining must
|
||||
// not log a rejected continuation.
|
||||
expect(warn).not.toHaveBeenCalledWith(expect.stringContaining('SessionStart hook failed'))
|
||||
})
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import { describe, expect, it } from 'vitest'
|
||||
import { parseCodexConfig, CODEX_EVENTS } from '@deepseek-ai/dsh-hooks-codex/src/config.ts'
|
||||
|
||||
describe('parseCodexConfig', () => {
|
||||
it('honors only the five Codex events, dropping unknown ones', () => {
|
||||
it('honors only the five bridge-supported Codex events, dropping the rest', () => {
|
||||
const { config } = parseCodexConfig({
|
||||
PreToolUse: [{ hooks: [{ type: 'command', command: 'a.sh' }] }],
|
||||
SubagentStop: [{ hooks: [{ type: 'command', command: 'b.sh' }] }], // not a Codex event
|
||||
Notification: [{ hooks: [{ type: 'command', command: 'c.sh' }] }], // not a Codex event
|
||||
SubagentStop: [{ hooks: [{ type: 'command', command: 'b.sh' }] }], // current Codex event, unsupported by this bridge
|
||||
Notification: [{ hooks: [{ type: 'command', command: 'c.sh' }] }], // unknown to current Codex
|
||||
})
|
||||
expect(Object.keys(config)).toEqual(['PreToolUse'])
|
||||
expect(CODEX_EVENTS).toContain('PreToolUse')
|
||||
@@ -18,7 +18,7 @@ describe('parseCodexConfig', () => {
|
||||
Stop: [{ hooks: [{ type: 'command', command: '${NOT_SUBSTITUTED}/s.sh', timeout: 10 }] }],
|
||||
UserPromptSubmit: [{ hooks: [{ type: 'command', command: 'u.sh', timeoutSec: 20 }] }],
|
||||
})
|
||||
// Codex does NO substitution — the literal ${…} survives.
|
||||
// The parser performs no config-time substitution; shell expansion happens later.
|
||||
expect(config.Stop).toEqual([{ hooks: [{ command: '${NOT_SUBSTITUTED}/s.sh', timeoutSec: 10 }] }])
|
||||
expect(config.UserPromptSubmit).toEqual([{ hooks: [{ command: 'u.sh', timeoutSec: 20 }] }])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user