mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
b5afa5f91e
* feat(browser): attribute native execution and listener sources Integrate finite native precise-coverage windows and direct DOM listener inspection through shared CLI/MCP workflows, exact session script identities, inline source evidence and owned instrumentation cleanup. Refs #881, #774 Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) * fix(browser): preserve runtime identity and settle fixture cleanup Retain complete reported initiator objects, reject changing loader availability, propagate the selected operation through main-frame authorization and complete effect/help inventories. Close only the synthetic owned Chrome via its native endpoint, await process-group settlement and preserve primary verification errors during cleanup. Refs #881 Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) * fix(browser): settle invalid events and preserve document evidence Accept same-document URL changes with proven stable loader identity, retain metadata without inventing ownership, terminate malformed event collection immediately, and sanitize known transport URLs without erasing sourceURL declarations. Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) * fix(browser): end detached target windows and sanitize coverage identities Reuse the existing flat-session event matcher, retain prearming termination, and apply the source identity policy to coverage URLs. Add real MCP target-close verification and producer regressions for detachment orderings. Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) * fix(browser): retain final sample metadata and settle fatal protocol failures Keep source inventory open through the resetting sample, propagate fatal wire failures without outstanding commands, and check listener collection failures after final document validation. Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) * fix(browser): require default execution worlds for website attribution Preserve native context and world metadata without treating same-frame isolated or unknown contexts as website sources. Verify actual Chrome isolated function execution before excluding its coverage, and cover ambiguous world metadata with producer regressions. Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) * fix(browser): revalidate runtime source joins and selector failures Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
434 lines
15 KiB
TypeScript
434 lines
15 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { describe, expect, it, onTestFinished } from "vitest";
|
|
import { CdpWebRuntimeProvider } from "../../../src/browser/execution/CdpWebRuntimeProvider.js";
|
|
import { observeWebExecutionInputSchema } from "../../../src/domain/webExecution.js";
|
|
import { inspectWebEventListenersInputSchema } from "../../../src/domain/webEventListeners.js";
|
|
import {
|
|
runtimeSourceA,
|
|
runtimeSourceB,
|
|
startRuntimeBrowser,
|
|
} from "../../fixtures/webRuntime.js";
|
|
import type { FakeOptions } from "../../fixtures/fakeCdpBrowser.js";
|
|
|
|
const fixture = async (options: FakeOptions = {}) => {
|
|
const browser = await startRuntimeBrowser(options);
|
|
onTestFinished(() => browser.close());
|
|
const provider = new CdpWebRuntimeProvider();
|
|
const input = {
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "allowed-page",
|
|
observation_ms: 5,
|
|
};
|
|
return {
|
|
browser,
|
|
provider,
|
|
execution: observeWebExecutionInputSchema.parse(input),
|
|
listeners: inspectWebEventListenersInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "allowed-page",
|
|
selector: "#selected",
|
|
}),
|
|
};
|
|
};
|
|
|
|
describe("native runtime producer boundary", () => {
|
|
it("binds complete producer stacks by script ID and ignores foreign sessions without URL guessing", async () => {
|
|
const { browser, provider, execution } = await fixture();
|
|
const params = {
|
|
requestId: "selected-request",
|
|
frameId: "runtime-main",
|
|
timestamp: 12.5,
|
|
request: {
|
|
url: `${browser.endpoint}/request?caller-value=retained`,
|
|
method: "POST",
|
|
},
|
|
initiator: {
|
|
type: "script",
|
|
producerExtension: "retain-unknown-fields",
|
|
stack: {
|
|
description: "observed-sync-stack",
|
|
callFrames: [
|
|
{
|
|
scriptId: "script-b",
|
|
url: `${browser.endpoint}/same.js`,
|
|
functionName: "alias",
|
|
lineNumber: 0,
|
|
columnNumber: 2,
|
|
},
|
|
],
|
|
parent: {
|
|
description: "observed-async-stack",
|
|
callFrames: [
|
|
{
|
|
scriptId: "unknown-script",
|
|
url: `${browser.endpoint}/same.js`,
|
|
functionName: "unknown",
|
|
lineNumber: 0,
|
|
columnNumber: 3,
|
|
},
|
|
],
|
|
parentId: { id: "async-parent", debuggerId: "other-debugger" },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const result = await provider.observeExecution(
|
|
{ ...execution, observation_ms: 30 },
|
|
{
|
|
progress: {
|
|
report: async (update) => {
|
|
if (update.completed !== 1) return;
|
|
browser.emitEvent({
|
|
method: "Network.requestWillBeSent",
|
|
params,
|
|
sessionId: "foreign-session",
|
|
});
|
|
browser.emitEvent({
|
|
method: "Network.requestWillBeSent",
|
|
params,
|
|
sessionId: "session-1",
|
|
});
|
|
},
|
|
},
|
|
},
|
|
);
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.requests).toHaveLength(1);
|
|
expect(result.value.requests[0]?.reported_initiator).toEqual(
|
|
params.initiator,
|
|
);
|
|
expect(result.value.requests[0]?.callsites).toMatchObject([
|
|
{ script_id: "script-b", source_association: "script_id" },
|
|
{ script_id: "unknown-script", source_association: "unknown" },
|
|
]);
|
|
expect(result.value.requests[0]?.async_parent_ids).toEqual([
|
|
{ id: "async-parent", debugger_id: "other-debugger" },
|
|
]);
|
|
expect(result.value.requests[0]?.causal_attribution).toBe("unknown");
|
|
});
|
|
});
|
|
|
|
describe("runtime window and producer granularity", () => {
|
|
it("ends at document replacement and leaves coverage explicitly unavailable", async () => {
|
|
const { browser, provider, execution } = await fixture();
|
|
const result = await provider.observeExecution(
|
|
{ ...execution, observation_ms: 30_000 },
|
|
{
|
|
progress: {
|
|
report: async (update) => {
|
|
if (update.completed === 1)
|
|
browser.emitEvent({
|
|
method: "Page.frameNavigated",
|
|
sessionId: "session-1",
|
|
params: {
|
|
frame: {
|
|
id: "runtime-main",
|
|
url: `${browser.endpoint}/new-document`,
|
|
},
|
|
},
|
|
});
|
|
},
|
|
},
|
|
},
|
|
);
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.window.end_reason).toBe("document_changed");
|
|
expect(result.value.coverage.state).toBe("unavailable");
|
|
expect(result.value.instrumentation.takes_one_resetting_sample).toBe(false);
|
|
});
|
|
|
|
it("reports disconnected cleanup as unknown instead of successful detachment", async () => {
|
|
const { provider, execution } = await fixture({
|
|
closeOnMethod: "Profiler.takePreciseCoverage",
|
|
});
|
|
const result = await provider.observeExecution(execution);
|
|
if (result.ok)
|
|
throw new Error("Disconnect cannot confirm instrumentation cleanup");
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
});
|
|
|
|
it("retains function-only producer granularity without fabricating zero branches", async () => {
|
|
const { provider, execution } = await fixture({
|
|
commandResult: (command, origin) =>
|
|
command.method === "Profiler.takePreciseCoverage"
|
|
? {
|
|
timestamp: 13,
|
|
result: [
|
|
{
|
|
scriptId: "script-a",
|
|
url: `${origin}/same.js`,
|
|
functions: [
|
|
{
|
|
functionName: "selected",
|
|
isBlockCoverage: false,
|
|
ranges: [
|
|
{
|
|
startOffset: 0,
|
|
endOffset: runtimeSourceA.length,
|
|
count: 1,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}
|
|
: undefined,
|
|
});
|
|
const result = await provider.observeExecution(execution);
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.coverage.scripts[0]?.functions[0]).toMatchObject({
|
|
is_block_coverage: false,
|
|
ranges: [{ count: 1 }],
|
|
});
|
|
expect(result.value.script_inventory.not_reported_script_ids).toContain(
|
|
"script-b",
|
|
);
|
|
expect(result.value.script_inventory.coverage_absence).toBe("unknown");
|
|
});
|
|
});
|
|
|
|
describe("runtime source and listener identity", () => {
|
|
it("retains same-URL script identities, UTF-16 bounds, nested zero counts and actual cleanup", async () => {
|
|
const { browser, provider, execution } = await fixture();
|
|
const result = await provider.observeExecution(execution);
|
|
if (!result.ok) throw result.error;
|
|
expect(
|
|
result.value.coverage.scripts.map((script) => script.script_id),
|
|
).toEqual(["script-a", "script-b"]);
|
|
expect(result.value.coverage.excluded_scripts).toBe(1);
|
|
expect(
|
|
result.value.sources.map((script) =>
|
|
script.source.state === "captured" ? script.source.text : null,
|
|
),
|
|
).toEqual([runtimeSourceA, runtimeSourceB]);
|
|
const source = result.value.sources[0];
|
|
expect(source?.producer_hash).toBe("producer-reported-hash");
|
|
expect(source?.source).toMatchObject({
|
|
sha256: createHash("sha256").update(runtimeSourceA).digest("hex"),
|
|
utf16_units: runtimeSourceA.length,
|
|
utf8_bytes: Buffer.byteLength(runtimeSourceA),
|
|
});
|
|
expect(
|
|
result.value.coverage.scripts[0]?.functions[0]?.ranges[1],
|
|
).toMatchObject({ count: 0, source_bounds: "verified" });
|
|
expect(
|
|
browser.commands.some(
|
|
(command) => command.method === "Profiler.stopPreciseCoverage",
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
browser.commands.some(
|
|
(command) => command.method === "Target.detachFromTarget",
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
browser.commands.some((command) =>
|
|
["Browser.close", "Target.closeTarget", "Runtime.evaluate"].includes(
|
|
command.method,
|
|
),
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("inspects listener source and releases only its owned object group", async () => {
|
|
const { browser, provider, listeners } = await fixture();
|
|
const result = await provider.inspectEventListeners(listeners);
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.listeners[0]).toMatchObject({
|
|
type: "click",
|
|
location: {
|
|
script_id: "script-a",
|
|
source_association: "script_id",
|
|
line_number: 1,
|
|
},
|
|
execution: "unknown",
|
|
});
|
|
const resolve = browser.commands.find(
|
|
(command) => command.method === "DOM.resolveNode",
|
|
);
|
|
const release = browser.commands.find(
|
|
(command) => command.method === "Runtime.releaseObjectGroup",
|
|
);
|
|
expect(release?.params.objectGroup).toBe(resolve?.params.objectGroup);
|
|
expect(resolve?.params.objectGroup).toMatch(/^rea-listeners-/u);
|
|
});
|
|
|
|
it("returns an explicit missing node without source/handler evaluation", async () => {
|
|
const { provider, listeners } = await fixture();
|
|
const result = await provider.inspectEventListeners({
|
|
...listeners,
|
|
selector: "#missing",
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.selected_node.match).toBe("not_found");
|
|
expect(result.value.listeners).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("runtime lifecycle and invalid producer data", () => {
|
|
it("releases instrumentation after cancellation delivered at the actual armed point", async () => {
|
|
const { browser, provider, execution } = await fixture();
|
|
const controller = new AbortController();
|
|
const result = await provider.observeExecution(
|
|
{ ...execution, observation_ms: 30_000 },
|
|
{
|
|
signal: controller.signal,
|
|
progress: {
|
|
report: async (update) => {
|
|
if (update.completed === 1) controller.abort();
|
|
},
|
|
},
|
|
},
|
|
);
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok)
|
|
throw new Error("Cancellation must not produce successful evidence");
|
|
expect(result.error._tag).toBe("AnalysisCancelledError");
|
|
expect(
|
|
browser.commands.some(
|
|
(command) => command.method === "Profiler.stopPreciseCoverage",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("returns cleanup failure when coverage stop cannot be confirmed", async () => {
|
|
const { browser, provider, execution } = await fixture({
|
|
unsupportedMethods: ["Profiler.stopPreciseCoverage"],
|
|
});
|
|
const result = await provider.observeExecution(execution);
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) throw new Error("Failed cleanup must not return success");
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expect(
|
|
browser.commands.some(
|
|
(command) => command.method === "Target.detachFromTarget",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("preserves the unsupported command reason", async () => {
|
|
const { provider, execution } = await fixture({
|
|
unsupportedMethods: ["Profiler.startPreciseCoverage"],
|
|
});
|
|
const result = await provider.observeExecution(execution);
|
|
if (result.ok) throw new Error("Unsupported instrumentation must fail");
|
|
expect(result.error.userMessage).toContain("Profiler.startPreciseCoverage");
|
|
expect(result.error.userMessage).toContain("Method not found");
|
|
});
|
|
|
|
it("rejects a malformed sample and still stops coverage", async () => {
|
|
const { browser, provider, execution } = await fixture({
|
|
commandResult: (command) =>
|
|
command.method === "Profiler.takePreciseCoverage"
|
|
? { timestamp: 1, result: "wrong" }
|
|
: undefined,
|
|
});
|
|
const result = await provider.observeExecution(execution);
|
|
if (result.ok) throw new Error("Malformed producer coverage must fail");
|
|
expect(result.error._tag).toBe("AnalysisOutputError");
|
|
expect(
|
|
browser.commands.some(
|
|
(command) => command.method === "Profiler.stopPreciseCoverage",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("rejects a source range measured as UTF-8 bytes instead of UTF-16 units", async () => {
|
|
const { provider, execution } = await fixture({
|
|
commandResult: (command, origin) =>
|
|
command.method === "Profiler.takePreciseCoverage"
|
|
? {
|
|
timestamp: 13,
|
|
result: [
|
|
{
|
|
scriptId: "script-a",
|
|
url: `${origin}/same.js`,
|
|
functions: [
|
|
{
|
|
functionName: "bad",
|
|
isBlockCoverage: true,
|
|
ranges: [
|
|
{
|
|
startOffset: 0,
|
|
endOffset: Buffer.byteLength(runtimeSourceA),
|
|
count: 1,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}
|
|
: undefined,
|
|
});
|
|
const result = await provider.observeExecution(execution);
|
|
if (result.ok)
|
|
throw new Error("Byte offsets must not be mislabeled UTF-16");
|
|
expect(result.error._tag).toBe("AnalysisOutputError");
|
|
});
|
|
});
|
|
|
|
describe("runtime document identity and authorization failures", () => {
|
|
it.each(["execution", "listeners"] as const)(
|
|
"rejects newly reported loader identity for %s when the initial loader was unknown",
|
|
async (action) => {
|
|
const { provider, execution, listeners } = await fixture({
|
|
commandResult: (command, origin, frameReads) =>
|
|
command.method === "Page.getFrameTree"
|
|
? {
|
|
frameTree: {
|
|
frame: {
|
|
id: "runtime-main",
|
|
url: `${origin}/allowed`,
|
|
...(frameReads === 1 ? {} : { loaderId: "new-document" }),
|
|
},
|
|
},
|
|
}
|
|
: undefined,
|
|
});
|
|
const result =
|
|
action === "execution"
|
|
? await provider.observeExecution(execution)
|
|
: await provider.inspectEventListeners(listeners);
|
|
if (result.ok)
|
|
throw new Error(
|
|
"Unknown loader must not accept a newly reported document identity",
|
|
);
|
|
expect(result.error).toMatchObject({ reason: "target_changed" });
|
|
},
|
|
);
|
|
|
|
it.each(["execution", "listeners"] as const)(
|
|
"reports the selected %s operation when the live frame origin is denied",
|
|
async (action) => {
|
|
const { provider, execution, listeners } = await fixture({
|
|
commandResult: (command) =>
|
|
command.method === "Page.getFrameTree"
|
|
? {
|
|
frameTree: {
|
|
frame: {
|
|
id: "runtime-main",
|
|
url: "https://unselected.example/document",
|
|
loaderId: "denied",
|
|
},
|
|
},
|
|
}
|
|
: undefined,
|
|
});
|
|
const result =
|
|
action === "execution"
|
|
? await provider.observeExecution(execution)
|
|
: await provider.inspectEventListeners(listeners);
|
|
if (result.ok) throw new Error("Unselected live origin must be denied");
|
|
expect(result.error).toMatchObject({
|
|
reason: "target_not_allowed",
|
|
operation:
|
|
action === "execution"
|
|
? "observe_web_execution"
|
|
: "inspect_web_event_listeners",
|
|
});
|
|
},
|
|
);
|
|
});
|