mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
fix(electron): refuse captures handed off to an already-running app (#10989)
* fix(electron): refuse captures handed off to an already-running app When another instance holds the app's single-instance lock, Electron forwards the launch and its arguments to that instance. The active hook now stops the launched app at the refused requestSingleInstanceLock call and reports the refusal in its snapshot; the provider fails the capture with application_already_running before driving it. * fix(electron): stop pending actions on a late lock refusal and keep the final snapshot * docs(electron): describe late single-instance refusals during actions * test(electron): join the late-refusal capture even when the test fails
This commit is contained in:
1 parent
7134053d27
commit
483f53fdef
11 files changed
+356
-85
No files matched your search
@@ -167,6 +167,16 @@ This operation actively launches and interacts with the selected target. It is
|
||||
not a passive CDP observation. The owned process keeps normal host filesystem
|
||||
and network privileges, so lifecycle ownership does not make it a sandbox.
|
||||
|
||||
The app runs with the operator's real profile. If the app is already running,
|
||||
or another capture of the same app is in progress, the launched instance can
|
||||
fail to obtain the app's single-instance lock. Electron then hands the launch,
|
||||
including its arguments, to the running instance. The active hook stops the
|
||||
launched app at that call, and the capture fails with
|
||||
`application_already_running` without observing or driving the running
|
||||
instance. If the app requests the lock while actions run, the pending actions
|
||||
stop and the capture fails the same way, keeping its final snapshot. Close the
|
||||
running app, or wait for the other capture, and retry.
|
||||
|
||||
The CLI and MCP surfaces accept the same schema. For a JSON request file:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1195,6 +1195,47 @@ const patchWebContents = (webContents, windowId) => {
|
||||
boundaries.patchWindowOpenHandler(webContents, contentsId);
|
||||
};
|
||||
|
||||
// When another instance holds the app's single-instance lock, Electron has
|
||||
// already handed this launch's arguments to that instance, and the launched
|
||||
// process is no longer the capture target. Stop the application's calling path
|
||||
// and keep the process alive so the provider reads the refusal, fails the
|
||||
// capture, and terminates the process it owns.
|
||||
let singleInstanceLockRefused = false;
|
||||
// Resolves when the lock is refused so the provider can stop actions that
|
||||
// wait for a window the stopped path would have opened. The snapshot's
|
||||
// `single_instance_lock_refused` remains the fact the provider reports.
|
||||
let announceSingleInstanceLockRefusal = () => undefined;
|
||||
globalThis.__reaElectronSingleInstanceLockRefusal = new Promise((resolve) => {
|
||||
announceSingleInstanceLockRefusal = () => resolve(true);
|
||||
});
|
||||
const singleInstanceLockRefusal = new Error(
|
||||
"Another instance holds this application's single-instance lock",
|
||||
);
|
||||
const patchSingleInstanceLock = (app) => {
|
||||
const request = app?.requestSingleInstanceLock;
|
||||
if (typeof request !== "function") return;
|
||||
app.requestSingleInstanceLock = function requestSingleInstanceLock() {
|
||||
const held = applyFunction(request, this, arguments);
|
||||
if (held !== false) return held;
|
||||
if (!singleInstanceLockRefused) {
|
||||
singleInstanceLockRefused = true;
|
||||
recordRuntime("app-lifecycle", "requestSingleInstanceLock", "blocked", {
|
||||
target: "app",
|
||||
process_type: "main",
|
||||
error: true,
|
||||
});
|
||||
// The stopped path surfaces as an uncaught exception; without a
|
||||
// listener, Electron would show a modal error dialog or exit first.
|
||||
applyFunction(nodeEventEmitterMethods.on ?? process.on, process, [
|
||||
"uncaughtException",
|
||||
() => undefined,
|
||||
]);
|
||||
announceSingleInstanceLockRefusal();
|
||||
}
|
||||
throw singleInstanceLockRefusal;
|
||||
};
|
||||
};
|
||||
|
||||
const patchWindows = (app) => {
|
||||
if (app === null || app === undefined) return;
|
||||
app.on("browser-window-created", (_event, window) => {
|
||||
@@ -1318,6 +1359,7 @@ try {
|
||||
boundaries.patchIpcRenderer(electron.ipcRenderer);
|
||||
boundaries.patchContextBridge(electron.contextBridge);
|
||||
patchWindows(electron.app);
|
||||
patchSingleInstanceLock(electron.app);
|
||||
boundaries.patchUtilityProcess(electron.utilityProcess);
|
||||
boundaries.patchShell(electron.shell);
|
||||
boundaries.patchSessionManager(electron.session, electron.app);
|
||||
@@ -1388,4 +1430,5 @@ globalThis.__reaElectronActiveSnapshot = () => ({
|
||||
observed_ipc: observedIpc,
|
||||
observed_runtime: observedRuntime,
|
||||
hook_error: globalThis.__reaElectronActiveHookError === true,
|
||||
single_instance_lock_refused: singleInstanceLockRefused,
|
||||
});
|
||||
@@ -599,6 +599,7 @@ const finalState = {
|
||||
observed_ipc: 0,
|
||||
observed_runtime: 0,
|
||||
hook_error: false,
|
||||
single_instance_lock_refused: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ const hookSnapshotSchema = z.strictObject({
|
||||
observed_ipc: retention.observed_ipc,
|
||||
observed_runtime: retention.observed_runtime,
|
||||
hook_error: z.boolean(),
|
||||
/** Null when the hook did not run, so the lock outcome is unknown. */
|
||||
single_instance_lock_refused: z.boolean().nullable(),
|
||||
});
|
||||
|
||||
type ElectronAction = ElectronActiveObservationInput["actions"][number];
|
||||
@@ -382,6 +384,40 @@ const emitDeepLink = (
|
||||
{ delivery: action.delivery, url: action.url },
|
||||
);
|
||||
|
||||
/**
|
||||
* Abort when the hook announces a refused single-instance lock. The hook
|
||||
* stops the application path that requested the lock, so a window that path
|
||||
* would have opened never appears; aborting lets pending actions stop. The
|
||||
* hook snapshot's `single_instance_lock_refused` remains the reported fact.
|
||||
*/
|
||||
export const watchSingleInstanceLockRefusal = (
|
||||
application: ElectronApplication,
|
||||
): AbortSignal => {
|
||||
const refusal = new AbortController();
|
||||
void application
|
||||
.evaluate(() => {
|
||||
const announced = Reflect.get(
|
||||
globalThis,
|
||||
"__reaElectronSingleInstanceLockRefusal",
|
||||
);
|
||||
return announced instanceof Promise ? announced : false;
|
||||
})
|
||||
.then(
|
||||
(refused) => {
|
||||
if (refused === true)
|
||||
refusal.abort(
|
||||
new BrowserObservationError(
|
||||
OPERATION,
|
||||
"application_already_running",
|
||||
),
|
||||
);
|
||||
},
|
||||
// The pending read settles when the application closes; nothing to report.
|
||||
() => undefined,
|
||||
);
|
||||
return refusal.signal;
|
||||
};
|
||||
|
||||
/** Read the current Electron windows, metrics, and hook snapshot. */
|
||||
export const readApplicationState = async (
|
||||
application: ElectronApplication,
|
||||
@@ -456,6 +492,7 @@ export const readApplicationState = async (
|
||||
observed_ipc: 0,
|
||||
observed_runtime: 0,
|
||||
hook_error: true,
|
||||
single_instance_lock_refused: null,
|
||||
};
|
||||
})
|
||||
.then((value) => hookSnapshotSchema.parse(value));
|
||||
|
||||
@@ -65,6 +65,7 @@ it.each([
|
||||
observed_ipc: 0,
|
||||
observed_runtime: 3,
|
||||
hook_error: false,
|
||||
single_instance_lock_refused: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -117,6 +118,7 @@ it("projects hook retention and dropped event families through the result schema
|
||||
observed_ipc: 1,
|
||||
observed_runtime: 1,
|
||||
hook_error: false,
|
||||
single_instance_lock_refused: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -199,6 +201,7 @@ it("keeps renderer coverage unavailable when only the main process was observed"
|
||||
observed_ipc: 0,
|
||||
observed_runtime: 1,
|
||||
hook_error: false,
|
||||
single_instance_lock_refused: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -239,6 +242,7 @@ it("does not correlate legacy dropped families and roles", () => {
|
||||
observed_ipc: 1,
|
||||
observed_runtime: 1,
|
||||
hook_error: false,
|
||||
single_instance_lock_refused: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -415,6 +415,10 @@ it("passes required Windows runtime variables without inheriting unrelated value
|
||||
let ownership: PersistentOwnedProcessGroup;
|
||||
|
||||
beforeEach(() => {
|
||||
// Fake applications have no hook to announce a refused lock.
|
||||
vi.spyOn(electronActions, "watchSingleInstanceLockRefusal").mockReturnValue(
|
||||
new AbortController().signal,
|
||||
);
|
||||
ownership = {
|
||||
runId: "run",
|
||||
leaderPid: 4242,
|
||||
@@ -439,6 +443,7 @@ const emptyHookSnapshot = () => ({
|
||||
observed_ipc: 0,
|
||||
observed_runtime: 0,
|
||||
hook_error: false,
|
||||
single_instance_lock_refused: false,
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -693,6 +698,148 @@ it("retains the initial snapshot when the final state read fails", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["initial", "final"] as const)(
|
||||
"refuses an application handed off to a running instance at the %s snapshot",
|
||||
async (stage) => {
|
||||
const state = (refused: boolean) => ({
|
||||
windows: [],
|
||||
metrics: [],
|
||||
electronVersion: "44.0.0",
|
||||
hookSnapshot: {
|
||||
...emptyHookSnapshot(),
|
||||
single_instance_lock_refused: refused,
|
||||
},
|
||||
});
|
||||
vi.spyOn(electronActions, "readApplicationState").mockResolvedValueOnce(
|
||||
state(stage === "initial"),
|
||||
);
|
||||
vi.spyOn(
|
||||
electronActions,
|
||||
"readFinalApplicationState",
|
||||
).mockResolvedValueOnce(state(stage === "final"));
|
||||
const runActions = vi
|
||||
.spyOn(electronActions, "runElectronActions")
|
||||
.mockResolvedValue({ actions: [] });
|
||||
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,
|
||||
}),
|
||||
application_root: dirname(process.execPath),
|
||||
});
|
||||
|
||||
if (result.ok) throw new Error("Expected the handed-off launch to fail");
|
||||
expect(runActions).toHaveBeenCalledTimes(stage === "initial" ? 0 : 1);
|
||||
expect(application.close).toHaveBeenCalled();
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BrowserObservationError",
|
||||
reason: "application_already_running",
|
||||
});
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
category: "unavailable",
|
||||
message: expect.stringContaining("The application is already running"),
|
||||
remediation: {
|
||||
action: expect.stringContaining("Close the running application"),
|
||||
},
|
||||
});
|
||||
// A refusal first seen in the final snapshot keeps that snapshot, which
|
||||
// holds the blocked lock request and the action effects.
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
kind: "electron-active-observation",
|
||||
capture: {
|
||||
limitations: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
stage === "initial"
|
||||
? "initial pre-action snapshot"
|
||||
: "failed after its final snapshot",
|
||||
),
|
||||
]),
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("stops pending actions when the lock is refused while they wait", async () => {
|
||||
const state = (refused: boolean) => ({
|
||||
windows: [],
|
||||
metrics: [],
|
||||
electronVersion: "44.0.0",
|
||||
hookSnapshot: {
|
||||
...emptyHookSnapshot(),
|
||||
single_instance_lock_refused: refused,
|
||||
},
|
||||
});
|
||||
vi.spyOn(electronActions, "readApplicationState").mockResolvedValueOnce(
|
||||
state(false),
|
||||
);
|
||||
vi.spyOn(electronActions, "readFinalApplicationState").mockResolvedValueOnce(
|
||||
state(true),
|
||||
);
|
||||
// The refused path never opens the window the action waits for.
|
||||
const refusal = new AbortController();
|
||||
vi.spyOn(electronActions, "watchSingleInstanceLockRefusal").mockReturnValue(
|
||||
refusal.signal,
|
||||
);
|
||||
const windows = vi.fn(() => []);
|
||||
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 capture = provider.capture({
|
||||
...electronActiveObservationInputSchema.parse({
|
||||
executable_path: process.execPath,
|
||||
application_path: process.execPath,
|
||||
actions: [
|
||||
{ step_id: "open", kind: "click", window_index: 0, selector: "#go" },
|
||||
],
|
||||
}),
|
||||
application_root: dirname(process.execPath),
|
||||
});
|
||||
let result: Awaited<typeof capture>;
|
||||
try {
|
||||
await waitFor(() => expect(windows).toHaveBeenCalled());
|
||||
refusal.abort();
|
||||
result = await capture;
|
||||
} finally {
|
||||
// A failed wait still stops and joins the capture before mocks restore.
|
||||
await provider.close();
|
||||
await capture;
|
||||
}
|
||||
|
||||
if (result.ok) throw new Error("Expected the handed-off launch to fail");
|
||||
expect(application.close).toHaveBeenCalled();
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BrowserObservationError",
|
||||
reason: "application_already_running",
|
||||
partialObservation: {
|
||||
capture: { actions: [{ step_id: "open", status: "cancelled" }] },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects active capture before launch when EnableNodeCliInspectArguments fuse is disabled", async () => {
|
||||
const root = await createTestTempDirectory("rea-active-provider-fuse-");
|
||||
const disabledBinary = join(root, "disabled-electron");
|
||||
|
||||
@@ -50,6 +50,8 @@ import {
|
||||
runElectronActions,
|
||||
readApplicationState,
|
||||
readFinalApplicationState,
|
||||
watchSingleInstanceLockRefusal,
|
||||
type ElectronHookSnapshot,
|
||||
} from "./PlaywrightElectronActiveActions.js";
|
||||
import { projectElectronActiveCapture } from "./PlaywrightElectronActiveProjection.js";
|
||||
import { readElectronFuseImage } from "../electron/ElectronFuseReader.js";
|
||||
@@ -124,8 +126,12 @@ export class PlaywrightElectronActiveProvider implements ElectronActiveObservati
|
||||
let lineage: ProcessLineageObservation | undefined;
|
||||
let capture: ElectronActiveObservationResult | undefined;
|
||||
let paths: ElectronPaths | undefined;
|
||||
let initialState:
|
||||
| Awaited<ReturnType<typeof readApplicationState>>
|
||||
// The latest completed snapshot, retained when a later stage fails.
|
||||
let retained:
|
||||
| {
|
||||
readonly stage: "initial" | "final";
|
||||
readonly state: Awaited<ReturnType<typeof readApplicationState>>;
|
||||
}
|
||||
| undefined;
|
||||
let actions: ElectronActiveObservationResult["actions"] = [];
|
||||
let outcome: Result<ElectronActiveObservationResult, AnalysisError>;
|
||||
@@ -224,15 +230,27 @@ export class PlaywrightElectronActiveProvider implements ElectronActiveObservati
|
||||
...(captureBaseline === undefined ? {} : { captureBaseline }),
|
||||
};
|
||||
lineage = await observeOwnedProcessLineage(ownership);
|
||||
initialState = await runWithExecutionLimits(
|
||||
const initialState = await runWithExecutionLimits(
|
||||
readApplicationState(application),
|
||||
options.signal,
|
||||
startupDeadline,
|
||||
);
|
||||
retained = { stage: "initial", state: initialState };
|
||||
refuseHandedOffInstance(initialState.hookSnapshot);
|
||||
// A lock refused while actions run stops the path that would open their
|
||||
// windows, so the refusal also ends pending actions; the final snapshot
|
||||
// then reports it.
|
||||
const handedOff = watchSingleInstanceLockRefusal(application);
|
||||
const actionOutcome = await runElectronActions(
|
||||
application,
|
||||
input,
|
||||
options,
|
||||
{
|
||||
...options,
|
||||
signal:
|
||||
options.signal === undefined
|
||||
? handedOff
|
||||
: AbortSignal.any([options.signal, handedOff]),
|
||||
},
|
||||
this.#resources,
|
||||
);
|
||||
actions = actionOutcome.actions;
|
||||
@@ -242,20 +260,26 @@ export class PlaywrightElectronActiveProvider implements ElectronActiveObservati
|
||||
application,
|
||||
options.signal,
|
||||
);
|
||||
capture = projectElectronActiveCapture(paths, actions, state);
|
||||
const finalCapture = projectElectronActiveCapture(paths, actions, state);
|
||||
retained = { stage: "final", state };
|
||||
refuseHandedOffInstance(state.hookSnapshot);
|
||||
capture = finalCapture;
|
||||
outcome = ok(capture);
|
||||
} catch (cause: unknown) {
|
||||
if (paths !== undefined && initialState !== undefined) {
|
||||
if (paths !== undefined && retained !== undefined) {
|
||||
const retainedCapture = projectElectronActiveCapture(
|
||||
paths,
|
||||
actions,
|
||||
initialState,
|
||||
retained.state,
|
||||
);
|
||||
const reason = cause instanceof Error ? cause.message : String(cause);
|
||||
capture = {
|
||||
...retainedCapture,
|
||||
limitations: [
|
||||
...retainedCapture.limitations,
|
||||
`Capture stopped after the initial pre-action snapshot: ${cause instanceof Error ? cause.message : String(cause)}. Final Windows, metrics, IPC, and timeline state is unknown; the retained snapshot may omit action effects.`,
|
||||
retained.stage === "initial"
|
||||
? `Capture stopped after the initial pre-action snapshot: ${reason}. Final Windows, metrics, IPC, and timeline state is unknown; the retained snapshot may omit action effects.`
|
||||
: `Capture failed after its final snapshot: ${reason}. The retained final snapshot includes action effects.`,
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -346,6 +370,24 @@ export class PlaywrightElectronActiveProvider implements ElectronActiveObservati
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A launch that was refused the app's single-instance lock was handed to the
|
||||
* instance holding it; the launched process is not the target, so stop.
|
||||
*/
|
||||
const refuseHandedOffInstance = (snapshot: ElectronHookSnapshot): void => {
|
||||
if (snapshot.single_instance_lock_refused === true)
|
||||
throw new BrowserObservationError(
|
||||
OPERATION,
|
||||
"application_already_running",
|
||||
{
|
||||
detail:
|
||||
"The application is already running: the launched instance did not obtain the app's single-instance lock, so Electron handed this launch and its arguments to the running instance. REA stopped the launched instance at that call and failed the capture; it did not observe or drive the running instance.",
|
||||
remediationAction:
|
||||
"Close the running application, or wait for another capture of the same application to finish, then retry.",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/** Normalize only explicit facts emitted by the inspection owner. */
|
||||
const inspectOwnership = async <T>(
|
||||
inspect: () => Promise<T> | undefined,
|
||||
|
||||
@@ -403,6 +403,7 @@ const browserErrorCategory = (
|
||||
if (
|
||||
reason === "target_not_found" ||
|
||||
reason === "target_not_allowed" ||
|
||||
reason === "application_already_running" ||
|
||||
reason === "service_worker_blocked" ||
|
||||
reason === "target_changed" ||
|
||||
reason === "target_session_detached" ||
|
||||
|
||||
@@ -619,6 +619,7 @@ const BROWSER_ERROR_CODES: Readonly<
|
||||
window_not_found: "unreadable_output",
|
||||
window_metadata_missing: "unreadable_output",
|
||||
process_ownership_unavailable: "unreadable_output",
|
||||
application_already_running: "target_unavailable",
|
||||
cleanup_failed: "execution_failure",
|
||||
cancelled: "cancelled",
|
||||
timeout: "provider_timeout",
|
||||
|
||||
@@ -33,6 +33,7 @@ export type BrowserObservationFailureReason =
|
||||
| "window_not_found"
|
||||
| "window_metadata_missing"
|
||||
| "process_ownership_unavailable"
|
||||
| "application_already_running"
|
||||
| "cleanup_failed"
|
||||
| "cancelled"
|
||||
| "timeout";
|
||||
@@ -1,23 +1,22 @@
|
||||
import { withVerifierCleanup } from "../../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { createTestProcessRegistry } from "../../support/process/processFixture.js";
|
||||
import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { afterEach, expect, it, onTestFinished } from "vitest";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
|
||||
import { electronActiveTimelineEventSchema } from "../../../src/domain/javascript/electronActiveObservation.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const temporary: string[] = [];
|
||||
|
||||
const electronModuleSource = `const { EventEmitter } = require("node:events");
|
||||
const app = new EventEmitter();
|
||||
app.isReady = () => true;
|
||||
app.relaunch = () => undefined;
|
||||
app.setAsDefaultProtocolClient = () => true;
|
||||
app.setLoginItemSettings = () => undefined;
|
||||
app.requestSingleInstanceLock = () => globalThis.__singleInstanceLockHeld !== false;
|
||||
app.moveToApplicationsFolder = () => {
|
||||
throw new Error("moveToApplicationsFolder must not run");
|
||||
};
|
||||
@@ -141,17 +140,8 @@ process.stdout.write(JSON.stringify({ snapshot, default_popup_decision: defaultP
|
||||
})();
|
||||
`;
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporary
|
||||
.splice(0)
|
||||
.map(async (path) => rm(path, { force: true, recursive: true })),
|
||||
);
|
||||
});
|
||||
|
||||
it("retains active hook evidence within its event-retention budget", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "exercise.cjs");
|
||||
await writeFile(script, activeCaptureSource);
|
||||
@@ -265,7 +255,6 @@ it.each([
|
||||
"denies permissions before app registration and after replacement for %s sessions",
|
||||
async (admission) => {
|
||||
const root = await createTestTempDirectory("rea-electron-session-policy-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(
|
||||
root,
|
||||
admission === "deferred-default"
|
||||
@@ -341,7 +330,6 @@ console.log(JSON.stringify({ initial, replaced, cleared, applicationCalls,
|
||||
|
||||
it("blocks synchronous shell effects with their native return contracts and retains argument shapes only", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-shell-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "shell.cjs");
|
||||
await writeFile(
|
||||
@@ -393,9 +381,66 @@ process.stdout.write(JSON.stringify({
|
||||
electronActiveTimelineEventSchema.array().parse(result.snapshot.events);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ held: true, refused: false },
|
||||
{ held: false, refused: true },
|
||||
])(
|
||||
"stops an application handed off by its single-instance lock (held: $held)",
|
||||
async ({ held, refused }) => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-lock-");
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "lock.cjs");
|
||||
await writeFile(
|
||||
script,
|
||||
`const { app } = require("electron");
|
||||
globalThis.__singleInstanceLockHeld = ${String(held)};
|
||||
setImmediate(async () => {
|
||||
const snapshot = globalThis.__reaElectronActiveSnapshot();
|
||||
// An already-announced refusal settles first; otherwise false wins.
|
||||
const announced = await Promise.race([
|
||||
globalThis.__reaElectronSingleInstanceLockRefusal,
|
||||
Promise.resolve(false),
|
||||
]);
|
||||
process.stdout.write(JSON.stringify({
|
||||
refused: snapshot.single_instance_lock_refused,
|
||||
announced,
|
||||
events: snapshot.events.filter((event) => event.event === "requestSingleInstanceLock"),
|
||||
}) + "\\n");
|
||||
});
|
||||
const primary = app.requestSingleInstanceLock();
|
||||
process.stdout.write(JSON.stringify({ application_continued: primary }) + "\\n");
|
||||
`,
|
||||
);
|
||||
const execution = await runNode(script, root);
|
||||
expect(execution.code, execution.stderr).toBe(0);
|
||||
const lines = execution.stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line) as Record<string, unknown>);
|
||||
// A refused launch belongs to the running instance: application code
|
||||
// after the lock request must not run, and the process stays readable.
|
||||
expect(lines).toEqual([
|
||||
...(held ? [{ application_continued: true }] : []),
|
||||
{
|
||||
refused,
|
||||
announced: refused,
|
||||
events: refused
|
||||
? [
|
||||
expect.objectContaining({
|
||||
kind: "app-lifecycle",
|
||||
phase: "blocked",
|
||||
target: "app",
|
||||
error: true,
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it("blocks trashItem asynchronously, records the attempt, and preserves the selected file", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-trash-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const selected = join(root, "retained.txt");
|
||||
await writeFile(selected, "must remain");
|
||||
@@ -433,7 +478,6 @@ it("classifies a dropped browser preload as preload configuration at capture", a
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-browser-preload-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "browser-preload.cjs");
|
||||
await writeFile(
|
||||
@@ -471,7 +515,6 @@ process.stdout.write(JSON.stringify(snapshot) + "\\n");
|
||||
|
||||
it("removes observed ipcMain listeners through their original function", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ipc-removal-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-removal.cjs");
|
||||
await writeFile(
|
||||
@@ -515,7 +558,6 @@ process.stdout.write(JSON.stringify({
|
||||
|
||||
it("removes the newest matching ipcMain registration across unpatched aliases", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ipc-aliases-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-aliases.cjs");
|
||||
await writeFile(
|
||||
@@ -558,7 +600,6 @@ process.stdout.write(JSON.stringify({
|
||||
|
||||
it("records ipcMain listeners registered by a newListener callback during once", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ipc-nested-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-nested.cjs");
|
||||
await writeFile(
|
||||
@@ -589,7 +630,6 @@ process.stdout.write(JSON.stringify({
|
||||
|
||||
it("keeps removal working after many fired ipcMain once registrations", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ipc-repeat-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-repeat.cjs");
|
||||
await writeFile(
|
||||
@@ -624,7 +664,6 @@ process.stdout.write(JSON.stringify({
|
||||
|
||||
it("removes the newest registration of an ipcMain listener that has its own listener property", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ipc-wrapped-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-wrapped.cjs");
|
||||
await writeFile(
|
||||
@@ -656,7 +695,6 @@ process.stdout.write(JSON.stringify({
|
||||
|
||||
it("reports original ipcMain listeners to newListener and removeListener observers", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ipc-meta-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-meta.cjs");
|
||||
await writeFile(
|
||||
@@ -703,7 +741,6 @@ process.stdout.write(JSON.stringify({
|
||||
|
||||
it("registers and removes ipcMain once listeners through decorators installed after the hook", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ipc-decor-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-decorators.cjs");
|
||||
await writeFile(
|
||||
@@ -752,7 +789,6 @@ process.stdout.write(JSON.stringify({
|
||||
|
||||
it("keeps hooked ipcMain listener semantics identical to an unhooked EventEmitter", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ipc-diff-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-differential.cjs");
|
||||
await writeFile(
|
||||
@@ -836,7 +872,6 @@ process.stdout.write(JSON.stringify({
|
||||
|
||||
it("keeps a replacement registered by a removeListener observer during ipcMain removeAllListeners", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ipc-replace-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-replacement.cjs");
|
||||
await writeFile(
|
||||
@@ -912,7 +947,6 @@ it("does not infer hook dispatch from an application's listener metadata", async
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-indirect-owner-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "indirect-owner.cjs");
|
||||
await writeFile(
|
||||
@@ -942,7 +976,6 @@ process.stdout.write(JSON.stringify({calls,remaining:ipcMain.listenerCount("oute
|
||||
|
||||
it("keeps ipcMain once self-removal through an on decorator that wraps the hook", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ipc-wrap-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-wrapping-decorator.cjs");
|
||||
await writeFile(
|
||||
@@ -1016,7 +1049,6 @@ it.each(["stable", "replaced", "readonly", "getter", "proxy"])(
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-ipc-remove-decorator-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-removal-decorator.cjs");
|
||||
await writeFile(
|
||||
@@ -1089,7 +1121,6 @@ it("preserves native removal before mutated raw listener metadata throws", async
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-ipc-mutated-metadata-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-mutated-listener-metadata.cjs");
|
||||
await writeFile(
|
||||
@@ -1124,7 +1155,6 @@ process.stdout.write(JSON.stringify({ plain: scenario(new EventEmitter()), hooke
|
||||
|
||||
it("matches native once dispatch and retains only established invocations", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-native-once-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "electron-once-runtime.mjs");
|
||||
await copyFile(
|
||||
@@ -1192,7 +1222,6 @@ it.each(["single-on", "multiple-on", "empty-once", "event-once"])(
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-ipc-dispatch-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-native-dispatch.cjs");
|
||||
await writeFile(
|
||||
@@ -1249,7 +1278,6 @@ it.each([
|
||||
"primitive",
|
||||
])("preserves raw callback apply with %s argument lists", async (mode) => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ipc-apply-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-apply.cjs");
|
||||
await writeFile(
|
||||
@@ -1303,7 +1331,6 @@ it("preserves the native once argument-list object passed to custom apply", asyn
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-ipc-once-args-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-once-arguments.cjs");
|
||||
await writeFile(
|
||||
@@ -1354,7 +1381,6 @@ it.each([
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-ipc-prototype-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-inherited-listener.cjs");
|
||||
await writeFile(
|
||||
@@ -1398,7 +1424,6 @@ it.each(["on", "multiple-on", "once"])(
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-ipc-species-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-array-species.cjs");
|
||||
await writeFile(
|
||||
@@ -1456,7 +1481,6 @@ it.each(["array", "arguments", "array-like", "null"])(
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-custom-apply-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-custom-apply.cjs");
|
||||
await writeFile(
|
||||
@@ -1523,7 +1547,6 @@ it.each([false, true])(
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-borrowed-apply-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-borrowed-apply.cjs");
|
||||
await writeFile(
|
||||
@@ -1567,7 +1590,6 @@ it("uses the captured Array.isArray intrinsic while selecting a native listener
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-captured-array-is-array-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "captured-array-is-array.cjs");
|
||||
await writeFile(
|
||||
@@ -1625,7 +1647,6 @@ it.each([
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-late-intrinsic-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-late-intrinsic.cjs");
|
||||
await writeFile(
|
||||
@@ -1764,7 +1785,6 @@ it.each([
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-chain-coverage-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-chain-coverage.cjs");
|
||||
await writeFile(
|
||||
@@ -1833,7 +1853,6 @@ it.each(["array", "map"])(
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-snapshot-iterator-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-snapshot-iterator.cjs");
|
||||
await writeFile(
|
||||
@@ -1870,7 +1889,6 @@ console.log(JSON.stringify({ calls, reads, failure, copied: !!copied, retained:
|
||||
|
||||
it("invokes handle callbacks without reading callable method properties", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ipc-callable-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-callable-properties.cjs");
|
||||
await writeFile(
|
||||
@@ -1931,7 +1949,6 @@ const scenario = async (emitter, observed) => {
|
||||
|
||||
it("keeps native removal traversal when an ipcMain observer prepends a listener", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ipc-prepend-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-prepend-during-removal.cjs");
|
||||
await writeFile(
|
||||
@@ -1981,7 +1998,6 @@ it.each(["accessor", "proxy"])(
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-ipc-accessor-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "ipc-listener-accessor.cjs");
|
||||
await writeFile(
|
||||
@@ -2034,7 +2050,6 @@ process.stdout.write(JSON.stringify({ plain: scenario(new EventEmitter()), hooke
|
||||
|
||||
it("isolates hook and exercise from ancestor Electron packages", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-ancestor-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root, 'module.exports = "/ambient/Electron";');
|
||||
const ambientHook = await stageHook(join(root, "producer", "scripts"));
|
||||
expect(createRequire(ambientHook)("electron")).toBe("/ambient/Electron");
|
||||
@@ -2064,7 +2079,6 @@ it("does not read an application rawListeners accessor during hook setup", async
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-raw-listeners-accessor-",
|
||||
);
|
||||
temporary.push(root);
|
||||
const source = electronModuleSource.replace(
|
||||
"const ipcMain = new EventEmitter();",
|
||||
`const ipcMain = new EventEmitter();
|
||||
@@ -2101,7 +2115,6 @@ it("leaves an accessor removal method untouched and suppresses uncertain invocat
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-removal-method-accessor-",
|
||||
);
|
||||
temporary.push(root);
|
||||
const source = electronModuleSource.replace(
|
||||
"const ipcMain = new EventEmitter();",
|
||||
`const ipcMain = new EventEmitter();
|
||||
@@ -2155,7 +2168,6 @@ it("preserves native last-registration order when the last raw listener link is
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-removal-mutated-lifo-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "removal-mutated-lifo.cjs");
|
||||
await writeFile(
|
||||
@@ -2226,7 +2238,6 @@ it("lets a later data listener link win over an earlier owned callback", async (
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-removal-later-data-link-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "removal-later-data-link.cjs");
|
||||
await writeFile(
|
||||
@@ -2282,7 +2293,6 @@ it("normalizes a later raw link to an earlier owned registration", async () => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-removal-owned-link-chain-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "removal-owned-link-chain.cjs");
|
||||
await writeFile(
|
||||
@@ -2338,7 +2348,6 @@ it("preserves native direct-removal cleanup with throwing listener metadata", as
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-removal-accessor-gap-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "removal-accessor-gap.cjs");
|
||||
await writeFile(
|
||||
@@ -2385,7 +2394,6 @@ it("forwards native direct-removal bookkeeping to the real emitter", async () =>
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-removal-bookkeeping-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "removal-bookkeeping.cjs");
|
||||
await writeFile(
|
||||
@@ -2451,7 +2459,6 @@ it.each([1, 2] as const)(
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-removal-meta-accessor-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "removal-meta-accessor.cjs");
|
||||
await writeFile(
|
||||
@@ -2522,7 +2529,6 @@ it("keeps listener ownership when native function prototype metadata changes", a
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-wrapper-constructor-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "wrapper-constructor.cjs");
|
||||
await writeFile(
|
||||
@@ -2559,7 +2565,6 @@ it("matches native custom inspection that mutates the raw listener", async () =>
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-removal-inspect-mutation-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "removal-inspect-mutation.cjs");
|
||||
await writeFile(
|
||||
@@ -2603,7 +2608,6 @@ it("normalizes an earlier raw owner in a later removal meta payload", async () =
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-removal-owned-meta-link-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "removal-owned-meta-link.cjs");
|
||||
await writeFile(
|
||||
@@ -2643,7 +2647,6 @@ it("preserves the native argument that proves a chained removal match", async ()
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-removal-chain-argument-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "removal-chain-argument.cjs");
|
||||
await writeFile(
|
||||
@@ -2688,7 +2691,6 @@ it("falls back to native errors for a revoked listener-array proxy", async () =>
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-revoked-listener-array-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "revoked-listener-array.cjs");
|
||||
await writeFile(
|
||||
@@ -2741,7 +2743,6 @@ it("records a gap when an opaque later registration blocks ownership ordering",
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-removal-opaque-gap-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "removal-opaque-gap.cjs");
|
||||
await writeFile(
|
||||
@@ -2789,7 +2790,6 @@ it("preserves pre-hook removeListener decorator receiver and callback identity",
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-preinstalled-removal-decorator-",
|
||||
);
|
||||
temporary.push(root);
|
||||
const source = electronModuleSource.replace(
|
||||
"const ipcMain = new EventEmitter();",
|
||||
`const ipcMain = new EventEmitter();
|
||||
@@ -2862,7 +2862,6 @@ it("forwards pre-hook on and once decorators exact registrations with an explici
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-preinstalled-registration-decorators-",
|
||||
);
|
||||
temporary.push(root);
|
||||
const source = electronModuleSource.replace(
|
||||
"const ipcMain = new EventEmitter();",
|
||||
`const ipcMain = new EventEmitter();
|
||||
@@ -2980,7 +2979,6 @@ it("forwards borrowed on and once registrations without attributing calls to ipc
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-borrowed-registration-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "borrowed-registration.cjs");
|
||||
await writeFile(
|
||||
@@ -3076,7 +3074,6 @@ it("does not inspect object event channels while recording registration gaps", a
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-object-channel-gap-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "object-channel-gap.cjs");
|
||||
await writeFile(
|
||||
@@ -3120,7 +3117,6 @@ it("preserves borrowed removeAllListeners callback identity", async () => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-borrowed-remove-all-identity-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "borrowed-remove-all-identity.cjs");
|
||||
await writeFile(
|
||||
@@ -3179,7 +3175,6 @@ it("does not coerce object channels before native listener removal", async () =>
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-object-channel-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "object-channel.cjs");
|
||||
await writeFile(
|
||||
@@ -3223,7 +3218,6 @@ it("removes observed ipcMain registrations after the public listener link is del
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-removal-deleted-link-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "removal-deleted-link.cjs");
|
||||
await writeFile(
|
||||
@@ -3283,7 +3277,6 @@ it("passes canonical owned callbacks through post-hook removeAll decorators desp
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-remove-all-decorator-accessor-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "remove-all-decorator-accessor.cjs");
|
||||
await writeFile(
|
||||
@@ -3357,7 +3350,6 @@ it.each(["removeListener", "off"] as const)(
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-removal-direct-link-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "removal-direct-link.cjs");
|
||||
await writeFile(
|
||||
@@ -3433,7 +3425,6 @@ it("preserves warning-only rejections and records later blocked shell rejections
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-rejection-warn-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "warnings.cjs");
|
||||
await writeFile(
|
||||
@@ -3473,7 +3464,6 @@ it("preserves application rejection listeners and borrowed process emit behavior
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-rejection-listener-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "listeners.cjs");
|
||||
await writeFile(
|
||||
@@ -3514,7 +3504,6 @@ it("delivers revoked proxy rejections to the application's original handler", as
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-revoked-rejection-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "revoked-rejections.cjs");
|
||||
await writeFile(
|
||||
@@ -3555,7 +3544,6 @@ it("preserves an Electron-style uncaught exception listener policy", async () =>
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-uncaught-policy-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(
|
||||
root,
|
||||
electronModuleSource +
|
||||
@@ -3584,7 +3572,6 @@ it.each(["throw", "strict"])(
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-rejection-fatal-",
|
||||
);
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const script = join(root, "fatal-rejection.cjs");
|
||||
await writeFile(
|
||||
@@ -3613,7 +3600,6 @@ Promise.reject(new Error("controlled fatal rejection"));
|
||||
|
||||
it("preserves fatal process termination after recording uncaught exceptions", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-fatal-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(
|
||||
root,
|
||||
'throw new Error("controlled Electron unavailable");',
|
||||
@@ -3634,7 +3620,6 @@ it.each([{ sandboxPrefix: [] }, { sandboxPrefix: ["--no-sandbox"] }])(
|
||||
"restores the exact application argv with Playwright prefix %j",
|
||||
async ({ sandboxPrefix }) => {
|
||||
const root = await createTestTempDirectory("rea-electron-argv-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const hook = join(root, "electron-active-hook.cjs");
|
||||
const callerArguments = [
|
||||
@@ -3688,7 +3673,6 @@ it.each([
|
||||
"no-entry",
|
||||
])("preserves observed argv for an %s capture handoff", async (mode) => {
|
||||
const root = await createTestTempDirectory("rea-electron-argv-guard-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const hook = join(root, "electron-active-hook.cjs");
|
||||
const plan = ["-r", hook, "/selected/app", "caller-document"];
|
||||
|
||||
Reference in new issue
Block a user