mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
0154141304
* fix(browser): preserve capture evidence fidelity * fix(browser): align frame identity comparisons * fix(browser): retain historical frame evidence * fix(electron): retain same-document script owners * fix(electron): bind frame proof to tree reads * fix(browser): retain malformed frame scope * fix(browser): retain WebMCP event scope * fix(browser): retain malformed timeline identity * fix(browser): unify frame identity and scope * fix(electron): prove enclosing frame ancestry * fix(electron): separate ancestry from file policy * fix(browser): preserve transient fragment semantics * fix(browser): retry uncertain initial frame scope * fix(browser): preserve runtime frame identity * refactor(browser): separate independent capture fixes * fix(electron): detect tree-first document replacement * fix(electron): retain same-document tree authority --------- Co-authored-by: eco-null <eco-null@users.noreply.github.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
205 lines
6.1 KiB
TypeScript
205 lines
6.1 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
|
import { CdpWebRuntimeProvider } from "../../../src/browser/execution/CdpWebRuntimeProvider.js";
|
|
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
|
|
import { observeWebSessionInputSchema } from "../../../src/domain/browserSession.js";
|
|
import { observeWebExecutionInputSchema } from "../../../src/domain/webExecution.js";
|
|
import { startRuntimeBrowser } from "../../fixtures/webRuntime.js";
|
|
import type { FakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
|
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
|
|
|
const operations = ["inspect", "session", "runtime"] as const;
|
|
type Operation = (typeof operations)[number];
|
|
const run = async (
|
|
browser: FakeCdpBrowser,
|
|
operation: Operation,
|
|
signal?: AbortSignal,
|
|
) => {
|
|
const input = {
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "allowed-page",
|
|
allowed_origins: [browser.allowedOrigin],
|
|
observation_ms: 1,
|
|
};
|
|
const options = signal === undefined ? {} : { signal };
|
|
switch (operation) {
|
|
case "inspect":
|
|
return new CdpBrowserProvider().inspectPage(
|
|
inspectWebPageInputSchema.parse(input),
|
|
options,
|
|
);
|
|
case "session":
|
|
return new CdpBrowserProvider().observeSession(
|
|
observeWebSessionInputSchema.parse(input),
|
|
options,
|
|
);
|
|
case "runtime":
|
|
return new CdpWebRuntimeProvider().observeExecution(
|
|
observeWebExecutionInputSchema.parse(input),
|
|
options,
|
|
);
|
|
}
|
|
};
|
|
|
|
it.each(
|
|
operations.flatMap((operation) =>
|
|
["about:blank", "about:srcdoc", "file:///local.html", "not a URL"].flatMap(
|
|
(url) =>
|
|
[7, "bad-fragment"].map((urlFragment) => ({
|
|
operation,
|
|
url,
|
|
urlFragment,
|
|
})),
|
|
),
|
|
),
|
|
)(
|
|
"retries uncertain initial $url/$urlFragment before $operation authorizes",
|
|
async ({ operation, url, urlFragment }) => {
|
|
let reads = 0;
|
|
const browser = await startRuntimeBrowser({
|
|
commandResult: ({ method }) => {
|
|
if (method !== "Page.getFrameTree" || ++reads > 2) return undefined;
|
|
return {
|
|
frameTree: { frame: { id: "runtime-main", url, urlFragment } },
|
|
};
|
|
},
|
|
});
|
|
trackBrowser(browser);
|
|
const result = await run(browser, operation);
|
|
expect(result.ok).toBe(true);
|
|
expect(reads).toBeGreaterThan(2);
|
|
},
|
|
);
|
|
|
|
it.each(
|
|
operations.flatMap((operation) =>
|
|
[false, true].flatMap((foreign) =>
|
|
[7, "bad-fragment"].map((urlFragment) => ({
|
|
operation,
|
|
foreign,
|
|
urlFragment,
|
|
})),
|
|
),
|
|
),
|
|
)(
|
|
"rejects known $foreign scope with malformed $urlFragment immediately for $operation",
|
|
async ({ operation, foreign, urlFragment }) => {
|
|
let reads = 0;
|
|
const browser = await startRuntimeBrowser({
|
|
commandResult: ({ method }, origin) => {
|
|
if (method !== "Page.getFrameTree") return undefined;
|
|
reads += 1;
|
|
return {
|
|
frameTree: {
|
|
frame: {
|
|
id: "runtime-main",
|
|
url: foreign ? "https://foreign.example/page" : `${origin}/app`,
|
|
urlFragment,
|
|
},
|
|
},
|
|
};
|
|
},
|
|
});
|
|
trackBrowser(browser);
|
|
expect(await run(browser, operation)).toMatchObject({
|
|
ok: false,
|
|
error: { reason: foreign ? "target_not_allowed" : "protocol_error" },
|
|
});
|
|
expect(reads).toBe(1);
|
|
},
|
|
);
|
|
|
|
it("bounds unsupported malformed frame retries without authorizing execution", async () => {
|
|
let reads = 0;
|
|
const browser = await startRuntimeBrowser({
|
|
commandResult: ({ method }) => {
|
|
if (method !== "Page.getFrameTree") return undefined;
|
|
reads += 1;
|
|
return {
|
|
frameTree: {
|
|
frame: { id: "runtime-main", url: "about:blank", urlFragment: 7 },
|
|
},
|
|
};
|
|
},
|
|
});
|
|
trackBrowser(browser);
|
|
expect(await run(browser, "runtime")).toMatchObject({
|
|
ok: false,
|
|
error: { reason: "target_not_allowed" },
|
|
});
|
|
expect(reads).toBe(80);
|
|
expect(browser.commands.map(({ method }) => method)).not.toContain(
|
|
"Debugger.enable",
|
|
);
|
|
});
|
|
|
|
it("preserves cancellation while an unsupported malformed frame awaits authorization", async () => {
|
|
const controller = new AbortController();
|
|
const browser = await startRuntimeBrowser({
|
|
commandResult: ({ method }) => {
|
|
if (method !== "Page.getFrameTree") return undefined;
|
|
setTimeout(() => controller.abort("caller stopped authorization"), 5);
|
|
return {
|
|
frameTree: {
|
|
frame: { id: "runtime-main", url: "about:blank", urlFragment: 7 },
|
|
},
|
|
};
|
|
},
|
|
});
|
|
trackBrowser(browser);
|
|
expect(await run(browser, "inspect", controller.signal)).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisCancelledError",
|
|
cause: "caller stopped authorization",
|
|
},
|
|
});
|
|
expect(
|
|
browser.commands.filter(({ method }) => method === "Page.getFrameTree"),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
// Ordinary startup uncertainty and known policy exits keep their existing semantics.
|
|
it.each(
|
|
operations.flatMap((operation) =>
|
|
["unsupported", "missing", "foreign"].map((kind) => ({ operation, kind })),
|
|
),
|
|
)(
|
|
"preserves $kind authorization behavior for $operation",
|
|
async ({ operation, kind }) => {
|
|
let reads = 0;
|
|
const browser = await startRuntimeBrowser({
|
|
commandResult: ({ method }) => {
|
|
if (method !== "Page.getFrameTree" || ++reads > 2) return undefined;
|
|
return {
|
|
frameTree: {
|
|
frame: {
|
|
id: "runtime-main",
|
|
...(kind === "missing"
|
|
? {}
|
|
: {
|
|
url:
|
|
kind === "foreign"
|
|
? "https://foreign.example/page"
|
|
: "about:blank",
|
|
}),
|
|
},
|
|
},
|
|
};
|
|
},
|
|
});
|
|
trackBrowser(browser);
|
|
const result = await run(browser, operation);
|
|
if (kind === "foreign") {
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { reason: "target_not_allowed" },
|
|
});
|
|
expect(reads).toBe(1);
|
|
} else {
|
|
expect(result.ok).toBe(true);
|
|
expect(reads).toBeGreaterThan(2);
|
|
}
|
|
},
|
|
);
|