diff --git a/scripts/anthropic-prompt-probe.ts b/scripts/anthropic-prompt-probe.ts index 1deb21fbf8d..1efd66efec1 100644 --- a/scripts/anthropic-prompt-probe.ts +++ b/scripts/anthropic-prompt-probe.ts @@ -120,6 +120,7 @@ type StoppableGatewayChild = { }; type ClosableLogFile = { + appendFile?(data: string | Uint8Array): Promise; close(): Promise; }; @@ -520,11 +521,32 @@ async function startGatewayProcess(params: { stdio: ["ignore", "pipe", "pipe"], }, ); - child.stdout.on("data", (chunk) => void logFile.appendFile(chunk)); - child.stderr.on("data", (chunk) => void logFile.appendFile(chunk)); + const pendingLogWrites = new Set>(); + const logWriteErrors: unknown[] = []; + const trackLogWrite = (chunk: Buffer) => { + const write = logFile.appendFile(chunk).catch((error: unknown) => { + logWriteErrors.push(error); + throw error; + }); + pendingLogWrites.add(write); + void write + .finally(() => { + pendingLogWrites.delete(write); + }) + .catch(() => undefined); + }; + child.stdout.on("data", trackLogWrite); + child.stderr.on("data", trackLogWrite); return { async stop(): Promise { - return await stopGatewayPromptChild(child, logFile); + return await stopGatewayPromptChild( + child, + logFile, + 1_500, + 1_500, + pendingLogWrites, + logWriteErrors, + ); }, }; } @@ -534,6 +556,8 @@ async function stopGatewayPromptChild( logFile: ClosableLogFile, sigintTimeoutMs = 1_500, sigkillTimeoutMs = 1_500, + pendingLogWrites: Iterable> = [], + logWriteErrors: readonly unknown[] = [], ): Promise { let exited = child.exitCode !== null || child.signalCode !== null; const exitPromise = exited @@ -560,7 +584,14 @@ async function stopGatewayPromptChild( () => false, ); } + const failedLogWrite = (await Promise.allSettled(pendingLogWrites)).find( + (result): result is PromiseRejectedResult => result.status === "rejected", + ); await logFile.close(); + const logWriteError = failedLogWrite?.reason ?? logWriteErrors[0]; + if (logWriteError) { + throw new Error(`Anthropic prompt gateway log write failed: ${String(logWriteError)}`); + } return exited; } diff --git a/test/scripts/dev-tooling-safety.test.ts b/test/scripts/dev-tooling-safety.test.ts index 79b62fdd43d..d4e620075c2 100644 --- a/test/scripts/dev-tooling-safety.test.ts +++ b/test/scripts/dev-tooling-safety.test.ts @@ -422,6 +422,42 @@ describe("script-specific dev tooling hardening", () => { expect(closeCalls).toBe(1); }); + it("waits for Anthropic prompt gateway log writes before closing the log file", async () => { + let resolveWrite: (() => void) | undefined; + const order: string[] = []; + const pendingWrite = new Promise((resolve) => { + resolveWrite = () => { + order.push("write"); + resolve(); + }; + }); + const stop = promptProbeTesting.stopGatewayPromptChild( + { + exitCode: 0, + signalCode: null, + kill: () => true, + once(_event: "exit", _listener: () => void) {}, + }, + { + close: async () => { + order.push("close"); + }, + }, + 1, + 1, + [pendingWrite], + ); + + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + expect(order).toEqual([]); + + resolveWrite?.(); + await expect(stop).resolves.toBe(true); + expect(order).toEqual(["write", "close"]); + }); + it("uses exact Claude cookie host matchers instead of broad substring matches", () => { expect(claudeUsageTesting.CLAUDE_COOKIE_HOST_SQL).toContain("host_key = 'claude.ai'"); expect(claudeUsageTesting.CLAUDE_COOKIE_HOST_SQL).toContain("LIKE '%.claude.ai'");