From 2886cc1efe3ac14d47eb2cdc2ffc9330449ad619 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 06:29:39 +0800 Subject: [PATCH] test(process): compare progress with retained PTY chunks --- .../ProcessCaptureTerminalRetention.test.ts | 39 +++++++++++++++++++ .../process/processCaptureProgress.test.ts | 26 +++++++++---- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/process/capture/ProcessCaptureTerminalRetention.test.ts b/src/process/capture/ProcessCaptureTerminalRetention.test.ts index 0c1f288570..22287233cd 100644 --- a/src/process/capture/ProcessCaptureTerminalRetention.test.ts +++ b/src/process/capture/ProcessCaptureTerminalRetention.test.ts @@ -138,3 +138,42 @@ it.each([false, true])( } }, ); + +it("retains PTY chunk boundaries when two output messages arrive fragmented", () => { + let onData: ((data: string) => void) | undefined; + const terminal = { + onData: (listener: (data: string) => void) => { + onData = listener; + return { dispose: () => undefined }; + }, + } as unknown as IPty; + const scenario = parseProcessScenario({ + executable: "unused", + normalization: { paths: false, pids: false, ports: false }, + }); + const frames: TerminalFrame[] = []; + const captureRetention = captureTerminalFrames({ + terminal, + scenario, + frames, + started: 0, + onOutput: () => undefined, + renderer: { write: () => undefined } as never, + normalizeText: createBoundedProcessTextNormalizer(scenario, "/tmp", -1), + recordEvent: () => undefined, + identifiers: () => [], + }); + // A PTY transports chunks, independently of the child's write boundaries. + const chunks = ["SPARSE_CHILD_", "STARTED\r\n", "SPARSE_CHILD_", "DONE\r\n"]; + for (const chunk of chunks) onData?.(chunk); + + expect(frames.map(({ data }) => data)).toEqual(chunks); + expect(frames.map(({ sequence }) => sequence)).toEqual([0, 1, 2, 3]); + expect(frames.map(({ data }) => data).join("")).toBe( + "SPARSE_CHILD_STARTED\r\nSPARSE_CHILD_DONE\r\n", + ); + expect(captureRetention().raw_terminal).toMatchObject({ + observed_frames: 4, + retained_frames: 4, + }); +}); diff --git a/tests/boundary/process/processCaptureProgress.test.ts b/tests/boundary/process/processCaptureProgress.test.ts index 86c556c3b7..5cf1b2aa7c 100644 --- a/tests/boundary/process/processCaptureProgress.test.ts +++ b/tests/boundary/process/processCaptureProgress.test.ts @@ -260,9 +260,9 @@ itWithCaptureCapability( operation: "capture_process_scenario", predicate_type: "rea.process-capture", }); - expect(recorded.frames.map(({ data }) => data).join("")).toContain( - "SPARSE_CHILD_STARTED", - ); + const terminalText = recorded.frames.map(({ data }) => data).join(""); + expect(terminalText).toContain("SPARSE_CHILD_STARTED"); + expect(terminalText).toContain("SPARSE_CHILD_DONE"); expect(capture.stderr).not.toContain("SPARSE_CHILD_STARTED"); expect(capture.stderr).not.toContain("SPARSE_CHILD_DONE"); const updates = capture.lines.map(({ update }) => update); @@ -273,15 +273,27 @@ itWithCaptureCapability( expect(Math.max(...elapsed) - Math.min(...elapsed)).toBeGreaterThanOrEqual( 1_000, ); - expect( - running.every(({ details }) => details.frames <= 2), - `running progress details: ${JSON.stringify(running.map(({ details }) => details))}`, - ).toBe(true); + // Frames count retained PTY chunks; child writes may be split or coalesced. + for (const [index, update] of running.entries()) { + expect(update.details.frames).toBeLessThanOrEqual(recorded.frames.length); + const previous = running[index - 1]; + if (previous !== undefined) + expect(update.details.frames).toBeGreaterThanOrEqual( + previous.details.frames, + ); + } + assertRateBound( + updates.filter(({ terminal }) => terminal !== true).length, + updates.at(-1)?.details.elapsed_ms ?? 0, + ); expect(updates.at(-1)).toMatchObject({ phase: "cleanup", terminal: true, details: { disposition: "exited", + frames: recorded.frames.length, + samples: recorded.process_samples.length, + interactions: recorded.interaction_events.length, owned_process_group: "cleaned", terminal_renderer: "cleaned", temporary_root: "cleaned",