rea/tests/boundary/process/processCaptureMultisource.t...

270 lines
7.3 KiB
TypeScript

import { rm } from "node:fs/promises";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
captureProcessScenario,
probeProcessCaptureCapability,
} from "../../../src/application/ProcessHarness.js";
import {
parseProcessScenario,
type ProcessCapture,
} from "../../../src/domain/processCapture.js";
import { processCaptureIssues } from "../../../src/domain/processCapture.fixture.js";
type EventSource = "process" | "filesystem" | "http" | "websocket" | "shim";
function eventTrigger(
source: EventSource,
exact: Record<string, unknown>,
ignoreFields: readonly string[],
) {
return {
kind: "event" as const,
source,
exact,
ignore_fields: ignoreFields,
since: { kind: "scenario_start" as const },
consume: true,
cardinality: { min: 1, max: 1 },
};
}
function collectionTriggers() {
const terminalTrigger = (literal: string) => ({
kind: "terminal_text" as const,
view: "decoded" as const,
encoding: "utf8" as const,
literal,
case_sensitive: true,
control_sequences: "include" as const,
occurrence: 1,
since: { kind: "scenario_start" as const },
consume: false,
});
return [
{
kind: "any" as const,
triggers: [
terminalTrigger("Collecting"),
terminalTrigger("unreachable-alternative"),
],
},
eventTrigger(
"process",
{
pid: 3,
parent_pid: 1,
command: "rea-reactive-worker",
process_group_id: 1,
session_id: process.platform === "darwin" ? null : 1,
},
["at_ms"],
),
eventTrigger(
"filesystem",
{
name: "ready_snapshot",
files: [],
effects: [],
truncated: false,
},
["at_ms"],
),
eventTrigger(
"shim",
{
command: "codex",
route_index: 0,
arguments: ["probe"],
working_directory: "<working-directory>",
outcome: "matched",
},
["sequence", "at_ms"],
),
{
kind: "sequence" as const,
triggers: [
eventTrigger(
"http",
{
protocol: "http",
direction: "request",
method: "GET",
path: "/reactive",
data: "",
outcome: "matched",
},
["sequence", "at_ms"],
),
eventTrigger(
"websocket",
{
protocol: "websocket",
direction: "received",
method: null,
path: "/ws",
data: "reactive-client",
outcome: "matched",
},
["sequence", "at_ms"],
),
],
},
];
}
function createMultiSourceScenario(root: string, script: string) {
return parseProcessScenario({
approved: true,
executable: process.execPath,
arguments: [script, "codex"],
working_directory: root,
replay: {
http: [{ method: "GET", path: "/reactive", status: 200, body: "ok" }],
websocket_messages: ["reactive-server"],
},
command_shims: [
{
name: "codex",
routes: [
{
arguments: ["probe"],
outputs: [],
termination: { type: "exit", code: 0 },
},
],
},
],
reactive: {
version: 1,
initial_state: "ready",
deadline_ms: 8_000,
states: [
{
id: "ready",
max_visits: 1,
deadline_ms: 3_000,
on: [
{
id: "checkpoint_ready",
priority: 0,
max_uses: 1,
when: {
kind: "terminal_text",
view: "decoded",
encoding: "utf8",
literal: "Ready",
case_sensitive: true,
control_sequences: "include",
occurrence: 1,
since: { kind: "scenario_start" },
consume: true,
},
actions: [{ type: "checkpoint", name: "ready_snapshot" }],
target: { kind: "goto", state: "collecting" },
},
],
},
{
id: "collecting",
max_visits: 1,
deadline_ms: 5_000,
on: [
{
id: "finish_all_sources",
priority: 0,
max_uses: 1,
when: { kind: "all", triggers: collectionTriggers() },
actions: [],
target: { kind: "finish", outcome: "passed" },
},
],
},
],
},
});
}
// This is the single multi-source acceptance matrix for the process boundary.
it("runs the committed multi-source reactive fixture deterministically", async () => {
const root = await createTestTempDirectory("rea-reactive-e2e-");
const script = fileURLToPath(
new URL("../../fixtures/processReactiveScenario.mjs", import.meta.url),
);
const run = () =>
captureProcessScenario(createMultiSourceScenario(root, script), {
status: "enabled",
executableRoots: [dirname(process.execPath)],
workingRoots: [root],
allowedEnvironment: [],
networkAccess: "external",
});
try {
const capability = await probeProcessCaptureCapability();
if (!capability.available) return;
const first = await run();
const second = await run();
if (!first.ok) throw first.error;
if (!second.ok) throw second.error;
expect(first.ok).toBe(true);
expect(second.ok).toBe(true);
const trace = (capture: ProcessCapture) =>
capture.reactive_run?.transitions.map((transition) => ({
transition_id: transition.transition_id,
from: transition.state_before,
to: transition.state_after,
outcome: transition.outcome,
trigger_collections: transition.trigger_event_ids.map(
(id) => id.split(".")[1],
),
}));
expect(
first.value.reactive_run?.outcome,
JSON.stringify({
run: first.value.reactive_run,
processes: first.value.process_samples,
checkpoints: first.value.filesystem_checkpoints,
shims: first.value.shim_events,
protocols: first.value.protocol_events,
}),
).toBe("passed");
expect(trace(first.value)).toEqual(trace(second.value));
expect(trace(first.value)).toEqual([
expect.objectContaining({ transition_id: "checkpoint_ready" }),
expect.objectContaining({
transition_id: "finish_all_sources",
trigger_collections: expect.arrayContaining([
"frames",
"process_samples",
"filesystem_checkpoints",
"shim_events",
"protocol_events",
]),
}),
]);
for (const outcome of ["action_rejected", "target_lost"] as const) {
const failedActionCapture = {
...first.value,
reactive_run: {
status: "finished" as const,
outcome,
active_state: "ready",
transitions: [],
controls: [],
},
};
expect(
processCaptureIssues(failedActionCapture).filter(({ path }) =>
path.startsWith("reactive_run"),
),
).toEqual([]);
}
expect(first.value.truncated).toBe(false);
} finally {
await rm(root, { recursive: true, force: true });
}
}, 30_000);