Files
rea/src/application/WebNetworkCaptureService.test.ts
T
morluto 0155b89953 fix(browser): name web-bundle budgets and recover web deadlines (#6036)
* fix(browser): name web-bundle budgets and recover web deadlines

Name the existing heap, per-script, and total web-bundle phases, and reject a per-script deadline above the total. Promote capture, CDP command, source-map decode, and source-map fetch deadlines at today's defaults. Name DevTools timeoutMs, and state that discovery, cleanup grace, byte budgets, and Electron hook retention have no runtime override.

Refs #5910

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(browser): apply selected web deadlines to the owning phase

Keep HTTP and browser-socket discovery on the fixed discovery deadline so a
short command_timeout_ms starts at the handshake. Apply scenario
command_timeout_ms to launch startup and the primary connect session, and
keep service-worker restoration on the fixed 5000 ms grace. Omit
command_timeout_ms from recovery text when the operation does not accept it.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(browser): retain scenario commands that finish after their deadline

A timed-out scenario command stays owned so cleanup can close a late
browser or context and restore a mutation that lands after the deadline.
Child storage replies use that deadline, scenario recovery names
command_timeout_ms in the capture JSON, and the session test connection
implements beginCleanupGrace.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(browser): keep child command timeouts as the public failure

A nested command such as Page.enable that is delivered but never answered
was stored as protocol_error, hiding the command name, selected deadline,
and recovery. Store the typed timeout and assert that public error.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(browser): keep runtime cleanup on the fixed grace

Switch runtime restoration to the cleanup grace before the first send so a
short command_timeout_ms cannot fail an otherwise successful observation.
Stop advertising command_timeout_ms on list_browser_targets, which never
bounds a post-discovery command.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* test(browser): prove a short command deadline cannot fail runtime cleanup

Drive restoration through the runtime fixture. A cleanup command that
outlives command_timeout_ms still succeeds, and a stalled restoration
names the fixed 5000 ms grace.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(browser): name the effective source-map decode deadline

codecBudgetLimitation reported a fixed 20-second deadline after a
caller selected decode_timeout_ms. Result and Evidence limitations
now name that effective millisecond deadline.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(browser): separate handshake and target-attach deadlines

A browser-level Target.attachToTarget no longer shares the WebSocket
handshake budget. Blocked service-worker attachment keeps discovery on
its fixed deadline and applies command_timeout_ms to the later handshake
and attach.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(browser): name the web-bundle resource each runtime exhausts

Bun ignores the V8 old-space flag. A Windows Job reports the process
commit limit, and other Bun hosts no longer suggest raising max_heap_mb.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(browser): cite the browser-socket discovery message budget

WebSocket discovery enforces the 64 MiB protocol budget. Timeout
recovery no longer advertises the 8 MiB HTTP document budget.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(browser): bound the blocked service-worker check before discovery

The first registration check has its own fixed 10s deadline. Discovery,
handshake, and Target.attachToTarget stay on their separate deadlines.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(browser): name the operand that stopped web-bundle admission

Host heap pressure no longer claims the fixed result budget is exhausted.
A Windows Bun allocation before the commit limit still names that limit.

Co-authored-by: morluto <morluto@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-22 05:47:38 +08:00

596 lines
20 KiB
TypeScript

import { expect, it } from "vitest";
import { WebNetworkCaptureService } from "./WebNetworkCaptureService.js";
import { historicalHar } from "../../tests/fixtures/historicalHar.js";
import { err, ok } from "../domain/result.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { parseEvidence } from "../domain/evidence.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import {
webNetworkCaptureSchema,
type WebNetworkCapture,
} from "../domain/webNetworkCapture.js";
import {
AnalysisCleanupLease,
AnalysisCleanupLeaseGroup,
} from "../artifacts/AnalysisCleanupLease.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
it.each(["REDACTED", "[", "]", "/capture.har"])(
"excludes sensitive path text even when a replacement marker would collide: %s",
async (literal) => {
const report = fixture();
report.artifact.path = `/capture.har/${literal}`;
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(report)),
}).inspect({
capture_path: report.artifact.path,
format: "har",
sensitive_values: [literal],
});
if (!result.ok) throw result.error;
expect(result.value.subject?.local_path).not.toContain(literal);
expect(result.value.parameters.capture_path).not.toContain(literal);
expect(
result.value.locations.every(
(location) =>
location.kind !== "artifact-path" || !location.path.includes(literal),
),
).toBe(true);
expect(result.value.subject?.digest.sha256).toBe(report.artifact.sha256);
},
);
it("records an explicit capture deadline and omits the default", async () => {
const seen: Array<number | undefined> = [];
const service = new WebNetworkCaptureService({
inspect: (input) => {
seen.push(input.capture_timeout_ms);
return Promise.resolve(ok(fixture()));
},
});
const explicit = await service.inspect({
capture_path: "/capture.har",
format: "har",
capture_timeout_ms: 250,
});
if (!explicit.ok) throw explicit.error;
expect(explicit.value.parameters.capture_timeout_ms).toBe(250);
const omitted = await service.inspect({
capture_path: "/capture.har",
format: "har",
});
if (!omitted.ok) throw omitted.error;
expect(omitted.value.parameters).not.toHaveProperty("capture_timeout_ms");
expect(seen).toEqual([250, undefined]);
});
it("keeps observed artifact identity when the entire sensitive path is excluded", async () => {
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(fixture())),
}).inspect({
capture_path: "/capture.har",
format: "har",
sensitive_values: ["/", "[", "REDACTED"],
});
if (!result.ok) throw result.error;
expect(result.value.subject).toMatchObject({
local_path: "",
digest: { sha256: "a".repeat(64) },
});
expect(result.value.locations).toEqual([]);
});
const fixture = (): WebNetworkCapture => ({
decoder: { id: "test-historical-port", name: "test port", version: "1" },
container: {
reported: null,
numeric_literals: [],
redactions: [],
records_pointer: "/log/entries",
},
total_records: 1,
records: [
{
ordinal: 0,
location: { kind: "json-pointer", pointer: "/log/entries/0" },
reported: historicalHar().log.entries[0] ?? null,
numeric_literals: [],
binary_fields: [],
redactions: [],
limitations: [],
},
],
artifact: { path: "/capture.har", sha256: "a".repeat(64), bytes: 10 },
format: "har",
runtime_attribution: "unknown",
limitations: ["historical fixture"],
});
it.each(["a".repeat(64), "aaaa"])(
"marks an explicitly sensitive artifact digest unknown without inventing another identity: %s",
async (literal) => {
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(fixture())),
}).inspect({
capture_path: "/capture.har",
format: "har",
sensitive_values: [literal],
});
if (!result.ok) throw result.error;
expect(result.value.subject).toBeNull();
expect(result.value.normalized_result).toMatchObject({
artifact: { path: "/capture.har", sha256: null, bytes: 10 },
});
expect(result.value.parameters.capture_path).toBe("/capture.har");
expect(result.value.locations).toEqual([
{ kind: "artifact-path", path: "/capture.har" },
]);
expect(parseEvidence(result.value)).toEqual(result.value);
},
);
it("projects the unknown-subject explanation before generating its Evidence ID", async () => {
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(fixture())),
}).inspect({
capture_path: "/capture.har",
format: "har",
sensitive_values: ["a"],
});
if (!result.ok) throw result.error;
expect(result.value.subject).toBeNull();
expect(result.value.limitations.every((text) => !text.includes("a"))).toBe(
true,
);
expect(parseEvidence(result.value)).toEqual(result.value);
});
it("rejects an adapter omitting its original observed digest before caller projection", async () => {
const report = fixture();
report.artifact.sha256 = null;
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(report)),
}).inspect({ capture_path: report.artifact.path, format: "har" });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError");
});
it("retains valid completed capture Evidence on late cancellation and preserves invalid output", async () => {
const controller = new AbortController();
const report = fixture();
const late = await new WebNetworkCaptureService({
inspect: () => {
controller.abort();
return Promise.resolve(ok(report));
},
}).inspect(
{ capture_path: report.artifact.path, format: "har" },
{ signal: controller.signal },
);
expect(late.ok).toBe(false);
if (!late.ok) {
expect(late.error).toBeInstanceOf(AnalysisCancelledError);
expect(parseEvidence(late.error.partialObservation)).toMatchObject({
operation: "inspect_web_network_capture",
normalized_result: expect.objectContaining({ total_records: 1 }),
});
}
const malformed = fixture();
malformed.artifact.path = "/other.har";
const outputController = new AbortController();
const invalid = await new WebNetworkCaptureService({
inspect: () => {
outputController.abort();
return Promise.resolve(ok(malformed));
},
}).inspect(
{ capture_path: "/capture.har", format: "har" },
{ signal: outputController.signal },
);
expect(invalid.ok).toBe(false);
if (!invalid.ok) expect(invalid.error).toBeInstanceOf(AnalysisOutputError);
});
it("projects completed raw capture facts on failure through the caller's selection and sensitivity rules", async () => {
const report = fixture();
report.artifact.path = "/private-marker/capture.har";
report.limitations = ["private-marker producer limitation"];
const reason = { request: "cancel after decoder completion" };
const error = new AnalysisCancelledError("inspect_web_network_capture", {
cause: reason,
partialObservation: report,
});
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve({ ok: false, error }),
}).inspect({
capture_path: report.artifact.path,
format: "har",
record_ordinals: [0],
sensitive_values: ["private-marker"],
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Cancellation must remain the primary result");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
const partial = parseEvidence(result.error.partialObservation);
expect(partial).toMatchObject({
operation: "inspect_web_network_capture",
parameters: { record_ordinals: [0] },
subject: { local_path: "" },
normalized_result: {
artifact: { path: "", sha256: "a".repeat(64) },
records: [{ ordinal: 0 }],
},
});
expect(JSON.stringify(partial)).not.toContain("private-marker");
});
it("does not expose a malformed capture-shaped partial observation", async () => {
const malformed = fixture();
malformed.artifact.path = "/private-marker";
malformed.artifact.sha256 = "invalid";
const error = new AnalysisCancelledError("inspect_web_network_capture", {
partialObservation: malformed,
});
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve({ ok: false, error }),
}).inspect({
capture_path: "/capture.har",
format: "har",
sensitive_values: ["private-marker"],
});
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error.partialObservation).toBeUndefined();
});
it("keeps a redacted cleanup error linked to its live owner through service close", async () => {
const report = fixture();
const cleanupPath = "/tmp/private-marker-capture-root";
const lease = new AnalysisCleanupLease(async () => {}, [cleanupPath]);
const original = new ProviderCleanupError(
"test-capture",
[cleanupPath],
{ reason: "private-marker cleanup remains pending" },
{
operation: "inspect_web_network_capture",
partialObservation: report,
},
);
new AnalysisCleanupLeaseGroup(original, [lease]);
const service = new WebNetworkCaptureService({
inspect: () => Promise.resolve(err(original)),
close: () => lease.release(),
});
const result = await service.inspect({
capture_path: report.artifact.path,
format: "har",
sensitive_values: ["private-marker"],
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Retained cleanup must remain a failure");
const returnedError = result.error;
expect(returnedError.cleanupIncomplete).toBe(true);
expect(returnedError.cleanupResources).toEqual([
"/tmp/[REDACTED]-capture-root",
]);
expect(JSON.stringify(projectAnalysisError(returnedError))).not.toContain(
"private-marker",
);
expect(parseEvidence(returnedError.partialObservation)).toMatchObject({
operation: "inspect_web_network_capture",
});
await service.close();
expect(returnedError.cleanupIncomplete).toBe(false);
expect(returnedError.cleanupResources).toEqual([]);
});
it.each([
["producer"],
["artifact"],
["coordinates"],
["producer", "REDACTED", "…"],
["producer", "[", "]"],
])(
"excludes declared literals from all authored limitations: %j",
async (...literals) => {
const report = fixture();
report.artifact.path = "/artifact/capture.har";
report.limitations = [
"Historical producer artifact coordinates are retained.",
];
const first = report.records[0];
if (first === undefined) throw new Error("fixture missing record");
first.limitations = ["This is producer-decoded artifact evidence."];
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(report)),
}).inspect({
capture_path: report.artifact.path,
format: "har",
sensitive_values: literals,
});
if (!result.ok) throw result.error;
const projected = webNetworkCaptureSchema.parse(
result.value.normalized_result,
);
const texts = [
result.value.limitations,
projected.limitations,
...projected.records.map((record) => record.limitations),
].flat();
for (const literal of literals)
expect(texts.every((text) => !text.includes(literal))).toBe(true);
expect(result.value.subject?.digest.sha256).toBe(report.artifact.sha256);
expect(projected.decoder).toEqual(report.decoder);
expect(projected.records[0]?.reported).toEqual(first.reported);
},
);
it("keeps explicitly sensitive values out of Evidence parameters and preserves historical authority", async () => {
const service = new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(fixture())),
});
const result = await service.inspect({
capture_path: "/capture.har",
format: "har",
sensitive_values: ["explicit-private-value"],
});
if (!result.ok) throw result.error;
expect(result.value.parameters).toMatchObject({
sensitive_value_count: 1,
record_ordinals: [0],
});
expect(JSON.stringify(result.value)).not.toContain("explicit-private-value");
expect(result.value.authority).toBe("historical-reference");
expect(result.value.provider).toEqual(fixture().decoder);
});
it.each(["har", "ha"])(
"omits a format parameter containing a declared sensitive literal: %s",
async (literal) => {
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(fixture())),
}).inspect({
capture_path: "/capture.har",
format: "har",
sensitive_values: [literal],
});
if (!result.ok) throw result.error;
expect(result.value.parameters).not.toHaveProperty("format");
expect(JSON.stringify(result.value.parameters)).not.toContain(literal);
expect(result.value.normalized_result).toMatchObject({ format: "har" });
},
);
it.each([
{ record_ordinals: [0, 0] },
{ record_ordinals: [1] },
{ capture_path: "relative.har" },
])("rejects invalid caller selection %j", async (extra) => {
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(fixture())),
}).inspect({ capture_path: "/capture.har", format: "har", ...extra });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error._tag).toBe("AnalysisInputError");
});
it("rejects a port changing the artifact identity or producer ordinal sequence", async () => {
const report = fixture();
report.artifact.path = "/different.har";
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(report)),
}).inspect({ capture_path: "/capture.har", format: "har" });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError");
});
it("applies an explicit sensitive path literal after verifying the selected artifact", async () => {
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(fixture())),
}).inspect({
capture_path: "/capture.har",
format: "har",
sensitive_values: ["capture.har"],
});
if (!result.ok) throw result.error;
expect(JSON.stringify(result.value)).not.toContain("capture.har");
expect(result.value.parameters.capture_path).toBe("");
});
it.each(["log", "entries"])(
"excludes a sensitive producer coordinate without inventing a replacement: %s",
async (literal) => {
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(fixture())),
}).inspect({
capture_path: "/capture.har",
format: "har",
sensitive_values: [literal],
});
if (!result.ok) throw result.error;
expect(result.value.normalized_result).toMatchObject({
container: { records_pointer: null },
records: [
{
ordinal: 0,
location: { kind: "unknown", reason: "explicit-sensitive-value" },
},
],
});
expect(JSON.stringify(result.value)).not.toContain(literal);
},
);
it("excludes an escaped sensitive sidecar coordinate while preserving its reported field", async () => {
const report = fixture();
report.container.reported = { "field/name": 42 };
report.container.numeric_literals = [
{ pointer: "/field~1name", producer_type: "json-number", literal: "42" },
];
report.container.redactions = [
{ pointer: "/field~1name", reason: "explicit-sensitive-value" },
];
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(report)),
}).inspect({
capture_path: "/capture.har",
format: "har",
sensitive_values: ["~1"],
});
if (!result.ok) throw result.error;
expect(result.value.normalized_result).toMatchObject({
container: {
reported: { "field/name": 42 },
numeric_literals: [],
redactions: [],
},
});
expect(JSON.stringify(result.value)).not.toContain("~1");
});
it.each([
{ literal: "12345", input: { record_ordinals: [12345] } },
{ literal: "Each", input: { record_ordinals: [0, 0] } },
{ literal: "absolute", input: { capture_path: "relative.har" } },
{ literal: "secret", input: { mysecret: true } },
])(
"excludes declared text from application-generated input errors: $literal",
async ({ literal, input }) => {
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(fixture())),
}).inspect({
capture_path: "/capture.har",
format: "har",
sensitive_values: [literal],
...input,
});
if (result.ok) throw new Error("Expected invalid selection");
expect(result.error._tag).toBe("AnalysisInputError");
expect(JSON.stringify(projectAnalysisError(result.error))).not.toContain(
literal,
);
},
);
it.each(["12345", "67890", "234", "789"])(
"excludes sensitive native byte-range coordinates: %s",
async (literal) => {
const report = fixture();
report.format = "mitmproxy";
report.artifact.path = "/capture.mitm";
const record = report.records[0];
if (record === undefined) throw new Error("record missing");
record.location = { kind: "byte-range", offset: 12345, bytes: 67890 };
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(report)),
}).inspect({
capture_path: report.artifact.path,
format: "mitmproxy",
sensitive_values: [literal],
});
if (!result.ok) throw result.error;
expect(result.value.normalized_result).toMatchObject({
records: [
{ location: { kind: "unknown", reason: "explicit-sensitive-value" } },
],
});
expect(result.value.raw_result).toBeNull();
expect(parseEvidence(result.value)).toEqual(result.value);
},
);
it("preserves a pre-abort reason without entering the capture adapter", async () => {
const reason = new Error("stop historical capture inspection");
const controller = new AbortController();
controller.abort(reason);
let providerCalls = 0;
const service = new WebNetworkCaptureService({
inspect: async () => {
providerCalls += 1;
return ok(fixture());
},
});
const result = await service.inspect(
{
capture_path: "/capture.har",
format: "har",
sensitive_values: [],
},
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
}
expect(providerCalls).toBe(0);
});
it("reports malformed retained facts while keeping their cleanup owner retryable", async () => {
const report = fixture();
report.artifact.sha256 = "invalid";
const lease = new AnalysisCleanupLease(async () => {}, [
"/tmp/private-marker-root",
]);
const cleanup = new ProviderCleanupError(
"test-historical-port",
["/tmp/private-marker-root"],
{ reason: "close denied" },
{ operation: "inspect_web_network_capture", partialObservation: report },
);
new AnalysisCleanupLeaseGroup(cleanup, [lease]);
const service = new WebNetworkCaptureService({
inspect: async () => err(cleanup),
close: async () => {
await lease.release();
},
});
const result = await service.inspect({
capture_path: "/capture.har",
format: "har",
sensitive_values: ["private-marker"],
});
if (result.ok) throw new Error("Expected malformed retained evidence");
expect(result.error.cleanupIncomplete).toBe(true);
expect(result.error.cleanupPrimary).toBeInstanceOf(AnalysisOutputError);
expect(result.error.partialObservation).toBeUndefined();
expect(JSON.stringify(projectAnalysisError(result.error))).not.toContain(
"private-marker",
);
await service.close();
expect(result.error.cleanupIncomplete).toBe(false);
expect(projectAnalysisError(result.error).code).toBe("unreadable_output");
});
it("rejects relative descriptor paths before starting a capture decoder", async () => {
const service = new WebNetworkCaptureService({
inspect: () => {
throw new Error("Invalid input must fail before provider admission");
},
});
const result = await service.inspect({
capture_path: "/capture.har",
format: "har",
record_ordinals: [0],
protobuf: {
schema: {
descriptor_set_path: "relative.pb",
message_type: "capture.Message",
},
},
});
if (result.ok) throw new Error("Expected typed invalid input");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "invalid_request",
details: {
issues: [{ path: ["protobuf", "schema", "descriptor_set_path"] }],
},
});
});