diff --git a/src/codex/prompt-text-probe.ts b/src/codex/prompt-text-probe.ts index 6db8d1e49e..11fdf897d7 100644 --- a/src/codex/prompt-text-probe.ts +++ b/src/codex/prompt-text-probe.ts @@ -100,7 +100,7 @@ function resolveCodexBinary(): string | null { /** 8 MiB is far above any real prompt and far below anything that hurts the server. */ const MAX_PROBE_OUTPUT_BYTES = 8 * 1024 * 1024; -function runProbe(binary: string, cwd: string, timeoutMs: number): Promise { +function runProbe(binary: string, cwd: string, timeoutMs: number, signal?: AbortSignal): Promise { return new Promise(resolve => { // A probe must never hang OR balloon the management API: it is bounded in // time AND in bytes, and every failure degrades to "unavailable" rather than @@ -118,11 +118,15 @@ function runProbe(binary: string, cwd: string, timeoutMs: number): Promise settle(null); const timer = setTimeout(() => settle(null), timeoutMs); + signal?.addEventListener("abort", abort, { once: true }); + if (signal?.aborted) abort(); child.stdout?.on("data", (chunk: Buffer) => { size += chunk.length; if (size > MAX_PROBE_OUTPUT_BYTES) { settle(null); return; } @@ -182,7 +186,9 @@ export const extractSectionsForTests = extractSections; * `cwd` matters: AGENTS.md and environment context are directory-dependent, so a * probe from the wrong place would describe a prompt the user never sees. */ -export async function probePromptText(timeoutMs = 15_000): Promise { +let probeActive = false; + +export async function probePromptText(timeoutMs = 15_000, signal?: AbortSignal): Promise { // The probe runs in CODEX_HOME, never in a caller-supplied directory. A `cwd` // parameter let an authenticated request read any readable folder's AGENTS.md, // and it also described a prompt that depends on where Codex happened to run. @@ -192,7 +198,21 @@ export async function probePromptText(timeoutMs = 15_000): Promise { // Decoding per chunk corrupts UTF-8 that straddles a chunk boundary. expect(probe).toContain("Buffer.concat(chunks).toString(\"utf8\")"); }); + + test("27. the probe bounds aggregate subprocesses and follows request cancellation", async () => { + const probe = await Bun.file(new URL("../src/codex/prompt-text-probe.ts", import.meta.url)).text(); + expect(probe).toContain("if (probeActive)"); + expect(probe).toContain("probeActive = true"); + expect(probe).toContain("probeActive = false"); + expect(probe).toContain('signal?.addEventListener("abort", abort'); + + const routes = await Bun.file(new URL("../src/server/management/codex-prompt-routes.ts", import.meta.url)).text(); + const textRoute = routes.slice(routes.indexOf('/api/codex-prompt/text')); + expect(textRoute.slice(0, 1_200)).toContain("req.signal"); + }); test("24. every ownership state is named, not collapsed into a boolean", async () => { // developerInstructionsOwned:false covers an ABSENT key and an EXTERNAL one, and // a GUI that cannot tell them apart hides its own create affordance from every