Files
reverse-engineering-anything/tests/boundary/browser/cdpBrowserProvider.lifecycle_failures.test.ts
morluto b5e98915c6 fix(tools)!: simplify direct invocation and harden boundary contracts (#555)
Remove REA permission grants, scope ceilings, approval confirmations, and inert settings. Preserve selected local evidence, accept caller-chosen commands and paths, and report actual runtime failures.

Harden provider results, schema serialization, snapshot evidence binding, archive paths, source maps, native screenshots, managed EH parsing, and cancellation cleanup. Regenerate tool metadata and document direct invocation.

BREAKING CHANGE: Removed permission configuration and policy commands, approval fields, filesystem scope inputs, and fixed confirmation options. Process capture inherits the host environment and accepts command names; callers must refresh tool schemas and migrate removed fields.
2026-10-05 20:16:44 +08:00

470 lines
16 KiB
TypeScript

import { expect, it } from "vitest";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import {
inspectWebPageInputSchema,
listBrowserTargetsInputSchema,
} from "../../../src/domain/browserObservation.js";
import { analyzeWebBundleInputSchema } from "../../../src/domain/webBundleAnalysis.js";
import { observeWebSessionInputSchema } from "../../../src/domain/browserSession.js";
import {
startFakeCdpBrowser,
type FakeCdpBrowser,
} from "../../fixtures/fakeCdpBrowser.js";
import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js";
describeBrowser("CdpBrowserProvider: lifecycle failures 1", () => {
it("projects direct-session cancellation onto the requested operation", async () => {
const browser = await startFakeCdpBrowser({
pageScopedVersionWebSocket: true,
omitTargetWebSocket: true,
});
trackBrowser(browser);
const controller = new AbortController();
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 1_000,
}),
{
signal: controller.signal,
progress: {
report(update) {
if (update.completed === 1) controller.abort();
return Promise.resolve();
},
},
},
);
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCancelledError",
operation: "observe_web_session",
},
});
const methods = browser.commands.map(({ method }) => method);
expect(methods).not.toContain("Target.attachToTarget");
expect(methods).not.toContain("Target.detachFromTarget");
expect(methods).not.toContain("Target.closeTarget");
expect(methods).not.toContain("Browser.close");
});
it("projects capture-window cancellation onto bundle analysis", async () => {
const browser = await startFakeCdpBrowser({
pageScopedVersionWebSocket: true,
omitTargetWebSocket: true,
});
trackBrowser(browser);
const controller = new AbortController();
const result = await new CdpBrowserProvider().analyzeBundle(
analyzeWebBundleInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 1_000,
}),
{
signal: controller.signal,
progress: {
report(update) {
if (update.completed === 2) controller.abort();
return Promise.resolve();
},
},
},
);
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCancelledError",
operation: "analyze_web_bundle",
},
});
});
it("observes user-driven reload, SPA, redirect, failure, and lifecycle events", async () => {
const browser = await startFakeCdpBrowser({
sessionTimeline: "same_origin",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 20,
}),
);
if (!result.ok) throw result.error;
expect(result.value.window.end_reason).toBe("window_elapsed");
expect(result.value.timeline.map(({ type }) => type)).toEqual([
"navigation_requested",
"same_origin_reload",
"same_document_navigation",
"redirect",
"load_failed",
"lifecycle",
]);
expect(result.value.timeline).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: "same_document_navigation",
url: `${browser.allowedOrigin}/reloaded?token=session-secret#spa-secret`,
}),
expect.objectContaining({
type: "load_failed",
detail: "net::ERR_CONNECTION_REFUSED",
}),
]),
);
expect(result.value.completeness.status).toBe("attach_limited");
expect(JSON.stringify(result.value)).toContain("session-secret#spa-secret");
expect(JSON.stringify(result.value)).toContain("redirect-secret");
expect(browser.commands.map(({ method }) => method)).toContain(
"Target.detachFromTarget",
);
});
});
describeBrowser("CdpBrowserProvider: lifecycle failures 2", () => {
it("ends a browser session when its flat target session detaches", async () => {
const browser = await startFakeCdpBrowser({
sessionTimeline: "target_detached",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 1_000,
}),
);
if (!result.ok) throw result.error;
expect(result.value.window.end_reason).toBe("target_terminated");
expect(result.value.timeline.at(-1)).toMatchObject({
type: "target_terminated",
detail: "target_terminated",
});
});
it("ends a direct page session when its transport disconnects", async () => {
const browser = await startFakeCdpBrowser({
pageScopedVersionWebSocket: true,
omitTargetWebSocket: true,
closeAfterMethod: "Page.setLifecycleEventsEnabled",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 1_000,
}),
);
if (!result.ok) throw result.error;
expect(result.value.window.end_reason).toBe("target_terminated");
expect(result.value.timeline.at(-1)).toMatchObject({
type: "target_terminated",
detail: "target_terminated",
});
const methods = browser.commands.map(({ method }) => method);
expect(methods).not.toContain("Target.attachToTarget");
expect(methods).not.toContain("Target.detachFromTarget");
expect(methods).not.toContain("Target.closeTarget");
});
it("ends immediately when a redirect leaves policy without exposing its URL", async () => {
const browser = await startFakeCdpBrowser({
sessionTimeline: "outside_policy",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 1_000,
}),
);
if (!result.ok) throw result.error;
expect(result.value.window.end_reason).toBe("target_left_scope");
expect(result.value.timeline.at(-1)).toMatchObject({
type: "redirect",
url: null,
destination_scope: "outside_policy",
});
expect(result.value.completeness).toMatchObject({
status: "policy_filtered",
policy_filtered_sections: ["timeline"],
excluded: [
{
section: "timeline",
reason: "out_of_target_scope",
count: 1,
},
],
});
expect(JSON.stringify(result.value)).not.toContain("private.example.test");
});
it("fails closed when the final session frame leaves policy without an event", async () => {
const browser = await startFakeCdpBrowser({
frameUrlAfterFirstRead: "https://private.example.test/final",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 1,
}),
);
if (!result.ok) throw result.error;
expect(result.value.window.end_reason).toBe("target_left_scope");
expect(result.value.target.final_url).toBeNull();
expect(result.value.completeness).toMatchObject({
status: "policy_filtered",
policy_filtered_sections: ["timeline"],
});
expect(JSON.stringify(result.value)).not.toContain("private.example.test");
});
});
describeBrowser("CdpBrowserProvider: lifecycle failures 3", () => {
it("degrades optional domains but propagates protocol and payload failures", async () => {
const optional = await startFakeCdpBrowser({
unsupportedMethods: ["Accessibility.getFullAXTree"],
});
trackBrowser(optional);
const degraded = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: optional.endpoint,
allowed_origins: [optional.allowedOrigin],
target_id: "allowed-page",
observation_ms: 0,
}),
);
if (!degraded.ok) throw degraded.error;
expect(degraded.value.accessibility.nodes).toEqual([]);
expect(degraded.value.accessibility.text_capture.status).toBe(
"unavailable",
);
expect(degraded.value.completeness.unavailable_sections).toContain(
"accessibility",
);
expect(degraded.value.limitations).toContain(
"Accessibility.getFullAXTree was unavailable from this browser target.",
);
for (const [options, reason] of [
[{ oversizedDiscovery: true }, "invalid_endpoint_response"],
[{ invalidBrowserWebSocket: true }, "invalid_endpoint_response"],
] as const) {
const failed = await startFakeCdpBrowser(options);
trackBrowser(failed);
const result = await new CdpBrowserProvider().listTargets(
listBrowserTargetsInputSchema.parse({
cdp_endpoint: failed.endpoint,
allowed_origins: [failed.allowedOrigin],
}),
);
expect(result.ok).toBe(false);
if (!result.ok)
expect(result.error).toMatchObject({
_tag: "BrowserObservationError",
reason,
});
}
const malformed = await startFakeCdpBrowser({
malformedMessageOnMethod: "Page.getFrameTree",
});
trackBrowser(malformed);
const protocol = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: malformed.endpoint,
allowed_origins: [malformed.allowedOrigin],
target_id: "allowed-page",
observation_ms: 0,
}),
);
expect(protocol).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "protocol_error" },
});
const malformedEvent = await startFakeCdpBrowser({
malformedEventOnMethod: "Debugger.enable",
});
trackBrowser(malformedEvent);
const eventProtocol = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: malformedEvent.endpoint,
allowed_origins: [malformedEvent.allowedOrigin],
target_id: "allowed-page",
observation_ms: 0,
}),
);
expect(eventProtocol).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "protocol_error" },
});
const malformedEventShape = await startFakeCdpBrowser({
malformedEventShapeOnMethod: "Debugger.enable",
});
trackBrowser(malformedEventShape);
const eventShapeProtocol = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: malformedEventShape.endpoint,
allowed_origins: [malformedEventShape.allowedOrigin],
target_id: "allowed-page",
observation_ms: 0,
}),
);
expect(eventShapeProtocol).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "protocol_error" },
});
});
it("cancels observation and still detaches without closing the page", async () => {
const browser = await startFakeCdpBrowser();
trackBrowser(browser);
const controller = new AbortController();
const pending = new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 1_000,
}),
{ signal: controller.signal },
);
// Wait until the attach response has been consumed and capture has begun.
// Seeing the attach request alone races its response under worker load.
await waitForCommand(browser, "Page.getFrameTree");
controller.abort();
const result = await pending;
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCancelledError",
operation: "inspect_web_page",
},
});
const methods = browser.commands.map(({ method }) => method);
expect(methods).toContain("Target.detachFromTarget");
expect(methods).not.toContain("Target.closeTarget");
expect(methods).not.toContain("Browser.close");
});
});
describeBrowser("CdpBrowserProvider: lifecycle failures 4", () => {
it("waits for navigation commit and rechecks the attached main-frame origin", async () => {
const transitioning = await startFakeCdpBrowser({
transitionalFrameReads: 2,
});
trackBrowser(transitioning);
const input = (browser: FakeCdpBrowser) =>
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 0,
});
const committed = await new CdpBrowserProvider().inspectPage(
input(transitioning),
);
if (!committed.ok) throw committed.error;
expect(committed.value.target.origin).toBe(transitioning.allowedOrigin);
expect(
transitioning.commands.filter(
({ method }) => method === "Page.getFrameTree",
),
).toHaveLength(5);
const navigated = await startFakeCdpBrowser({
attachedFrameUrl: "https://unapproved.example.test/after-attach",
});
trackBrowser(navigated);
const denied = await new CdpBrowserProvider().inspectPage(input(navigated));
expect(denied).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "target_not_allowed" },
});
const methods = navigated.commands.map(({ method }) => method);
expect(methods).toContain("Target.detachFromTarget");
expect(methods).not.toContain("DOMSnapshot.captureSnapshot");
expect(methods).not.toContain("Storage.getUsageAndQuota");
const duringObservation = await startFakeCdpBrowser({
navigateDuringObservationUrl:
"https://unapproved.example.test/during-observation",
});
trackBrowser(duringObservation);
const interrupted = await new CdpBrowserProvider().inspectPage(
input(duringObservation),
);
expect(interrupted).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "target_not_allowed" },
});
expect(
duringObservation.commands.map(({ method }) => method),
).not.toContain("Accessibility.getFullAXTree");
const captureOptions: { navigateDuringCaptureUrl?: string } = {};
const duringCapture = await startFakeCdpBrowser(captureOptions);
captureOptions.navigateDuringCaptureUrl = `${duringCapture.allowedOrigin}/changed`;
trackBrowser(duringCapture);
const discarded = await new CdpBrowserProvider().inspectPage(
input(duringCapture),
);
expect(discarded).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "target_changed" },
});
expect(duringCapture.commands.map(({ method }) => method)).toContain(
"Target.detachFromTarget",
);
const crossOriginCapture = await startFakeCdpBrowser({
navigateDuringCaptureUrl:
"https://unapproved.example.test/during-capture",
});
trackBrowser(crossOriginCapture);
const rejectedCapture = await new CdpBrowserProvider().inspectPage(
input(crossOriginCapture),
);
expect(rejectedCapture).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "target_not_allowed" },
});
});
});
const waitForCommand = async (
browser: FakeCdpBrowser,
method: string,
): Promise<void> => {
for (let attempt = 0; attempt < 200; attempt += 1) {
if (browser.commands.some((command) => command.method === method)) return;
await new Promise<void>((resolve) => setTimeout(resolve, 5));
}
throw new Error(`Timed out waiting for fake CDP command ${method}`);
};