diff --git a/sdk/typescript/src/worker-progress.ts b/sdk/typescript/src/worker-progress.ts index 83fa1242a..d4f366179 100644 --- a/sdk/typescript/src/worker-progress.ts +++ b/sdk/typescript/src/worker-progress.ts @@ -68,13 +68,8 @@ export function scanProgressUpdatesFromEvent( : null; if (typeof output !== "string") return []; const updates: ScanProgress[] = []; - let codeFence = false; - for (const line of output.split(/\r?\n/u)) { - if (/^\s*```/u.test(line)) { - codeFence = !codeFence; - continue; - } - if (codeFence || !line.startsWith(SCAN_PROGRESS_PREFIX)) continue; + for (const line of linesOutsideCodeFences(output)) { + if (!line.startsWith(SCAN_PROGRESS_PREFIX)) continue; const progress = scanProgressFromMarker(line); if (progress !== null) updates.push(progress); } @@ -165,9 +160,9 @@ function dispatchStatus( item: Readonly>, ): ScanWorkerStatus | null { if (typeof item["text"] !== "string") return null; - const markers = item["text"] - .split(/\r?\n/u) - .filter((line) => line.startsWith(WORKER_STATUS_PREFIX)); + const markers = linesOutsideCodeFences(item["text"]).filter((line) => + line.startsWith(WORKER_STATUS_PREFIX), + ); const marker = markers[0]; if (markers.length !== 1 || marker === undefined) return null; let payload: unknown; @@ -194,6 +189,34 @@ function dispatchStatus( }; } +function linesOutsideCodeFences(value: string): string[] { + const lines: string[] = []; + let fence: { marker: "`" | "~"; length: number } | null = null; + for (const line of value.split(/\r?\n/u)) { + if (fence !== null) { + const closing = /^[ \t]{0,3}(`{3,}|~{3,})[ \t]*$/u.exec(line)?.[1]; + if ( + closing !== undefined && + closing[0] === fence.marker && + closing.length >= fence.length + ) { + fence = null; + } + continue; + } + const opening = /^[ \t]{0,3}(`{3,}|~{3,})/u.exec(line)?.[1]; + if (opening !== undefined) { + fence = { + marker: opening[0] as "`" | "~", + length: opening.length, + }; + continue; + } + lines.push(line); + } + return lines; +} + function isProgressCount(value: unknown): value is number { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0; } diff --git a/sdk/typescript/tests-ts/worker-progress-fences.test.ts b/sdk/typescript/tests-ts/worker-progress-fences.test.ts new file mode 100644 index 000000000..747c08441 --- /dev/null +++ b/sdk/typescript/tests-ts/worker-progress-fences.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from "bun:test"; +import { + scanProgressUpdatesFromEvent, + workerStatusFromEvent, +} from "../src/worker-progress.js"; + +function messageEvent(text: string): Record { + return { + type: "item.completed", + item: { id: "message-1", type: "agent_message", text }, + }; +} + +describe("worker progress Markdown fences", () => { + test("does not close a longer outer fence with a shorter backtick run", () => { + const text = [ + "````markdown", + "```text", + 'CODEX_SECURITY_SCAN_PROGRESS {"phase":"discovery","filesCompleted":8,"filesTotal":8}', + "```", + "````", + ].join("\n"); + + expect(scanProgressUpdatesFromEvent(messageEvent(text))).toEqual([]); + }); + + test("ignores worker-status markers inside fenced examples", () => { + const text = [ + "Example:", + "```text", + 'CODEX_SECURITY_WORKER_STATUS {"phase":"file_review","planned":6,"started":6}', + "```", + ].join("\n"); + + expect(workerStatusFromEvent(messageEvent(text))).toBeNull(); + }); + + test("supports tilde fences and still reads markers after a fence closes", () => { + const text = [ + "~~~text", + 'CODEX_SECURITY_SCAN_PROGRESS {"phase":"discovery","filesCompleted":7,"filesTotal":8}', + 'CODEX_SECURITY_WORKER_STATUS {"phase":"file_review","planned":6,"started":5}', + "~~~", + 'CODEX_SECURITY_SCAN_PROGRESS {"phase":"validation","filesCompleted":8,"filesTotal":8}', + 'CODEX_SECURITY_WORKER_STATUS {"phase":"validation","planned":2,"started":2}', + ].join("\n"); + + expect(scanProgressUpdatesFromEvent(messageEvent(text))).toEqual([ + { phase: "validation", filesCompleted: 8, filesTotal: 8 }, + ]); + expect(workerStatusFromEvent(messageEvent(text))).toEqual({ + kind: "dispatch", + phase: "validation", + planned: 2, + started: 2, + }); + }); +});