fix(browser): skip Windows Electron signals without verified lineage (#1588)

capture_electron_scenario called taskkill on the recorded leader PID before lineage could be verified. On Windows the process table is empty, so that proof never arrives and the signal still ran. Refuse the cleanup without signaling. A verified lineage still terminates the leader and its descendants.

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Adolaniumandmorluto authored and GitHub committed 2026-10-11 06:22:54 +08:00
1 parent afc9d819aa
commit 1a6ccec636
2 files changed
+84 -6

No files matched your search

@@ -1,9 +1,13 @@
import { afterEach, expect, it, vi } from "vitest";
import * as processOwnership from "../process/ProcessOwnership.js";
import { electronActiveObservationInputSchema } from "../domain/javascript/electronActiveObservation.js";
import * as electronActions from "./PlaywrightElectronActiveActions.js";
import { PlaywrightElectronActiveProvider } from "./PlaywrightElectronActiveProvider.js";
import { electronActiveObservationInputSchema } from "../domain/javascript/electronActiveObservation.js";
import type { OwnedProcessGroup } from "../process/ProcessOwnership.js";
import {
cleanupElectronProcesses,
PlaywrightElectronActiveProvider,
} from "./PlaywrightElectronActiveProvider.js";
afterEach(() => {
vi.restoreAllMocks();
@@ -39,6 +43,71 @@ it("passes the selected environment and ownership token to Electron launch", asy
});
});
const ownership = {
runId: "run",
leaderPid: 4242,
processGroupId: 4242,
expectedParentPid: 1,
} satisfies OwnedProcessGroup;
it("does not signal a Windows Electron PID when lineage is unavailable", async () => {
const terminateTree = vi.fn(async () => ({
cleaned: true as const,
signaled: true,
}));
const result = await cleanupElectronProcesses(
ownership,
{
status: "unavailable",
observedAt: "2026-10-10T00:00:00.000Z",
runId: ownership.runId,
launcherPid: ownership.leaderPid,
processGroupId: ownership.processGroupId,
reason: "owned launcher is not live",
},
{ platform: "win32", terminateTree },
);
expect(terminateTree).not.toHaveBeenCalled();
expect(result).toEqual({
cleaned: false,
reason:
"owned Electron lineage was unavailable; helper cleanup was not proven",
});
});
it("signals a Windows Electron tree only after lineage is verified", async () => {
const terminateTree = vi.fn(async () => ({
cleaned: true as const,
signaled: true,
}));
const result = await cleanupElectronProcesses(
ownership,
{
status: "verified",
observedAt: "2026-10-10T00:00:00.000Z",
lineage: {
runId: ownership.runId,
launcherPid: ownership.leaderPid,
launcherParentPid: 1,
processGroupId: ownership.processGroupId,
descendants: [
{
pid: 4343,
parentPid: ownership.leaderPid,
processGroupId: ownership.processGroupId,
},
],
},
},
{ platform: "win32", terminateTree },
);
expect(terminateTree).toHaveBeenNthCalledWith(1, 4242);
expect(terminateTree).toHaveBeenNthCalledWith(2, 4343);
expect(result).toEqual({ cleaned: true, signaled: true });
});
it("retains the partial capture without taskkill when Windows lineage is unavailable", async () => {
// Exercise the Windows decision branch; this is not native Windows coverage.
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
@@ -259,21 +259,30 @@ export class PlaywrightElectronActiveProvider implements ElectronActiveObservati
}
}
const cleanupElectronProcesses = async (
/** Clean up an Electron launch. Windows signals a process only after lineage is verified. */
export const cleanupElectronProcesses = async (
ownership: OwnedProcessGroup,
lineage: ProcessLineageObservation | undefined,
host: {
readonly platform: NodeJS.Platform;
readonly terminateTree: typeof cleanupWindowsProcessTree;
} = {
platform: process.platform,
terminateTree: cleanupWindowsProcessTree,
},
): Promise<ProcessCleanupResult> => {
if (process.platform === "win32") {
if (host.platform === "win32") {
// An empty Windows process table is not ownership proof for this PID.
if (lineage?.status !== "verified")
return {
cleaned: false,
reason:
"owned Electron lineage was unavailable; helper cleanup was not proven",
};
const root = await cleanupWindowsProcessTree(ownership.leaderPid);
const root = await host.terminateTree(ownership.leaderPid);
if (!root.cleaned) return root;
for (const descendant of lineage.lineage.descendants) {
const result = await cleanupWindowsProcessTree(descendant.pid);
const result = await host.terminateTree(descendant.pid);
if (!result.cleaned) return result;
}
return root;