1- import { spawnSync } from "node:child_process" ;
1+ import { type ChildProcessWithoutNullStreams , spawn , spawnSync } from "node:child_process" ;
22import { appendFileSync , mkdtempSync , readFileSync , rmSync , writeFileSync } from "node:fs" ;
33import { tmpdir } from "node:os" ;
44import path from "node:path" ;
@@ -7,6 +7,9 @@ import { createJsonlRequestTailer } from "../../scripts/e2e/lib/codex-media-path
77import { readPositiveIntEnv } from "../../scripts/e2e/lib/codex-media-path/limits.mjs" ;
88
99const tempRoots : string [ ] = [ ] ;
10+ const fakeAppServerPath = path . resolve (
11+ "scripts/e2e/lib/codex-media-path/fake-codex-app-server.mjs" ,
12+ ) ;
1013const writeConfigPath = path . resolve ( "scripts/e2e/lib/codex-media-path/write-config.mjs" ) ;
1114
1215function makeTempRoot ( ) : string {
@@ -42,6 +45,42 @@ function runWriteConfig(root: string, env: Record<string, string> = {}) {
4245 } ) ;
4346}
4447
48+ async function readStdoutLine ( child : ChildProcessWithoutNullStreams ) : Promise < string > {
49+ return await new Promise < string > ( ( resolve , reject ) => {
50+ let stdout = "" ;
51+ const timeout = setTimeout ( ( ) => {
52+ reject ( new Error ( `timed out waiting for fake app-server response: ${ stdout } ` ) ) ;
53+ } , 3_000 ) ;
54+ child . stdout . setEncoding ( "utf8" ) ;
55+ child . stdout . on ( "data" , ( chunk : string ) => {
56+ stdout += chunk ;
57+ const line = stdout . split ( "\n" ) . find ( ( entry ) => entry . trim ( ) ) ;
58+ if ( line ) {
59+ clearTimeout ( timeout ) ;
60+ resolve ( line ) ;
61+ }
62+ } ) ;
63+ child . once ( "error" , ( error ) => {
64+ clearTimeout ( timeout ) ;
65+ reject ( error ) ;
66+ } ) ;
67+ child . once ( "exit" , ( code , signal ) => {
68+ clearTimeout ( timeout ) ;
69+ reject ( new Error ( `fake app-server exited before response: code=${ code } signal=${ signal } ` ) ) ;
70+ } ) ;
71+ } ) ;
72+ }
73+
74+ async function stopChild ( child : ChildProcessWithoutNullStreams ) : Promise < void > {
75+ if ( child . exitCode !== null || child . signalCode !== null ) {
76+ return ;
77+ }
78+ await new Promise < void > ( ( resolve ) => {
79+ child . once ( "close" , ( ) => resolve ( ) ) ;
80+ child . kill ( "SIGTERM" ) ;
81+ } ) ;
82+ }
83+
4584afterEach ( ( ) => {
4685 for ( const root of tempRoots . splice ( 0 ) ) {
4786 rmSync ( root , { recursive : true , force : true } ) ;
@@ -87,6 +126,40 @@ describe("codex media path limits", () => {
87126 } ) ;
88127} ) ;
89128
129+ describe ( "codex media path fake app-server" , ( ) => {
130+ it ( "returns a structured error when request logging fails" , async ( ) => {
131+ const requestLogDirectory = makeTempRoot ( ) ;
132+ const child : ChildProcessWithoutNullStreams = spawn ( process . execPath , [ fakeAppServerPath ] , {
133+ env : {
134+ ...process . env ,
135+ OPENCLAW_CODEX_MEDIA_PATH_APP_SERVER_LOG : requestLogDirectory ,
136+ } ,
137+ stdio : [ "pipe" , "pipe" , "pipe" ] ,
138+ } ) ;
139+ let stderr = "" ;
140+ child . stderr . setEncoding ( "utf8" ) ;
141+ child . stderr . on ( "data" , ( chunk : string ) => {
142+ stderr += chunk ;
143+ } ) ;
144+
145+ try {
146+ const responseLine = readStdoutLine ( child ) ;
147+ child . stdin . write ( jsonl ( { id : "request-1" , method : "initialize" } ) ) ;
148+ const response = JSON . parse ( await responseLine ) ;
149+
150+ expect ( response ) . toMatchObject ( {
151+ error : {
152+ message : expect . stringContaining ( "fake Codex app-server request log write failed" ) ,
153+ } ,
154+ id : "request-1" ,
155+ } ) ;
156+ expect ( stderr ) . toContain ( "fake Codex app-server request log write failed" ) ;
157+ } finally {
158+ await stopChild ( child ) ;
159+ }
160+ } ) ;
161+ } ) ;
162+
90163describe ( "codex media path JSONL tailer" , ( ) => {
91164 it ( "keeps parsed app-server requests and reads only appended lines" , ( ) => {
92165 const logPath = path . join ( makeTempRoot ( ) , "app-server.jsonl" ) ;
0 commit comments