From 77696dff9144fd6c110247711bbff144ec63b19b Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:17:42 +0000 Subject: [PATCH] fix(process): normalize resize frame timestamps --- src/application/ProcessCaptureJournal.ts | 14 ++++++++++++-- src/application/ProcessCaptureLifecycle.ts | 10 +++++----- src/application/ProcessNormalization.ts | 11 +++++++++-- tests/processCapture.test.ts | 3 ++- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/application/ProcessCaptureJournal.ts b/src/application/ProcessCaptureJournal.ts index 8de674232..b3696e435 100644 --- a/src/application/ProcessCaptureJournal.ts +++ b/src/application/ProcessCaptureJournal.ts @@ -6,7 +6,10 @@ import type { ProcessScenario, RecordProcessCaptureEvent, } from "../domain/processCapture.js"; -import { normalizeProcessText } from "./ProcessNormalization.js"; +import { + normalizeProcessElapsedTime, + normalizeProcessText, +} from "./ProcessNormalization.js"; import type { TerminalRenderer } from "./TerminalRenderer.js"; /** Mutable observation-order ledger whose writer is shared by capture producers. */ @@ -55,7 +58,14 @@ export const scheduleScenarioInteractions = ( if (event.type === "input") terminal.write(event.data); else if (event.type === "resize") { terminal.resize(event.columns, event.rows); - renderer.resize(event.columns, event.rows, dispatchedAt); + renderer.resize( + event.columns, + event.rows, + normalizeProcessElapsedTime( + dispatchedAt, + scenario.normalization.time_bucket_ms, + ), + ); } else terminal.kill(event.signal); } catch { outcome = "failed"; diff --git a/src/application/ProcessCaptureLifecycle.ts b/src/application/ProcessCaptureLifecycle.ts index 61e4908d4..cc9ac2eb3 100644 --- a/src/application/ProcessCaptureLifecycle.ts +++ b/src/application/ProcessCaptureLifecycle.ts @@ -41,6 +41,7 @@ import { assertRealPathAuthority, } from "./ProcessCaptureAuthority.js"; import { + normalizeProcessElapsedTime, normalizeProcessSamples, normalizeProcessText, redactProtocolEvents, @@ -364,11 +365,10 @@ export const captureTerminalFrames = (options: { return; } outputBytes += bytes; - const atMs = - Math.floor( - (Date.now() - options.started) / - options.scenario.normalization.time_bucket_ms, - ) * options.scenario.normalization.time_bucket_ms; + const atMs = normalizeProcessElapsedTime( + Date.now() - options.started, + options.scenario.normalization.time_bucket_ms, + ); const normalized = normalizeProcessText( data, options.scenario, diff --git a/src/application/ProcessNormalization.ts b/src/application/ProcessNormalization.ts index 712486fde..44a65bd03 100644 --- a/src/application/ProcessNormalization.ts +++ b/src/application/ProcessNormalization.ts @@ -4,6 +4,12 @@ import type { ProcessScenario, } from "../domain/processCapture.js"; +/** Bucket one elapsed process-capture timestamp under scenario normalization. */ +export const normalizeProcessElapsedTime = ( + elapsedMs: number, + timeBucketMs: number, +): number => Math.floor(elapsedMs / timeBucketMs) * timeBucketMs; + /** Normalize and redact one terminal/protocol payload under scenario rules. */ export const normalizeProcessText = ( value: string, @@ -69,9 +75,10 @@ export const normalizeProcessSamples = ( : normalized; }; return samples.map((sample) => ({ - at_ms: - Math.floor(sample.at_ms / scenario.normalization.time_bucket_ms) * + at_ms: normalizeProcessElapsedTime( + sample.at_ms, scenario.normalization.time_bucket_ms, + ), pid: mapping.get(sample.pid) ?? 1, parent_pid: mapping.get(sample.parent_pid) ?? 0, process_group_id: diff --git a/tests/processCapture.test.ts b/tests/processCapture.test.ts index f3f2b2db2..009667ec2 100644 --- a/tests/processCapture.test.ts +++ b/tests/processCapture.test.ts @@ -1694,6 +1694,7 @@ describe("process capture adapter", () => { { type: "resize", at_ms: 300, columns: 100, rows: 40 }, { type: "signal", at_ms: 700, signal: "SIGINT" }, ], + normalization: { time_bucket_ms: 60_000 }, timeout_ms: 2_000, idle_timeout_ms: 2_000, }), @@ -1705,8 +1706,8 @@ describe("process capture adapter", () => { allowExternalNetwork: true, }, ); - expect(result.ok).toBe(true); if (!result.ok) throw result.error; + expect(result.ok).toBe(true); const output = result.value.frames.map(({ data }) => data).join(""); expect(output).toContain("prompt>"); expect(output).toContain("input:answer unicode:雪");