Files
rea/tests/boundary/browser/browserLifecycleFollowup.test.ts
T

224 lines
8.1 KiB
TypeScript

import { expect, it, onTestFinished } from "vitest";
import { WebRuntimeService } from "../../../src/application/WebRuntimeService.js";
import { CdpWebRuntimeProvider } from "../../../src/browser/execution/CdpWebRuntimeProvider.js";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import { observeWebSessionInputSchema } from "../../../src/domain/browserSession.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import {
webExecutionSchema,
webExecutionObservationSchema,
} from "../../../src/domain/webExecution.js";
import {
webEventListenersSchema,
webEventListenersObservationSchema,
} from "../../../src/domain/webEventListeners.js";
import { startRuntimeBrowser } from "../../fixtures/webRuntime.js";
import { waitFor } from "./cdpBrowserProvider.support.js";
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
it.each(["observe", "inspect"] as const)(
"retains truthful %s facts after rejected cleanup and preserves the successful control",
async (operation) => {
let deny = false;
const browser = await startRuntimeBrowser({
commandError: (command) =>
deny && command.method === "Page.disable"
? { code: -32000, message: "injected cleanup rejection" }
: undefined,
});
const provider = new CdpWebRuntimeProvider();
onTestFinished(async () => {
await provider.close();
await browser.close();
});
const service = new WebRuntimeService(provider);
const input = { cdp_endpoint: browser.endpoint, target_id: "allowed-page" };
const run = () =>
operation === "observe"
? service.observe({ ...input, observation_ms: 5 })
: service.inspect({ ...input, selector: "#missing" });
const healthy = await run();
if (!healthy.ok) throw healthy.error;
const cleanup = (
value: ReturnType<typeof parseEvidence>["normalized_result"],
) =>
operation === "observe"
? webExecutionObservationSchema.parse(value).instrumentation.cleanup
: webEventListenersObservationSchema.parse(value).cleanup;
expect(cleanup(healthy.value.normalized_result)).toBe("confirmed");
deny = true;
const failed = await run();
if (failed.ok) throw new Error("Rejected cleanup cannot be successful");
expect(failed.error.cleanupIncomplete).toBe(true);
const partial = parseEvidence(failed.error.partialObservation);
expect(cleanup(partial.normalized_result)).toBe("unconfirmed");
expect(
(operation === "observe"
? webExecutionSchema
: webEventListenersSchema
).safeParse(partial.normalized_result).success,
).toBe(false);
expect(partial.normalized_result).toMatchObject(
operation === "observe"
? {
target: { target_id: "allowed-page" },
coverage: { state: "captured" },
sources: expect.any(Array),
}
: {
target: { target_id: "allowed-page" },
selected_node: { match: "not_found" },
listeners: [],
},
);
expect(JSON.stringify(failed.error.diagnostics)).toContain("Page.disable");
expect(browser.commands.map(({ method }) => method)).toContain(
"Target.detachFromTarget",
);
expect(browser.commands.map(({ method }) => method)).not.toContain(
"Browser.close",
);
expect(browser.commands.map(({ method }) => method)).not.toContain(
"Target.closeTarget",
);
},
);
it("classifies failures by their admitted document request and retires completed identities", async () => {
const browser = await startFakeCdpBrowser();
const provider = new CdpBrowserProvider();
onTestFinished(async () => {
await provider.close();
await browser.close();
});
const result = await provider.observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 20,
}),
{
progress: {
report: async (update) => {
if (update.completed !== 1) return;
const request = (
id: string | number,
type: string,
frameId: string,
redirect = false,
) =>
browser.emitEvent({
method: "Network.requestWillBeSent",
sessionId: "session-1",
params: {
requestId: id,
type,
frameId,
loaderId: "observed-loader",
request: {
url: `${browser.allowedOrigin}/destination`,
method: "GET",
},
timestamp: 100,
...(redirect ? { redirectResponse: { status: 302 } } : {}),
},
});
const fail = (id: string, type: string) =>
browser.emitEvent({
method: "Network.loadingFailed",
sessionId: "session-1",
params: {
requestId: id,
type,
timestamp: 101,
errorText: "net::ERR_CONNECTION_REFUSED",
},
});
request("image", "Image", "frame-main");
fail("image", "Image");
request("child", "Document", "child-frame");
fail("child", "Document");
fail("unobserved", "Document");
request(42, "Document", "frame-main");
request("complete", "Document", "frame-main");
browser.emitEvent({
method: "Network.loadingFinished",
sessionId: "session-1",
params: { requestId: "complete", timestamp: 101 },
});
fail("complete", "Document");
request("reused", "Document", "frame-main");
request("reused", "Image", "frame-main");
fail("reused", "Image");
request("navigation", "Document", "frame-main", true);
fail("navigation", "Document");
fail("navigation", "Document");
},
},
},
);
if (!result.ok) throw result.error;
expect(
result.value.timeline.map(({ type, request_id }) => [type, request_id]),
).toEqual([
["redirect", "navigation"],
["load_failed", "navigation"],
]);
expect(result.value.completeness.excluded).toContainEqual({
section: "timeline",
reason: "invalid_protocol_value",
count: 1,
});
expect(result.value.timeline[1]).toMatchObject({
frame_id: "frame-main",
loader_id: "observed-loader",
detail: "net::ERR_CONNECTION_REFUSED",
});
});
it.each([false, true])(
"keeps cleanup facts truthful when cancellation arrives during finalization (rejected=$0)",
async (reject) => {
const controller = new AbortController();
const reason = { stop: "during final cleanup" };
const browser = await startRuntimeBrowser({
commandError: (command) => {
if (command.method !== "Page.disable") return undefined;
controller.abort(reason);
return reject
? { code: -32000, message: "injected cleanup rejection after abort" }
: undefined;
},
});
const provider = new CdpWebRuntimeProvider();
onTestFinished(async () => {
await provider.close();
await browser.close();
});
const result = await new WebRuntimeService(provider).observe(
{
cdp_endpoint: browser.endpoint,
target_id: "allowed-page",
observation_ms: 5,
},
{ signal: controller.signal },
);
if (result.ok)
throw new Error("Finalization cancellation must be retained");
const primary = result.error.cleanupPrimary ?? result.error;
expect(primary).toMatchObject({ _tag: "AnalysisCancelledError" });
expect(primary.cause).toBe(reason);
expect(result.error.cleanupIncomplete).toBe(reject);
const partial = parseEvidence(result.error.partialObservation);
expect(
webExecutionObservationSchema.parse(partial.normalized_result)
.instrumentation.cleanup,
).toBe(reject ? "unconfirmed" : "confirmed");
await waitFor(
() => browser.activeConnectionCount() === 0,
"Runtime transport did not close after finalization",
);
},
);