Files
reverse-engineering-anything/src/domain/browserScenarioCapture.test.ts
T

417 lines
11 KiB
TypeScript

import { createHash } from "node:crypto";
import { expect, it } from "vitest";
import { sanitizeBrowserUrl } from "./browserObservation.js";
import {
browserScenarioCompletenessSchema,
browserScenarioEventSchema,
browserStepArtifactsSchema,
} from "./browserScenarioCaptureValues.js";
import { browserScenarioCaptureSchema } from "./browserScenarioCapture.js";
import { DEFAULT_BROWSER_SCENARIO_CAPTURE_LIMITS } from "./browserScenarioCaptureLimits.js";
const networkEvent = {
sequence: 1,
step_index: 0,
transaction_id: "request-1",
redirected_from_transaction_id: null,
redirected_from_transaction_state: "root",
method: "GET",
url: sanitizeBrowserUrl("https://example.test/resource"),
resource_type: "document",
header_names: [],
} as const;
it("requires transaction IDs and explicit unknown redirect predecessors", () => {
const request = {
...networkEvent,
kind: "request",
status: null,
failure: null,
} as const;
expect(
browserScenarioEventSchema.safeParse({
...request,
transaction_id: undefined,
}).success,
).toBe(false);
expect(
browserScenarioEventSchema.safeParse({
...request,
redirected_from_transaction_id: undefined,
}).success,
).toBe(false);
expect(
browserScenarioEventSchema.safeParse({
...request,
redirected_from_transaction_id: "request-0",
redirected_from_transaction_state: "unknown",
}).success,
).toBe(false);
});
it("parses each network outcome without contradictory status or failure data", () => {
expect(
browserScenarioEventSchema.safeParse({
...networkEvent,
kind: "response",
status: null,
failure: null,
}).success,
).toBe(false);
expect(
browserScenarioEventSchema.safeParse({
...networkEvent,
kind: "request-failed",
status: 500,
failure: "connection reset",
}).success,
).toBe(false);
});
it("accepts three-digit HTTP response status codes including statuses above 599", () => {
expect(
browserScenarioEventSchema.safeParse({
...networkEvent,
kind: "response",
status: 999,
failure: null,
}).success,
).toBe(true);
expect(
browserScenarioEventSchema.safeParse({
...networkEvent,
kind: "response",
status: 1000,
failure: null,
}).success,
).toBe(false);
expect(
browserScenarioEventSchema.safeParse({
...networkEvent,
kind: "response",
status: 99,
failure: null,
}).success,
).toBe(false);
});
it("keeps WebSocket payload representation tied to its payload type", () => {
const frame = {
sequence: 1,
step_index: 0,
kind: "websocket-frame-received",
url: sanitizeBrowserUrl("https://example.test/socket"),
payload_bytes: 3,
truncated: false,
} as const;
expect(
browserScenarioEventSchema.safeParse({
...frame,
payload_type: "binary",
payload_text: "abc",
}).success,
).toBe(false);
expect(
browserScenarioEventSchema.safeParse({
...frame,
payload_type: "text",
payload_text: "abc",
}).success,
).toBe(true);
});
it("does not attach a content digest to redacted storage secrets", () => {
expect(
browserStepArtifactsSchema.safeParse({
screenshot: { state: "not_requested" },
dom: { state: "not_requested" },
accessibility: { state: "not_requested" },
url: { state: "not_requested" },
history: { state: "not_requested" },
storage: {
state: "captured",
value: {
cookies: [],
local_storage: [
{
name: "session",
value_state: "redacted-secret",
value_sha256: "a".repeat(64),
},
],
session_storage: [],
},
},
}).success,
).toBe(false);
});
it("accepts complete storage, history, and text artifacts beyond former caps", () => {
const text = "x".repeat(1_048_577);
const fingerprint = {
name: "entry",
value_state: "hashed",
value_sha256: "a".repeat(64),
} as const;
const parsed = browserStepArtifactsSchema.safeParse({
screenshot: { state: "not_requested" },
dom: {
state: "captured",
value: {
sha256: createHash("sha256").update(text).digest("hex"),
bytes: Buffer.byteLength(text),
text,
},
},
accessibility: { state: "not_requested" },
url: { state: "not_requested" },
history: {
state: "captured",
value: {
length: 257,
current_url: sanitizeBrowserUrl("https://example.test/"),
navigation_entries: Array.from({ length: 257 }, () => ({
type: "navigate",
name: sanitizeBrowserUrl("https://example.test/"),
})),
},
},
storage: {
state: "captured",
value: {
cookies: Array.from({ length: 513 }, () => ({
name: fingerprint.name,
domain: "example.test",
path: "/",
secure: true,
http_only: false,
same_site: "Lax",
value_state: fingerprint.value_state,
value_sha256: fingerprint.value_sha256,
})),
local_storage: Array.from({ length: 513 }, () => fingerprint),
session_storage: Array.from({ length: 513 }, () => fingerprint),
},
},
});
expect(parsed.success).toBe(true);
});
it("accepts complete scenario output beyond former string caps", () => {
const longText = "detail".repeat(1_000);
const step = (index: number) => ({
step_index: index,
step_id: longText,
action: longText,
elapsed_ms: 0,
before_url: sanitizeBrowserUrl("https://example.test/"),
after_url: sanitizeBrowserUrl("https://example.test/"),
event_sequence_start: 1,
event_sequence_end: 0,
artifacts: {
screenshot: { state: "not_requested" },
dom: { state: "not_requested" },
accessibility: { state: "not_requested" },
url: { state: "not_requested" },
history: { state: "not_requested" },
storage: { state: "not_requested" },
},
completeness: {
status: "complete",
equality_eligible: true,
missing_sections: [],
truncated_sections: [],
},
status: "completed",
error: null,
});
const parsed = browserScenarioCaptureSchema.parse({
browser: {
mode: "launch",
process_ownership: "provider-owned",
cleanup: "terminated-owned-process",
product: longText,
version: longText,
},
scenario: {
start_origin: longText,
action_count: 1,
secret_references: [longText],
network_content: {
request_body: false,
response_body: false,
header_values: false,
},
event_families: [],
limits: DEFAULT_BROWSER_SCENARIO_CAPTURE_LIMITS,
},
duration_ms: 0,
steps: [step(0), step(1)],
events: {
retained: 0,
dropped: 0,
items: [],
omissions: {
by_kind: [],
network_transactions: [],
network_transaction_range: null,
},
},
completeness: {
status: "complete",
equality_eligible: true,
missing_sections: [],
truncated_sections: [],
},
limitations: Array.from({ length: 65 }, () => longText),
});
expect(parsed.steps[0]?.step_id).toBe(longText);
expect(parsed.limitations).toHaveLength(65);
});
it("validates reported event limits and omission accounting", () => {
const outputStep = (index: number) => ({
step_index: index,
step_id: `step-${index}`,
action: "wait_for_timeout",
elapsed_ms: 0,
before_url: sanitizeBrowserUrl("http://127.0.0.1/"),
after_url: sanitizeBrowserUrl("http://127.0.0.1/"),
event_sequence_start: 1,
event_sequence_end: 0,
artifacts: {
screenshot: { state: "not_requested" },
dom: { state: "not_requested" },
accessibility: { state: "not_requested" },
url: { state: "not_requested" },
history: { state: "not_requested" },
storage: { state: "not_requested" },
},
completeness: {
status: "complete",
equality_eligible: true,
missing_sections: [],
truncated_sections: [],
},
status: "completed",
error: null,
});
const capture = browserScenarioCaptureSchema.safeParse({
browser: {
mode: "launch",
process_ownership: "provider-owned",
cleanup: "terminated-owned-process",
product: "Chromium",
version: "fixture",
},
scenario: {
start_origin: "http://127.0.0.1",
action_count: 1,
secret_references: [],
network_content: {
request_body: false,
response_body: true,
header_values: false,
},
event_families: ["network"],
limits: {
...DEFAULT_BROWSER_SCENARIO_CAPTURE_LIMITS,
max_total_event_allocation_bytes: 32 * 1024 * 1024,
},
},
duration_ms: 1,
steps: [outputStep(0), outputStep(1)],
events: {
retained: 0,
dropped: 2,
items: [],
omissions: {
by_kind: [
{
kind: "network-content",
count: 1,
reasons: ["max-total-event-allocation-bytes"],
},
{
kind: "request",
count: 1,
reasons: ["max-network-transactions"],
},
],
network_transactions: [
{
transaction_id: "request-1",
omitted_kinds: ["network-content"],
reasons: ["max-total-event-allocation-bytes"],
},
],
network_transaction_range: {
first_transaction_id: "request-2",
last_transaction_id: "request-2",
count: 1,
reason: "max-network-transactions",
},
},
},
completeness: {
status: "incomplete",
equality_eligible: false,
missing_sections: ["events"],
truncated_sections: [],
},
limitations: ["Event coverage is incomplete."],
});
if (!capture.success) throw new Error("Expected valid omission accounting");
expect(capture.data.scenario.limits.max_total_event_allocation_bytes).toBe(
32 * 1024 * 1024,
);
expect(
browserScenarioCaptureSchema.safeParse({
...capture.data,
events: {
...capture.data.events,
dropped: 3,
},
}).success,
).toBe(false);
for (const limits of [
{},
{ max_events: DEFAULT_BROWSER_SCENARIO_CAPTURE_LIMITS.max_events },
])
expect(
browserScenarioCaptureSchema.safeParse({
...capture.data,
scenario: { ...capture.data.scenario, limits },
}).success,
).toBe(false);
expect(
browserScenarioCaptureSchema.safeParse({
...capture.data,
scenario: { ...capture.data.scenario, limits: undefined },
}).success,
).toBe(false);
});
it("derives equality eligibility from exact completeness state", () => {
expect(
browserScenarioCompletenessSchema.safeParse({
status: "complete",
equality_eligible: false,
missing_sections: [],
truncated_sections: [],
}).success,
).toBe(false);
expect(
browserScenarioCompletenessSchema.safeParse({
status: "incomplete",
equality_eligible: false,
missing_sections: ["dom"],
truncated_sections: ["events"],
}).success,
).toBe(false);
});