From e304de99bcb122fbf1f924ba0ce2f093fbb621a5 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:36:11 +0800 Subject: [PATCH] fix(desktop): retain published capture evidence on late cancellation (#9591) fix(desktop): retain published capture on late cancellation --- .../desktop/DesktopApplicationCapture.test.ts | 39 +++++++++++++++++++ .../desktop/DesktopApplicationCapture.ts | 1 + 2 files changed, 40 insertions(+) diff --git a/src/application/desktop/DesktopApplicationCapture.test.ts b/src/application/desktop/DesktopApplicationCapture.test.ts index 3c0b62a913..4dc0d54ee9 100644 --- a/src/application/desktop/DesktopApplicationCapture.test.ts +++ b/src/application/desktop/DesktopApplicationCapture.test.ts @@ -20,6 +20,7 @@ import { } from "../../domain/desktop/desktopCapture.js"; import type { SessionRequest } from "../../desktop/DesktopApplicationSession.js"; import { OwnedFileHandle } from "../../filesystem/OwnedFileHandle.js"; +import { CaptureFile } from "./DesktopCaptureOutput.js"; import { captureDesktopApplication, type DesktopCaptureRequest, @@ -225,6 +226,44 @@ describe("captureDesktopApplication refusals", () => { }); }); +it.skipIf(process.platform !== "linux")( + "returns the published evidence when cancellation arrives during publication", + async () => { + const { root, home, environment } = await fixture(); + const output = join(root, "out"); + const statePath = join(home, ".config/fakeapp/state.ini"); + const controller = new AbortController(); + const publish = CaptureFile.prototype.publish; + const spy = vi + .spyOn(CaptureFile.prototype, "publish") + .mockImplementation(async function (this: CaptureFile, evidence) { + const path = await publish.call(this, evidence); + controller.abort("cancelled after publication"); + return path; + }); + try { + const result = await captureDesktopApplication( + request(output), + host(environment, () => + readFileSyncSafe(statePath) === "count=1\n" + ? { kind: "stop" } + : undefined, + ), + controller.signal, + ); + if (!("error" in result)) throw new Error("Expected cancellation"); + const published = JSON.parse( + await readFile(join(output, "capture.json"), "utf8"), + ); + expect(result.code).toBe("cancelled"); + expect(result.details?.partial_observation).toEqual(published); + } finally { + spy.mockRestore(); + } + }, + 30_000, +); + describe.skipIf(process.platform !== "linux")( "captureDesktopApplication on Linux", () => { diff --git a/src/application/desktop/DesktopApplicationCapture.ts b/src/application/desktop/DesktopApplicationCapture.ts index 086a0e927b..b825dd358d 100644 --- a/src/application/desktop/DesktopApplicationCapture.ts +++ b/src/application/desktop/DesktopApplicationCapture.ts @@ -372,6 +372,7 @@ const runCapture = async ( if (signal?.aborted === true && session.end_reason !== "cancelled") throw new AnalysisCancelledError(DESKTOP_OPERATION, { cause: signal.reason, + partialObservation: evidence, }); return evidence; };