rea/src/browser/PlaywrightElectronActivePro...

256 lines
7.5 KiB
TypeScript

import { afterEach, expect, it, vi } from "vitest";
import { dirname } from "node:path";
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,
safeElectronEnvironment,
} 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,
}),
application_root: dirname(process.execPath),
});
expect(result.ok).toBe(false);
expect(launchedEnvironment).toEqual({
LANG: "selected-language",
PATH: "/selected/bin",
REA_PROCESS_RUN_ID: expect.any(String),
});
});
it("passes required Windows runtime variables without inheriting unrelated values", () => {
expect(
safeElectronEnvironment(
{
SYSTEMROOT: "C:\\Windows",
LOCALAPPDATA: "C:\\Users\\analyst\\AppData\\Local",
USERPROFILE: "C:\\Users\\analyst",
APP_SECRET: "must-not-pass",
},
"run-id",
"win32",
),
).toEqual({
SYSTEMROOT: "C:\\Windows",
LOCALAPPDATA: "C:\\Users\\analyst\\AppData\\Local",
USERPROFILE: "C:\\Users\\analyst",
REA_PROCESS_RUN_ID: "run-id",
});
});
const ownership = {
runId: "run",
leaderPid: 4242,
processGroupId: 4242,
expectedParentPid: 1,
} satisfies OwnedProcessGroup;
const emptyHookSnapshot = () => ({
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,
});
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 });
vi.spyOn(electronActions, "readApplicationState").mockResolvedValue({
windows: [],
metrics: [],
electronVersion: "44.0.0",
hookSnapshot: emptyHookSnapshot(),
});
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,
}),
application_root: dirname(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);
}
});
it("retains the initial snapshot when the final state read fails", async () => {
const initialState = {
windows: [],
metrics: [],
electronVersion: "44.0.0",
hookSnapshot: emptyHookSnapshot(),
};
vi.spyOn(electronActions, "readApplicationState")
.mockResolvedValueOnce(initialState)
.mockRejectedValueOnce(new Error("final renderer state unavailable"));
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>
>,
);
const result = await provider.capture({
...electronActiveObservationInputSchema.parse({
executable_path: process.execPath,
application_path: process.execPath,
actions: [],
}),
application_root: dirname(process.execPath),
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected final state capture to fail");
const partialObservation = result.error.partialObservation;
expect(partialObservation).toMatchObject({
kind: "electron-active-observation",
capture: {
application: { electron_version: "44.0.0" },
limitations: expect.arrayContaining([
expect.stringContaining("initial pre-action snapshot"),
]),
},
});
});