mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
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>
185 lines
5.4 KiB
TypeScript
185 lines
5.4 KiB
TypeScript
import { afterEach, expect, it, vi } from "vitest";
|
|
|
|
import * as processOwnership from "../process/ProcessOwnership.js";
|
|
import * as electronActions from "./PlaywrightElectronActiveActions.js";
|
|
import { electronActiveObservationInputSchema } from "../domain/javascript/electronActiveObservation.js";
|
|
import type { OwnedProcessGroup } from "../process/ProcessOwnership.js";
|
|
import {
|
|
cleanupElectronProcesses,
|
|
PlaywrightElectronActiveProvider,
|
|
} from "./PlaywrightElectronActiveProvider.js";
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it("passes the selected environment and ownership token to Electron launch", async () => {
|
|
vi.stubEnv("LANG", "ambient-language");
|
|
let launchedEnvironment: unknown;
|
|
const provider = new PlaywrightElectronActiveProvider(
|
|
{
|
|
LANG: "selected-language",
|
|
PATH: "/selected/bin",
|
|
REA_PRIVATE_VALUE: "not-a-launch-setting",
|
|
},
|
|
async (options) => {
|
|
launchedEnvironment = options?.env;
|
|
throw new Error("launch boundary reached");
|
|
},
|
|
);
|
|
const result = await provider.capture(
|
|
electronActiveObservationInputSchema.parse({
|
|
executable_path: process.execPath,
|
|
application_path: process.execPath,
|
|
}),
|
|
);
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(launchedEnvironment).toEqual({
|
|
LANG: "selected-language",
|
|
PATH: "/selected/bin",
|
|
REA_PROCESS_RUN_ID: expect.any(String),
|
|
});
|
|
});
|
|
|
|
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");
|
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
const terminateTree = vi
|
|
.spyOn(processOwnership, "cleanupWindowsProcessTree")
|
|
.mockResolvedValue({ cleaned: true, signaled: true });
|
|
const hookSnapshot = {
|
|
events: [],
|
|
retention_budget_bytes: 0,
|
|
estimated_retained_bytes: 0,
|
|
event_serialized_byte_upper_bound: 0,
|
|
retained: 0,
|
|
dropped: 0,
|
|
dropped_ipc: 0,
|
|
dropped_runtime: 0,
|
|
dropped_event_families: [],
|
|
dropped_event_roles: [],
|
|
observed: 0,
|
|
observed_ipc: 0,
|
|
observed_runtime: 0,
|
|
hook_error: false,
|
|
};
|
|
vi.spyOn(electronActions, "readApplicationState").mockResolvedValue({
|
|
windows: [],
|
|
metrics: [],
|
|
electronVersion: "44.0.0",
|
|
hookSnapshot,
|
|
});
|
|
const application = {
|
|
process: () => ({ pid: 424242 }),
|
|
close: vi.fn(async () => undefined),
|
|
windows: () => [],
|
|
};
|
|
const provider = new PlaywrightElectronActiveProvider(
|
|
{},
|
|
async () =>
|
|
application as unknown as Awaited<
|
|
ReturnType<typeof import("playwright-core")._electron.launch>
|
|
>,
|
|
);
|
|
|
|
try {
|
|
const result = await provider.capture(
|
|
electronActiveObservationInputSchema.parse({
|
|
executable_path: process.execPath,
|
|
application_path: process.execPath,
|
|
}),
|
|
);
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) throw new Error("Expected unverified cleanup to fail");
|
|
expect(terminateTree).not.toHaveBeenCalled();
|
|
expect(result.error).toMatchObject({
|
|
_tag: "BrowserObservationError",
|
|
reason: "cleanup_failed",
|
|
cleanup: {
|
|
reason:
|
|
"owned Electron lineage was unavailable; helper cleanup was not proven",
|
|
},
|
|
partialObservation: {
|
|
kind: "electron-active-observation",
|
|
capture: {
|
|
application: {
|
|
cleanup: "unverified",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
} finally {
|
|
if (originalPlatform !== undefined)
|
|
Object.defineProperty(process, "platform", originalPlatform);
|
|
}
|
|
});
|