mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
3460ceac3f
Transfer canonical leases through opening, session and storage boundaries. Keep response deadlines separate from actual completion, drain independent owners, and join late opening failure cleanup before confirming recovery. Remove error copies in favor of canonical primary/cleanup composition. Preserve exact caller reasons, completed observations and independent physical failures through final-close cancellation and retry. Closes #4869. Validation:155 tests pass across15 focused source, service, CLI subprocess and MCP files; static/docs/discovery1551 pass. Two original-revision controls fail as expected. Bounded authored-session public CLI comparison preserves equivalent Evidence and1282 reply token estimates; no real Chromium qualification or forced-termination recovery claim (#4659).
154 lines
6.4 KiB
TypeScript
154 lines
6.4 KiB
TypeScript
import type { BrowserScenario } from "../domain/browserScenario.js";
|
|
import { PLAYWRIGHT_RESPONSE_BODY_PROVENANCE_LIMITATION } from "../domain/browserNetworkEvidence.js";
|
|
import type {
|
|
BrowserScenarioCaptureData,
|
|
BrowserScenarioPartialObservation,
|
|
} from "../domain/browserScenarioCapture.js";
|
|
import type {
|
|
BrowserScenarioEvent,
|
|
BrowserScenarioStep,
|
|
} from "../domain/browserScenarioCaptureValues.js";
|
|
import type { BrowserScenarioSessionPort } from "./BrowserScenarioSessionPort.js";
|
|
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
|
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
|
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
|
import { PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY } from "./providerIdentities.js";
|
|
|
|
const OPERATION = "capture_browser_scenario" as const;
|
|
|
|
const browserScenarioLimitations = (
|
|
session: BrowserScenarioSessionPort,
|
|
scenario: BrowserScenario,
|
|
events: readonly BrowserScenarioEvent[],
|
|
): string[] => {
|
|
const capturedResponseBody =
|
|
scenario.capture.network.response_body &&
|
|
events.some(
|
|
(event) =>
|
|
event.kind === "network-content" &&
|
|
event.phase === "response" &&
|
|
event.body.state === "captured" &&
|
|
(event.body.representation === "playwright-response-body-bytes" ||
|
|
event.body.representation === "browser-decoded-response-bytes"),
|
|
);
|
|
return [
|
|
"Event sequence records provider receipt order; simultaneous browser causality is not inferred.",
|
|
"Network content is retained only when selected; Playwright response-body bytes are API results and are not guaranteed to match wire bytes.",
|
|
"Request bytes are limited to what Playwright exposes; not_exposed does not establish body absence or multipart file coverage.",
|
|
"Network content reads settle within 5 seconds each and the observer does not request bodies from unfinished responses.",
|
|
...(capturedResponseBody
|
|
? [PLAYWRIGHT_RESPONSE_BODY_PROVENANCE_LIMITATION]
|
|
: []),
|
|
"Playwright scenario events do not expose request initiator stacks; receipt order does not prove causality.",
|
|
"Storage values are hashed only after declared-secret redaction.",
|
|
...(scenario.storage.local_storage.length === 0 &&
|
|
scenario.storage.session_storage.length === 0
|
|
? []
|
|
: [
|
|
"Non-cookie storage seeds use CDP pre-script instrumentation; only pauses in the scenario's private seed script are resumed by this observer.",
|
|
]),
|
|
...(session.eventLimitations?.() ?? []),
|
|
...(session.mode === "connect"
|
|
? [
|
|
"CDP attachment cannot recover pre-attach events or guarantee launch-time context options.",
|
|
`Attached locale ${scenario.environment.locale} applies to Intl formatting only; navigator.language(s) and Accept-Language remain caller-controlled. Use launch mode for matching page and request languages.`,
|
|
"Detaching a CDP page session may clear caller-owned navigator.platform emulation. REA cannot snapshot the caller's complete user-agent/client-hint state before attachment; use launch mode to avoid mutating that external browser.",
|
|
]
|
|
: []),
|
|
];
|
|
};
|
|
|
|
/** Project one session into the common capture fields used by success and failure. */
|
|
export const browserScenarioCaptureData = (input: {
|
|
readonly session: BrowserScenarioSessionPort;
|
|
readonly scenario: BrowserScenario;
|
|
readonly startedAt: number;
|
|
readonly steps: readonly BrowserScenarioStep[];
|
|
readonly cleanup:
|
|
| "terminated-owned-process"
|
|
| "disconnected-external"
|
|
| "incomplete";
|
|
readonly limitations?: readonly string[];
|
|
}): BrowserScenarioCaptureData => {
|
|
const events = input.session.events();
|
|
return {
|
|
browser: {
|
|
mode: input.session.mode,
|
|
process_ownership: input.session.processOwnership,
|
|
cleanup: input.cleanup,
|
|
product: input.session.product,
|
|
version: input.session.version,
|
|
},
|
|
scenario: {
|
|
start_origin: new URL(input.scenario.start_url.url).origin,
|
|
action_count: input.scenario.actions.length,
|
|
secret_references: input.scenario.secrets
|
|
.map(({ secret_id: id }) => id)
|
|
.sort(),
|
|
network_content: input.scenario.capture.network,
|
|
event_families: [...new Set(input.scenario.capture.events)].sort(),
|
|
limits: input.scenario.capture.limits,
|
|
},
|
|
duration_ms: Math.max(0, Math.round(performance.now() - input.startedAt)),
|
|
steps: [...input.steps],
|
|
events,
|
|
limitations: [
|
|
...browserScenarioLimitations(
|
|
input.session,
|
|
input.scenario,
|
|
events.items,
|
|
),
|
|
...(input.limitations ?? []),
|
|
],
|
|
};
|
|
};
|
|
|
|
/** Describe the cleanup failure without discarding its resource evidence. */
|
|
export const browserScenarioCleanupObservation = (
|
|
cause: unknown,
|
|
): { readonly reason: string; readonly resources: readonly string[] } =>
|
|
cause instanceof AnalysisError && cause.cleanup !== undefined
|
|
? cause.cleanup
|
|
: {
|
|
reason: cause instanceof Error ? cause.message : String(cause),
|
|
resources:
|
|
cause instanceof AnalysisError && cause.cleanupResources.length > 0
|
|
? cause.cleanupResources
|
|
: ["browser_transport"],
|
|
};
|
|
|
|
/** Retain operation diagnostics and capture data after cleanup settles. */
|
|
export const browserScenarioOperationFailure = (
|
|
cause: unknown,
|
|
partialObservation: BrowserScenarioPartialObservation,
|
|
cleanupFailure?: { readonly cause: unknown },
|
|
): AnalysisError => {
|
|
const primary =
|
|
cause instanceof AnalysisError
|
|
? cause
|
|
: new ProviderAdapterError(
|
|
PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY.id,
|
|
OPERATION,
|
|
{ cause },
|
|
);
|
|
primary.retainPartialObservation(partialObservation);
|
|
if (cleanupFailure === undefined) return primary;
|
|
const observation = browserScenarioCleanupObservation(cleanupFailure.cause);
|
|
const cleanup =
|
|
cleanupFailure.cause instanceof AnalysisError &&
|
|
cleanupFailure.cause.cleanupIncomplete
|
|
? cleanupFailure.cause
|
|
: new ProviderCleanupError(
|
|
PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY.id,
|
|
observation.resources,
|
|
{ reason: observation.reason },
|
|
{ operation: "close_browser_scenario", cause: cleanupFailure.cause },
|
|
);
|
|
return analysisErrorWithCleanupFailure(
|
|
primary,
|
|
cleanup,
|
|
OPERATION,
|
|
).bindCleanupDependencies([primary, cleanup]);
|
|
};
|