188 lines
5.6 KiB
TypeScript
188 lines
5.6 KiB
TypeScript
import { rm, writeFile } from "node:fs/promises";
|
|
import { dirname, join } from "node:path";
|
|
import { expect, it } from "vitest";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
import {
|
|
captureProcessScenario,
|
|
probeProcessCaptureCapability,
|
|
} from "../../../src/application/ProcessHarness.js";
|
|
import { parseProcessScenario } from "../../../src/domain/processCapture.js";
|
|
|
|
const buildTerminalScenario = (root: string, script: string) =>
|
|
parseProcessScenario({
|
|
executable: process.execPath,
|
|
arguments: [script],
|
|
working_directory: root,
|
|
checkpoints: [
|
|
{
|
|
name: "probe_seen",
|
|
trigger: { type: "terminal_literal", value: "probe:codex 1.2.3" },
|
|
},
|
|
],
|
|
command_shims: [
|
|
{
|
|
name: "codex",
|
|
routes: [
|
|
{
|
|
arguments: ["--version"],
|
|
outputs: [{ at_ms: 0, stream: "stdout", data: "codex 1.2.3\n" }],
|
|
termination: { type: "exit", code: 0 },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "node",
|
|
routes: [
|
|
{
|
|
arguments: ["--version"],
|
|
outputs: [{ at_ms: 0, stream: "stdout", data: "node 9.8.7\n" }],
|
|
termination: { type: "exit", code: 0 },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
reactive: {
|
|
initial_state: "waiting_for_shim",
|
|
deadline_ms: 5_000,
|
|
states: [
|
|
{
|
|
id: "waiting_for_shim",
|
|
max_visits: 1,
|
|
deadline_ms: 2_000,
|
|
on: [
|
|
{
|
|
id: "record_shim",
|
|
priority: 0,
|
|
max_uses: 1,
|
|
when: {
|
|
kind: "event",
|
|
source: "shim",
|
|
exact: {
|
|
command: "codex",
|
|
route_index: 0,
|
|
arguments: ["--version"],
|
|
working_directory: "<working-directory>",
|
|
outcome: "matched",
|
|
},
|
|
ignore_fields: ["sequence", "at_ms"],
|
|
since: { kind: "scenario_start" },
|
|
consume: true,
|
|
cardinality: { min: 1, max: 1 },
|
|
},
|
|
actions: [{ type: "checkpoint", name: "reactive_shim_seen" }],
|
|
target: {
|
|
kind: "goto",
|
|
state: "waiting_for_checkpoint",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "waiting_for_checkpoint",
|
|
max_visits: 1,
|
|
deadline_ms: 2_000,
|
|
on: [
|
|
{
|
|
id: "finish_checkpoint",
|
|
priority: 0,
|
|
max_uses: 1,
|
|
when: {
|
|
kind: "event",
|
|
source: "filesystem",
|
|
exact: {
|
|
name: "reactive_shim_seen",
|
|
files: [],
|
|
effects: [],
|
|
truncated: false,
|
|
},
|
|
ignore_fields: ["at_ms"],
|
|
since: { kind: "scenario_start" },
|
|
consume: true,
|
|
cardinality: { min: 1, max: 1 },
|
|
},
|
|
actions: [],
|
|
target: { kind: "finish", outcome: "passed" },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
// The assertions share one captured process and therefore one cleanup scope.
|
|
it("renders terminal state, records shim invocations, and captures literal checkpoints", async () => {
|
|
const root = await createTestTempDirectory("rea-v3-test-");
|
|
const script = join(root, "scenario.mjs");
|
|
await writeFile(
|
|
script,
|
|
[
|
|
'import { spawnSync } from "node:child_process";',
|
|
'const result = spawnSync("codex", ["--version"], { encoding: "utf8" });',
|
|
'const node = spawnSync("node", ["--version"], { encoding: "utf8" });',
|
|
"process.stdout.write(`probe:${result.stdout}`);",
|
|
"process.stdout.write(`runtime:${node.stdout}`);",
|
|
].join("\n"),
|
|
);
|
|
try {
|
|
const capability = await probeProcessCaptureCapability();
|
|
if (!capability.available) return;
|
|
const result = await captureProcessScenario(
|
|
buildTerminalScenario(root, script),
|
|
{
|
|
status: "enabled",
|
|
executableRoots: [dirname(process.execPath)],
|
|
workingRoots: [root],
|
|
allowedEnvironment: [],
|
|
networkAccess: "external",
|
|
},
|
|
);
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.rendered_frames.at(-1)?.lines.join("\n")).toContain(
|
|
"probe:codex 1.2.3",
|
|
);
|
|
expect(result.value.shim_events).toEqual([
|
|
expect.objectContaining({
|
|
command: "codex",
|
|
arguments: ["--version"],
|
|
outcome: "matched",
|
|
}),
|
|
expect.objectContaining({
|
|
command: "node",
|
|
arguments: ["--version"],
|
|
outcome: "matched",
|
|
}),
|
|
]);
|
|
expect(result.value.filesystem_checkpoints.map(({ name }) => name)).toEqual(
|
|
expect.arrayContaining([
|
|
"before",
|
|
"reactive_shim_seen",
|
|
"probe_seen",
|
|
"after_settlement",
|
|
]),
|
|
);
|
|
expect(result.value.reactive_run).toMatchObject({
|
|
status: "finished",
|
|
outcome: "passed",
|
|
transitions: [
|
|
expect.objectContaining({
|
|
transition_id: "record_shim",
|
|
trigger_event_ids: ["obs.shim_events.0"],
|
|
action_event_ids: [
|
|
expect.stringMatching(/^obs\.filesystem_checkpoints\./u),
|
|
],
|
|
}),
|
|
expect.objectContaining({
|
|
transition_id: "finish_checkpoint",
|
|
trigger_event_ids: [
|
|
expect.stringMatching(/^obs\.filesystem_checkpoints\./u),
|
|
],
|
|
}),
|
|
],
|
|
});
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
}, 20_000);
|