From 5c5cab69034cb2108b3a7d78d06a035024d623ec Mon Sep 17 00:00:00 2001 From: neurot1cal Date: Tue, 26 May 2026 16:39:53 -0700 Subject: [PATCH] fix: don't stream when the append IPC is unavailable Codex re-review: if openRecordingStream exists but appendRecordingChunk does not (renderer/main version skew), the recorder would open the stream and switch to streaming mode, but every append silently no-ops and the save ends up empty. Require both IPC methods before streaming; otherwise fall back to in-memory buffering. Adds a regression test. Verified: tsc --noEmit clean; biome clean; vitest 183/183. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/hooks/recorderHandle.test.ts | 20 ++++++++++++++++++++ src/hooks/recorderHandle.ts | 8 +++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/hooks/recorderHandle.test.ts b/src/hooks/recorderHandle.test.ts index adb5370..3f16437 100644 --- a/src/hooks/recorderHandle.test.ts +++ b/src/hooks/recorderHandle.test.ts @@ -205,6 +205,26 @@ describe("createRecorderHandle", () => { expect(blob.size).toBe(2); }); + it("buffers in memory when appendRecordingChunk is unavailable (version skew)", async () => { + const openRecordingStream = vi.fn(async () => ({ success: true })); + // appendRecordingChunk intentionally omitted to simulate renderer/main skew. + stubElectronAPI({ openRecordingStream }); + + const handle = createRecorderHandle({} as MediaStream, { mimeType: "video/webm" }, "rec.webm"); + const fake = driver(handle); + + fake.emit(new Blob(["a"])); + await tick(); + fake.emit(new Blob(["b"])); + fake.stop(); + + const blob = await handle.recordedBlobPromise; + // Never even attempts to open the stream when it can't append to it. + expect(openRecordingStream).not.toHaveBeenCalled(); + expect(handle.isStreaming()).toBe(false); + expect(blob.size).toBe(2); + }); + it("discard closes the disk stream for a streamed recording", async () => { const closeRecordingStream = vi.fn(async () => ({ success: true })); stubElectronAPI({ diff --git a/src/hooks/recorderHandle.ts b/src/hooks/recorderHandle.ts index d547bf9..e98000e 100644 --- a/src/hooks/recorderHandle.ts +++ b/src/hooks/recorderHandle.ts @@ -76,8 +76,14 @@ export function createRecorderHandle( }); }; + // Require BOTH stream IPC methods before attempting to stream. If only + // openRecordingStream exists (renderer/main version skew), streaming would + // open but every append would silently no-op, saving an empty file — so in + // that case fall through to in-memory buffering instead. const openPromise: Promise<{ success: boolean; error?: string }> = - fileName && api?.openRecordingStream + fileName !== undefined && + typeof api?.openRecordingStream === "function" && + typeof api?.appendRecordingChunk === "function" ? api.openRecordingStream(fileName) : Promise.resolve({ success: false });