refactor(browser): remove arbitrary capture ceilings

This commit is contained in:
morluto
2026-09-29 15:31:27 +08:00
parent 4cea84d7cb
commit 5dd3b4655e
16 changed files with 228 additions and 125 deletions
+20 -21
View File
@@ -4,8 +4,6 @@ const {
createElectronActiveBoundaryPatches,
} = require("./electron-active-hook-boundaries.cjs");
const MAX_EVENT_NAME_LENGTH = 128;
const MAX_IDENTIFIER_LENGTH = 256;
const ipcEventKinds = new Set([
"main-handler-invocation",
"main-event-invocation",
@@ -24,9 +22,6 @@ let observedRuntime = 0;
let sequence = 0;
let correlationSequence = 0;
const boundedString = (value, maximum) =>
typeof value === "string" ? value.slice(0, maximum) : null;
const shape = (value, depth = 0) => {
if (depth > 2 || value === null) return value === null ? "null" : "nested";
if (Array.isArray(value)) return "array";
@@ -64,7 +59,7 @@ const isAllowedNavigation = (value) => {
const processType = () =>
typeof process.type === "string" && process.type.length > 0
? process.type.slice(0, MAX_IDENTIFIER_LENGTH)
? process.type
: "main";
const identity = (value, prefix) => {
@@ -72,9 +67,9 @@ const identity = (value, prefix) => {
const id = value.id;
const processId = value.pid;
return typeof id === "string" || typeof id === "number"
? `${prefix}:${String(id)}`.slice(0, MAX_IDENTIFIER_LENGTH)
? `${prefix}:${String(id)}`
: typeof processId === "number"
? `${prefix}:pid:${String(processId)}`.slice(0, MAX_IDENTIFIER_LENGTH)
? `${prefix}:pid:${String(processId)}`
: null;
};
@@ -82,7 +77,7 @@ const frameIdentity = (event) => {
const frame = event?.senderFrame ?? event?.frame;
const id = frame?.frameTreeNodeId ?? frame?.routingId ?? frame?.id;
return typeof id === "string" || typeof id === "number"
? `frame:${String(id)}`.slice(0, MAX_IDENTIFIER_LENGTH)
? `frame:${String(id)}`
: null;
};
@@ -119,20 +114,24 @@ const record = (event) => {
: [];
events.push({
...raw,
correlation_id: boundedString(raw.correlation_id, MAX_IDENTIFIER_LENGTH),
event: boundedString(raw.event, MAX_EVENT_NAME_LENGTH),
correlation_id: raw.correlation_id,
event: typeof raw.event === "string" ? raw.event : null,
channel: typeof raw.channel === "string" ? raw.channel : null,
sender: boundedString(raw.sender, MAX_IDENTIFIER_LENGTH),
receiver: boundedString(raw.receiver, MAX_IDENTIFIER_LENGTH),
frame: boundedString(raw.frame, MAX_IDENTIFIER_LENGTH),
target: boundedString(raw.target, MAX_IDENTIFIER_LENGTH),
argument_shapes: argumentShapes.map(
(value) => boundedString(value, 64) ?? "unknown",
sender: typeof raw.sender === "string" ? raw.sender : null,
receiver: typeof raw.receiver === "string" ? raw.receiver : null,
frame: typeof raw.frame === "string" ? raw.frame : null,
target: typeof raw.target === "string" ? raw.target : null,
argument_shapes: argumentShapes.map((value) =>
typeof value === "string" ? value : "unknown",
),
result_shape: boundedString(raw.result_shape, 64),
process_type: boundedString(raw.process_type, MAX_IDENTIFIER_LENGTH),
artifact_path: boundedString(raw.artifact_path, 16_384),
artifact_sha256: boundedString(raw.artifact_sha256, 64),
result_shape:
typeof raw.result_shape === "string" ? raw.result_shape : null,
process_type:
typeof raw.process_type === "string" ? raw.process_type : null,
artifact_path:
typeof raw.artifact_path === "string" ? raw.artifact_path : null,
artifact_sha256:
typeof raw.artifact_sha256 === "string" ? raw.artifact_sha256 : null,
});
return events.at(-1);
};
-5
View File
@@ -35,11 +35,6 @@ export const requiredRecord = (value: unknown): UnknownRecord => {
return record;
};
export const boundedText = (value: unknown, maximum = 1_024): string | null => {
const text = stringValue(value);
return text === undefined ? null : text.slice(0, maximum);
};
export const isHttpUrl = (value: string | undefined): boolean => {
if (value === undefined) return false;
try {
+1 -1
View File
@@ -17,7 +17,7 @@ export const authorizedElectronFile = async (
value: string,
roots: readonly string[],
): Promise<string | undefined> => {
if (value.length > 65_536 || /%(?:2f|5c)/iu.test(value)) return undefined;
if (/%(?:2f|5c)/iu.test(value)) return undefined;
let url: URL;
try {
url = new URL(value);
@@ -7,7 +7,7 @@ import {
import { createElectronActiveObservationFixtureResult } from "../domain/electronActiveObservation.fixture.js";
import { runElectronActions } from "./PlaywrightElectronActiveActions.js";
it("parses bounded window, renderer, and deep-link actions for agents", () => {
it("parses window, renderer, and deep-link actions for agents", () => {
const input = electronActiveObservationInputSchema.parse({
executable_path: "/opt/electron",
application_path: "/opt/app/main.js",
@@ -122,12 +122,7 @@ it("redacts action inputs from Playwright failures", async () => {
windows: () => [page],
};
const result = await runElectronActions(
application as never,
input,
{},
Date.now() + 1_000,
);
const result = await runElectronActions(application as never, input, {});
expect(result[0]).toMatchObject({
status: "failed",
@@ -154,7 +149,6 @@ it("retains complete action errors and Electron window text", async () => {
{ windows: () => [page] } as never,
input,
{},
Date.now() + 1_000,
);
const result = createElectronActiveObservationFixtureResult("/opt/app");
const longUrl = `file://${"/segment".repeat(10_000)}`;
@@ -281,12 +275,7 @@ it("runs an untargeted deep-link without requiring a BrowserWindow", async () =>
evaluate: async () => true,
};
const result = await runElectronActions(
application as never,
input,
{},
Date.now() + 1_000,
);
const result = await runElectronActions(application as never, input, {});
expect(result).toMatchObject([
{
@@ -297,6 +286,30 @@ it("runs an untargeted deep-link without requiring a BrowserWindow", async () =>
]);
});
it("honors caller-selected Electron waits beyond the former action timeout", async () => {
const input = electronActiveObservationInputSchema.parse({
executable_path: "/opt/electron",
application_path: "/opt/app/main.js",
application_root: "/opt/app",
actions: [{ step_id: "wait-long", kind: "wait", duration_ms: 60_000 }],
});
let duration = 0;
const page = {
waitForTimeout: async (value: number) => {
duration = value;
},
};
const application = {
windows: () => [page],
firstWindow: async () => page,
};
const result = await runElectronActions(application as never, input, {});
expect(result[0]).toMatchObject({ status: "completed" });
expect(duration).toBe(60_000);
});
it("rejects action outcomes that disagree with their error state", () => {
const result = createElectronActiveObservationFixtureResult("/opt/app");
const action = result.actions[0];
+28 -54
View File
@@ -10,7 +10,6 @@ import type {
} from "../domain/electronActiveObservation.js";
const OPERATION = "capture_electron_scenario" as const;
const ACTION_TIMEOUT_MS = 5_000;
const hookEventSchema = z.strictObject({
sequence: z.number().int().min(1),
@@ -108,7 +107,6 @@ export const runElectronActions = async (
application: ElectronApplication,
input: ElectronActiveObservationInput,
options: ExecutionOptions,
deadline: number,
): Promise<ElectronActions> => {
const actions: ElectronActions = [];
for (const action of input.actions) {
@@ -118,18 +116,13 @@ export const runElectronActions = async (
const windowIndex = actionWindowIndex(action);
let selectedWindow: Page | undefined;
try {
const remaining = deadline - Date.now();
if (remaining <= 0)
throw new BrowserObservationError(OPERATION, "timeout");
selectedWindow =
windowIndex === null
? undefined
: await waitForWindow({
application,
windowIndex,
timeout: ACTION_TIMEOUT_MS,
signal: options.signal,
deadline,
});
if (
(action.kind === "click" ||
@@ -143,10 +136,7 @@ export const runElectronActions = async (
application,
action,
selectedWindow,
remaining,
actionTimeoutMs: ACTION_TIMEOUT_MS,
options,
deadline,
});
actions.push({
step_id: action.step_id,
@@ -179,20 +169,14 @@ type RunActionContext = {
readonly application: ElectronApplication;
readonly action: ElectronAction;
readonly selectedWindow: Page | undefined;
readonly remaining: number;
readonly actionTimeoutMs: number;
readonly options: ExecutionOptions;
readonly deadline: number;
};
const runAction = async ({
application,
action,
selectedWindow,
remaining,
actionTimeoutMs,
options,
deadline,
}: RunActionContext): Promise<void> => {
switch (action.kind) {
case "click":
@@ -200,25 +184,21 @@ const runAction = async ({
throw new BrowserObservationError(OPERATION, "window_not_found");
await runWithExecutionLimits(
selectedWindow.locator(action.selector).click({
timeout: Math.min(actionTimeoutMs, remaining),
timeout: 0,
}),
options.signal,
deadline,
);
return;
case "wait": {
const page =
selectedWindow ??
(await application.firstWindow({
timeout: Math.min(actionTimeoutMs, remaining),
timeout: 0,
}));
await runWithExecutionLimits(
page.waitForTimeout(Math.min(action.duration_ms, remaining)),
page.waitForTimeout(action.duration_ms),
options.signal,
deadline,
);
if (action.duration_ms > remaining)
throw new BrowserObservationError(OPERATION, "timeout");
return;
}
case "renderer-reload":
@@ -226,10 +206,9 @@ const runAction = async ({
throw new BrowserObservationError(OPERATION, "window_not_found");
await runWithExecutionLimits(
selectedWindow.reload({
timeout: Math.min(actionTimeoutMs, remaining),
timeout: 0,
}),
options.signal,
deadline,
);
return;
case "renderer-crash": {
@@ -241,7 +220,6 @@ const runAction = async ({
return true;
}, action.window_index),
options.signal,
deadline,
);
if (!crashed)
throw new BrowserObservationError(OPERATION, "window_not_found");
@@ -251,7 +229,6 @@ const runAction = async ({
await runWithExecutionLimits(
emitDeepLink(application, action),
options.signal,
deadline,
);
return;
}
@@ -273,20 +250,15 @@ const actionWindowIndex = (action: ElectronAction): number | null => {
type WaitForWindowContext = {
readonly application: ElectronApplication;
readonly windowIndex: number;
readonly timeout: number;
readonly signal: AbortSignal | undefined;
readonly deadline: number;
};
const waitForWindow = async ({
application,
windowIndex,
timeout,
signal,
deadline,
}: WaitForWindowContext): Promise<Page> => {
const stopAt = Math.min(deadline, Date.now() + timeout);
while (Date.now() < stopAt) {
while (true) {
if (signal?.aborted === true)
throw new BrowserObservationError(OPERATION, "cancelled");
const window = application.windows()[windowIndex];
@@ -294,10 +266,8 @@ const waitForWindow = async ({
await runWithExecutionLimits(
new Promise<void>((resolve) => setTimeout(resolve, 25)),
signal,
stopAt,
);
}
throw new BrowserObservationError(OPERATION, "window_not_found");
};
const emitDeepLink = (
@@ -430,35 +400,39 @@ export const readApplicationState = async (
};
};
/** Apply a deadline and caller cancellation to one Playwright operation. */
/** Apply an optional lifecycle deadline and caller cancellation to an operation. */
export const runWithExecutionLimits = async <Value>(
operation: Promise<Value>,
signal: AbortSignal | undefined,
deadline: number,
deadline?: number,
): Promise<Value> => {
if (signal?.aborted === true)
throw new BrowserObservationError(OPERATION, "cancelled");
if (signal === undefined && deadline === undefined) return operation;
let timer: ReturnType<typeof setTimeout> | undefined;
let abortListener: (() => void) | undefined;
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(
() => reject(new BrowserObservationError(OPERATION, "timeout")),
Math.max(1, deadline - Date.now()),
const interruptions: Promise<never>[] = [];
if (deadline !== undefined)
interruptions.push(
new Promise<never>((_, reject) => {
timer = setTimeout(
() => reject(new BrowserObservationError(OPERATION, "timeout")),
Math.max(1, deadline - Date.now()),
);
}),
);
if (signal !== undefined)
interruptions.push(
new Promise<never>((_, reject) => {
abortListener = () =>
reject(new BrowserObservationError(OPERATION, "cancelled"));
signal.addEventListener("abort", abortListener, { once: true });
}),
);
});
const cancellation =
signal === undefined
? timeout
: Promise.race([
timeout,
new Promise<never>((_, reject) => {
abortListener = () =>
reject(new BrowserObservationError(OPERATION, "cancelled"));
signal.addEventListener("abort", abortListener, { once: true });
}),
]);
try {
return await Promise.race([operation, cancellation]);
return interruptions.length === 0
? await operation
: await Promise.race([operation, ...interruptions]);
} finally {
if (timer !== undefined) clearTimeout(timer);
if (abortListener !== undefined)
@@ -41,7 +41,7 @@ import {
} from "./PlaywrightElectronActiveActions.js";
const OPERATION = "capture_electron_scenario" as const;
const RUN_TIMEOUT_MS = 60_000;
const STARTUP_TIMEOUT_MS = 60_000;
/** Public identity for provider-owned Electron runtime experiments. */
export const PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY: ProviderIdentity =
@@ -165,14 +165,14 @@ export class PlaywrightElectronActiveProvider
try {
if (options.signal?.aborted === true)
throw new BrowserObservationError(OPERATION, "cancelled");
const deadline = Date.now() + RUN_TIMEOUT_MS;
const startupDeadline = Date.now() + STARTUP_TIMEOUT_MS;
const paths = await canonicalPaths(input);
application = await electron.launch({
executablePath: paths.executable,
cwd: paths.root,
env: safeElectronEnvironment(runId),
args: ["-r", hookPath, paths.application, ...input.args],
timeout: Math.max(1, deadline - Date.now()),
timeout: Math.max(1, startupDeadline - Date.now()),
});
const leaderPid = application.process().pid;
if (
@@ -192,16 +192,10 @@ export class PlaywrightElectronActiveProvider
expectedCommand: paths.executable,
sweepTokenOwnedProcesses: true,
};
const actions = await runElectronActions(
application,
input,
options,
deadline,
);
const actions = await runElectronActions(application, input, options);
const state = await runWithExecutionLimits(
readApplicationState(application),
options.signal,
deadline,
);
outcome = ok(createResult(paths, input, actions, state));
} catch (cause: unknown) {
@@ -341,7 +335,7 @@ const createResult = (
},
coverage: createCoverage(hookSnapshot),
limitations: [
"IPC payloads are represented only by bounded value shapes; values are never retained, channels are capped at 1,024 characters, and argument-shape arrays at 32 entries.",
"IPC payloads are represented by value shapes; payload values are never retained.",
"IPC direction and sender/receiver identifiers are observed only where Electron exposes them at the hooked boundary.",
"The runtime timeline records lifecycle, navigation, shell, permission, popup, download, protocol, preload, native-addon, process, and IPC events; activity before hook installation is unavailable.",
"The preload and renderer process contexts are not instrumented by the main-process -r hook; preload configuration and contextBridge API-shape events are main-boundary observations, not proof of renderer-side execution.",
+33
View File
@@ -165,3 +165,36 @@ it("accepts complete browser inputs beyond former string length caps", () => {
: undefined,
).toBe(longToken);
});
it("accepts caller-selected viewport sizes and click counts without ceilings", () => {
const input = {
browser: {
mode: "launch",
executable_path: "/opt/chromium",
headless: true,
user_data: "temporary-owned",
cleanup: "close-and-delete-profile",
},
start_url: { url: "https://app.example.test/" },
allowed_origins: ["https://app.example.test"],
environment: {
viewport: { width: 20_000, height: 10_000, device_scale_factor: 8 },
},
actions: [
{
step_id: "many-clicks",
action: "click",
locator: { kind: "css", selector: "#item" },
click_count: 4,
},
],
};
const parsed = browserScenarioSchema.parse(input);
expect(parsed.environment.viewport).toEqual({
width: 20_000,
height: 10_000,
device_scale_factor: 8,
});
expect(parsed.actions[0]).toMatchObject({ click_count: 4 });
});
+64
View File
@@ -8,6 +8,7 @@ import {
browserScenarioEventSchema,
browserStepArtifactsSchema,
} from "./browserScenarioCaptureValues.js";
import { browserScenarioCaptureSchema } from "./browserScenarioCapture.js";
const networkEvent = {
sequence: 1,
@@ -140,6 +141,69 @@ it("accepts complete storage, history, and text artifacts beyond former caps", (
expect(parsed.success).toBe(true);
});
it("accepts complete scenario output beyond former string and origin caps", () => {
const longText = "detail".repeat(1_000);
const origins = Array.from(
{ length: 33 },
(_, index) => `https://site-${index}.example.test`,
);
const step = (index: number) => ({
step_index: index,
step_id: longText,
action: longText,
elapsed_ms: 0,
before_url: sanitizeBrowserUrl("https://example.test/"),
after_url: sanitizeBrowserUrl("https://example.test/"),
event_sequence_start: 1,
event_sequence_end: 0,
artifacts: {
screenshot: { state: "not_requested" },
dom: { state: "not_requested" },
accessibility: { state: "not_requested" },
url: { state: "not_requested" },
history: { state: "not_requested" },
storage: { state: "not_requested" },
},
completeness: {
status: "complete",
equality_eligible: true,
missing_sections: [],
truncated_sections: [],
},
status: "completed",
error: null,
});
const parsed = browserScenarioCaptureSchema.parse({
browser: {
mode: "launch",
process_ownership: "provider-owned",
cleanup: "terminated-owned-process",
product: longText,
version: longText,
},
scenario: {
start_origin: longText,
allowed_origins: origins,
action_count: 1,
secret_references: [longText],
},
duration_ms: 0,
steps: [step(0), step(1)],
events: { retained: 0, dropped: 0, items: [] },
completeness: {
status: "complete",
equality_eligible: true,
missing_sections: [],
truncated_sections: [],
},
limitations: Array.from({ length: 65 }, () => longText),
});
expect(parsed.scenario.allowed_origins).toHaveLength(33);
expect(parsed.steps[0]?.step_id).toBe(longText);
expect(parsed.limitations).toHaveLength(65);
});
it("derives equality eligibility from exact completeness state", () => {
expect(
browserScenarioCompletenessSchema.safeParse({
+7 -7
View File
@@ -21,21 +21,21 @@ export {
type BrowserStepArtifacts,
} from "./browserScenarioCaptureValues.js";
/** Step-indexed, explicitly bounded browser scenario observation. */
/** Step-indexed browser scenario observation. */
export const browserScenarioCaptureSchema = z
.strictObject({
browser: z.strictObject({
mode: z.enum(["launch", "connect"]),
process_ownership: z.enum(["provider-owned", "external"]),
cleanup: z.enum(["terminated-owned-process", "disconnected-external"]),
product: z.string().min(1).max(1_024),
version: z.string().min(1).max(256),
product: z.string().min(1),
version: z.string().min(1),
}),
scenario: z.strictObject({
start_origin: z.string().min(1).max(2_048),
allowed_origins: z.array(z.string().min(1).max(2_048)).min(1).max(32),
start_origin: z.string().min(1),
allowed_origins: z.array(z.string().min(1)).min(1),
action_count: z.number().int().min(1),
secret_references: z.array(z.string().min(1).max(64)),
secret_references: z.array(z.string().min(1)),
}),
duration_ms: z.number().int().min(0),
steps: z.array(browserScenarioStepSchema).min(1),
@@ -45,7 +45,7 @@ export const browserScenarioCaptureSchema = z
items: z.array(browserScenarioEventSchema),
}),
completeness: browserScenarioCompletenessSchema,
limitations: z.array(z.string().min(1).max(2_048)).max(64),
limitations: z.array(z.string().min(1)),
})
.superRefine((capture, context) => {
if (capture.events.retained !== capture.events.items.length)
+4 -4
View File
@@ -30,13 +30,13 @@ export const captureStateSchema = <Schema extends z.ZodType>(value: Schema) =>
}),
z.strictObject({
state: z.literal("missing"),
reason: z.string().min(1).max(1_024),
reason: z.string().min(1),
}),
z.strictObject({
state: z.literal("truncated"),
observed: z.number().int().min(0),
retained: z.number().int().min(0),
reason: z.string().min(1).max(1_024),
reason: z.string().min(1),
}),
]);
@@ -281,8 +281,8 @@ export const classifyBrowserScenarioCompleteness = (
const browserScenarioStepShape = {
step_index: z.number().int().min(0),
step_id: z.string().min(1).max(64),
action: z.string().min(1).max(64),
step_id: z.string().min(1),
action: z.string().min(1),
elapsed_ms: z.number().int().min(0),
before_url: sanitizedBrowserUrlSchema,
after_url: sanitizedBrowserUrlSchema,
+6 -6
View File
@@ -95,9 +95,9 @@ export const browserScenarioEnvironmentSchema = z
.strictObject({
viewport: z
.strictObject({
width: z.number().int().min(320).max(7_680).default(1_280),
height: z.number().int().min(240).max(4_320).default(720),
device_scale_factor: z.number().min(0.5).max(4).default(1),
width: z.number().int().positive().default(1_280),
height: z.number().int().positive().default(720),
device_scale_factor: z.number().positive().default(1),
})
.default({ width: 1_280, height: 720, device_scale_factor: 1 }),
locale: z
@@ -182,7 +182,7 @@ export const browserScenarioActionSchema = z.discriminatedUnion("action", [
action: z.literal("click"),
locator: locatorSchema,
button: z.enum(["left", "middle", "right"]).default("left"),
click_count: z.number().int().min(1).max(3).default(1),
click_count: z.number().int().positive().default(1),
}),
z.strictObject({
...stepBase,
@@ -382,8 +382,8 @@ const snapshotKindSchema = z.enum([
export const browserScenarioCaptureSchema = z
.strictObject({
after_each_step: z.array(snapshotKindSchema).max(6).default([]),
at_end: z.array(snapshotKindSchema).max(6).default(["url"]),
after_each_step: z.array(snapshotKindSchema).default([]),
at_end: z.array(snapshotKindSchema).default(["url"]),
events: z
.array(
z.enum([
@@ -51,6 +51,6 @@ export const createElectronActiveObservationFixtureResult = (
observed: 1,
},
limitations: [
"IPC payloads are represented only by bounded value shapes; values are never retained.",
"IPC payloads are represented by value shapes; payload values are never retained.",
],
});
+11
View File
@@ -35,4 +35,15 @@ describe("inline web screenshots", () => {
}).success,
).toBe(false);
});
it("accepts a caller-supplied target ID without a length ceiling", () => {
const targetId = "target-".repeat(100);
expect(
captureWebScreenshotInputSchema.parse({
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: ["https://app.example.test"],
target_id: targetId,
}).target_id,
).toBe(targetId);
});
});
+1 -1
View File
@@ -35,7 +35,7 @@ export type WebScreenshotArtifact = z.infer<typeof webScreenshotArtifactSchema>;
export const captureWebScreenshotInputSchema = z.strictObject({
cdp_endpoint: browserEndpointSchema,
allowed_origins: browserAllowedOriginsSchema,
target_id: z.string().trim().min(1).max(256),
target_id: z.string().trim().min(1),
});
export type CaptureWebScreenshotInput = z.infer<
typeof captureWebScreenshotInputSchema
@@ -87,6 +87,8 @@ electron.ipcMain.handle(channel, async () => ({ ok: true }));
await electron.ipcMain.handlers.get(channel)({ sender: { id: 9 }, senderFrame: { routingId: 7 } }, ...Array.from({ length: 40 }, () => ({ value: true })));
window.webContents.send("main-to-renderer", { value: true });
window.webContents.emit("render-process-gone", { reason: "crashed" });
const longEventName = "l".repeat(2048);
window.webContents.emit(longEventName);
const utility = electron.utilityProcess.fork("utility.js");
utility.emit("message", { data: { value: true } });
utility.postMessage({ value: true });
@@ -163,6 +165,14 @@ it("retains active hook lifecycle and IPC evidence without a count ceiling", asy
});
expect(ipc?.channel).toBe("x".repeat(2_048));
expect(ipc?.argument_shapes).toHaveLength(40);
expect(snapshot.events).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "web-contents-lifecycle",
event: "l".repeat(2048),
}),
]),
);
});
it("preserves fatal process termination after recording uncaught exceptions", async () => {
@@ -56,4 +56,14 @@ describe("Electron file scope", () => {
await authorizedElectronFile("file:///tmp/root%2Findex.html", roots),
).toBeUndefined();
});
it("accepts file URLs with long query text inside a root", async () => {
const root = await createTestTempDirectory("rea-electron-long-url-");
const path = join(root, "index.html");
await writeFile(path, "allowed");
const roots = await canonicalElectronRoots([root]);
const value = `${pathToFileURL(path).href}?${"x".repeat(70_000)}`;
await expect(authorizedElectronFile(value, roots)).resolves.toBe(path);
});
});