diff --git a/scripts/electron-active-hook.cjs b/scripts/electron-active-hook.cjs index a8b0c6e38..4185ffc78 100644 --- a/scripts/electron-active-hook.cjs +++ b/scripts/electron-active-hook.cjs @@ -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); }; diff --git a/src/browser/CdpCaptureValues.ts b/src/browser/CdpCaptureValues.ts index fce0b4a77..3d0e7c5d2 100644 --- a/src/browser/CdpCaptureValues.ts +++ b/src/browser/CdpCaptureValues.ts @@ -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 { diff --git a/src/browser/ElectronFileScope.ts b/src/browser/ElectronFileScope.ts index 9c80afd49..d87138e53 100644 --- a/src/browser/ElectronFileScope.ts +++ b/src/browser/ElectronFileScope.ts @@ -17,7 +17,7 @@ export const authorizedElectronFile = async ( value: string, roots: readonly string[], ): Promise => { - 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); diff --git a/src/browser/PlaywrightElectronActiveActions.test.ts b/src/browser/PlaywrightElectronActiveActions.test.ts index f0e717189..2ddee2a80 100644 --- a/src/browser/PlaywrightElectronActiveActions.test.ts +++ b/src/browser/PlaywrightElectronActiveActions.test.ts @@ -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]; diff --git a/src/browser/PlaywrightElectronActiveActions.ts b/src/browser/PlaywrightElectronActiveActions.ts index f8b7bcd85..8ec8262e6 100644 --- a/src/browser/PlaywrightElectronActiveActions.ts +++ b/src/browser/PlaywrightElectronActiveActions.ts @@ -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 => { 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 => { 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 => { - 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((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 ( operation: Promise, signal: AbortSignal | undefined, - deadline: number, + deadline?: number, ): Promise => { if (signal?.aborted === true) throw new BrowserObservationError(OPERATION, "cancelled"); + if (signal === undefined && deadline === undefined) return operation; let timer: ReturnType | undefined; let abortListener: (() => void) | undefined; - const timeout = new Promise((_, reject) => { - timer = setTimeout( - () => reject(new BrowserObservationError(OPERATION, "timeout")), - Math.max(1, deadline - Date.now()), + const interruptions: Promise[] = []; + if (deadline !== undefined) + interruptions.push( + new Promise((_, reject) => { + timer = setTimeout( + () => reject(new BrowserObservationError(OPERATION, "timeout")), + Math.max(1, deadline - Date.now()), + ); + }), + ); + if (signal !== undefined) + interruptions.push( + new Promise((_, reject) => { + abortListener = () => + reject(new BrowserObservationError(OPERATION, "cancelled")); + signal.addEventListener("abort", abortListener, { once: true }); + }), ); - }); - const cancellation = - signal === undefined - ? timeout - : Promise.race([ - timeout, - new Promise((_, 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) diff --git a/src/browser/PlaywrightElectronActiveProvider.ts b/src/browser/PlaywrightElectronActiveProvider.ts index c4ba199f4..110314a0a 100644 --- a/src/browser/PlaywrightElectronActiveProvider.ts +++ b/src/browser/PlaywrightElectronActiveProvider.ts @@ -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.", diff --git a/src/domain/browserScenario.test.ts b/src/domain/browserScenario.test.ts index 757b8ed05..3eaa90d4e 100644 --- a/src/domain/browserScenario.test.ts +++ b/src/domain/browserScenario.test.ts @@ -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 }); +}); diff --git a/src/domain/browserScenarioCapture.test.ts b/src/domain/browserScenarioCapture.test.ts index 034343919..2de0b0bdc 100644 --- a/src/domain/browserScenarioCapture.test.ts +++ b/src/domain/browserScenarioCapture.test.ts @@ -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({ diff --git a/src/domain/browserScenarioCapture.ts b/src/domain/browserScenarioCapture.ts index 3ae14dad0..5f2c261fb 100644 --- a/src/domain/browserScenarioCapture.ts +++ b/src/domain/browserScenarioCapture.ts @@ -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) diff --git a/src/domain/browserScenarioCaptureValues.ts b/src/domain/browserScenarioCaptureValues.ts index 5b12b5400..cf4fcdf48 100644 --- a/src/domain/browserScenarioCaptureValues.ts +++ b/src/domain/browserScenarioCaptureValues.ts @@ -30,13 +30,13 @@ export const captureStateSchema = (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, diff --git a/src/domain/browserScenarioValues.ts b/src/domain/browserScenarioValues.ts index 7806dd6aa..2895bb41e 100644 --- a/src/domain/browserScenarioValues.ts +++ b/src/domain/browserScenarioValues.ts @@ -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([ diff --git a/src/domain/electronActiveObservation.fixture.ts b/src/domain/electronActiveObservation.fixture.ts index d38edbac5..144dccca8 100644 --- a/src/domain/electronActiveObservation.fixture.ts +++ b/src/domain/electronActiveObservation.fixture.ts @@ -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.", ], }); diff --git a/src/domain/webScreenshot.test.ts b/src/domain/webScreenshot.test.ts index 82fb8d42b..60f4ebd37 100644 --- a/src/domain/webScreenshot.test.ts +++ b/src/domain/webScreenshot.test.ts @@ -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); + }); }); diff --git a/src/domain/webScreenshot.ts b/src/domain/webScreenshot.ts index 536541bc1..f319e6c61 100644 --- a/src/domain/webScreenshot.ts +++ b/src/domain/webScreenshot.ts @@ -35,7 +35,7 @@ export type WebScreenshotArtifact = z.infer; 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 diff --git a/tests/boundary/browser/electronActiveHook.test.ts b/tests/boundary/browser/electronActiveHook.test.ts index 7ad2c4d33..75ad44f83 100644 --- a/tests/boundary/browser/electronActiveHook.test.ts +++ b/tests/boundary/browser/electronActiveHook.test.ts @@ -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 () => { diff --git a/tests/boundary/filesystem/electronFileScope.test.ts b/tests/boundary/filesystem/electronFileScope.test.ts index 9dbc7b7b5..68bf6f388 100644 --- a/tests/boundary/filesystem/electronFileScope.test.ts +++ b/tests/boundary/filesystem/electronFileScope.test.ts @@ -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); + }); });