256 lines
7.5 KiB
TypeScript
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"),
|
|
]),
|
|
},
|
|
});
|
|
});
|