fix: retain cancellation during Electron handle finalization
This commit is contained in:
1 parent
5eada7e410
commit
4e2a566d6c
2 files changed
+58
-2
No files matched your search
@@ -748,3 +748,40 @@ it.each(["acquisition", "identity"] as const)(
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
},
|
||||
);
|
||||
|
||||
it("retains the caller reason when cancellation precedes failed handle disposal", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { stop: "during handle disposal" };
|
||||
const dispose = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => {
|
||||
controller.abort(reason);
|
||||
throw new Error("dispose rejected");
|
||||
})
|
||||
.mockResolvedValue(undefined);
|
||||
const page = { locator: () => ({ click: async () => {} }) };
|
||||
const resources = new ArtifactResourceScope();
|
||||
const outcome = await runElectronActions(
|
||||
{
|
||||
windows: () => [page],
|
||||
browserWindow: async () => ({ evaluate: async () => 99, dispose }),
|
||||
} as unknown as ElectronApplication,
|
||||
electronActiveObservationInputSchema.parse({
|
||||
executable_path: "/opt/electron",
|
||||
application_path: "/opt/app/main.js",
|
||||
application_root: "/opt/app",
|
||||
actions: [{ step_id: "selected", kind: "click", selector: "#selected" }],
|
||||
}),
|
||||
{ signal: controller.signal },
|
||||
resources,
|
||||
);
|
||||
expect(outcome.actions[0]).toMatchObject({
|
||||
status: "completed",
|
||||
target: "window:99",
|
||||
});
|
||||
expect(outcome.cleanupError?.cleanupPrimary?.cause).toBe(reason);
|
||||
expect(outcome.cleanupError?.cleanupIncomplete).toBe(true);
|
||||
await resources.close();
|
||||
expect(outcome.cleanupError?.cleanupIncomplete).toBe(false);
|
||||
expect(outcome.cleanupError?.cleanupPrimary?.cause).toBe(reason);
|
||||
});
|
||||
@@ -9,6 +9,8 @@ import type {
|
||||
} from "../artifacts/ArtifactResourceScope.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import { BrowserObservationError } from "../domain/browserObservationError.js";
|
||||
import {
|
||||
electronActiveTimelineEventSchema,
|
||||
@@ -159,7 +161,12 @@ export const runElectronActions = async (
|
||||
error: actionError,
|
||||
});
|
||||
if (handleOwner !== undefined) {
|
||||
cleanupError = await releaseActionWindow(resources, handleOwner);
|
||||
cleanupError = await releaseActionWindow(
|
||||
resources,
|
||||
handleOwner,
|
||||
stoppedBy ?? "completed",
|
||||
options.signal,
|
||||
);
|
||||
if (cleanupError !== undefined) stoppedBy = "failed";
|
||||
}
|
||||
}
|
||||
@@ -206,6 +213,8 @@ const ownActionWindow = (
|
||||
const releaseActionWindow = async (
|
||||
resources: ArtifactResourceScope,
|
||||
owner: ArtifactResourceOwner,
|
||||
actionStatus: ElectronActions[number]["status"],
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<AnalysisError | undefined> => {
|
||||
const released = await resources.release(owner);
|
||||
if (released.kind === "released") return undefined;
|
||||
@@ -219,7 +228,17 @@ const releaseActionWindow = async (
|
||||
);
|
||||
error.bindCleanupResourceOwner(released.lease);
|
||||
released.lease.bindCleanupError(error);
|
||||
return error;
|
||||
if (!signal?.aborted) return error;
|
||||
const cancellation = new AnalysisCancelledError(OPERATION, {
|
||||
cause: signal.reason,
|
||||
});
|
||||
return actionStatus === "failed"
|
||||
? error.retainSecondaryFailure(cancellation)
|
||||
: analysisErrorWithCleanupFailure(
|
||||
cancellation,
|
||||
error,
|
||||
OPERATION,
|
||||
).bindCleanupDependencies([cancellation, error]);
|
||||
};
|
||||
|
||||
type RunActionContext = {
|
||||
|
||||
Reference in new issue
Block a user