fix(subagent): finalize Codex provider composition
This commit is contained in:
@@ -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)
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user