fix: address codex review round 2

Translate ctx.bash.run() REJECTIONS into the SEARCH_* taxonomy. The seam
contract has run() reject for infrastructure failures (a pre-aborted
signal, an unusable/deleted session workdir, a missing shell); the bare
await let those escape as plain Errors, so the tool registry produced
isError results without the structured SearchError { name, code } the
package documents. A pre-aborted spec.signal now maps to SEARCH_ABORTED
and any other start failure to SEARCH_FAILED, original error chained as
cause. Covered by fake-executor tests for both branches plus real-executor
integration tests pinning the exact pre-aborted-signal and deleted-cwd
paths.
This commit is contained in:
Dudu-0223
2026-07-09 21:28:42 +08:00
parent e94305d99e
commit 590f520949
3 changed files with 61 additions and 2 deletions

View File

@@ -158,4 +158,27 @@ describe.skipIf(!hasRg)('search tools over the real bash executor + real rg', ()
}
})
})
describe('bash-start infrastructure failures stay in the SEARCH_* taxonomy', () => {
it('a pre-aborted exec.signal (real executor rejects before spawn) is SEARCH_ABORTED', async () => {
const controller = new AbortController()
controller.abort()
const result = await ctx.tools.execute({
callId: CallId(`it-${++callCounter}`),
name: 'grep',
arguments: { pattern: 'x' },
signal: controller.signal,
})
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ name: 'SearchError', code: 'SEARCH_ABORTED' })
})
it('an unusable session cwd (spawn failure) is SEARCH_FAILED', async () => {
const gone = join(dir, 'deleted-session-dir')
const result = await call('glob', { pattern: '*' }, { session: { header: { id: 'session-int', cwd: gone } } })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ name: 'SearchError', code: 'SEARCH_FAILED' })
expect(text(result)).toContain('could not start')
})
})
})

View File

@@ -279,6 +279,27 @@ describe('workdir derivation and signal forwarding', () => {
expect(result.error).toMatchObject({ code: 'SEARCH_ABORTED' })
expect(text(result)).toContain('timed out after 1234ms')
})
it('translates a run() rejection under a pre-aborted signal into SEARCH_ABORTED', async () => {
// The seam contract: run() REJECTS for a pre-aborted signal (it never
// spawns). The plain rejection must not escape the SEARCH_* taxonomy.
const { ctx, bash } = await setup()
const controller = new AbortController()
controller.abort()
bash.handler = () => { throw new Error('aborted before spawn') }
const result = await call(ctx, 'grep', { pattern: 'x' }, { signal: controller.signal })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ name: 'SearchError', code: 'SEARCH_ABORTED' })
})
it('translates a run() rejection without an abort (unusable workdir) into SEARCH_FAILED', async () => {
const { ctx, bash } = await setup()
bash.handler = () => { throw new Error('spawn bash ENOENT') }
const result = await call(ctx, 'glob', { pattern: '*' })
expect(result.isError).toBe(true)
expect(result.error).toMatchObject({ name: 'SearchError', code: 'SEARCH_FAILED' })
expect(text(result)).toContain('could not start')
})
})
describe('exit semantics and failure classification', () => {