mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
325 lines
10 KiB
TypeScript
325 lines
10 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { WebRuntimeService } from "./WebRuntimeService.js";
|
|
import {
|
|
recordingWebRuntimePort,
|
|
webExecutionFixture,
|
|
webListenersFixture,
|
|
} from "./WebRuntimeService.fixture.js";
|
|
import { err, ok } from "../domain/result.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisOutputError,
|
|
AnalysisTimeoutError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
|
import {
|
|
expectDroppedPartial,
|
|
expectKeptPartial,
|
|
expectOwnerError,
|
|
} from "./ownerObservationExpectation.fixture.js";
|
|
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import { parseEvidence } from "../domain/evidence.js";
|
|
|
|
it("rejects malformed input and prior cancellation without calling a provider", async () => {
|
|
const provider = recordingWebRuntimePort();
|
|
const service = new WebRuntimeService({
|
|
identity: () => provider.identity(),
|
|
observeExecution: () => {
|
|
throw new Error("Provider must not be called");
|
|
},
|
|
inspectEventListeners: () => {
|
|
throw new Error("Provider must not be called");
|
|
},
|
|
});
|
|
const malformed = await service.observe({
|
|
cdp_endpoint: "https://remote.example",
|
|
target_id: "page",
|
|
});
|
|
if (malformed.ok)
|
|
throw new Error("Invalid endpoint must fail before execution");
|
|
expect(malformed.error._tag).toBe("AnalysisInputError");
|
|
const reason = { requested: "stop before provider admission" };
|
|
const cancelled = await service.inspect(
|
|
{},
|
|
{ signal: AbortSignal.abort(reason) },
|
|
);
|
|
if (cancelled.ok)
|
|
throw new Error("Prior cancellation must fail before inspection");
|
|
expect(cancelled.error._tag).toBe("AnalysisCancelledError");
|
|
expect(cancelled.error.cause).toBe(reason);
|
|
});
|
|
|
|
it("preserves actual provider failures and rejects changed target/selector identities", async () => {
|
|
const provider = recordingWebRuntimePort();
|
|
const args = {
|
|
cdp_endpoint: "http://127.0.0.1:9222",
|
|
target_id: "selected-page",
|
|
selector: "#selected",
|
|
};
|
|
const captured = webListenersFixture();
|
|
const timeout = new AnalysisTimeoutError("observe_web_execution", 20_000);
|
|
const service = new WebRuntimeService({
|
|
identity: () => provider.identity(),
|
|
observeExecution: async () => err(timeout),
|
|
inspectEventListeners: async () =>
|
|
ok({
|
|
...captured,
|
|
selected_node: { ...captured.selected_node, selector: "#different" },
|
|
}),
|
|
});
|
|
const failed = await service.observe({
|
|
cdp_endpoint: args.cdp_endpoint,
|
|
target_id: args.target_id,
|
|
observation_ms: 5,
|
|
});
|
|
expect(failed).toEqual(err(timeout));
|
|
const changed = await service.inspect(args);
|
|
if (changed.ok) throw new Error("Changed selection must be rejected");
|
|
expect(changed.error._tag).toBe("AnalysisOutputError");
|
|
});
|
|
|
|
it("converts raw execution retained by provider cleanup failure into partial Evidence", async () => {
|
|
const args = {
|
|
cdp_endpoint: "http://127.0.0.1:9222",
|
|
target_id: "selected-page",
|
|
observation_ms: 5,
|
|
};
|
|
const execution = webExecutionFixture(args.target_id, args.observation_ms);
|
|
const failure = new ProviderCleanupError(
|
|
"rea-cdp-browser",
|
|
["selected-page"],
|
|
{ reason: "target detach failed" },
|
|
{ operation: "observe_web_execution", partialObservation: execution },
|
|
);
|
|
const result = await new WebRuntimeService({
|
|
identity: runtimeIdentity,
|
|
observeExecution: async () => err(failure),
|
|
inspectEventListeners: async () => err(failure),
|
|
}).observe(args);
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(result.error).toBe(failure);
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
|
|
operation: "observe_web_execution",
|
|
normalized_result: execution,
|
|
});
|
|
}
|
|
});
|
|
|
|
it("retains completed execution Evidence on late abort and keeps invalid output primary", async () => {
|
|
const input = {
|
|
cdp_endpoint: "http://127.0.0.1:9222",
|
|
target_id: "selected-page",
|
|
observation_ms: 5,
|
|
};
|
|
const unused = new AnalysisTimeoutError(
|
|
"inspect_web_event_listeners",
|
|
20_000,
|
|
);
|
|
const controller = new AbortController();
|
|
const cancellationReason = new Error(
|
|
"caller cancelled the runtime observation",
|
|
);
|
|
const completed = await new WebRuntimeService({
|
|
identity: () => ({
|
|
id: "web-runtime",
|
|
name: "Runtime fixture",
|
|
version: "1",
|
|
}),
|
|
observeExecution: async () => {
|
|
controller.abort(cancellationReason);
|
|
return ok(webExecutionFixture(input.target_id, input.observation_ms));
|
|
},
|
|
inspectEventListeners: async () => err(unused),
|
|
}).observe(input, { signal: controller.signal });
|
|
if (completed.ok) throw new Error("Expected late cancellation");
|
|
expect(completed.error._tag).toBe("AnalysisCancelledError");
|
|
expect(completed.error.cause).toBe(cancellationReason);
|
|
expect(parseEvidence(completed.error.partialObservation)).toMatchObject({
|
|
operation: "observe_web_execution",
|
|
normalized_result: webExecutionFixture(
|
|
input.target_id,
|
|
input.observation_ms,
|
|
),
|
|
});
|
|
|
|
const late = new AbortController();
|
|
const invalid = await new WebRuntimeService({
|
|
identity: () => ({
|
|
id: "web-runtime",
|
|
name: "Runtime fixture",
|
|
version: "1",
|
|
}),
|
|
observeExecution: async () => {
|
|
late.abort();
|
|
return ok(webExecutionFixture(input.target_id, input.observation_ms));
|
|
},
|
|
inspectEventListeners: async () => err(unused),
|
|
}).observe(
|
|
{ ...input, allowed_origins: ["https://allowed.example"] },
|
|
{ signal: late.signal },
|
|
);
|
|
expect(invalid).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisOutputError" },
|
|
});
|
|
if (!invalid.ok) expect(invalid.error).toBeInstanceOf(AnalysisOutputError);
|
|
});
|
|
|
|
const runtimeIdentity = () => ({
|
|
id: "web-runtime",
|
|
name: "Runtime fixture",
|
|
version: "1",
|
|
});
|
|
|
|
const callRuntime = (
|
|
operation: "observe" | "inspect",
|
|
failure: AnalysisError,
|
|
) => {
|
|
const service = new WebRuntimeService({
|
|
identity: runtimeIdentity,
|
|
observeExecution: async () => err(failure),
|
|
inspectEventListeners: async () => err(failure),
|
|
});
|
|
return operation === "observe"
|
|
? service.observe({
|
|
cdp_endpoint: "http://127.0.0.1:9222",
|
|
target_id: "selected-page",
|
|
observation_ms: 5,
|
|
})
|
|
: service.inspect({
|
|
cdp_endpoint: "http://127.0.0.1:9222",
|
|
target_id: "selected-page",
|
|
selector: "#selected",
|
|
});
|
|
};
|
|
|
|
it.each([
|
|
{
|
|
operation: "observe" as const,
|
|
partial: {
|
|
instrumentation: { cleanup: "unconfirmed" },
|
|
window: { requested_ms: 5 },
|
|
},
|
|
},
|
|
{
|
|
operation: "inspect" as const,
|
|
partial: {
|
|
selected_node: { selector: "#selected" },
|
|
listeners: [],
|
|
},
|
|
},
|
|
])(
|
|
"keeps $operation cancellation when the retained observation is incomplete",
|
|
async ({ operation, partial }) => {
|
|
const reason = { requested: `stop incomplete ${operation}` };
|
|
const failure = new AnalysisCancelledError(
|
|
operation === "observe"
|
|
? "observe_web_execution"
|
|
: "inspect_web_event_listeners",
|
|
{ cause: reason, partialObservation: partial as never },
|
|
);
|
|
expectKeptPartial(
|
|
expectOwnerError(await callRuntime(operation, failure), failure, reason),
|
|
partial,
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{
|
|
operation: "observe" as const,
|
|
partial: {
|
|
instrumentation: { cleanup: "unconfirmed" },
|
|
window: { requested_ms: Number.NaN },
|
|
},
|
|
reason: "Provider retained a non-JSON incomplete execution observation.",
|
|
},
|
|
{
|
|
operation: "inspect" as const,
|
|
partial: {
|
|
selected_node: { selector: "#selected" },
|
|
listeners: [undefined],
|
|
},
|
|
reason:
|
|
"Provider retained a non-JSON incomplete event-listener observation.",
|
|
},
|
|
])(
|
|
"drops a non-JSON $operation partial and keeps cancellation primary",
|
|
async ({ operation, partial, reason: outputReason }) => {
|
|
const reason = { requested: `stop unsafe ${operation}` };
|
|
const failure = new AnalysisCancelledError(
|
|
operation === "observe"
|
|
? "observe_web_execution"
|
|
: "inspect_web_event_listeners",
|
|
{ cause: reason, partialObservation: partial as never },
|
|
);
|
|
expectDroppedPartial(
|
|
expectOwnerError(await callRuntime(operation, failure), failure, reason),
|
|
outputReason,
|
|
);
|
|
},
|
|
);
|
|
|
|
it("drops a non-JSON execution partial and keeps timeout primary", async () => {
|
|
const failure = new AnalysisTimeoutError("observe_web_execution", 5, {
|
|
partialObservation: {
|
|
instrumentation: { samples: [undefined] },
|
|
window: { requested_ms: 5 },
|
|
} as never,
|
|
});
|
|
const error = expectOwnerError(
|
|
await callRuntime("observe", failure),
|
|
failure,
|
|
undefined,
|
|
);
|
|
expectDroppedPartial(
|
|
error,
|
|
"Provider retained a non-JSON incomplete execution observation.",
|
|
"provider_timeout",
|
|
);
|
|
expect(projectAnalysisError(error).category).toBe("timeout");
|
|
});
|
|
|
|
it.each(["observe", "inspect"] as const)(
|
|
"keeps %s cancellation primary when completed-shaped partial cannot be promoted within origin scope",
|
|
async (operation) => {
|
|
const partial =
|
|
operation === "observe" ? webExecutionFixture() : webListenersFixture();
|
|
const reason = { stop: "retained fact projection" };
|
|
const failure = new AnalysisCancelledError(
|
|
operation === "observe"
|
|
? "observe_web_execution"
|
|
: "inspect_web_event_listeners",
|
|
{ cause: reason, partialObservation: partial },
|
|
);
|
|
const service = new WebRuntimeService({
|
|
identity: runtimeIdentity,
|
|
observeExecution: async () => err(failure),
|
|
inspectEventListeners: async () => err(failure),
|
|
});
|
|
const input = {
|
|
cdp_endpoint: "http://127.0.0.1:9222",
|
|
target_id: "selected-page",
|
|
allowed_origins: ["https://selected.example"],
|
|
};
|
|
const result =
|
|
operation === "observe"
|
|
? await service.observe({ ...input, observation_ms: 5 })
|
|
: await service.inspect({ ...input, selector: "#selected" });
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisCancelledError",
|
|
cause: reason,
|
|
partialObservation: partial,
|
|
},
|
|
});
|
|
if (result.ok) throw new Error("Expected original cancellation");
|
|
expect(result.error).toBe(failure);
|
|
expect(result.error.partialObservation).not.toHaveProperty("evidence_id");
|
|
},
|
|
);
|