243 lines
8.3 KiB
TypeScript
243 lines
8.3 KiB
TypeScript
import { execFile } from 'node:child_process'
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { delimiter, join, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { promisify } from 'node:util'
|
|
import { Context } from 'cordis'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
import SubagentService from '@deepseek-ai/dsh-subagent'
|
|
import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
|
|
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
|
import * as codex from '../src/index.ts'
|
|
import {
|
|
startResponsesFixture,
|
|
type ResponsesBehavior,
|
|
type ResponsesFixture,
|
|
} from './responses-fixture.ts'
|
|
|
|
const execFileAsync = promisify(execFile)
|
|
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
|
|
const codexBinDir = join(packageRoot, 'node_modules', '.bin')
|
|
const codexEntry = join(packageRoot, 'node_modules', '@openai', 'codex', 'bin', 'codex.js')
|
|
const codexPackage = JSON.parse(readFileSync(
|
|
join(packageRoot, 'node_modules', '@openai', 'codex', 'package.json'),
|
|
'utf8',
|
|
)) as { version: string }
|
|
|
|
const roots: string[] = []
|
|
const fixtures: ResponsesFixture[] = []
|
|
const contexts: Context[] = []
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
|
|
await Promise.all(fixtures.splice(0).map(fixture => fixture.close()))
|
|
for (const root of roots.splice(0)) {
|
|
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
|
|
}
|
|
})
|
|
|
|
interface RealHarness {
|
|
readonly ctx: Context
|
|
readonly handles: SubprocessHandle[]
|
|
readonly parent: Agent
|
|
readonly env: Record<string, string>
|
|
readonly workspace: string
|
|
}
|
|
|
|
async function realHarness(script: readonly ResponsesBehavior[]): Promise<{
|
|
readonly harness: RealHarness
|
|
readonly fixture: ResponsesFixture
|
|
}> {
|
|
const root = mkdtempSync(join(tmpdir(), 'dsh-codex-real-'))
|
|
roots.push(root)
|
|
const workspace = join(root, 'workspace')
|
|
const codexHome = join(root, 'codex-home')
|
|
const fixture = await startResponsesFixture(script)
|
|
fixtures.push(fixture)
|
|
mkdirSync(workspace)
|
|
mkdirSync(codexHome)
|
|
writeFileSync(join(codexHome, 'config.toml'), [
|
|
'model = "fixture-model"',
|
|
'model_provider = "fixture"',
|
|
'approval_policy = "on-request"',
|
|
'sandbox_mode = "read-only"',
|
|
'disable_response_storage = true',
|
|
'check_for_update_on_startup = false',
|
|
'',
|
|
'[model_providers.fixture]',
|
|
'name = "Fixture Responses"',
|
|
`base_url = "${fixture.baseUrl}"`,
|
|
'env_key = "OPENAI_API_KEY"',
|
|
'wire_api = "responses"',
|
|
'requires_openai_auth = false',
|
|
'',
|
|
'[analytics]',
|
|
'enabled = false',
|
|
'',
|
|
].join('\n'))
|
|
const env = {
|
|
OPENAI_API_KEY: 'dsh-fake-openai-key',
|
|
CODEX_HOME: codexHome,
|
|
HOME: root,
|
|
XDG_CONFIG_HOME: join(root, 'xdg'),
|
|
PATH: `${codexBinDir}${delimiter}${process.env.PATH ?? ''}`,
|
|
HTTP_PROXY: '',
|
|
HTTPS_PROXY: '',
|
|
ALL_PROXY: '',
|
|
NO_PROXY: '127.0.0.1,localhost',
|
|
}
|
|
const ctx = new Context()
|
|
contexts.push(ctx)
|
|
await ctx.plugin(SubagentService)
|
|
await ctx.plugin(LocalSubprocessService)
|
|
const handles: SubprocessHandle[] = []
|
|
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
|
|
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {
|
|
const handle = spawn(spec)
|
|
handles.push(handle)
|
|
return handle
|
|
})
|
|
await ctx.plugin(codex, { env, disposeGraceMs: 2_000 })
|
|
const parent = {
|
|
id: 'real-parent',
|
|
session: { header: { cwd: workspace } },
|
|
} as unknown as Agent
|
|
return { harness: { ctx, handles, parent, env, workspace }, fixture }
|
|
}
|
|
|
|
async function expectQuiescent(handles: readonly SubprocessHandle[]): Promise<void> {
|
|
expect(handles.length).toBeGreaterThan(0)
|
|
for (const handle of handles) {
|
|
await expect(handle.waitForExit()).resolves.toBe(true)
|
|
const outcome = await handle.done
|
|
expect(outcome).toHaveProperty('exitCode')
|
|
expect(outcome).toHaveProperty('signal')
|
|
}
|
|
}
|
|
|
|
function responseInputTexts(body: Record<string, unknown>): string[] {
|
|
if (!Array.isArray(body.input)) return []
|
|
return body.input.flatMap((item): string[] => {
|
|
if (item === null || typeof item !== 'object') return []
|
|
const content = (item as Record<string, unknown>).content
|
|
if (!Array.isArray(content)) return []
|
|
return content.flatMap((part): string[] => (
|
|
part !== null
|
|
&& typeof part === 'object'
|
|
&& typeof (part as Record<string, unknown>).text === 'string'
|
|
? [(part as Record<string, unknown>).text as string]
|
|
: []
|
|
))
|
|
})
|
|
}
|
|
|
|
describe('real @openai/codex 0.147.0 product', () => {
|
|
it('passes the exact task and fake authentication to local Responses and returns exact text', async () => {
|
|
const sentinel = 'REAL_CODEX_SENTINEL_0_147_0'
|
|
const task = 'Return the fixture sentinel exactly.'
|
|
const { harness, fixture } = await realHarness([
|
|
{ kind: 'complete', text: sentinel },
|
|
])
|
|
expect(codexPackage.version).toBe('0.147.0')
|
|
const version = await execFileAsync(process.execPath, [codexEntry, '--version'], {
|
|
env: { ...process.env, ...harness.env },
|
|
})
|
|
expect(version.stdout.trim()).toBe('codex-cli 0.147.0')
|
|
|
|
const run = await harness.ctx.subagents.start('codex', {
|
|
prompt: [{ type: 'text', text: task }],
|
|
parent: harness.parent,
|
|
signal: new AbortController().signal,
|
|
})
|
|
await expect(run.result).resolves.toEqual({
|
|
output: [{ type: 'text', text: sentinel }],
|
|
stopReason: 'completed',
|
|
})
|
|
await run.dispose()
|
|
|
|
expect(fixture.requests).toHaveLength(1)
|
|
const recorded = fixture.requests[0]!
|
|
expect(recorded.method).toBe('POST')
|
|
expect(recorded.path).toBe('/v1/responses')
|
|
expect(recorded.headers.authorization).toBe('Bearer dsh-fake-openai-key')
|
|
expect(responseInputTexts(recorded.body)).toContain(task)
|
|
await expectQuiescent(harness.handles)
|
|
}, 60_000)
|
|
|
|
it('cancels a real app-server command approval without executing the command', async () => {
|
|
const command = process.platform === 'win32'
|
|
? 'cmd /c type nul > approval-side-effect'
|
|
: 'touch approval-side-effect'
|
|
const commandCalls = [
|
|
{
|
|
name: 'exec_command',
|
|
arguments: {
|
|
cmd: command,
|
|
sandbox_permissions: 'require_escalated',
|
|
justification: 'exercise the unattended approval boundary',
|
|
},
|
|
},
|
|
{
|
|
name: 'shell_command',
|
|
arguments: {
|
|
command,
|
|
sandbox_permissions: 'require_escalated',
|
|
justification: 'exercise the unattended approval boundary',
|
|
},
|
|
},
|
|
] as const
|
|
const { harness, fixture } = await realHarness([
|
|
{
|
|
kind: 'advertisedFunctionCall',
|
|
choices: commandCalls,
|
|
},
|
|
])
|
|
const sideEffect = join(harness.workspace, 'approval-side-effect')
|
|
const run = await harness.ctx.subagents.start('codex', {
|
|
prompt: [{ type: 'text', text: 'Attempt the fixture command.' }],
|
|
parent: harness.parent,
|
|
signal: new AbortController().signal,
|
|
})
|
|
await expect(run.result).resolves.toEqual({
|
|
output: [],
|
|
stopReason: 'error',
|
|
})
|
|
await run.dispose()
|
|
|
|
expect(existsSync(sideEffect)).toBe(false)
|
|
expect(fixture.requests).toHaveLength(1)
|
|
const tools = fixture.requests[0]!.body.tools as Array<Record<string, unknown>>
|
|
expect(commandCalls.some(call => tools.some(tool => (
|
|
tool.type === 'function' && tool.name === call.name
|
|
)))).toBe(true)
|
|
expect(fixture.requests.every(requestEntry =>
|
|
requestEntry.headers.authorization === 'Bearer dsh-fake-openai-key',
|
|
)).toBe(true)
|
|
await expectQuiescent(harness.handles)
|
|
}, 60_000)
|
|
|
|
it('settles cancellation locally and leaves the real app-server tree quiescent', async () => {
|
|
const { harness, fixture } = await realHarness([{ kind: 'hold' }])
|
|
const controller = new AbortController()
|
|
const run = await harness.ctx.subagents.start('codex', {
|
|
prompt: [{ type: 'text', text: 'Wait for cancellation.' }],
|
|
parent: harness.parent,
|
|
signal: controller.signal,
|
|
})
|
|
await fixture.requestStarted
|
|
controller.abort(new Error('real product cancellation'))
|
|
await expect(run.result).resolves.toMatchObject({ stopReason: 'aborted' })
|
|
await run.dispose()
|
|
await expectQuiescent(harness.handles)
|
|
}, 60_000)
|
|
})
|