2026-08-15 03:25:43 +08:00
import { readFileSync } from 'node:fs'
2026-08-15 04:12:41 +08:00
import { dirname , resolve } from 'node:path'
2026-08-04 16:02:17 +08:00
import { PassThrough } from 'node:stream'
2026-08-15 03:25:43 +08:00
import { fileURLToPath } from 'node:url'
build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.
Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.
The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.
Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:06 +08:00
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
2026-08-15 03:25:43 +08:00
import * as yaml from 'js-yaml'
2026-08-04 16:02:17 +08:00
import { describe , expect , it , vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
2026-08-13 00:36:22 +08:00
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
2026-08-05 04:21:29 +08:00
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
2026-08-04 16:02:17 +08:00
import type {
SubprocessHandle ,
SubprocessOutcome ,
2026-08-18 02:56:44 +08:00
SubprocessSpawnSpec ,
2026-08-04 16:02:17 +08:00
} from '@deepseek-ai/dsh-subprocess'
2026-08-13 00:36:22 +08:00
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
2026-08-04 16:02:17 +08:00
import * as codex from '../src/index.ts'
import * as invariant from '../src/invariant.ts'
import {
2026-08-15 18:07:08 +08:00
CODEX_PERMISSION_MODES ,
DEFAULT_CODEX_PERMISSION_MODE ,
2026-08-05 07:03:04 +08:00
codexAppServerArgv ,
2026-08-04 16:02:17 +08:00
DEFAULT_DISPOSE_GRACE_MS ,
disposeCodexChild ,
startCodexRun ,
textTask ,
type CodexRunSpec ,
} from '../src/run.ts'
import { CodexAppServerWire } from '../src/wire.ts'
2026-08-15 19:33:03 +08:00
const { hostStderrWrite } = vi . hoisted ( ( ) = > ( {
hostStderrWrite : {
capture : false ,
failNext : false ,
chunks : [ ] as Buffer [ ] ,
} ,
} ) )
vi . mock ( 'node:fs' , async ( importOriginal ) = > {
const actual = await importOriginal < typeof import ( 'node:fs' ) > ( )
return {
. . . actual ,
2026-08-15 20:09:04 +08:00
writeFileSync (
2026-08-15 19:58:08 +08:00
fd : number ,
value : string | Uint8Array ,
2026-08-15 20:09:04 +08:00
) : void {
2026-08-15 19:33:03 +08:00
if ( fd === 2 && hostStderrWrite . capture ) {
if ( hostStderrWrite . failNext ) {
hostStderrWrite . failNext = false
throw Object . assign ( new Error ( 'host stderr broke' ) , { code : 'EIO' } )
}
const bytes = typeof value === 'string'
? Buffer . from ( value )
: Buffer . from ( value . buffer , value . byteOffset , value . byteLength )
2026-08-15 20:09:04 +08:00
hostStderrWrite . chunks . push ( bytes )
return
2026-08-15 19:33:03 +08:00
}
2026-08-15 20:09:04 +08:00
actual . writeFileSync ( fd , value )
2026-08-15 19:33:03 +08:00
} ,
}
} )
2026-08-04 16:02:17 +08:00
type JsonObject = Record < string , unknown >
2026-08-15 03:25:43 +08:00
const CODEX_VERSION = '0.147.0'
const CODEX_PLATFORM_PACKAGES = [
'@openai/codex-darwin-arm64' ,
'@openai/codex-darwin-x64' ,
'@openai/codex-linux-arm64' ,
'@openai/codex-linux-x64' ,
'@openai/codex-win32-arm64' ,
'@openai/codex-win32-x64' ,
] as const
2026-08-04 16:02:17 +08:00
const fakeParent = {
id : 'parent' ,
session : { header : { cwd : process.cwd ( ) } } ,
} as unknown as Agent
function request (
prompt : ContentBlock [ ] = [ { type : 'text' , text : 'do the task' } ] ,
signal = new AbortController ( ) . signal ,
) {
return { prompt , parent : fakeParent , signal }
}
async function nextTask ( ) : Promise < void > {
await new Promise < void > ( ( resolve ) = > { setImmediate ( resolve ) } )
}
class ProtocolPeer {
private buffer = ''
private readonly frames : JsonObject [ ] = [ ]
private readonly wakeups = new Set < ( ) = > void > ( )
constructor (
input : PassThrough ,
private readonly output : PassThrough ,
) {
input . on ( 'data' , ( chunk : Buffer | string ) = > {
this . buffer += chunk . toString ( )
for ( ; ; ) {
const newline = this . buffer . indexOf ( '\n' )
if ( newline < 0 ) break
const line = this . buffer . slice ( 0 , newline )
this . buffer = this . buffer . slice ( newline + 1 )
if ( line . trim ( ) . length > 0 ) this . frames . push ( JSON . parse ( line ) as JsonObject )
}
for ( const wake of this . wakeups ) wake ( )
this . wakeups . clear ( )
} )
}
async next ( predicate : ( frame : JsonObject ) = > boolean ) : Promise < JsonObject > {
for ( ; ; ) {
const index = this . frames . findIndex ( predicate )
if ( index >= 0 ) return this . frames . splice ( index , 1 ) [ 0 ] !
await new Promise < void > ( ( resolve ) = > { this . wakeups . add ( resolve ) } )
}
}
nextMethod ( method : string ) : Promise < JsonObject > {
return this . next ( frame = > frame . method === method )
}
nextResponse ( id : unknown ) : Promise < JsonObject > {
return this . next ( frame = > frame . id === id && frame . method === undefined )
}
send ( . . . frames : readonly JsonObject [ ] ) : void {
this . output . write ( ` ${ frames . map ( frame = > JSON . stringify ( frame ) ) . join ( '\n' ) } \ n ` )
}
respond ( requestFrame : JsonObject , result : unknown ) : void {
this . send ( { id : requestFrame.id , result } )
}
}
interface FakeChildOptions {
readonly pid? : number
readonly exitOnTerminate? : boolean
readonly doneError? : Error
2026-08-18 03:40:11 +08:00
readonly waitForExitError? : Error
2026-08-04 16:02:17 +08:00
}
interface FakeChild {
readonly handle : SubprocessHandle
readonly peer : ProtocolPeer
readonly fromChild : PassThrough
readonly toChild : PassThrough
2026-08-15 18:07:08 +08:00
readonly stderr : PassThrough
2026-08-04 16:02:17 +08:00
readonly settle : ( outcome? : SubprocessOutcome ) = > void
readonly fail : ( error : Error ) = > void
2026-08-15 06:03:47 +08:00
readonly setStderr : ( text : string ) = > void
2026-08-04 16:02:17 +08:00
readonly terminate : ( ) = > void
readonly waitForExit : ( signal? : AbortSignal ) = > Promise < boolean >
}
function fakeChild ( options : FakeChildOptions = { } ) : FakeChild {
const fromChild = new PassThrough ( )
const toChild = new PassThrough ( )
2026-08-15 18:07:08 +08:00
const stderr = new PassThrough ( )
2026-08-04 16:02:17 +08:00
const peer = new ProtocolPeer ( toChild , fromChild )
let exited = false
let resolveDone ! : ( outcome : SubprocessOutcome ) = > void
let rejectDone ! : ( error : Error ) = > void
const done = new Promise < SubprocessOutcome > ( ( resolve , reject ) = > {
resolveDone = resolve
rejectDone = reject
} )
const settle = (
outcome : SubprocessOutcome = { exitCode : 0 , signal : null } ,
) : void = > {
if ( exited ) return
exited = true
resolveDone ( outcome )
}
const fail = ( error : Error ) : void = > {
if ( exited ) return
exited = true
rejectDone ( error )
}
if ( options . doneError !== undefined ) fail ( options . doneError )
const terminate = vi . fn ( ( ) = > {
if ( options . exitOnTerminate !== false ) settle ( )
} )
const waitForExit = vi . fn ( async ( signal? : AbortSignal ) = > {
2026-08-18 03:40:11 +08:00
if ( options . waitForExitError !== undefined ) {
throw options . waitForExitError
}
2026-08-04 16:02:17 +08:00
if ( exited ) return true
if ( signal === undefined ) {
await done . catch ( ( ) = > { } )
return true
}
return await new Promise < boolean > ( ( resolve ) = > {
const onAbort = ( ) : void = > { resolve ( false ) }
signal . addEventListener ( 'abort' , onAbort , { once : true } )
void done . then (
( ) = > {
signal . removeEventListener ( 'abort' , onAbort )
resolve ( true )
} ,
( ) = > {
signal . removeEventListener ( 'abort' , onAbort )
resolve ( true )
} ,
)
} )
} )
const handle : SubprocessHandle = {
pid : options.pid ? ? 1234 ,
2026-08-04 17:13:38 +08:00
stdin : toChild ,
stdout : fromChild ,
2026-08-15 18:07:08 +08:00
stderr ,
2026-08-04 16:02:17 +08:00
collected : { } ,
done ,
terminate ,
waitForExit ,
}
return {
handle ,
peer ,
fromChild ,
toChild ,
2026-08-15 18:07:08 +08:00
stderr ,
2026-08-04 16:02:17 +08:00
settle ,
fail ,
2026-08-18 18:29:37 +08:00
setStderr : ( text : string ) : void = > { stderr . write ( text ) } ,
2026-08-04 16:02:17 +08:00
terminate ,
waitForExit ,
}
}
2026-08-15 19:02:04 +08:00
function defaultWire ( child : FakeChild ) : CodexAppServerWire {
return new CodexAppServerWire (
child . handle . stdout ! ,
child . handle . stdin ! ,
DEFAULT_CODEX_PERMISSION_MODE ,
)
}
2026-08-04 16:02:17 +08:00
function runSpec (
child : FakeChild ,
overrides : Partial < CodexRunSpec > = { } ,
) : CodexRunSpec {
return {
cwd : process.cwd ( ) ,
2026-08-15 18:07:08 +08:00
permissionMode : DEFAULT_CODEX_PERMISSION_MODE ,
2026-08-04 16:02:17 +08:00
env : { } ,
disposeGraceMs : DEFAULT_DISPOSE_GRACE_MS ,
spawn : ( ) = > child . handle ,
. . . overrides ,
}
}
async function initializeWire ( ) : Promise < {
readonly child : FakeChild
readonly wire : CodexAppServerWire
} > {
const child = fakeChild ( )
2026-08-15 19:02:04 +08:00
const wire = defaultWire ( child )
2026-08-04 16:02:17 +08:00
wire . start ( )
const initializing = wire . initialize ( new AbortController ( ) . signal )
const initialize = await child . peer . nextMethod ( 'initialize' )
2026-08-08 23:56:39 +08:00
child . peer . respond ( initialize , { userAgent : 'codex-cli 0.147.0' } )
2026-08-04 16:02:17 +08:00
await initializing
expect ( await child . peer . nextMethod ( 'initialized' ) ) . toEqual ( {
jsonrpc : '2.0' ,
method : 'initialized' ,
} )
const starting = wire . startThread ( process . cwd ( ) , new AbortController ( ) . signal )
const threadStart = await child . peer . nextMethod ( 'thread/start' )
child . peer . respond ( threadStart , { thread : { id : 'thread-1' , ephemeral : true } } )
2026-08-04 19:55:39 +08:00
await starting
2026-08-04 16:02:17 +08:00
return { child , wire }
}
async function publishRun (
child = fakeChild ( ) ,
signal = new AbortController ( ) . signal ,
specOverrides : Partial < CodexRunSpec > = { } ,
) {
const starting = startCodexRun ( request ( undefined , signal ) , runSpec ( child , specOverrides ) )
const initialize = await child . peer . nextMethod ( 'initialize' )
2026-08-08 23:56:39 +08:00
child . peer . respond ( initialize , { userAgent : 'codex-cli 0.147.0' } )
2026-08-04 16:02:17 +08:00
await child . peer . nextMethod ( 'initialized' )
const threadStart = await child . peer . nextMethod ( 'thread/start' )
child . peer . respond ( threadStart , { thread : { id : 'thread-1' , ephemeral : true } } )
const run = await starting
const turnStart = await child . peer . nextMethod ( 'turn/start' )
return { child , run , turnStart }
}
function agentMessage (
text : unknown ,
phase : unknown ,
turnId = 'turn-1' ,
threadId = 'thread-1' ,
) : JsonObject {
return {
method : 'item/completed' ,
params : {
threadId ,
turnId ,
item : { type : 'agentMessage' , text , phase } ,
} ,
}
}
function turnCompleted (
status : unknown ,
turnId = 'turn-1' ,
threadId = 'thread-1' ,
error : unknown = null ,
) : JsonObject {
return {
method : 'turn/completed' ,
params : {
threadId ,
turn : { id : turnId , status , error } ,
} ,
}
}
2026-08-18 03:40:11 +08:00
function expectedFailureDiagnostic (
stage : 'initialize' | 'thread-start' | 'turn-start' | 'turn' | 'process' | 'teardown' ,
category : string ,
options : {
readonly httpStatus? : number
readonly outcome? : Partial < SubprocessOutcome >
} = { } ,
) : string {
const fields = [
'product: Codex' ,
` stage: ${ stage } ` ,
` category: ${ category } ` ,
]
if ( options . httpStatus !== undefined ) {
fields . push ( ` HTTP status: ${ options . httpStatus } ` )
}
if (
options . outcome ? . exitCode !== null
&& options . outcome ? . exitCode !== undefined
) {
fields . push ( ` exit code: ${ options . outcome . exitCode } ` )
}
if (
options . outcome ? . signal !== null
&& options . outcome ? . signal !== undefined
) {
fields . push ( ` signal: ${ options . outcome . signal } ` )
}
return ` Product subagent failure ( ${ fields . join ( '; ' ) } ) `
}
2026-08-04 16:02:17 +08:00
describe ( 'task admission and package contracts' , ( ) = > {
2026-08-15 03:25:43 +08:00
it ( 'ships one independently installable provider-only Bundle patch' , ( ) = > {
const root = fileURLToPath ( new URL ( '..' , import . meta . url ) )
const manifest = JSON . parse ( readFileSync ( resolve ( root , 'package.json' ) , 'utf8' ) ) as {
dependencies? : Record < string , string >
files? : string [ ]
dsh ? : { bundle ? : { patch? : string } }
}
expect ( manifest . dsh ? . bundle ? . patch ) . toBe ( './cordis.patch.yml' )
expect ( manifest . files ) . toContain ( 'cordis.patch.yml' )
expect ( manifest . dependencies ) . toHaveProperty (
'@deepseek-ai/dsh-sdk-protocol' ,
'workspace:^' ,
)
expect ( manifest . dependencies ) . toHaveProperty ( '@openai/codex' , CODEX_VERSION )
expect ( manifest . dependencies ) . not . toHaveProperty ( '@deepseek-ai/dsh-subagent-claude-code' )
const codexPackageJson = fileURLToPath ( import . meta . resolve ( '@openai/codex/package.json' ) )
const codexManifest = JSON . parse ( readFileSync ( codexPackageJson , 'utf8' ) ) as {
version : string
2026-08-15 04:12:41 +08:00
bin : { codex : string }
2026-08-15 03:25:43 +08:00
optionalDependencies : Record < string , string >
}
expect ( codexManifest . version ) . toBe ( CODEX_VERSION )
expect ( codexManifest . bin ) . toEqual ( { codex : 'bin/codex.js' } )
expect ( codexManifest . optionalDependencies ) . toEqual ( Object . fromEntries (
CODEX_PLATFORM_PACKAGES . map ( packageName = > [
packageName ,
` npm:@openai/codex@ ${ CODEX_VERSION } - ${ packageName . slice ( '@openai/codex-' . length ) } ` ,
] ) ,
) )
2026-08-15 04:12:41 +08:00
expect ( codexAppServerArgv ( ) ) . toEqual ( [
process . execPath ,
resolve ( dirname ( codexPackageJson ) , codexManifest . bin . codex ) ,
2026-08-05 07:03:04 +08:00
'app-server' ,
'--stdio' ,
] )
2026-08-15 03:25:43 +08:00
const lockfile = readFileSync ( resolve ( root , '../../../pnpm-lock.yaml' ) , 'utf8' )
for ( const packageName of CODEX_PLATFORM_PACKAGES ) {
const suffix = packageName . slice ( '@openai/codex-' . length )
expect ( lockfile ) . toContain ( ` '@openai/codex@ ${ CODEX_VERSION } - ${ suffix } ': ` )
expect ( lockfile ) . toContain (
` ' ${ packageName } ': '@openai/codex@ ${ CODEX_VERSION } - ${ suffix } ' ` ,
)
}
const parsed = yaml . load ( readFileSync ( resolve ( root , manifest . dsh ! . bundle ! . patch ! ) , 'utf8' ) )
const rows = Array . isArray ( parsed )
? ( parsed as Array < { insert? : Array < { id? : string ; name? : string } > } > ) . flatMap ( entry = > entry . insert ? ? [ ] )
: [ ]
expect ( rows ) . toEqual ( [ {
id : 'subagent-codex' ,
name : '@deepseek-ai/dsh-subagent-codex' ,
} ] )
expect ( JSON . stringify ( rows ) ) . not . toContain ( 'tool-subagent' )
2026-08-05 07:03:04 +08:00
} )
2026-08-04 16:02:17 +08:00
it ( 'accepts one or more text blocks and rejects empty or non-text tasks' , ( ) = > {
expect ( textTask ( [
{ type : 'text' , text : 'one' } ,
{ type : 'text' , text : 'two' } ,
] ) ) . toEqual ( [ 'one' , 'two' ] )
expect ( ( ) = > textTask ( [ ] ) ) . toThrow ( 'only text blocks' )
expect ( ( ) = > textTask ( [ { type : 'reasoning' , text : 'hidden' } ] ) )
. toThrow ( 'only text blocks' )
expect ( ( ) = > textTask ( [ { type : 'text' , text : ' \n ' } ] ) )
. toThrow ( 'must not be empty' )
} )
2026-08-18 02:56:44 +08:00
it ( 'registers the default descriptor, validates config, and unregisters on HMR' , async ( ) = > {
2026-08-04 16:02:17 +08:00
const ctx = new Context ( )
2026-08-13 00:36:22 +08:00
await ctx . plugin ( SubagentRuntime )
await ctx . plugin ( LocalSubprocessRuntime )
2026-08-04 16:02:17 +08:00
const fiber = await ctx . plugin ( codex , { } )
const provider = ctx . subagents . getProvider ( 'codex' ) !
expect ( provider ) . toMatchObject ( {
name : 'codex' ,
capabilities : {
outputSchema : false ,
depthLimit : false ,
toolFilter : false ,
persona : false ,
} ,
inheritsParentContext : false ,
} )
expect ( ctx . subagents . list ( ) ) . toEqual ( [ 'codex' ] )
await fiber . dispose ( )
expect ( ctx . subagents . list ( ) ) . toEqual ( [ ] )
for ( const disposeGraceMs of [ 0 , - 1 , Number . NaN , Number . POSITIVE_INFINITY ] ) {
await expect ( ctx . plugin ( codex , { disposeGraceMs } ) )
. rejects . toThrow ( 'disposeGraceMs must be a positive finite number' )
}
2026-08-05 04:21:29 +08:00
await expect ( ctx . plugin ( codex , { disposeGraceMs : MAX_TIMER_DELAY_MS + 1 } ) )
. rejects . toThrow ( ` disposeGraceMs must be no greater than ${ MAX_TIMER_DELAY_MS } ` )
2026-08-04 16:02:17 +08:00
await ctx . fiber . dispose ( )
} )
2026-08-18 02:56:44 +08:00
it ( 'keeps named instances, runs, and HMR ownership isolated' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( SubagentRuntime )
await ctx . plugin ( LocalSubprocessRuntime )
const safeChild = fakeChild ( )
const bypassChild = fakeChild ( )
const spawnSpecs : SubprocessSpawnSpec [ ] = [ ]
vi . spyOn ( ctx . subprocess , 'spawn' ) . mockImplementation ( ( spec ) = > {
spawnSpecs . push ( spec )
return spec . env ? . DSH_CODEX_INSTANCE === 'safe'
? safeChild . handle
: bypassChild . handle
} )
const added : string [ ] = [ ]
const started : string [ ] = [ ]
const ended : string [ ] = [ ]
const removed : string [ ] = [ ]
ctx . on ( 'subagent/provider-added' , provider = > void added . push ( provider . name ) )
ctx . on ( 'subagent/start' , info = > void started . push ( info . provider ) )
ctx . on ( 'subagent/end' , info = > void ended . push ( info . provider ) )
ctx . on ( 'subagent/provider-removed' , providerName = > void removed . push ( providerName ) )
const safeFiber = await ctx . plugin ( codex , {
providerName : 'codex-safe' ,
env : { DSH_CODEX_INSTANCE : 'safe' } ,
permissionMode : 'never' ,
disposeGraceMs : 11 ,
} )
const bypassFiber = await ctx . plugin ( codex , {
providerName : 'codex-bypass' ,
env : { DSH_CODEX_INSTANCE : 'bypass' } ,
permissionMode : 'dangerously-bypass-approvals-and-sandbox' ,
disposeGraceMs : 29 ,
} )
expect ( ctx . subagents . list ( ) ) . toEqual ( [ 'codex-safe' , 'codex-bypass' ] )
expect ( added ) . toEqual ( [ 'codex-safe' , 'codex-bypass' ] )
const safeController = new AbortController ( )
const safeStarting = ctx . subagents . start (
'codex-safe' ,
request ( undefined , safeController . signal ) ,
)
const bypassStarting = ctx . subagents . start ( 'codex-bypass' , request ( ) )
for ( const child of [ safeChild , bypassChild ] ) {
const initialize = await child . peer . nextMethod ( 'initialize' )
child . peer . respond ( initialize , { userAgent : 'codex-cli 0.147.0' } )
await child . peer . nextMethod ( 'initialized' )
const threadStart = await child . peer . nextMethod ( 'thread/start' )
child . peer . respond ( threadStart , {
thread : { id : 'thread-1' , ephemeral : true } ,
} )
}
const [ safeRun , bypassRun ] = await Promise . all ( [
safeStarting ,
bypassStarting ,
] )
await safeFiber . dispose ( )
expect ( ctx . subagents . list ( ) ) . toEqual ( [ 'codex-bypass' ] )
expect ( removed ) . toEqual ( [ 'codex-safe' ] )
await expect ( ctx . subagents . start ( 'codex-safe' , request ( ) ) )
. rejects . toMatchObject ( { code : 'NO_PROVIDER' } )
const safeTurn = await safeChild . peer . nextMethod ( 'turn/start' )
const bypassTurn = await bypassChild . peer . nextMethod ( 'turn/start' )
safeChild . peer . respond ( safeTurn , { turn : { id : 'turn-safe' } } )
bypassChild . peer . send (
{ id : bypassTurn.id , result : { turn : { id : 'turn-bypass' } } } ,
agentMessage ( 'bypass answer' , 'final_answer' , 'turn-bypass' ) ,
turnCompleted ( 'completed' , 'turn-bypass' ) ,
)
await expect ( bypassRun . result ) . resolves . toEqual ( {
output : [ { type : 'text' , text : 'bypass answer' } ] ,
stopReason : 'completed' ,
} )
safeController . abort ( new Error ( 'stop only the safe instance' ) )
await expect ( safeRun . result ) . resolves . toEqual ( {
output : [ ] ,
stopReason : 'aborted' ,
} )
expect ( spawnSpecs . map ( spec = > ( {
instance : spec.env?.DSH_CODEX_INSTANCE ,
graceMs : spec.graceMs ,
} ) ) ) . toEqual ( [
{ instance : 'safe' , graceMs : 11 } ,
{ instance : 'bypass' , graceMs : 29 } ,
] )
await Promise . all ( [ safeRun . dispose ( ) , bypassRun . dispose ( ) ] )
expect ( [ . . . started ] . sort ( ) ) . toEqual ( [ 'codex-bypass' , 'codex-safe' ] )
expect ( [ . . . ended ] . sort ( ) ) . toEqual ( [ 'codex-bypass' , 'codex-safe' ] )
expect ( safeChild . terminate ) . toHaveBeenCalledOnce ( )
expect ( bypassChild . terminate ) . toHaveBeenCalledOnce ( )
await bypassFiber . dispose ( )
expect ( removed ) . toEqual ( [ 'codex-safe' , 'codex-bypass' ] )
await ctx . fiber . dispose ( )
} )
it ( 'rejects duplicate provider names without replacing the first instance' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( SubagentRuntime )
await ctx . plugin ( LocalSubprocessRuntime )
const firstFiber = await ctx . plugin ( codex , {
providerName : 'codex-duplicate' ,
} )
const first = ctx . subagents . getProvider ( 'codex-duplicate' )
await expect ( ctx . plugin ( codex , {
providerName : 'codex-duplicate' ,
permissionMode : 'dangerously-bypass-approvals-and-sandbox' ,
} ) ) . rejects . toMatchObject ( { code : 'DUPLICATE_PROVIDER' } )
expect ( ctx . subagents . getProvider ( 'codex-duplicate' ) ) . toBe ( first )
expect ( ctx . subagents . list ( ) ) . toEqual ( [ 'codex-duplicate' ] )
await firstFiber . dispose ( )
await ctx . fiber . dispose ( )
} )
2026-08-15 18:07:08 +08:00
it ( 'accepts only the three fixed non-interactive permission modes' , ( ) = > {
2026-08-18 02:56:44 +08:00
expect ( codex . Config ( { } ) . providerName ) . toBe ( 'codex' )
expect ( codex . Config ( { providerName : 'codex-safe' } ) . providerName )
. toBe ( 'codex-safe' )
expect ( ( ) = > codex . Config ( { providerName : '' } ) ) . toThrow ( )
2026-08-15 18:07:08 +08:00
expect ( codex . Config ( { } ) . permissionMode ) . toBe ( DEFAULT_CODEX_PERMISSION_MODE )
for ( const permissionMode of CODEX_PERMISSION_MODES ) {
expect ( codex . Config ( { permissionMode } ) . permissionMode ) . toBe ( permissionMode )
}
for ( const permissionMode of [ 'on-request' , 'untrusted' , 'future-mode' ] ) {
expect ( ( ) = > codex . Config ( { permissionMode } as never ) ) . toThrow ( )
}
} )
it ( 'resolves the safe permission default when apply is called directly' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( SubagentRuntime )
await ctx . plugin ( LocalSubprocessRuntime )
codex . apply ( ctx , { env : { } , disposeGraceMs : 3_000 } )
expect ( ctx . subagents . getProvider ( 'codex' ) ) . toBeDefined ( )
await ctx . fiber . dispose ( )
} )
it . each ( [
[ 'never' , { approvalPolicy : 'never' } ] ,
[ 'approve-for-me' , {
approvalPolicy : 'on-request' ,
approvalsReviewer : 'auto_review' ,
sandbox : 'workspace-write' ,
} ] ,
[ 'dangerously-bypass-approvals-and-sandbox' , {
approvalPolicy : 'never' ,
sandbox : 'danger-full-access' ,
} ] ,
] as const ) ( 'maps %s to the official thread/start fields' , async ( permissionMode , expected ) = > {
const child = fakeChild ( )
const wire = new CodexAppServerWire (
child . handle . stdout ! ,
child . handle . stdin ! ,
permissionMode ,
)
wire . start ( )
const initializing = wire . initialize ( new AbortController ( ) . signal )
const initialize = await child . peer . nextMethod ( 'initialize' )
child . peer . respond ( initialize , { userAgent : 'codex-cli 0.147.0' } )
await initializing
await child . peer . nextMethod ( 'initialized' )
const starting = wire . startThread ( '/workspace' , new AbortController ( ) . signal )
const threadStart = await child . peer . nextMethod ( 'thread/start' )
expect ( threadStart . params ) . toEqual ( {
cwd : '/workspace' ,
ephemeral : true ,
. . . expected ,
} )
child . peer . respond ( threadStart , { thread : { id : 'thread-1' , ephemeral : true } } )
await starting
wire . close ( )
} )
2026-08-05 06:12:42 +08:00
it ( 'requires a parent session cwd without suggesting unsupported config' , async ( ) = > {
const ctx = new Context ( )
2026-08-13 00:36:22 +08:00
await ctx . plugin ( SubagentRuntime )
await ctx . plugin ( LocalSubprocessRuntime )
2026-08-05 06:12:42 +08:00
const spawn = vi . spyOn ( ctx . subprocess , 'spawn' )
await ctx . plugin ( codex , { } )
await expect ( ctx . subagents . start ( 'codex' , {
prompt : [ { type : 'text' , text : 'task' } ] ,
parent : {
id : 'parent-without-cwd' ,
session : { header : { } } ,
} as unknown as Agent ,
signal : new AbortController ( ) . signal ,
} ) ) . rejects . toThrow (
'subagent-codex: no working directory for the child — delegate from a parent session that has one' ,
)
expect ( spawn ) . not . toHaveBeenCalled ( )
await ctx . fiber . dispose ( )
} )
2026-08-04 16:02:17 +08:00
it ( 'keeps the namespace export shape and package-owned empty invariant' , async ( ) = > {
expect ( 'default' in codex ) . toBe ( false )
expect ( codex . name ) . toBe ( 'subagent-codex' )
expect ( codex . inject ) . toEqual ( [ 'subagents' , 'subprocess' ] )
const loader = Object . create ( Loader . prototype ) as Loader
expect ( loader . unwrapExports ( codex ) ) . toBe ( codex )
const dispose = vi . fn ( )
const register = vi . fn ( (
_packageName : string ,
_installer : InvariantInstaller ,
) = > dispose )
const ctx = { invariants : { register } } as unknown as Context
await expect ( invariant . apply ( ctx ) ) . resolves . toBe ( dispose )
expect ( register ) . toHaveBeenCalledWith (
'@deepseek-ai/dsh-subagent-codex' ,
expect . any ( Function ) ,
)
const install = register . mock . calls [ 0 ] ! [ 1 ]
await install ( new Context ( ) , ( message ) = > { throw new Error ( message ) } )
expect ( invariant . name ) . toBe ( 'subagent-codex-invariant' )
expect ( invariant . inject ) . toEqual ( [ 'invariants' ] )
} )
} )
describe ( 'CodexAppServerWire' , ( ) = > {
it ( 'sends the fixed handshake, thread, and turn payloads and keeps final_answer' , async ( ) = > {
const child = fakeChild ( )
2026-08-15 19:02:04 +08:00
const wire = defaultWire ( child )
2026-08-04 16:02:17 +08:00
expect ( wire . collectOutput ( ) ) . toEqual ( [ ] )
wire . start ( )
const initializing = wire . initialize ( new AbortController ( ) . signal )
const initialize = await child . peer . nextMethod ( 'initialize' )
expect ( initialize . params ) . toEqual ( {
clientInfo : {
name : 'deepseek-harness' ,
title : 'DeepSeek Harness' ,
version : '0.0.1' ,
} ,
capabilities : {
experimentalApi : false ,
requestAttestation : false ,
} ,
} )
2026-08-08 23:56:39 +08:00
child . peer . respond ( initialize , { userAgent : 'codex-cli 0.147.0' } )
2026-08-04 16:02:17 +08:00
await initializing
await child . peer . nextMethod ( 'initialized' )
const starting = wire . startThread ( '/workspace' , new AbortController ( ) . signal )
const threadStart = await child . peer . nextMethod ( 'thread/start' )
2026-08-15 18:07:08 +08:00
expect ( threadStart . params ) . toEqual ( {
cwd : '/workspace' ,
ephemeral : true ,
approvalPolicy : 'never' ,
} )
2026-08-04 16:02:17 +08:00
child . peer . respond ( threadStart , { thread : { id : 'thread-1' , ephemeral : true } } )
await starting
const result = wire . runTurn (
[ 'first' , 'second' ] ,
new AbortController ( ) . signal ,
)
const turnStart = await child . peer . nextMethod ( 'turn/start' )
expect ( turnStart . params ) . toEqual ( {
threadId : 'thread-1' ,
input : [
{ type : 'text' , text : 'first' , text_elements : [ ] } ,
{ type : 'text' , text : 'second' , text_elements : [ ] } ,
] ,
} )
2026-08-04 20:26:45 +08:00
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
await nextTask ( )
2026-08-04 16:02:17 +08:00
child . peer . send (
{
method : 'turn/started' ,
params : { threadId : 'thread-1' , turn : { id : 'turn-1' } } ,
} ,
agentMessage ( 'other thread' , 'final_answer' , 'turn-1' , 'thread-2' ) ,
agentMessage ( 'other turn' , 'final_answer' , 'turn-2' ) ,
{
method : 'item/completed' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-1' ,
item : { type : 'reasoning' , text : 'not output' } ,
} ,
} ,
agentMessage ( 'commentary' , 'commentary' ) ,
agentMessage ( 'unphased' , null ) ,
agentMessage ( 'first final' , 'final_answer' ) ,
agentMessage ( 'last final' , 'final_answer' ) ,
turnCompleted ( 'completed' ) ,
)
await expect ( result ) . resolves . toEqual ( {
output : [ { type : 'text' , text : 'last final' } ] ,
stopReason : 'completed' ,
} )
expect ( wire . collectOutput ( ) ) . toEqual ( [ { type : 'text' , text : 'last final' } ] )
wire . close ( )
wire . close ( )
} )
it ( 'uses the last nullable-phase answer when no explicit final exists' , async ( ) = > {
const { child , wire } = await initializeWire ( )
2026-08-05 07:36:54 +08:00
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
2026-08-04 16:02:17 +08:00
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
child . peer . send (
agentMessage ( 'first' , null ) ,
agentMessage ( 'fallback' , null ) ,
turnCompleted ( 'completed' ) ,
)
await expect ( result ) . resolves . toEqual ( {
output : [ { type : 'text' , text : 'fallback' } ] ,
stopReason : 'completed' ,
} )
wire . close ( )
} )
2026-08-18 03:40:11 +08:00
it ( 'maps the complete string error union without changing stop reasons' , async ( ) = > {
const categories = [
'contextWindowExceeded' ,
'sessionBudgetExceeded' ,
'usageLimitExceeded' ,
'serverOverloaded' ,
'cyberPolicy' ,
'internalServerError' ,
'unauthorized' ,
'badRequest' ,
'threadRollbackFailed' ,
'sandboxError' ,
'other' ,
] as const
for ( const category of categories ) {
const { child , wire } = await initializeWire ( )
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
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 : 'SECRET_TOKEN in /private/secret.txt' ,
codexErrorInfo : category ,
} ) ,
)
if ( category === 'contextWindowExceeded' ) {
await expect ( result ) . resolves . toEqual ( {
output : [ { type : 'text' , text : 'partial answer' } ] ,
stopReason : 'max-tokens' ,
} )
} else {
await expect ( result ) . rejects . toThrow ( ` status failed: ${ category } ` )
}
expect ( wire . collectFailure ( ) ) . toEqual ( {
stage : 'turn' ,
category ,
} )
expect ( JSON . stringify ( wire . collectFailure ( ) ) ) . not . toContain ( 'SECRET_TOKEN' )
expect ( JSON . stringify ( wire . collectFailure ( ) ) ) . not . toContain ( '/private/secret.txt' )
wire . close ( )
}
} )
it ( 'maps all object error variants and only numeric HTTP status' , async ( ) = > {
const scenarios = [
[ 'httpConnectionFailed' , { httpStatusCode : 503 } , 503 ] ,
[ 'responseStreamConnectionFailed' , { httpStatusCode : null } , undefined ] ,
[ 'responseStreamDisconnected' , { } , undefined ] ,
[ 'responseTooManyFailedAttempts' , { httpStatusCode : '503' } , undefined ] ,
[ 'activeTurnNotSteerable' , { turnKind : 'review' } , undefined ] ,
] as const
for ( const [ category , detail , httpStatus ] of scenarios ) {
const { child , wire } = await initializeWire ( )
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
child . peer . send ( turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , {
message : 'SECRET_TOKEN in /private/secret.txt' ,
codexErrorInfo : { [ category ] : detail } ,
} ) )
await expect ( result ) . rejects . toThrow ( ` status failed: ${ category } ` )
expect ( wire . collectFailure ( ) ) . toEqual ( {
stage : 'turn' ,
category ,
. . . ( httpStatus === undefined ? { } : { httpStatus } ) ,
} )
expect ( JSON . stringify ( wire . collectFailure ( ) ) ) . not . toContain ( 'turnKind' )
wire . close ( )
}
} )
it ( 'uses unknown for version-external or malformed error info' , async ( ) = > {
for ( const codexErrorInfo of [
'futureError' ,
{ futureVariant : { message : 'SECRET_TOKEN' } } ,
{
httpConnectionFailed : { httpStatusCode : 503 } ,
otherVariant : { } ,
} ,
{ httpConnectionFailed : null } ,
] ) {
const { child , wire } = await initializeWire ( )
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
child . peer . send ( turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , {
message : 'SECRET_TOKEN in /private/secret.txt' ,
codexErrorInfo ,
} ) )
await expect ( result ) . rejects . toThrow ( 'status failed: unknown' )
expect ( wire . collectFailure ( ) ) . toEqual ( {
stage : 'turn' ,
category : 'unknown' ,
} )
wire . close ( )
}
2026-08-04 18:38:05 +08:00
} )
2026-08-04 16:02:17 +08:00
it ( 'rejects invalid handshake, thread, and turn response shapes' , async ( ) = > {
{
const child = fakeChild ( )
2026-08-15 19:02:04 +08:00
const wire = defaultWire ( child )
2026-08-04 16:02:17 +08:00
wire . start ( )
const pending = wire . initialize ( new AbortController ( ) . signal )
const frame = await child . peer . nextMethod ( 'initialize' )
child . peer . respond ( frame , null )
await expect ( pending ) . rejects . toThrow ( 'invalid initialize response' )
wire . close ( )
}
{
const child = fakeChild ( )
2026-08-15 19:02:04 +08:00
const wire = defaultWire ( child )
2026-08-04 16:02:17 +08:00
wire . start ( )
const pending = wire . startThread ( '/workspace' , new AbortController ( ) . signal )
const frame = await child . peer . nextMethod ( 'thread/start' )
child . peer . respond ( frame , { thread : { id : 'thread-1' , ephemeral : false } } )
await expect ( pending ) . rejects . toThrow ( 'did not create an ephemeral thread' )
wire . close ( )
}
{
const { child , wire } = await initializeWire ( )
2026-08-05 07:36:54 +08:00
const pending = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
2026-08-04 16:02:17 +08:00
const frame = await child . peer . nextMethod ( 'turn/start' )
child . peer . respond ( frame , { turn : { id : '' } } )
await expect ( pending ) . rejects . toThrow ( 'turn/start turn id' )
2026-08-18 03:40:11 +08:00
expect ( wire . collectFailure ( ) ) . toEqual ( {
stage : 'turn-start' ,
category : 'unknown' ,
} )
2026-08-04 16:02:17 +08:00
wire . close ( )
}
} )
it ( 'fails closed for empty output, malformed messages, phases, and terminal status' , async ( ) = > {
const scenarios : Array < {
readonly frames : JsonObject [ ]
readonly message : string
} > = [
{
frames : [ turnCompleted ( 'completed' ) ] ,
message : 'without a final answer' ,
} ,
{
frames : [
agentMessage ( 'fallback' , null ) ,
agentMessage ( ' \n ' , 'final_answer' ) ,
turnCompleted ( 'completed' ) ,
] ,
message : 'without a final answer' ,
} ,
{
frames : [ agentMessage ( 42 , 'final_answer' ) ] ,
message : 'invalid agent message' ,
} ,
{
frames : [ agentMessage ( 'answer' , 'future_phase' ) ] ,
message : 'unknown agent message phase' ,
} ,
{
frames : [ turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , { message : 'no' } ) ] ,
message : 'status failed' ,
} ,
2026-08-18 03:40:11 +08:00
{
frames : [ turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , 'SECRET_TOKEN' ) ] ,
message : 'status failed' ,
} ,
2026-08-04 16:02:17 +08:00
{
frames : [ turnCompleted ( 'interrupted' ) ] ,
message : 'status interrupted' ,
} ,
{
frames : [ turnCompleted ( 'inProgress' ) ] ,
message : 'invalid terminal turn status' ,
} ,
]
for ( const scenario of scenarios ) {
const { child , wire } = await initializeWire ( )
2026-08-05 07:36:54 +08:00
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
2026-08-04 16:02:17 +08:00
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
2026-08-18 03:40:11 +08:00
await nextTask ( )
2026-08-04 16:02:17 +08:00
child . peer . send ( . . . scenario . frames )
await expect ( result ) . rejects . toThrow ( scenario . message )
2026-08-18 03:40:11 +08:00
expect ( wire . collectFailure ( ) ) . toEqual ( {
stage : 'turn' ,
category : 'unknown' ,
} )
2026-08-04 16:02:17 +08:00
wire . close ( )
}
} )
2026-08-05 04:21:29 +08:00
it ( 'fails closed when terminal notification params are not an object' , async ( ) = > {
const { child , wire } = await initializeWire ( )
2026-08-05 07:36:54 +08:00
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
2026-08-05 04:21:29 +08:00
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
child . peer . send ( { method : 'turn/completed' , params : null } )
await expect ( result ) . rejects . toThrow ( 'invalid turn/completed thread id' )
wire . close ( )
} )
2026-08-04 20:26:45 +08:00
it ( 'keeps an unsupported request authoritative over an early terminal in the same chunk' , async ( ) = > {
2026-08-04 19:55:39 +08:00
const { child , wire } = await initializeWire ( )
2026-08-05 07:36:54 +08:00
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
2026-08-04 19:55:39 +08:00
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . send (
2026-08-04 20:26:45 +08:00
{ id : turnStart.id , result : { turn : { id : 'turn-1' } } } ,
{ id : 'future-request' , method : 'future/request' , params : { } } ,
agentMessage ( 'early answer' , 'final_answer' ) ,
2026-08-04 19:55:39 +08:00
turnCompleted ( 'completed' ) ,
)
2026-08-04 20:26:45 +08:00
await expect ( result ) . rejects . toThrow ( 'unsupported app-server request' )
2026-08-04 19:55:39 +08:00
wire . close ( )
} )
2026-08-04 18:38:05 +08:00
it ( 'answers all five unattended request classes without granting authority' , async ( ) = > {
2026-08-04 16:02:17 +08:00
const { child , wire } = await initializeWire ( )
2026-08-05 07:36:54 +08:00
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
2026-08-04 16:02:17 +08:00
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . send ( {
id : 'command' ,
method : 'item/commandExecution/requestApproval' ,
2026-08-04 18:38:05 +08:00
params : {
threadId : 'thread-1' ,
turnId : 'turn-1' ,
availableDecisions : [ 'decline' , 'cancel' ] ,
2026-08-15 18:07:08 +08:00
command : 'cat /private/secret.txt' ,
2026-08-04 18:38:05 +08:00
} ,
2026-08-04 16:02:17 +08:00
} )
expect ( await child . peer . nextResponse ( 'command' ) ) . toMatchObject ( {
2026-08-04 18:38:05 +08:00
result : { decision : 'cancel' } ,
2026-08-04 16:02:17 +08:00
} )
2026-08-15 19:58:08 +08:00
expect ( wire . collectDiagnostic ( ) ) . toBeUndefined ( )
2026-08-04 16:02:17 +08:00
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
await nextTask ( )
2026-08-15 19:58:08 +08:00
expect ( wire . collectDiagnostic ( ) ) . toBe (
'Codex unattended decision (mode: never; request: command approval; decision: cancelled): the provider does not grant interactive approval' ,
)
2026-08-04 16:02:17 +08:00
const requests = [
2026-08-15 18:39:30 +08:00
{
id : 'command-decline' ,
method : 'item/commandExecution/requestApproval' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-1' ,
availableDecisions : [ 'decline' ] ,
} ,
result : { decision : 'decline' } ,
diagnostic : 'Codex unattended decision (mode: never; request: command approval; decision: declined): the provider does not grant interactive approval' ,
} ,
2026-08-04 16:02:17 +08:00
{
id : 'file' ,
method : 'item/fileChange/requestApproval' ,
2026-08-04 18:38:05 +08:00
params : {
threadId : 'thread-1' ,
turnId : 'turn-1' ,
availableDecisions : [ 'decline' ] ,
} ,
result : { decision : 'decline' } ,
2026-08-15 18:07:08 +08:00
diagnostic : 'Codex unattended decision (mode: never; request: file approval; decision: declined): the provider does not grant interactive approval' ,
2026-08-04 18:38:05 +08:00
} ,
2026-08-15 18:39:30 +08:00
{
id : 'file-cancel' ,
method : 'item/fileChange/requestApproval' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-1' ,
availableDecisions : [ 'cancel' ] ,
} ,
result : { decision : 'cancel' } ,
diagnostic : 'Codex unattended decision (mode: never; request: file approval; decision: cancelled): the provider does not grant interactive approval' ,
} ,
2026-08-04 18:38:05 +08:00
{
id : 'file-default' ,
method : 'item/fileChange/requestApproval' ,
2026-08-04 16:02:17 +08:00
params : { threadId : 'thread-1' , turnId : 'turn-1' } ,
result : { decision : 'decline' } ,
2026-08-15 18:07:08 +08:00
diagnostic : 'Codex unattended decision (mode: never; request: file approval; decision: declined): the provider does not grant interactive approval' ,
2026-08-04 16:02:17 +08:00
} ,
{
id : 'permissions' ,
method : 'item/permissions/requestApproval' ,
params : { threadId : 'thread-1' , turnId : 'turn-1' } ,
result : { permissions : { } , scope : 'turn' } ,
2026-08-15 18:07:08 +08:00
diagnostic : 'Codex unattended decision (mode: never; request: permission grant; decision: denied): the provider grants no additional turn permissions' ,
2026-08-04 16:02:17 +08:00
} ,
2026-08-04 18:38:05 +08:00
{
id : 'user-input' ,
method : 'item/tool/requestUserInput' ,
params : { threadId : 'thread-1' , turnId : 'turn-1' , questions : [ ] } ,
result : { answers : { } } ,
2026-08-15 18:07:08 +08:00
diagnostic : 'Codex unattended decision (mode: never; request: user input; decision: empty response): the provider does not collect interactive answers' ,
2026-08-04 18:38:05 +08:00
} ,
2026-08-04 16:02:17 +08:00
{
id : 'mcp' ,
method : 'mcpServer/elicitation/request' ,
params : { threadId : 'thread-1' , turnId : null } ,
result : { action : 'decline' , content : null , _meta : null } ,
2026-08-15 18:07:08 +08:00
diagnostic : 'Codex unattended decision (mode: never; request: MCP elicitation; decision: declined): the provider does not collect interactive MCP input' ,
2026-08-04 16:02:17 +08:00
} ,
] as const
for ( const serverRequest of requests ) {
child . peer . send ( serverRequest )
expect ( await child . peer . nextResponse ( serverRequest . id ) ) . toMatchObject ( {
result : serverRequest.result ,
} )
2026-08-15 18:07:08 +08:00
expect ( wire . collectDiagnostic ( ) ) . toBe ( serverRequest . diagnostic )
2026-08-04 16:02:17 +08:00
}
2026-08-15 18:07:08 +08:00
expect ( wire . collectDiagnostic ( ) ) . not . toContain ( '/private/secret.txt' )
2026-08-04 16:02:17 +08:00
2026-08-15 18:07:08 +08:00
child . peer . send ( agentMessage ( 'answer' , 'final_answer' ) , turnCompleted ( 'completed' ) )
await expect ( result ) . resolves . toEqual ( {
output : [ { type : 'text' , text : 'answer' } ] ,
stopReason : 'completed' ,
} )
wire . close ( )
} )
it ( 'records only a safe diagnostic for an explicit sandbox failure' , async ( ) = > {
const { child , wire } = await initializeWire ( )
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
child . peer . send ( turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , {
message : 'failed at /private/secret.txt with SECRET_TOKEN' ,
additionalDetails : 'raw command payload' ,
codexErrorInfo : 'sandboxError' ,
} ) )
await expect ( result ) . rejects . toThrow ( 'status failed' )
expect ( wire . collectDiagnostic ( ) ) . toBe (
'Codex unattended decision (mode: never; request: sandbox execution; decision: failed): Codex reported a sandbox failure' ,
)
expect ( wire . collectDiagnostic ( ) ) . not . toContain ( 'SECRET_TOKEN' )
expect ( wire . collectDiagnostic ( ) ) . not . toContain ( '/private/secret.txt' )
wire . close ( )
} )
2026-08-15 18:39:30 +08:00
it ( 'records declined command and file items without retaining their payloads' , async ( ) = > {
2026-08-15 18:07:08 +08:00
const { child , wire } = await initializeWire ( )
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
2026-08-15 18:39:30 +08:00
child . peer . send ( {
method : 'item/completed' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-1' ,
item : {
type : 'commandExecution' ,
status : 'declined' ,
command : 'cat /private/secret.txt' ,
} ,
} ,
} )
await nextTask ( )
expect ( wire . collectDiagnostic ( ) ) . toBe (
'Codex unattended decision (mode: never; request: command execution; decision: declined): Codex declined the command under the selected permission mode' ,
)
expect ( wire . collectDiagnostic ( ) ) . not . toContain ( '/private/secret.txt' )
2026-08-15 18:07:08 +08:00
child . peer . send (
{
method : 'item/completed' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-1' ,
item : {
2026-08-15 18:39:30 +08:00
type : 'fileChange' ,
2026-08-15 18:07:08 +08:00
status : 'declined' ,
2026-08-15 18:39:30 +08:00
patch : 'SECRET_TOKEN in /private/secret.txt' ,
2026-08-15 18:07:08 +08:00
} ,
} ,
} ,
turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , {
message : 'SECRET_TOKEN in /private/secret.txt' ,
codexErrorInfo : 'other' ,
} ) ,
)
await expect ( result ) . rejects . toThrow ( 'status failed' )
expect ( wire . collectDiagnostic ( ) ) . toBe (
2026-08-15 18:39:30 +08:00
'Codex unattended decision (mode: never; request: file change; decision: declined): Codex declined the file change under the selected permission mode' ,
2026-08-15 18:07:08 +08:00
)
expect ( wire . collectDiagnostic ( ) ) . not . toContain ( 'SECRET_TOKEN' )
expect ( wire . collectDiagnostic ( ) ) . not . toContain ( '/private/secret.txt' )
wire . close ( )
} )
it ( 'recognizes large, split, and ordered stderr signatures without retaining raw text' , ( ) = > {
const first = fakeChild ( )
const largeWire = new CodexAppServerWire (
first . handle . stdout ! ,
first . handle . stdin ! ,
'never' ,
)
largeWire . observeStderr (
` SECRET_TOKEN approval policy is Never; reject command ${ 'x' . repeat ( 2 _048 ) } ` ,
)
expect ( largeWire . collectDiagnostic ( ) ) . toBe (
'Codex unattended decision (mode: never; request: command execution; decision: denied): Codex rejected an escalation because the selected policy never asks for approval' ,
)
expect ( largeWire . collectDiagnostic ( ) ) . not . toContain ( 'SECRET_TOKEN' )
const second = fakeChild ( )
const splitWire = new CodexAppServerWire (
second . handle . stdout ! ,
second . handle . stdin ! ,
'never' ,
)
splitWire . observeStderr ( 'SECRET_TOKEN approval policy is Ne' )
splitWire . observeStderr ( 'ver; reject command — /private/secret.txt' )
expect ( splitWire . collectDiagnostic ( ) ) . toBe (
'Codex unattended decision (mode: never; request: command execution; decision: denied): Codex rejected an escalation because the selected policy never asks for approval' ,
)
expect ( splitWire . collectDiagnostic ( ) ) . not . toContain ( 'SECRET_TOKEN' )
expect ( splitWire . collectDiagnostic ( ) ) . not . toContain ( '/private/secret.txt' )
const third = fakeChild ( )
const orderedWire = new CodexAppServerWire (
third . handle . stdout ! ,
third . handle . stdin ! ,
'dangerously-bypass-approvals-and-sandbox' ,
)
orderedWire . observeStderr (
'approval policy is Never; reject command; recorded sandbox violation: path=/private/secret.txt' ,
)
expect ( orderedWire . collectDiagnostic ( ) ) . toBe (
'Codex unattended decision (mode: dangerously-bypass-approvals-and-sandbox; request: sandbox execution; decision: failed): Codex reported a sandbox violation' ,
)
expect ( orderedWire . collectDiagnostic ( ) ) . not . toContain ( '/private/secret.txt' )
} )
it ( 'does not reapply an old stderr signature after a newer request diagnostic' , async ( ) = > {
const { child , wire } = await initializeWire ( )
2026-08-15 19:11:24 +08:00
wire . observeStderr ( 'recorded sandbox violation:' )
2026-08-15 18:07:08 +08:00
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
await nextTask ( )
child . peer . send ( {
id : 'file-approval' ,
method : 'item/fileChange/requestApproval' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-1' ,
availableDecisions : [ 'decline' ] ,
} ,
} )
await child . peer . nextResponse ( 'file-approval' )
expect ( wire . collectDiagnostic ( ) ) . toContain ( 'request: file approval' )
wire . observeStderr ( 'later benign stderr' )
expect ( wire . collectDiagnostic ( ) ) . toContain ( 'request: file approval' )
2026-08-04 16:02:17 +08:00
child . peer . send ( agentMessage ( 'answer' , 'final_answer' ) , turnCompleted ( 'completed' ) )
await expect ( result ) . resolves . toMatchObject ( { stopReason : 'completed' } )
wire . close ( )
} )
2026-08-15 19:11:24 +08:00
it ( 'keeps a newer request diagnostic after replaying an older early item' , async ( ) = > {
const { child , wire } = await initializeWire ( )
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . send ( {
method : 'item/completed' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-1' ,
item : { type : 'fileChange' , status : 'declined' } ,
} ,
} )
await nextTask ( )
child . peer . send ( {
id : 'newer-command-request' ,
method : 'item/commandExecution/requestApproval' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-1' ,
availableDecisions : [ 'cancel' ] ,
} ,
} )
await child . peer . nextResponse ( 'newer-command-request' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
child . peer . send ( agentMessage ( 'answer' , 'final_answer' ) , turnCompleted ( 'completed' ) )
await expect ( result ) . resolves . toMatchObject ( { stopReason : 'completed' } )
expect ( wire . collectDiagnostic ( ) ) . toContain ( 'request: command approval' )
wire . close ( )
} )
2026-08-15 19:58:08 +08:00
it ( 'keeps a newer stderr fact after replaying an older early terminal' , async ( ) = > {
hostStderrWrite . capture = true
hostStderrWrite . chunks . length = 0
const { child , wire } = await initializeWire ( )
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . send ( turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , {
message : 'sandbox failure' ,
codexErrorInfo : 'sandboxError' ,
} ) )
await nextTask ( )
wire . observeStderr ( 'approval policy is Never; reject command' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
await expect ( result ) . rejects . toThrow ( 'sandboxError' )
expect ( wire . collectDiagnostic ( ) ) . toContain ( 'request: command execution' )
wire . close ( )
hostStderrWrite . capture = false
} )
2026-08-04 16:02:17 +08:00
it ( 'fails the run on unknown requests or wrong request association' , async ( ) = > {
for ( const serverRequest of [
{
id : 'unknown' ,
2026-08-04 18:38:05 +08:00
method : 'future/request' ,
2026-08-04 16:02:17 +08:00
params : { threadId : 'thread-1' , turnId : 'turn-1' } ,
} ,
2026-08-04 18:38:05 +08:00
{
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' ,
} ,
} ,
2026-08-04 16:02:17 +08:00
{
id : 'thread' ,
method : 'item/fileChange/requestApproval' ,
params : { threadId : 'thread-2' , turnId : 'turn-1' } ,
} ,
{
id : 'turn' ,
method : 'item/fileChange/requestApproval' ,
params : { threadId : 'thread-1' , turnId : 'turn-2' } ,
} ,
] ) {
const { child , wire } = await initializeWire ( )
2026-08-05 07:36:54 +08:00
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
2026-08-04 16:02:17 +08:00
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
await nextTask ( )
child . peer . send ( serverRequest )
const response = await child . peer . nextResponse ( serverRequest . id )
expect ( response . error ) . toMatchObject ( { code : - 32603 } )
await expect ( result ) . rejects . toThrow ( )
wire . close ( )
}
} )
it ( 'rejects conflicting early turn identities before accepting output' , async ( ) = > {
const { child , wire } = await initializeWire ( )
2026-08-05 07:36:54 +08:00
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
2026-08-04 16:02:17 +08:00
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . send ( {
method : 'turn/started' ,
params : { threadId : 'thread-1' , turn : { id : 'turn-early' } } ,
} )
child . peer . respond ( turnStart , { turn : { id : 'turn-response' } } )
await expect ( result ) . rejects . toThrow ( 'did not match the active turn' )
wire . close ( )
} )
2026-08-15 19:33:03 +08:00
it ( 'does not retain a diagnostic from a mismatched early item' , async ( ) = > {
const { child , wire } = await initializeWire ( )
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . send ( {
method : 'item/completed' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-early' ,
item : { type : 'fileChange' , status : 'declined' } ,
} ,
} )
child . peer . respond ( turnStart , { turn : { id : 'turn-response' } } )
await expect ( result ) . rejects . toThrow ( 'did not match the active turn' )
expect ( wire . collectDiagnostic ( ) ) . toBeUndefined ( )
wire . close ( )
} )
2026-08-15 19:58:08 +08:00
it ( 'does not retain a diagnostic from a mismatched provisional request' , async ( ) = > {
const { child , wire } = await initializeWire ( )
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . send ( {
id : 'provisional-approval' ,
method : 'item/commandExecution/requestApproval' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-early' ,
availableDecisions : [ 'cancel' ] ,
} ,
} )
await child . peer . nextResponse ( 'provisional-approval' )
child . peer . respond ( turnStart , { turn : { id : 'turn-response' } } )
await expect ( result ) . rejects . toThrow ( 'did not match the active turn' )
expect ( wire . collectDiagnostic ( ) ) . toBeUndefined ( )
wire . close ( )
} )
2026-08-04 16:02:17 +08:00
it ( 'rejects conflicting early notifications and requests before turn/start' , async ( ) = > {
{
const { child , wire } = await initializeWire ( )
child . peer . send ( {
id : 'too-early' ,
method : 'item/fileChange/requestApproval' ,
params : { threadId : 'thread-1' , turnId : 'turn-1' } ,
} )
const response = await child . peer . nextResponse ( 'too-early' )
expect ( response . error ) . toMatchObject ( { code : - 32603 } )
wire . close ( )
}
{
const { child , wire } = await initializeWire ( )
2026-08-05 07:36:54 +08:00
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
2026-08-04 16:02:17 +08:00
await child . peer . nextMethod ( 'turn/start' )
child . peer . send (
{
method : 'turn/started' ,
params : { threadId : 'thread-1' , turn : { id : 'turn-1' } } ,
} ,
agentMessage ( 'wrong' , 'final_answer' , 'turn-2' ) ,
)
await expect ( result ) . rejects . toThrow ( 'conflicting turns' )
wire . close ( )
}
} )
it ( 'interrupts only an active open turn and contains remote interrupt failure' , async ( ) = > {
const { child , wire } = await initializeWire ( )
wire . interrupt ( )
2026-08-05 07:36:54 +08:00
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
2026-08-04 16:02:17 +08:00
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
await nextTask ( )
wire . interrupt ( )
const interrupt = await child . peer . nextMethod ( 'turn/interrupt' )
expect ( interrupt . params ) . toEqual ( { threadId : 'thread-1' , turnId : 'turn-1' } )
child . peer . send ( {
id : interrupt.id ,
error : { code : - 32000 , message : 'already done' } ,
} )
child . peer . send ( agentMessage ( 'answer' , 'final_answer' ) , turnCompleted ( 'completed' ) )
await expect ( result ) . resolves . toMatchObject ( { stopReason : 'completed' } )
wire . close ( )
wire . interrupt ( )
} )
it ( 'ignores unrelated and out-of-window notifications' , async ( ) = > {
const { child , wire } = await initializeWire ( )
child . peer . send (
{
method : 'turn/started' ,
params : { threadId : 'thread-2' , turn : { id : 'turn-other' } } ,
} ,
{
method : 'turn/started' ,
params : { threadId : 'thread-1' , turn : { id : 'turn-before' } } ,
} ,
agentMessage ( 'before' , 'final_answer' ) ,
{ method : 'future/notification' , params : { } } ,
turnCompleted ( 'completed' ) ,
turnCompleted ( 'completed' , 'turn-other' , 'thread-2' ) ,
)
await nextTask ( )
2026-08-05 07:36:54 +08:00
const result = wire . runTurn ( [ 'task' ] , new AbortController ( ) . signal )
2026-08-04 16:02:17 +08:00
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
await nextTask ( )
child . peer . send (
agentMessage ( 'wrong turn' , 'final_answer' , 'turn-2' ) ,
turnCompleted ( 'completed' , 'turn-2' ) ,
agentMessage ( 'answer' , 'final_answer' ) ,
turnCompleted ( 'completed' ) ,
)
await expect ( result ) . resolves . toEqual ( {
output : [ { type : 'text' , text : 'answer' } ] ,
stopReason : 'completed' ,
} )
wire . close ( )
} )
it ( 'rejects pending work on abort, EOF, and stream error' , async ( ) = > {
{
const child = fakeChild ( )
2026-08-15 19:02:04 +08:00
const wire = defaultWire ( child )
2026-08-04 16:02:17 +08:00
wire . start ( )
const controller = new AbortController ( )
controller . abort ( 'pre-aborted' )
await expect ( wire . initialize ( controller . signal ) )
. rejects . toThrow ( 'app-server request aborted: pre-aborted' )
wire . close ( )
}
{
const child = fakeChild ( )
2026-08-15 19:02:04 +08:00
const wire = defaultWire ( child )
2026-08-04 16:02:17 +08:00
wire . start ( )
const controller = new AbortController ( )
const pending = wire . initialize ( controller . signal )
await child . peer . nextMethod ( 'initialize' )
controller . abort ( new Error ( 'cancel initialize' ) )
await expect ( pending ) . rejects . toThrow ( 'cancel initialize' )
wire . close ( )
}
{
const child = fakeChild ( )
2026-08-15 19:02:04 +08:00
const wire = defaultWire ( child )
2026-08-04 16:02:17 +08:00
wire . start ( )
const pending = wire . initialize ( new AbortController ( ) . signal )
await child . peer . nextMethod ( 'initialize' )
child . fromChild . end ( )
await expect ( pending ) . rejects . toThrow ( /(?:protocol stream|JSON-RPC input) closed/ )
wire . close ( )
}
{
const child = fakeChild ( )
2026-08-15 19:02:04 +08:00
const wire = defaultWire ( child )
2026-08-04 16:02:17 +08:00
wire . start ( )
const pending = wire . initialize ( new AbortController ( ) . signal )
await child . peer . nextMethod ( 'initialize' )
child . fromChild . emit ( 'error' , new Error ( 'stdout broke' ) )
await expect ( pending ) . rejects . toThrow ( 'stdout broke' )
wire . close ( )
}
2026-08-04 17:13:38 +08:00
{
const child = fakeChild ( )
2026-08-15 19:02:04 +08:00
const wire = defaultWire ( child )
2026-08-04 17:13:38 +08:00
wire . start ( )
const pending = wire . initialize ( new AbortController ( ) . signal )
await child . peer . nextMethod ( 'initialize' )
child . toChild . emit ( 'error' , new Error ( 'stdin broke' ) )
await expect ( pending ) . rejects . toThrow ( 'stdin broke' )
wire . close ( )
child . toChild . emit ( 'error' , new Error ( 'late stdin close' ) )
}
2026-08-04 16:02:17 +08:00
} )
} )
describe ( 'run lifecycle and quiescence' , ( ) = > {
it ( 'spawns the fixed app-server, publishes after thread creation, and disposes once' , async ( ) = > {
const child = fakeChild ( )
const spawn = vi . fn ( ( ) = > child . handle )
const starting = startCodexRun (
request ( [ { type : 'text' , text : 'task' } ] ) ,
runSpec ( child , { env : { OPENAI_API_KEY : 'fake' } , spawn } ) ,
)
let published = false
void starting . then ( ( ) = > { published = true } )
const initialize = await child . peer . nextMethod ( 'initialize' )
expect ( published ) . toBe ( false )
2026-08-08 23:56:39 +08:00
child . peer . respond ( initialize , { userAgent : 'codex-cli 0.147.0' } )
2026-08-04 16:02:17 +08:00
await child . peer . nextMethod ( 'initialized' )
const threadStart = await child . peer . nextMethod ( 'thread/start' )
expect ( published ) . toBe ( false )
child . peer . respond ( threadStart , { thread : { id : 'thread-1' , ephemeral : true } } )
const run = await starting
expect ( spawn ) . toHaveBeenCalledWith ( {
2026-08-05 07:03:04 +08:00
argv : codexAppServerArgv ( ) ,
2026-08-04 16:02:17 +08:00
cwd : process.cwd ( ) ,
2026-08-15 18:07:08 +08:00
stdio : { stdin : 'pipe' , stdout : 'pipe' , stderr : 'pipe' } ,
2026-08-04 16:02:17 +08:00
graceMs : DEFAULT_DISPOSE_GRACE_MS ,
env : { OPENAI_API_KEY : 'fake' } ,
} )
expect ( run . localAgent ) . toBeUndefined ( )
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . send (
{ id : turnStart.id , result : { turn : { id : 'turn-1' } } } ,
agentMessage ( 'answer' , 'final_answer' ) ,
turnCompleted ( 'completed' ) ,
)
await expect ( run . result ) . resolves . toEqual ( {
output : [ { type : 'text' , text : 'answer' } ] ,
stopReason : 'completed' ,
} )
const disposal = run . dispose ( )
expect ( run . dispose ( ) ) . toBe ( disposal )
await disposal
await nextTask ( )
expect ( child . terminate ) . toHaveBeenCalledTimes ( 1 )
expect ( child . waitForExit ) . toHaveBeenCalledTimes ( 1 )
} )
it ( 'settles local cancellation immediately and sends best-effort interrupt' , async ( ) = > {
const controller = new AbortController ( )
const { child , run , turnStart } = await publishRun (
fakeChild ( ) ,
controller . signal ,
)
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
await nextTask ( )
controller . abort ( new Error ( 'stop' ) )
await expect ( run . result ) . resolves . toEqual ( {
output : [ ] ,
stopReason : 'aborted' ,
} )
expect ( await child . peer . nextMethod ( 'turn/interrupt' ) ) . toMatchObject ( {
params : { threadId : 'thread-1' , turnId : 'turn-1' } ,
} )
await run . dispose ( )
} )
2026-08-18 03:40:11 +08:00
it ( 'reports turn-start failures and omits captured facts after success' , async ( ) = > {
{
const { child , run , turnStart } = await publishRun ( )
child . peer . respond ( turnStart , { turn : { id : '' } } )
await expect ( run . result ) . resolves . toEqual ( {
output : [ ] ,
diagnostic : expectedFailureDiagnostic ( 'turn-start' , 'unknown' ) ,
stopReason : 'error' ,
} )
await run . dispose ( )
}
{
const { child , run , turnStart } = await publishRun ( )
child . peer . send ( {
id : 'successful-approval' ,
method : 'item/commandExecution/requestApproval' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-1' ,
availableDecisions : [ 'cancel' ] ,
} ,
} )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
await child . peer . nextResponse ( 'successful-approval' )
child . peer . send (
agentMessage ( 'answer' , 'final_answer' ) ,
turnCompleted ( 'completed' ) ,
)
await expect ( run . result ) . resolves . toEqual ( {
output : [ { type : 'text' , text : 'answer' } ] ,
stopReason : 'completed' ,
} )
await run . dispose ( )
}
} )
it ( 'preserves representative terminal categories, HTTP status, and mapping' , async ( ) = > {
const scenarios = [
[ 'contextWindowExceeded' , 'max-tokens' , undefined ] ,
[ 'sessionBudgetExceeded' , 'error' , undefined ] ,
[ { httpConnectionFailed : { httpStatusCode : 503 } } , 'error' , 503 ] ,
[ { activeTurnNotSteerable : { turnKind : 'review' } } , 'error' , undefined ] ,
[ 'futureError' , 'error' , undefined ] ,
] as const
for ( const [ codexErrorInfo , stopReason , httpStatus ] of scenarios ) {
const { child , run , turnStart } = await publishRun ( )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
child . peer . send (
agentMessage ( 'partial answer' , null ) ,
turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , {
message : 'SECRET_TOKEN in /private/secret.txt' ,
codexErrorInfo ,
} ) ,
)
const category = typeof codexErrorInfo === 'string'
&& codexErrorInfo !== 'futureError'
? codexErrorInfo
: typeof codexErrorInfo === 'object'
? Object . keys ( codexErrorInfo ) [ 0 ] !
: 'unknown'
const result = await run . result
expect ( result ) . toEqual ( {
output : [ { type : 'text' , text : 'partial answer' } ] ,
diagnostic : expectedFailureDiagnostic ( 'turn' , category , {
. . . ( httpStatus === undefined ? { } : { httpStatus } ) ,
} ) ,
stopReason ,
} )
expect ( result . diagnostic ) . not . toContain ( 'SECRET_TOKEN' )
expect ( result . diagnostic ) . not . toContain ( '/private/secret.txt' )
expect ( result . diagnostic ) . not . toContain ( 'turnKind' )
await run . dispose ( )
}
} )
2026-08-18 23:06:14 +08:00
it ( 'includes a queued stderr permission fact in a max-token result' , async ( ) = > {
const { child , run , turnStart } = await publishRun ( )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
setImmediate ( ( ) = > {
child . stderr . write ( 'approval policy is Never; reject command' )
2026-08-19 01:10:29 +08:00
child . peer . send (
agentMessage ( 'partial answer' , null ) ,
turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , {
codexErrorInfo : 'contextWindowExceeded' ,
} ) ,
)
2026-08-18 23:06:14 +08:00
} )
2026-08-19 01:10:29 +08:00
child . settle ( { exitCode : 17 , signal : null } )
2026-08-18 23:06:14 +08:00
await expect ( run . result ) . resolves . toEqual ( {
output : [ { type : 'text' , text : 'partial answer' } ] ,
2026-08-19 01:10:29 +08:00
diagnostic : ` ${ expectedFailureDiagnostic ( 'turn' , 'contextWindowExceeded' , { outcome : { exitCode : 17 , signal : null } })} \ nCodex unattended decision (mode: never; request: command execution; decision: denied): Codex rejected an escalation because the selected policy never asks for approval ` ,
2026-08-18 23:06:14 +08:00
stopReason : 'max-tokens' ,
} )
await run . dispose ( )
} )
2026-08-04 16:02:17 +08:00
it ( 'flattens child exit and protocol failures after publication' , async ( ) = > {
const errors : string [ ] = [ ]
2026-08-18 03:40:11 +08:00
const outcomes : SubprocessOutcome [ ] = [
{ exitCode : 9 , signal : null } ,
{ exitCode : null , signal : 'SIGABRT' } ,
{ exitCode : null , signal : null } ,
]
for ( const outcome of outcomes ) {
2026-08-04 16:02:17 +08:00
const child = fakeChild ( { exitOnTerminate : false } )
const { run } = await publishRun ( child , undefined , {
onError : ( error ) = > { errors . push ( error . message ) } ,
} )
2026-08-18 03:40:11 +08:00
child . settle ( outcome )
await expect ( run . result ) . resolves . toEqual ( {
output : [ ] ,
diagnostic : expectedFailureDiagnostic ( 'process' , 'process-exit' , {
outcome ,
} ) ,
stopReason : 'error' ,
} )
expect ( errors . at ( - 1 ) ) . toBe (
` subagent-codex: ${ expectedFailureDiagnostic ( 'process' , 'process-exit' , { outcome } )} ` ,
)
2026-08-04 16:02:17 +08:00
await run . dispose ( ) . catch ( ( ) = > { } )
}
2026-08-18 19:14:02 +08:00
{
2026-08-18 22:41:32 +08:00
const outcome = { exitCode : 17 , signal : null } as const
2026-08-18 19:14:02 +08:00
const child = fakeChild ( { exitOnTerminate : false } )
const { run , turnStart } = await publishRun ( child , undefined , {
2026-08-18 23:06:14 +08:00
disposeGraceMs : 0.5 ,
2026-08-18 19:14:02 +08:00
} )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
2026-08-18 23:06:14 +08:00
vi . spyOn ( child . handle , 'waitForExit' ) . mockImplementationOnce ( async ( signal? : AbortSignal ) = > {
expect ( signal ) . toBeDefined ( )
child . settle ( outcome )
return true
} )
2026-08-18 19:14:02 +08:00
child . fromChild . emit ( 'end' )
await expect ( run . result ) . resolves . toEqual ( {
output : [ ] ,
diagnostic : expectedFailureDiagnostic ( 'process' , 'process-exit' , {
outcome ,
} ) ,
stopReason : 'error' ,
} )
await run . dispose ( ) . catch ( ( ) = > { } )
}
{
const child = fakeChild ( { exitOnTerminate : false } )
const { run , turnStart } = await publishRun ( child )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
2026-08-19 00:29:40 +08:00
setImmediate ( ( ) = > {
child . peer . send ( turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , {
codexErrorInfo : 'other' ,
} ) )
} )
2026-08-18 22:48:24 +08:00
child . settle ( { exitCode : 17 , signal : null } )
2026-08-18 19:14:02 +08:00
await expect ( run . result ) . resolves . toEqual ( {
output : [ ] ,
2026-08-19 01:10:29 +08:00
diagnostic : expectedFailureDiagnostic ( 'turn' , 'other' , {
outcome : { exitCode : 17 , signal : null } ,
} ) ,
2026-08-18 19:14:02 +08:00
stopReason : 'error' ,
} )
await run . dispose ( ) . catch ( ( ) = > { } )
}
2026-08-18 20:02:37 +08:00
{
const child = fakeChild ( { exitOnTerminate : false } )
const { run , turnStart } = await publishRun ( child )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
child . peer . send (
agentMessage ( 'answer' , 'final_answer' ) ,
turnCompleted ( 'completed' ) ,
)
child . fromChild . end ( )
2026-08-18 22:48:24 +08:00
child . settle ( { exitCode : 17 , signal : null } )
2026-08-18 20:02:37 +08:00
await expect ( run . result ) . resolves . toEqual ( {
output : [ { type : 'text' , text : 'answer' } ] ,
stopReason : 'completed' ,
} )
2026-08-04 16:02:17 +08:00
await run . dispose ( ) . catch ( ( ) = > { } )
}
{
const child = fakeChild ( )
const { run , turnStart } = await publishRun ( child , undefined , {
2026-08-18 19:14:02 +08:00
disposeGraceMs : 10 ,
2026-08-04 16:02:17 +08:00
onError : ( ) = > { throw new Error ( 'diagnostic sink' ) } ,
} )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
child . fromChild . end ( )
2026-08-18 03:40:11 +08:00
await expect ( run . result ) . resolves . toEqual ( {
output : [ ] ,
diagnostic : expectedFailureDiagnostic ( 'turn' , 'unknown' ) ,
stopReason : 'error' ,
} )
2026-08-04 16:02:17 +08:00
await run . dispose ( )
}
2026-08-15 18:07:08 +08:00
{
const child = fakeChild ( )
2026-08-15 19:02:04 +08:00
const { run , turnStart } = await publishRun ( child )
2026-08-15 18:07:08 +08:00
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
child . stderr . emit ( 'error' , new Error ( 'stderr broke' ) )
2026-08-15 19:02:04 +08:00
child . peer . send ( agentMessage ( 'answer' , 'final_answer' ) , turnCompleted ( 'completed' ) )
await expect ( run . result ) . resolves . toEqual ( {
output : [ { type : 'text' , text : 'answer' } ] ,
stopReason : 'completed' ,
} )
2026-08-15 18:07:08 +08:00
await run . dispose ( )
expect ( child . stderr . listenerCount ( 'error' ) ) . toBe ( 0 )
}
} )
it ( 'attaches a safe permission diagnostic when a published run fails' , async ( ) = > {
const { child , run , turnStart } = await publishRun ( )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
await nextTask ( )
child . peer . send ( {
id : 'approval-diagnostic' ,
method : 'item/commandExecution/requestApproval' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-1' ,
availableDecisions : [ 'cancel' ] ,
command : 'cat /private/secret.txt' ,
} ,
} )
expect ( await child . peer . nextResponse ( 'approval-diagnostic' ) ) . toMatchObject ( {
result : { decision : 'cancel' } ,
} )
child . peer . send ( turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , {
message : 'SECRET_TOKEN in /private/secret.txt' ,
codexErrorInfo : 'other' ,
} ) )
await expect ( run . result ) . resolves . toEqual ( {
output : [ ] ,
2026-08-18 03:40:11 +08:00
diagnostic : ` ${ expectedFailureDiagnostic ( 'turn' , 'other' ) } \ nCodex unattended decision (mode: never; request: command approval; decision: cancelled): the provider does not grant interactive approval ` ,
2026-08-15 18:07:08 +08:00
stopReason : 'error' ,
} )
await run . dispose ( )
} )
2026-08-15 19:11:24 +08:00
it ( 'drains queued stderr before settling a failed published run' , async ( ) = > {
2026-08-15 19:33:03 +08:00
hostStderrWrite . capture = true
hostStderrWrite . chunks . length = 0
2026-08-15 19:11:24 +08:00
const { child , run , turnStart } = await publishRun ( )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
child . peer . send ( turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , {
message : 'fixture terminal failure' ,
codexErrorInfo : 'badRequest' ,
} ) )
setImmediate ( ( ) = > {
child . stderr . write ( 'approval policy is Never; reject command' )
} )
await expect ( run . result ) . resolves . toEqual ( {
output : [ ] ,
2026-08-18 03:40:11 +08:00
diagnostic : ` ${ expectedFailureDiagnostic ( 'turn' , 'badRequest' ) } \ nCodex unattended decision (mode: never; request: command execution; decision: denied): Codex rejected an escalation because the selected policy never asks for approval ` ,
2026-08-15 19:11:24 +08:00
stopReason : 'error' ,
} )
await run . dispose ( )
2026-08-15 19:33:03 +08:00
hostStderrWrite . capture = false
2026-08-15 19:11:24 +08:00
} )
2026-08-15 18:07:08 +08:00
it ( 'forwards stderr while extracting only a fixed safe permission signature' , async ( ) = > {
const child = fakeChild ( )
2026-08-15 19:33:03 +08:00
hostStderrWrite . capture = true
hostStderrWrite . chunks . length = 0
2026-08-15 18:07:08 +08:00
const { run , turnStart } = await publishRun ( child )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
child . stderr . write ( 'SECRET_TOKEN approval policy is Ne' )
child . stderr . write ( 'ver; reject command — /private/secret.txt' )
2026-08-15 19:33:03 +08:00
child . stderr . emit ( 'data' , 'string stderr suffix' )
2026-08-15 18:07:08 +08:00
child . peer . send ( turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , {
message : 'fixture terminal failure' ,
codexErrorInfo : 'badRequest' ,
} ) )
await expect ( run . result ) . resolves . toEqual ( {
output : [ ] ,
2026-08-18 03:40:11 +08:00
diagnostic : ` ${ expectedFailureDiagnostic ( 'turn' , 'badRequest' ) } \ nCodex unattended decision (mode: never; request: command execution; decision: denied): Codex rejected an escalation because the selected policy never asks for approval ` ,
2026-08-15 18:07:08 +08:00
stopReason : 'error' ,
} )
2026-08-15 19:33:03 +08:00
expect ( Buffer . concat ( hostStderrWrite . chunks ) . toString ( ) ) . toContain ( 'SECRET_TOKEN' )
2026-08-15 20:09:04 +08:00
expect ( hostStderrWrite . chunks ) . toHaveLength ( 3 )
2026-08-15 18:07:08 +08:00
await run . dispose ( )
expect ( child . stderr . listenerCount ( 'data' ) ) . toBe ( 0 )
2026-08-15 19:33:03 +08:00
hostStderrWrite . capture = false
2026-08-04 16:02:17 +08:00
} )
2026-08-15 19:33:03 +08:00
it ( 'contains host stderr write failures without changing run settlement' , async ( ) = > {
2026-08-15 19:11:24 +08:00
const child = fakeChild ( )
2026-08-15 19:33:03 +08:00
hostStderrWrite . capture = true
hostStderrWrite . failNext = true
2026-08-15 19:11:24 +08:00
const { run , turnStart } = await publishRun ( child )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
2026-08-15 19:58:08 +08:00
child . stderr . write ( 'approval policy is Never; reject command' )
child . peer . send ( turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , {
message : 'fixture terminal failure' ,
codexErrorInfo : 'badRequest' ,
} ) )
2026-08-15 19:11:24 +08:00
await expect ( run . result ) . resolves . toEqual ( {
2026-08-15 19:58:08 +08:00
output : [ ] ,
2026-08-18 03:40:11 +08:00
diagnostic : ` ${ expectedFailureDiagnostic ( 'turn' , 'badRequest' ) } \ nCodex unattended decision (mode: never; request: command execution; decision: denied): Codex rejected an escalation because the selected policy never asks for approval ` ,
2026-08-15 19:58:08 +08:00
stopReason : 'error' ,
2026-08-15 19:11:24 +08:00
} )
await run . dispose ( )
2026-08-15 19:33:03 +08:00
hostStderrWrite . capture = false
2026-08-15 19:11:24 +08:00
} )
2026-08-04 16:02:17 +08:00
it ( 'rejects before spawn when pre-aborted and rolls back startup failures' , async ( ) = > {
const controller = new AbortController ( )
controller . abort ( )
const spawn = vi . fn ( )
await expect ( startCodexRun (
request ( undefined , controller . signal ) ,
{
cwd : process.cwd ( ) ,
2026-08-15 18:07:08 +08:00
permissionMode : DEFAULT_CODEX_PERMISSION_MODE ,
2026-08-04 16:02:17 +08:00
env : { } ,
disposeGraceMs : 10 ,
spawn ,
} ,
) ) . rejects . toThrow ( 'aborted before app-server startup' )
expect ( spawn ) . not . toHaveBeenCalled ( )
2026-08-18 03:40:11 +08:00
const spawnFailure = startCodexRun ( request ( ) , {
cwd : process.cwd ( ) ,
permissionMode : DEFAULT_CODEX_PERMISSION_MODE ,
env : { } ,
disposeGraceMs : 10 ,
spawn : ( ) = > { throw new Error ( 'SECRET_TOKEN spawn failure' ) } ,
} )
await expect ( spawnFailure )
. rejects . toThrow ( expectedFailureDiagnostic ( 'initialize' , 'unknown' ) )
await expect ( spawnFailure ) . rejects . not . toThrow ( 'SECRET_TOKEN' )
2026-08-18 19:14:02 +08:00
const asyncSpawnFailureChild = fakeChild ( {
pid : - 1 ,
doneError : new Error ( 'SECRET_TOKEN async spawn failure' ) ,
} )
const asyncSpawnFailure = startCodexRun (
request ( ) ,
runSpec ( asyncSpawnFailureChild ) ,
)
await expect ( asyncSpawnFailure )
. rejects . toThrow ( expectedFailureDiagnostic ( 'initialize' , 'unknown' ) )
await expect ( asyncSpawnFailure ) . rejects . not . toThrow ( 'SECRET_TOKEN' )
expect ( asyncSpawnFailureChild . terminate ) . not . toHaveBeenCalled ( )
2026-08-04 16:02:17 +08:00
const child = fakeChild ( )
const starting = startCodexRun ( request ( ) , runSpec ( child ) )
const initialize = await child . peer . nextMethod ( 'initialize' )
2026-08-05 04:45:11 +08:00
child . peer . respond ( initialize , null )
2026-08-18 03:40:11 +08:00
await expect ( starting )
. rejects . toThrow ( expectedFailureDiagnostic ( 'initialize' , 'unknown' ) )
await expect ( starting ) . rejects . not . toThrow ( 'invalid initialize response' )
2026-08-04 16:02:17 +08:00
expect ( child . terminate ) . toHaveBeenCalledTimes ( 1 )
2026-08-15 18:07:08 +08:00
2026-08-18 19:14:02 +08:00
const cleanupFailureChild = fakeChild ( {
waitForExitError : new Error ( 'SECRET_TOKEN wait failure' ) ,
} )
const cleanupFailure = startCodexRun (
request ( ) ,
runSpec ( cleanupFailureChild ) ,
)
const cleanupFailureInitialize = await cleanupFailureChild . peer
. nextMethod ( 'initialize' )
cleanupFailureChild . peer . respond ( cleanupFailureInitialize , null )
const cleanupError : unknown = await cleanupFailure . then (
( ) = > undefined ,
( error : unknown ) = > error ,
)
expect ( cleanupError ) . toBeInstanceOf ( AggregateError )
expect ( String ( cleanupError ) ) . toContain (
expectedFailureDiagnostic ( 'initialize' , 'unknown' ) ,
)
expect ( String ( cleanupError ) ) . toContain ( expectedFailureDiagnostic (
'teardown' ,
'unknown' ,
{ outcome : { exitCode : 0 , signal : null } } ,
) )
expect ( String ( cleanupError ) ) . not . toContain ( 'SECRET_TOKEN' )
2026-08-18 03:40:11 +08:00
const cleanupRaceAbort = new AbortController ( )
const cleanupRaceChild = fakeChild ( { exitOnTerminate : false } )
const cleanupRace = startCodexRun (
request ( undefined , cleanupRaceAbort . signal ) ,
runSpec ( cleanupRaceChild ) ,
)
const cleanupRaceInitialize = await cleanupRaceChild . peer . nextMethod ( 'initialize' )
cleanupRaceChild . peer . respond ( cleanupRaceInitialize , null )
await nextTask ( )
cleanupRaceAbort . abort ( new Error ( 'cancelled during cleanup' ) )
cleanupRaceChild . settle ( )
await expect ( cleanupRace )
. rejects . toThrow ( 'aborted before run publication' )
const threadChild = fakeChild ( )
const threadStarting = startCodexRun ( request ( ) , runSpec ( threadChild ) )
const threadInitialize = await threadChild . peer . nextMethod ( 'initialize' )
threadChild . peer . respond ( threadInitialize , { userAgent : 'codex-cli 0.147.0' } )
await threadChild . peer . nextMethod ( 'initialized' )
const invalidThread = await threadChild . peer . nextMethod ( 'thread/start' )
threadChild . peer . respond ( invalidThread , { thread : { id : '' , ephemeral : true } } )
await expect ( threadStarting )
. rejects . toThrow ( expectedFailureDiagnostic ( 'thread-start' , 'unknown' ) )
await expect ( threadStarting ) . rejects . not . toThrow ( 'thread/start thread id' )
const exitedThreadChild = fakeChild ( { exitOnTerminate : false } )
const exitedThreadStarting = startCodexRun (
request ( ) ,
runSpec ( exitedThreadChild ) ,
)
const exitedThreadInitialize = await exitedThreadChild . peer . nextMethod ( 'initialize' )
exitedThreadChild . peer . respond ( exitedThreadInitialize , {
userAgent : 'codex-cli 0.147.0' ,
} )
await exitedThreadChild . peer . nextMethod ( 'initialized' )
await exitedThreadChild . peer . nextMethod ( 'thread/start' )
2026-08-18 22:48:24 +08:00
exitedThreadChild . settle ( { exitCode : null , signal : 'SIGABRT' } )
2026-08-18 03:40:11 +08:00
await expect ( exitedThreadStarting ) . rejects . toThrow ( expectedFailureDiagnostic (
'thread-start' ,
'unknown' ,
2026-08-18 22:48:24 +08:00
{ outcome : { exitCode : null , signal : 'SIGABRT' } } ,
2026-08-18 03:40:11 +08:00
) )
2026-08-18 23:59:43 +08:00
const eofBeforeCloseChild = fakeChild ( { exitOnTerminate : false } )
const eofBeforeCloseStarting = startCodexRun (
request ( ) ,
runSpec ( eofBeforeCloseChild ) ,
)
const eofBeforeCloseInitialize = await eofBeforeCloseChild . peer
. nextMethod ( 'initialize' )
eofBeforeCloseChild . peer . respond ( eofBeforeCloseInitialize , {
userAgent : 'codex-cli 0.147.0' ,
} )
await eofBeforeCloseChild . peer . nextMethod ( 'initialized' )
await eofBeforeCloseChild . peer . nextMethod ( 'thread/start' )
eofBeforeCloseChild . fromChild . emit ( 'end' )
setImmediate ( ( ) = > {
eofBeforeCloseChild . settle ( { exitCode : 23 , signal : null } )
} )
await expect ( eofBeforeCloseStarting ) . rejects . toThrow (
expectedFailureDiagnostic ( 'thread-start' , 'unknown' , {
outcome : { exitCode : 23 , signal : null } ,
} ) ,
)
2026-08-15 18:07:08 +08:00
const stderrChild = fakeChild ( )
const stderrStarting = startCodexRun ( request ( ) , runSpec ( stderrChild ) )
2026-08-15 19:02:04 +08:00
const stderrInitialize = await stderrChild . peer . nextMethod ( 'initialize' )
2026-08-15 18:07:08 +08:00
stderrChild . stderr . emit ( 'error' , new Error ( 'startup stderr broke' ) )
2026-08-15 19:02:04 +08:00
stderrChild . peer . respond ( stderrInitialize , { userAgent : 'codex-cli 0.147.0' } )
await stderrChild . peer . nextMethod ( 'initialized' )
const stderrThreadStart = await stderrChild . peer . nextMethod ( 'thread/start' )
stderrChild . peer . respond ( stderrThreadStart , {
thread : { id : 'thread-1' , ephemeral : true } ,
} )
const stderrRun = await stderrStarting
const stderrTurnStart = await stderrChild . peer . nextMethod ( 'turn/start' )
stderrChild . peer . send (
{ id : stderrTurnStart.id , result : { turn : { id : 'turn-1' } } } ,
agentMessage ( 'answer' , 'final_answer' ) ,
turnCompleted ( 'completed' ) ,
)
await expect ( stderrRun . result ) . resolves . toMatchObject ( { stopReason : 'completed' } )
await stderrRun . dispose ( )
2026-08-15 18:07:08 +08:00
expect ( stderrChild . stderr . listenerCount ( 'error' ) ) . toBe ( 0 )
2026-08-04 16:02:17 +08:00
} )
it ( 'rolls back an abort that wins immediately after thread creation' , async ( ) = > {
const controller = new AbortController ( )
const child = fakeChild ( )
const starting = startCodexRun (
request ( undefined , controller . signal ) ,
runSpec ( child ) ,
)
const initialize = await child . peer . nextMethod ( 'initialize' )
2026-08-08 23:56:39 +08:00
child . peer . respond ( initialize , { userAgent : 'codex-cli 0.147.0' } )
2026-08-04 16:02:17 +08:00
await child . peer . nextMethod ( 'initialized' )
const threadStart = await child . peer . nextMethod ( 'thread/start' )
2026-08-15 18:07:08 +08:00
expect ( threadStart . params ) . toEqual ( {
cwd : process.cwd ( ) ,
ephemeral : true ,
approvalPolicy : 'never' ,
} )
2026-08-04 16:02:17 +08:00
child . peer . respond ( threadStart , { thread : { id : 'thread-1' , ephemeral : true } } )
controller . abort ( 'startup race' )
2026-08-04 22:36:31 +08:00
await expect ( starting ) . rejects . toThrow ( 'aborted before run publication' )
2026-08-04 16:02:17 +08:00
expect ( child . terminate ) . toHaveBeenCalledTimes ( 1 )
} )
it ( 'keeps overlapping runs isolated' , async ( ) = > {
2026-08-15 19:33:03 +08:00
const initialStderrListeners = {
error : process.stderr.listenerCount ( 'error' ) ,
unpipe : process.stderr.listenerCount ( 'unpipe' ) ,
close : process.stderr.listenerCount ( 'close' ) ,
finish : process.stderr.listenerCount ( 'finish' ) ,
}
const runs = await Promise . all (
Array . from ( { length : 6 } , ( ) = > publishRun ( fakeChild ( ) ) ) ,
)
expect ( {
error : process.stderr.listenerCount ( 'error' ) ,
unpipe : process.stderr.listenerCount ( 'unpipe' ) ,
close : process.stderr.listenerCount ( 'close' ) ,
finish : process.stderr.listenerCount ( 'finish' ) ,
} ) . toEqual ( initialStderrListeners )
2026-08-04 16:02:17 +08:00
for ( const [ index , entry ] of runs . entries ( ) ) {
const id = ` turn- ${ index + 1 } `
entry . child . peer . send (
{ id : entry.turnStart.id , result : { turn : { id } } } ,
agentMessage ( ` answer- ${ index + 1 } ` , 'final_answer' , id ) ,
turnCompleted ( 'completed' , id ) ,
)
}
const results = await Promise . all ( runs . map ( entry = > entry . run . result ) )
2026-08-15 19:33:03 +08:00
expect ( results . map ( result = > result . output ) ) . toEqual (
Array . from ( { length : 6 } , ( _ , index ) = > [
{ type : 'text' , text : ` answer- ${ index + 1 } ` } ,
] ) ,
)
expect ( runs [ 0 ] ! . run . id ) . not . toBe ( runs [ 1 ] ! . run . id )
2026-08-04 16:02:17 +08:00
await Promise . all ( runs . map ( entry = > entry . run . dispose ( ) ) )
} )
2026-08-15 18:07:08 +08:00
it ( 'isolates permission modes and diagnostics across overlapping runs' , async ( ) = > {
const first = await publishRun ( fakeChild ( ) , undefined , {
permissionMode : 'never' ,
} )
const second = await publishRun ( fakeChild ( ) , undefined , {
permissionMode : 'dangerously-bypass-approvals-and-sandbox' ,
} )
first . child . peer . respond ( first . turnStart , { turn : { id : 'turn-never' } } )
second . child . peer . respond ( second . turnStart , { turn : { id : 'turn-bypass' } } )
await nextTask ( )
first . child . peer . send ( {
id : 'never-approval' ,
method : 'item/commandExecution/requestApproval' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-never' ,
availableDecisions : [ 'cancel' ] ,
} ,
} )
second . child . peer . send ( {
id : 'bypass-elicitation' ,
method : 'mcpServer/elicitation/request' ,
params : { threadId : 'thread-1' , turnId : null } ,
} )
await Promise . all ( [
first . child . peer . nextResponse ( 'never-approval' ) ,
second . child . peer . nextResponse ( 'bypass-elicitation' ) ,
] )
first . child . peer . send ( turnCompleted ( 'failed' , 'turn-never' , 'thread-1' , {
message : 'first failure' ,
codexErrorInfo : 'other' ,
} ) )
second . child . peer . send ( turnCompleted ( 'failed' , 'turn-bypass' , 'thread-1' , {
message : 'second failure' ,
codexErrorInfo : 'other' ,
} ) )
await expect ( first . run . result ) . resolves . toEqual ( {
output : [ ] ,
2026-08-18 03:40:11 +08:00
diagnostic : ` ${ expectedFailureDiagnostic ( 'turn' , 'other' ) } \ nCodex unattended decision (mode: never; request: command approval; decision: cancelled): the provider does not grant interactive approval ` ,
2026-08-15 18:07:08 +08:00
stopReason : 'error' ,
} )
await expect ( second . run . result ) . resolves . toEqual ( {
output : [ ] ,
2026-08-18 03:40:11 +08:00
diagnostic : ` ${ expectedFailureDiagnostic ( 'turn' , 'other' ) } \ nCodex unattended decision (mode: dangerously-bypass-approvals-and-sandbox; request: MCP elicitation; decision: declined): the provider does not collect interactive MCP input ` ,
2026-08-15 18:07:08 +08:00
stopReason : 'error' ,
} )
await Promise . all ( [ first . run . dispose ( ) , second . run . dispose ( ) ] )
} )
2026-08-04 16:02:17 +08:00
it ( 'uses the registered provider config and logs flattened errors' , async ( ) = > {
const ctx = new Context ( )
2026-08-13 00:36:22 +08:00
await ctx . plugin ( SubagentRuntime )
await ctx . plugin ( LocalSubprocessRuntime )
2026-08-04 16:02:17 +08:00
const child = fakeChild ( )
const spawn = vi . spyOn ( ctx . subprocess , 'spawn' ) . mockReturnValue ( child . handle )
const warnings : string [ ] = [ ]
ctx . logger . warn = ( ( message : unknown ) = > {
warnings . push ( String ( message ) )
} ) as typeof ctx . logger . warn
await ctx . plugin ( codex , {
2026-08-18 02:56:44 +08:00
providerName : 'codex-diagnostic' ,
2026-08-04 16:02:17 +08:00
env : { OPENAI_API_KEY : 'fake' } ,
2026-08-15 18:07:08 +08:00
permissionMode : 'approve-for-me' ,
2026-08-04 16:02:17 +08:00
disposeGraceMs : 25 ,
} )
2026-08-18 23:06:14 +08:00
const invalidCwdParent = {
id : 'parent-with-invalid-cwd' ,
session : { header : { cwd : 'relative/SECRET_TOKEN' } } ,
} as unknown as Agent
const invalidCwdError : unknown = await ctx . subagents . start ( 'codex-diagnostic' , {
prompt : [ { type : 'text' , text : 'task' } ] ,
parent : invalidCwdParent ,
signal : new AbortController ( ) . signal ,
} ) . then (
( ) = > undefined ,
( error : unknown ) = > error ,
)
expect ( invalidCwdError ) . toBeInstanceOf ( Error )
if ( ! ( invalidCwdError instanceof Error ) ) {
throw new Error ( 'expected safe invalid-cwd failure' )
}
expect ( invalidCwdError . message ) . toContain (
expectedFailureDiagnostic ( 'initialize' , 'unknown' ) ,
)
expect ( invalidCwdError . message ) . not . toContain ( 'relative/SECRET_TOKEN' )
expect ( invalidCwdError . cause ) . toBeInstanceOf ( Error )
expect ( ( invalidCwdError . cause as Error ) . message )
. toContain ( 'relative/SECRET_TOKEN' )
expect ( spawn ) . not . toHaveBeenCalled ( )
const invalidCwdAbort = new AbortController ( )
invalidCwdAbort . abort ( new Error ( 'cancel invalid cwd startup' ) )
await expect ( ctx . subagents . start ( 'codex-diagnostic' , {
prompt : [ { type : 'text' , text : 'task' } ] ,
parent : invalidCwdParent ,
signal : invalidCwdAbort.signal ,
} ) ) . rejects . toThrow ( 'aborted before app-server startup' )
expect ( spawn ) . not . toHaveBeenCalled ( )
2026-08-18 02:56:44 +08:00
const starting = ctx . subagents . start ( 'codex-diagnostic' , {
2026-08-04 16:02:17 +08:00
prompt : [ { type : 'text' , text : 'task' } ] ,
parent : fakeParent ,
signal : new AbortController ( ) . signal ,
} )
const initialize = await child . peer . nextMethod ( 'initialize' )
2026-08-08 23:56:39 +08:00
child . peer . respond ( initialize , { userAgent : 'codex-cli 0.147.0' } )
2026-08-04 16:02:17 +08:00
await child . peer . nextMethod ( 'initialized' )
const threadStart = await child . peer . nextMethod ( 'thread/start' )
2026-08-15 18:07:08 +08:00
expect ( threadStart . params ) . toEqual ( {
cwd : process.cwd ( ) ,
ephemeral : true ,
approvalPolicy : 'on-request' ,
approvalsReviewer : 'auto_review' ,
sandbox : 'workspace-write' ,
} )
2026-08-04 16:02:17 +08:00
child . peer . respond ( threadStart , { thread : { id : 'thread-1' , ephemeral : true } } )
const run = await starting
2026-08-15 18:07:08 +08:00
const turnStart = await child . peer . nextMethod ( 'turn/start' )
child . peer . respond ( turnStart , { turn : { id : 'turn-1' } } )
await nextTask ( )
child . peer . send ( {
id : 'provider-approval' ,
method : 'item/commandExecution/requestApproval' ,
params : {
threadId : 'thread-1' ,
turnId : 'turn-1' ,
availableDecisions : [ 'cancel' ] ,
command : 'cat /private/secret.txt' ,
} ,
} )
await child . peer . nextResponse ( 'provider-approval' )
child . peer . send ( turnCompleted ( 'failed' , 'turn-1' , 'thread-1' , {
message : 'SECRET_TOKEN in /private/secret.txt' ,
codexErrorInfo : 'other' ,
} ) )
await expect ( run . result ) . resolves . toEqual ( {
output : [ ] ,
2026-08-18 03:40:11 +08:00
diagnostic : ` ${ expectedFailureDiagnostic ( 'turn' , 'other' ) } \ nCodex unattended decision (mode: approve-for-me; request: command approval; decision: cancelled): the provider does not grant interactive approval ` ,
2026-08-15 18:07:08 +08:00
stopReason : 'error' ,
} )
2026-08-04 16:02:17 +08:00
expect ( spawn ) . toHaveBeenCalledWith ( expect . objectContaining ( {
env : { OPENAI_API_KEY : 'fake' } ,
graceMs : 25 ,
cwd : process.cwd ( ) ,
} ) )
expect ( warnings ) . toEqual ( [
2026-08-18 19:51:06 +08:00
expect . stringContaining (
` subagent-codex "codex-diagnostic": child run failed (error): subagent-codex: ${ expectedFailureDiagnostic ( 'turn' , 'other' ) } ` ,
) ,
2026-08-04 16:02:17 +08:00
] )
2026-08-15 18:07:08 +08:00
expect ( warnings . join ( '\n' ) ) . not . toContain ( 'SECRET_TOKEN' )
expect ( warnings . join ( '\n' ) ) . not . toContain ( '/private/secret.txt' )
await run . dispose ( )
2026-08-04 16:02:17 +08:00
await ctx . fiber . dispose ( )
} )
} )
describe ( 'disposeCodexChild' , ( ) = > {
it ( 'closes stdin, terminates, and waits for the managed tree' , async ( ) = > {
const child = fakeChild ( )
2026-08-15 19:02:04 +08:00
const wire = defaultWire ( child )
2026-08-04 16:02:17 +08:00
const end = vi . spyOn ( child . toChild , 'end' )
2026-08-04 22:36:31 +08:00
await disposeCodexChild ( wire , child . handle )
2026-08-04 16:02:17 +08:00
expect ( end ) . toHaveBeenCalled ( )
expect ( child . terminate ) . toHaveBeenCalledTimes ( 1 )
expect ( child . waitForExit ) . toHaveBeenCalledTimes ( 1 )
2026-08-04 22:36:31 +08:00
expect ( child . waitForExit ) . toHaveBeenCalledWith ( )
2026-08-04 16:02:17 +08:00
} )
2026-08-04 22:36:31 +08:00
it ( 'does not finish disposal before the managed tree exits' , async ( ) = > {
const child = fakeChild ( { exitOnTerminate : false } )
2026-08-15 19:02:04 +08:00
const wire = defaultWire ( child )
2026-08-04 22:36:31 +08:00
let disposed = false
const disposal = disposeCodexChild ( wire , child . handle ) . then ( ( ) = > {
disposed = true
} )
await new Promise < void > ( ( resolve ) = > { setImmediate ( resolve ) } )
expect ( disposed ) . toBe ( false )
child . settle ( )
await disposal
expect ( disposed ) . toBe ( true )
2026-08-04 19:55:39 +08:00
} )
2026-08-04 16:02:17 +08:00
it ( 'contains a concurrently closed stdin error' , async ( ) = > {
const child = fakeChild ( )
2026-08-15 19:02:04 +08:00
const wire = defaultWire ( child )
2026-08-04 16:02:17 +08:00
vi . spyOn ( child . toChild , 'end' ) . mockImplementation ( ( ) = > {
throw new Error ( 'already closed' )
} )
2026-08-04 22:36:31 +08:00
await expect ( disposeCodexChild ( wire , child . handle ) )
2026-08-04 16:02:17 +08:00
. resolves . toBeUndefined ( )
} )
it ( 'handles a spawn-level failure with no process tree' , async ( ) = > {
const child = fakeChild ( {
pid : - 1 ,
doneError : new Error ( 'spawn failed' ) ,
} )
2026-08-15 19:02:04 +08:00
const wire = defaultWire ( child )
2026-08-04 22:36:31 +08:00
await expect ( disposeCodexChild ( wire , child . handle ) )
2026-08-04 16:02:17 +08:00
. resolves . toBeUndefined ( )
expect ( child . terminate ) . not . toHaveBeenCalled ( )
expect ( child . waitForExit ) . not . toHaveBeenCalled ( )
} )
2026-08-18 19:14:02 +08:00
it ( 'reports tree-wait failure with safe teardown facts' , async ( ) = > {
2026-08-18 03:40:11 +08:00
const child = fakeChild ( {
waitForExitError : new Error ( 'SECRET_TOKEN wait failure' ) ,
} )
const wire = defaultWire ( child )
const disposal = disposeCodexChild ( wire , child . handle )
await expect ( disposal ) . rejects . toThrow ( expectedFailureDiagnostic (
'teardown' ,
'unknown' ,
{ outcome : { exitCode : 0 , signal : null } } ,
) )
await expect ( disposal ) . rejects . not . toThrow ( 'SECRET_TOKEN' )
} )
2026-08-18 23:59:43 +08:00
it ( 'does not wait for a pending process outcome after tree observation fails' , async ( ) = > {
const child = fakeChild ( {
exitOnTerminate : false ,
waitForExitError : new Error ( 'SECRET_TOKEN wait failure' ) ,
} )
const wire = defaultWire ( child )
let disposalError : unknown
const disposal = disposeCodexChild ( wire , child . handle ) . catch (
( error : unknown ) = > { disposalError = error } ,
)
await nextTask ( )
expect ( disposalError ) . toBeInstanceOf ( Error )
expect ( String ( disposalError ) ) . toContain (
expectedFailureDiagnostic ( 'teardown' , 'unknown' ) ,
)
expect ( String ( disposalError ) ) . not . toContain ( 'SECRET_TOKEN' )
child . settle ( )
await disposal
2026-08-04 16:02:17 +08:00
} )
} )