Files
rea/tests/acceptance/webSessionLifecycleWindow.test.ts
morluto 2a0410f171 fix(browser)!: bind session timelines to the observation window
Sample the browser monotonic clock with observation already subscribed, filter historical lifecycle and network facts, and preserve scope and termination handling when event time is unknown.

Bound pre-arm retention using transport-owned wire bytes. Fail closed when the relevant-event buffer fills, and keep unrelated protocol payloads outside that budget.

BREAKING CHANGE: observe_web_session timeline timestamps can be null when the producer supplies no comparable timestamp. Consumers must handle unknown time explicitly.

Refs #5609
2026-10-21 03:27:31 +08:00

141 lines
4.7 KiB
TypeScript

import { expect, onTestFinished } from "vitest";
import { z } from "zod";
import { webObservationSessionSchema } from "../../src/domain/browserSession.js";
import { toolContract } from "../../src/contracts/toolContracts.js";
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
import { startFakeCdpBrowser } from "../fixtures/fakeCdpBrowser.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest.for(["CLI", "stdio MCP"] as const)(
"filters replayed lifecycle milestones before the observation window through %s",
async (surface, { cli }) => {
const browser = await startFakeCdpBrowser({
eventsBeforeReply: [
"Performance.getMetrics",
"Page.setLifecycleEventsEnabled",
],
commandResult: (command) => {
if (command.method === "Performance.enable") return {};
if (command.method === "Performance.getMetrics")
return { metrics: [{ name: "Timestamp", value: 20 }] };
return undefined;
},
commandEvents: (command, origin) => {
if (command.method === "Performance.getMetrics")
return [
{
method: "Page.lifecycleEvent",
sessionId: command.sessionId,
params: {
frameId: "frame-main",
loaderId: "loader-before-window",
name: "load",
timestamp: 19,
},
},
{
method: "Page.navigatedWithinDocument",
sessionId: command.sessionId,
params: {
frameId: "frame-main",
url: `${origin}#during-clock-response`,
navigationType: "historyApi",
},
},
];
if (command.method === "Page.setLifecycleEventsEnabled")
return [
{
method: "Page.lifecycleEvent",
sessionId: command.sessionId,
params: {
frameId: "frame-main",
loaderId: "loader-before-window-replayed-after-arm",
name: "load",
timestamp: 19,
},
},
{
method: "Page.lifecycleEvent",
sessionId: command.sessionId,
params: {
frameId: "frame-main",
loaderId: "loader-current-window",
name: "load",
timestamp: 21,
},
},
];
return undefined;
},
});
onTestFinished(() => browser.close());
let evidence: unknown;
if (surface === "CLI") {
const response = await cli.run({
arguments: [
"observe-web-session",
browser.endpoint,
"allowed-page",
"--allowed-origins",
browser.allowedOrigin,
"--observation-ms",
"5",
"--json",
],
});
expect(response.exitCode, response.stderr).toBe(0);
evidence = response.json;
} else {
const { call } = await connectLocalToolsMcp();
const response = await call("observe_web_session", {
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 5,
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
evidence = response.structuredContent;
}
const contract = toolContract("observe_web_session");
const sourceEvidence = contract.outputSchema.parse(evidence);
const normalized = z
.object({ normalized_result: webObservationSessionSchema })
.parse(sourceEvidence).normalized_result;
expect(normalized.timeline).toContainEqual(
expect.objectContaining({
type: "lifecycle",
loader_id: "loader-current-window",
timestamp: 21,
}),
);
expect(normalized.timeline).not.toContainEqual(
expect.objectContaining({ loader_id: "loader-before-window" }),
);
expect(normalized.timeline).not.toContainEqual(
expect.objectContaining({
loader_id: "loader-before-window-replayed-after-arm",
}),
);
expect(normalized.timeline).toContainEqual(
expect.objectContaining({
type: "same_document_navigation",
url: `${browser.allowedOrigin}#during-clock-response`,
timestamp: null,
}),
);
expect(normalized.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.",
);
expect(browser.commands).toContainEqual(
expect.objectContaining({
method: "Performance.enable",
params: { timeDomain: "timeTicks" },
}),
);
},
);