fix(subagent): finalize Codex provider composition

This commit is contained in:
pku-xht
2026-08-04 18:38:05 +08:00
parent 8a24a68333
commit c09b20f96b
31 changed files with 270 additions and 405 deletions

View File

@@ -0,0 +1,53 @@
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import {
LOADER_SMOKE_TEST_TIMEOUT_MS,
runLoaderSmoke,
} from '@deepseek-ai/dsh-loader-smoke'
const fixtureDir = fileURLToPath(new URL(
'../../../../examples/acp-agent/tests/fixtures/subagent/subagent-codex/',
import.meta.url,
))
const driver = join(fixtureDir, 'driver.ts')
const configPath = join(fixtureDir, 'cordis.yml')
const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
describe('Codex provider public Loader composition', () => {
it('loads the opt-in package and foreground tool without starting Codex', async () => {
const { stdout, stderr } = await runLoaderSmoke({
label: 'subagent-codex Loader composition',
tempDirPrefix: 'dsh-subagent-codex-loader-',
binScript: driver,
libBinScript: driver,
configPath,
tsconfigPath: repoTsconfig,
env: {
// Loading the optional package must not probe or start a Codex binary.
PATH: '',
},
})
expect(stderr).toBe('')
expect(JSON.parse(stdout)).toEqual({
providers: ['codex'],
provider: {
name: 'codex',
capabilities: {
outputSchema: false,
depthLimit: false,
toolFilter: false,
persona: false,
},
inheritsParentContext: false,
},
tool: {
name: 'subagent_codex',
parameterNames: ['description', 'prompt'],
required: ['description', 'prompt'],
},
starts: 0,
})
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
})

View File

@@ -172,8 +172,7 @@ describe('real @openai/codex 0.146.0 product', () => {
await expectQuiescent(harness.handles)
}, 20_000)
it('declines a real app-server command approval without executing the command', async () => {
const sentinel = 'REAL_CODEX_APPROVAL_DECLINED'
it('cancels a real app-server command approval without executing the command', async () => {
const { harness, fixture } = await realHarness([
{
kind: 'functionCall',
@@ -184,7 +183,6 @@ describe('real @openai/codex 0.146.0 product', () => {
justification: 'exercise the unattended approval boundary',
},
},
{ kind: 'complete', text: sentinel },
])
const sideEffect = join(harness.workspace, 'approval-side-effect')
const run = await harness.ctx.subagents.start('codex', {
@@ -193,20 +191,17 @@ describe('real @openai/codex 0.146.0 product', () => {
signal: new AbortController().signal,
})
await expect(run.result).resolves.toEqual({
output: [{ type: 'text', text: sentinel }],
stopReason: 'completed',
output: [],
stopReason: 'error',
})
await run.dispose()
expect(existsSync(sideEffect)).toBe(false)
expect(fixture.requests).toHaveLength(2)
expect(fixture.requests).toHaveLength(1)
const tools = fixture.requests[0]!.body.tools as Array<Record<string, unknown>>
expect(tools).toEqual(expect.arrayContaining([
expect.objectContaining({ type: 'function', name: 'exec_command' }),
]))
const followup = JSON.stringify(fixture.requests[1]!.body)
expect(followup).toContain('call_fixture')
expect(followup).toContain('rejected by user')
expect(fixture.requests.every(requestEntry =>
requestEntry.headers.authorization === 'Bearer dsh-fake-openai-key',
)).toBe(true)

View File

@@ -417,6 +417,25 @@ describe('CodexAppServerWire', () => {
wire.close()
})
it('maps only an explicit context-window failure to max-tokens', async () => {
const { child, wire } = await initializeWire()
const result = wire.runTurn(['task'], new AbortController().signal, () => false)
const turnStart = await child.peer.nextMethod('turn/start')
child.peer.respond(turnStart, { turn: { id: 'turn-1' } })
child.peer.send(
agentMessage('partial answer', null),
turnCompleted('failed', 'turn-1', 'thread-1', {
message: 'too much context',
codexErrorInfo: 'contextWindowExceeded',
}),
)
await expect(result).resolves.toEqual({
output: [{ type: 'text', text: 'partial answer' }],
stopReason: 'max-tokens',
})
wire.close()
})
it('rejects invalid handshake, thread, and turn response shapes', async () => {
{
const child = fakeChild()
@@ -516,7 +535,7 @@ describe('CodexAppServerWire', () => {
wire.close()
})
it('answers all four unattended request classes without granting authority', async () => {
it('answers all five unattended request classes without granting authority', async () => {
const { child, wire } = await initializeWire()
const result = wire.runTurn(['task'], new AbortController().signal, () => false)
const turnStart = await child.peer.nextMethod('turn/start')
@@ -524,10 +543,14 @@ describe('CodexAppServerWire', () => {
child.peer.send({
id: 'command',
method: 'item/commandExecution/requestApproval',
params: { threadId: 'thread-1', turnId: 'turn-1' },
params: {
threadId: 'thread-1',
turnId: 'turn-1',
availableDecisions: ['decline', 'cancel'],
},
})
expect(await child.peer.nextResponse('command')).toMatchObject({
result: { decision: 'decline' },
result: { decision: 'cancel' },
})
child.peer.respond(turnStart, { turn: { id: 'turn-1' } })
@@ -536,6 +559,16 @@ describe('CodexAppServerWire', () => {
{
id: 'file',
method: 'item/fileChange/requestApproval',
params: {
threadId: 'thread-1',
turnId: 'turn-1',
availableDecisions: ['decline'],
},
result: { decision: 'decline' },
},
{
id: 'file-default',
method: 'item/fileChange/requestApproval',
params: { threadId: 'thread-1', turnId: 'turn-1' },
result: { decision: 'decline' },
},
@@ -545,6 +578,12 @@ describe('CodexAppServerWire', () => {
params: { threadId: 'thread-1', turnId: 'turn-1' },
result: { permissions: {}, scope: 'turn' },
},
{
id: 'user-input',
method: 'item/tool/requestUserInput',
params: { threadId: 'thread-1', turnId: 'turn-1', questions: [] },
result: { answers: {} },
},
{
id: 'mcp',
method: 'mcpServer/elicitation/request',
@@ -568,9 +607,27 @@ describe('CodexAppServerWire', () => {
for (const serverRequest of [
{
id: 'unknown',
method: 'item/tool/requestUserInput',
method: 'future/request',
params: { threadId: 'thread-1', turnId: 'turn-1' },
},
{
id: 'approval',
method: 'item/commandExecution/requestApproval',
params: {
threadId: 'thread-1',
turnId: 'turn-1',
availableDecisions: ['accept'],
},
},
{
id: 'malformed-approval',
method: 'item/fileChange/requestApproval',
params: {
threadId: 'thread-1',
turnId: 'turn-1',
availableDecisions: 'decline',
},
},
{
id: 'thread',
method: 'item/fileChange/requestApproval',