Files
rea/tests/boundary/browser/cdpObservationWindowScope.test.ts
T
morluto 99ff4344c0 fix(browser): centralize CDP network event facts
Parse shared request and response fields at the CDP boundary while preserving capture, session and runtime admission policies. Treat empty request IDs as unknown so unrelated events cannot be joined, and retain redirect occurrences and partial response evidence.

Validate 221 focused tests, fast checks, and real Chrome network and runtime verifiers.
2026-10-27 02:57:58 +08:00

451 lines
14 KiB
TypeScript

import { expect, it } from "vitest";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import { observeWebSessionInputSchema } from "../../../src/domain/browserSession.js";
import {
startFakeCdpBrowser,
type FakeOptions,
} from "../../fixtures/fakeCdpBrowser.js";
import { frameNavigated, trackBrowser } from "./cdpBrowserProvider.support.js";
const observeDuringClockSampling = async (
commandEvents: NonNullable<FakeOptions["commandEvents"]>,
) => {
return observeWithFake({
eventsBeforeReply: ["Performance.getMetrics"],
commandResult: (command) =>
command.method === "Performance.getMetrics"
? { metrics: [{ name: "Timestamp", value: 20 }] }
: undefined,
commandEvents,
});
};
const observeWithFake = async (options: FakeOptions) => {
const browser = await startFakeCdpBrowser(options);
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;
return { browser, observation: result.value };
};
const laterInWindowRequest = (
command: Parameters<NonNullable<FakeOptions["commandEvents"]>>[0],
origin: string,
) => ({
method: "Network.requestWillBeSent",
sessionId: command.sessionId,
params: {
requestId: "later-in-window-request",
timestamp: 21,
type: "Document",
frameId: "frame-main",
redirectResponse: { url: `${origin}/prior` },
request: { url: `${origin}/later` },
},
});
const expectNoCommittedFrameWithInvalidTimeline = (
observation: Awaited<ReturnType<typeof observeWithFake>>["observation"],
) => {
expect(observation.timeline).not.toContainEqual(
expect.objectContaining({ type: "navigation_committed" }),
);
expect(observation.completeness.excluded).toContainEqual(
expect.objectContaining({
section: "timeline",
reason: "invalid_protocol_value",
}),
);
};
it("fails closed on an untimestamped cross-origin main-frame commit during clock sampling", async () => {
const { observation } = await observeDuringClockSampling(
(command, origin) => {
if (command.method !== "Performance.getMetrics") return undefined;
return [
frameNavigated(command, {
id: "frame-main",
loaderId: "outside-loader",
url: "https://outside.invalid/path",
}),
laterInWindowRequest(command, origin),
];
},
);
expect(observation.window.end_reason).toBe("target_left_scope");
expect(observation.target.final_url).toBeNull();
expect(observation.timeline).toContainEqual(
expect.objectContaining({
type: "navigation_committed",
loader_id: "outside-loader",
timestamp: null,
destination_scope: "outside_policy",
url: null,
}),
);
expect(observation.timeline).not.toContainEqual(
expect.objectContaining({ request_id: "later-in-window-request" }),
);
});
it("keeps an unidentifiable frame commit unknown without inventing a main-frame event", async () => {
const { observation } = await observeDuringClockSampling((command) =>
command.method === "Performance.getMetrics"
? [frameNavigated(command, { url: "https://outside.invalid/unknown" })]
: undefined,
);
expect(observation.window.end_reason).toBe("window_elapsed");
expectNoCommittedFrameWithInvalidTimeline(observation);
});
it("marks a frame event with absent parameters as unknown timeline evidence", async () => {
const { observation } = await observeDuringClockSampling((command) =>
command.method === "Performance.getMetrics"
? [
{
method: "Page.frameNavigated",
sessionId: command.sessionId,
params: undefined,
},
]
: undefined,
);
expectNoCommittedFrameWithInvalidTimeline(observation);
expect(observation.completeness.unavailable_sections).toContain("timeline");
});
it("records a malformed child identity without treating it as the main frame", async () => {
const { observation } = await observeDuringClockSampling((command) =>
command.method === "Performance.getMetrics"
? [
frameNavigated(command, {
id: 3,
parentId: "frame-main",
url: "https://outside.invalid/child",
}),
]
: undefined,
);
expect(observation.window.end_reason).toBe("window_elapsed");
expectNoCommittedFrameWithInvalidTimeline(observation);
});
it("updates the main frame from a pre-arm root replacement without inventing a navigation row", async () => {
const { observation } = await observeDuringClockSampling((command, origin) =>
command.method === "Performance.getMetrics"
? [
frameNavigated(command, {
id: "replacement-main",
loaderId: "replacement-loader",
url: `${origin}/app?token=frame-secret`,
}),
{
method: "Page.lifecycleEvent",
sessionId: command.sessionId,
params: {
frameId: "replacement-main",
loaderId: "replacement-loader",
name: "DOMContentLoaded",
timestamp: 21,
},
},
]
: undefined,
);
expect(observation.timeline).not.toContainEqual(
expect.objectContaining({ type: "navigation_committed" }),
);
expect(observation.timeline).toContainEqual(
expect.objectContaining({
type: "lifecycle",
frame_id: "replacement-main",
loader_id: "replacement-loader",
timestamp: 21,
detail: "domcontentloaded",
}),
);
expect(observation.limitations).toEqual(
expect.arrayContaining([
expect.stringContaining("Buffered events with unknown timing"),
]),
);
});
it("fails closed on an untimestamped out-of-scope main-frame request during clock sampling", async () => {
const { observation } = await observeDuringClockSampling(
(command, origin) => {
if (command.method !== "Performance.getMetrics") return undefined;
return [
{
method: "Page.frameRequestedNavigation",
sessionId: command.sessionId,
params: {
frameId: "frame-main",
url: "https://outside.invalid/path",
reason: "scriptInitiated",
},
},
laterInWindowRequest(command, origin),
];
},
);
expect(observation.window.end_reason).toBe("target_left_scope");
expect(observation.timeline).toContainEqual(
expect.objectContaining({
type: "navigation_requested",
timestamp: null,
destination_scope: "outside_policy",
url: null,
}),
);
expect(observation.timeline).not.toContainEqual(
expect.objectContaining({ request_id: "later-in-window-request" }),
);
});
it("fails closed on an untimestamped out-of-scope redirect during clock sampling", async () => {
const { observation } = await observeDuringClockSampling(
(command, origin) => {
if (command.method !== "Performance.getMetrics") return undefined;
return [
{
method: "Network.requestWillBeSent",
sessionId: command.sessionId,
params: {
requestId: "unknown-time-outside-redirect",
type: "Document",
frameId: "frame-main",
redirectResponse: { url: `${origin}/prior` },
request: { url: "https://outside.invalid/redirected" },
},
},
{
method: "Network.requestWillBeSent",
sessionId: command.sessionId,
params: {
requestId: "later-in-window-request",
timestamp: 21,
type: "Document",
frameId: "frame-main",
redirectResponse: { url: `${origin}/prior` },
request: { url: `${origin}/later` },
},
},
];
},
);
expect(observation.window.end_reason).toBe("target_left_scope");
expect(observation.target.final_url).toBeNull();
expect(observation.timeline).toContainEqual(
expect.objectContaining({
type: "redirect",
request_id: "unknown-time-outside-redirect",
timestamp: null,
destination_scope: "outside_policy",
url: null,
}),
);
expect(observation.timeline).not.toContainEqual(
expect.objectContaining({ request_id: "later-in-window-request" }),
);
});
it.each([
[
"Inspector.targetCrashed",
"target_terminated",
"target_terminated",
{
method: "Inspector.targetCrashed",
sessionId: "session-1",
params: {},
},
],
[
"Target.detachedFromTarget",
"session_detached",
"session_detached",
{
method: "Target.detachedFromTarget",
params: { sessionId: "session-1", targetId: "allowed-page" },
},
],
] as const)(
"ends on buffered untimestamped %s before collecting later events",
async (_method, endReason, eventType, terminalEvent) => {
const { observation } = await observeWithFake({
eventsBeforeReply: ["Performance.getMetrics"],
commandResult: (command) =>
command.method === "Performance.getMetrics"
? { metrics: [{ name: "Timestamp", value: 20 }] }
: undefined,
commandEvents: (command, origin) => {
if (command.method !== "Performance.getMetrics") return undefined;
return [
terminalEvent,
{
method: "Network.requestWillBeSent",
sessionId: command.sessionId,
params: {
requestId: "after-terminal-event",
timestamp: 21,
type: "Document",
frameId: "frame-main",
redirectResponse: { url: `${origin}/prior` },
request: { url: `${origin}/later` },
},
},
];
},
});
expect(observation.window.end_reason).toBe(endReason);
expect(observation.timeline).toContainEqual(
expect.objectContaining({
type: eventType,
timestamp: null,
}),
);
expect(observation.timeline).not.toContainEqual(
expect.objectContaining({ request_id: "after-terminal-event" }),
);
expect(observation.limitations).toContain(
"Timeline rows without a comparable producer timestamp have a null timestamp. Buffered events with unknown timing are retained as context only; they do not establish activity inside the selected observation window.",
);
},
);
it("represents absent timeline timestamps as unknown and preserves an observed zero", async () => {
const { browser, observation } = await observeWithFake({
eventsBeforeReply: ["Page.setLifecycleEventsEnabled"],
commandEvents: (command, origin) => {
if (command.method !== "Page.setLifecycleEventsEnabled") return undefined;
return [
{
method: "Page.navigatedWithinDocument",
sessionId: command.sessionId,
params: {
frameId: "frame-main",
url: `${origin}/without-clock-value`,
navigationType: "historyApi",
},
},
{
method: "Network.requestWillBeSent",
sessionId: command.sessionId,
params: {
requestId: "actual-zero-clock-value",
type: "Document",
frameId: "frame-main",
timestamp: 0,
request: { url: `${origin}/document`, method: "GET" },
},
},
{
method: "Network.loadingFailed",
sessionId: command.sessionId,
params: {
requestId: "actual-zero-clock-value",
type: "Document",
errorText: "net::ERR_CONNECTION_REFUSED",
timestamp: 0,
},
},
];
},
});
expect(observation.timeline).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: "same_document_navigation",
url: `${browser.allowedOrigin}/without-clock-value`,
timestamp: null,
}),
expect.objectContaining({
type: "load_failed",
request_id: "actual-zero-clock-value",
timestamp: 0,
}),
]),
);
});
it("ends an armed observation at a fatal protocol failure without waiting out its window", async () => {
const browser = await startFakeCdpBrowser({
malformedEventOnMethod: "Page.setLifecycleEventsEnabled",
});
trackBrowser(browser);
const started = performance.now();
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 10_000,
}),
);
expect(performance.now() - started).toBeLessThan(5_000);
expect(result).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "protocol_error" },
});
}, 15_000);
it("does not join an empty document request identity to a later loading failure", async () => {
const { observation } = await observeDuringClockSampling(
(command, origin) => {
if (command.method !== "Performance.getMetrics") return undefined;
return [
{
...laterInWindowRequest(command, origin),
params: {
...laterInWindowRequest(command, origin).params,
requestId: "",
},
},
{
method: "Network.loadingFailed",
sessionId: command.sessionId,
params: {
requestId: "",
timestamp: 22,
errorText: "net::ERR_FAILED",
},
},
];
},
);
expect(observation.window.end_reason).toBe("window_elapsed");
expect(observation.timeline).toContainEqual(
expect.objectContaining({ type: "redirect", request_id: null }),
);
expect(observation.timeline).not.toContainEqual(
expect.objectContaining({ type: "load_failed" }),
);
expect(observation.timeline).not.toContainEqual(
expect.objectContaining({ request_id: "" }),
);
expect(observation.completeness.excluded).toContainEqual(
expect.objectContaining({
section: "timeline",
reason: "invalid_protocol_value",
}),
);
});