Files
rea/src/application/ProcessCaptureObservation.test.ts
T
morlutoandGitHub e07b81649e refactor!: simplify runtime tools and remove replay engines (#572)
* fix(hopper): report unavailable procedure body coverage

* refactor!: simplify runtime tools and remove replay engines

Keep direct inspection, capture, and comparison tools. Bind handlers to named contracts and let agents compose experiments with commands and local fixture servers.

BREAKING CHANGE: Remove controlled replay, Node characterization, finite replay-machine, managed runtime planning, and redundant string-tracing tools. Retire process replay, shims, reactive scenarios, custom checkpoints, and active browser replay and origin-scope inputs.
2026-10-05 21:15:04 +08:00

295 lines
8.0 KiB
TypeScript

import { expect, it } from "vitest";
import { snapshotRoots } from "./FilesystemSnapshot.js";
import { buildCaptureResult } from "./ProcessCaptureLifecycle.js";
import { normalizeProcessSamples } from "./ProcessNormalization.js";
import { isInitializedPtyRoot, readLinuxChildren } from "./ProcessSampling.js";
import { TerminalRenderer } from "./TerminalRenderer.js";
import {
compareProcessCaptures,
parseProcessCapture,
parseProcessScenario,
type ProcessCapture,
} from "../domain/processCapture.js";
import { emptyProcessCapture as emptyCapture } from "../domain/processCapture.fixture.js";
const base = {
executable: "/bin/sh",
working_directory: "/tmp",
};
it("returns detached terminal observations", async () => {
const observed: string[] = [];
const renderer = new TerminalRenderer({
columns: 40,
rows: 12,
scrollback: 100,
maxBytes: 100_000,
normalize: (value) => value,
recordEvent: (collection, index) =>
observed.push(`${collection}:${String(index)}`),
});
renderer.write("A", 20);
renderer.resize(40, 12, 10);
for (let index = 0; index < 20; index += 1)
renderer.resize(40, 12, 11 + index);
const frames = await renderer.frames();
expect(frames).toHaveLength(22);
expect(frames.slice(0, 2).map(({ at_ms }) => at_ms)).toEqual([20, 10]);
expect(observed).toHaveLength(22);
const frame = frames[0];
if (frame === undefined) throw new Error("expected observed frame");
const originalCursor = frame.cursor_x;
Reflect.set(frame, "cursor_x", 999);
const reread = await renderer.frames();
expect(reread).toHaveLength(frames.length);
expect(reread[0]?.cursor_x).toBe(originalCursor);
await renderer.dispose();
});
it("preserves rendered observation order instead of timestamp sorting", () => {
const capture = emptyCapture();
const renderedFrames: ProcessCapture["rendered_frames"] = [
{
sequence: 0,
at_ms: 20,
columns: 1,
rows: 1,
cursor_x: 0,
cursor_y: 0,
active_buffer: "normal",
lines: ["first"],
serialized_state: "first",
},
{
sequence: 1,
at_ms: 10,
columns: 1,
rows: 1,
cursor_x: 0,
cursor_y: 0,
active_buffer: "normal",
lines: ["second"],
serialized_state: "second",
},
];
const result = buildCaptureResult({
frames: [],
exit: { exitCode: 0, reason: "exited" },
samples: [],
before: { files: [], truncated: false },
after: { files: [], truncated: false },
truncated: false,
scenario: parseProcessScenario(base),
rootPid: 1,
samplingPartial: false,
renderedFrames,
interactions: [],
checkpoints: capture.filesystem_checkpoints,
settlement: {
state: capture.settlement.state,
elapsed_ms: capture.settlement.elapsed_ms,
},
manifest: capture.manifest,
eventJournal: [],
});
expect(result.rendered_frames).toEqual(renderedFrames);
});
it("marks redacted scripted input as an interaction unknown", () => {
const capture = emptyCapture();
const result = buildCaptureResult({
frames: [],
exit: { exitCode: 0, reason: "exited" },
samples: [],
before: { files: [], truncated: false },
after: { files: [], truncated: false },
truncated: false,
scenario: parseProcessScenario({
...base,
events: [{ type: "input", at_ms: 0, data: "secret", sensitive: true }],
}),
rootPid: 1,
samplingPartial: false,
renderedFrames: [],
interactions: [
{
sequence: 0,
scheduled_at_ms: 0,
dispatched_at_ms: 0,
type: "input",
data: "<redacted-input:6-bytes>",
outcome: "dispatched",
},
],
checkpoints: capture.filesystem_checkpoints,
settlement: {
state: capture.settlement.state,
elapsed_ms: capture.settlement.elapsed_ms,
},
manifest: capture.manifest,
eventJournal: [],
});
expect(result.residual_unknowns).toContainEqual({
scope: "interaction",
reason:
"Sensitive scripted input values are redacted from process capture Evidence.",
});
});
it("normalizes every sampled process identifier in command text", () => {
const samples = normalizeProcessSamples(
[
{
at_ms: 0,
pid: 101,
parent_pid: 0,
process_group_id: 101,
session_id: 101,
command: "root 101",
},
{
at_ms: 10,
pid: 202,
parent_pid: 101,
process_group_id: 101,
session_id: 101,
command: "child 202 peer=101 unrelated 1202",
},
],
parseProcessScenario(base),
101,
);
expect(samples[1]?.command).toBe("child <pid> peer=<pid> unrelated 1202");
});
it("collects and deduplicates children from every Linux thread", async () => {
const signal = new AbortController().signal;
expect(
await readLinuxChildren(100, signal, {
taskIds: () => Promise.resolve([100, 101, 102]),
children: (_pid, taskId) =>
Promise.resolve(
taskId === 100 ? "201 202" : taskId === 101 ? "202 203" : "",
),
}),
).toEqual([201, 202, 203]);
});
it("admits PTY samples only after stable session and token setup", () => {
const initialized = {
pid: 100,
parent_pid: 10,
process_group_id: 100,
session_id: 100,
startTime: "200",
};
expect(
isInitializedPtyRoot({
rootPid: 100,
expectedRunId: "run-token",
before: { ...initialized, process_group_id: 10, session_id: 10 },
observedRunId: undefined,
after: initialized,
}),
).toBe(false);
expect(
isInitializedPtyRoot({
rootPid: 100,
expectedRunId: "run-token",
before: initialized,
observedRunId: "run-token",
after: { ...initialized, startTime: "201" },
}),
).toBe(false);
expect(
isInitializedPtyRoot({
rootPid: 100,
expectedRunId: "run-token",
before: initialized,
observedRunId: "run-token",
after: initialized,
}),
).toBe(true);
expect(
isInitializedPtyRoot({
rootPid: 100,
expectedRunId: "run-token",
before: { ...initialized, session_id: null },
observedRunId: "run-token",
after: { ...initialized, session_id: null },
}),
).toBe(true);
});
it("keeps interaction and process residual uncertainty in separate scopes", () => {
const baseCapture = emptyCapture();
const interaction = parseProcessCapture({
...baseCapture,
residual_unknowns: [
{ scope: "interaction", reason: "Interaction capture was partial." },
],
});
const process = parseProcessCapture({
...baseCapture,
residual_unknowns: [
{ scope: "process", reason: "Process sampling was partial." },
],
});
expect(compareProcessCaptures(interaction, baseCapture)).toMatchObject({
status: "unknown",
terminal: "unchanged",
interaction: "unknown",
process: "unchanged",
});
expect(compareProcessCaptures(process, baseCapture)).toMatchObject({
status: "unknown",
terminal: "unchanged",
interaction: "unchanged",
process: "unknown",
});
});
it("cancels filesystem snapshots before traversing declared roots", async () => {
const controller = new AbortController();
controller.abort();
await expect(
snapshotRoots(
parseProcessScenario({
...base,
filesystem_observation_paths: ["/tmp"],
}),
controller.signal,
),
).rejects.toMatchObject({ name: "AbortError" });
});
it("parses bounded scenarios and rejects unordered events", () => {
const timeoutMs = parseProcessScenario(base).timeout_ms;
expect(() =>
parseProcessScenario({
...base,
events: [
{ type: "input", at_ms: 2, data: "a" },
{ type: "input", at_ms: 1, data: "b" },
],
}),
).toThrow(/ordered/);
expect(
parseProcessScenario({
...base,
environment: { HOME: "/caller-selected-home" },
}).environment.HOME,
).toBe("/caller-selected-home");
expect(() =>
parseProcessScenario({
...base,
events: [{ type: "input", at_ms: timeoutMs + 1, data: "late" }],
}),
).toThrow(/after the scenario timeout/);
});