Files
rea/src/domain/process/processCapture.comparison.test.ts
T
morluto 584120e802 refactor(process): remove the redundant scenario parsing alias
Use the authoritative scenario schema directly in all 25 test consumers. Preserve unknown-input annotations and every lifecycle, permission, normalization, identity and cleanup assertion; retain the separate branded capture validation boundary.
2026-10-24 09:57:15 +08:00

379 lines
11 KiB
TypeScript

import { expect, it } from "vitest";
import {
processScenarioSchema,
processScenarioCommitment,
processComparisonContract,
digestProcessCommitment,
} from "./processScenario.js";
import { parseProcessCapture } from "./processCaptureParsing.js";
import {
deriveProcessComparisonStatus,
PROCESS_COMPARISON_DIMENSIONS,
processCaptureComparisonSchema,
} from "./processComparison.js";
import {
accountFullyObservedProcessCapture,
compareUnverifiedProcessCaptures as compareProcessCaptures,
emptyProcessCapture as emptyCapture,
} from "./processCapture.fixture.js";
const compareFullyObserved = (
left: Parameters<typeof compareProcessCaptures>[0],
right: Parameters<typeof compareProcessCaptures>[1],
options?: Parameters<typeof compareProcessCaptures>[2],
) =>
compareProcessCaptures(
accountFullyObservedProcessCapture(left),
accountFullyObservedProcessCapture(right),
options,
);
it.each(PROCESS_COMPARISON_DIMENSIONS)(
"requires a contradiction aggregate for a contradictory %s dimension",
(dimension) => {
const baseline = compareFullyObserved(emptyCapture(), emptyCapture());
const result = { ...baseline, [dimension]: "contradiction" as const };
expect(
deriveProcessComparisonStatus(
PROCESS_COMPARISON_DIMENSIONS.map((name) => result[name]),
),
).toBe("contradiction");
expect(processCaptureComparisonSchema.safeParse(result).success).toBe(
false,
);
expect(
processCaptureComparisonSchema.parse({
...result,
status: "contradiction",
}),
).toMatchObject({ status: "contradiction", [dimension]: "contradiction" });
},
);
it.each([
["unchanged", "contradiction"],
["added", "contradiction"],
["removed", "contradiction"],
["changed", "contradiction"],
["unknown", "contradiction"],
["contradiction", "contradiction"],
["truncated", "truncated"],
] as const)(
"combines contradiction with %s as %s independently of dimension order",
(other, expected) => {
for (const [terminal, interaction] of [
["contradiction", other],
[other, "contradiction"],
] as const) {
expect(deriveProcessComparisonStatus([terminal, interaction])).toBe(
expected,
);
expect(
processCaptureComparisonSchema.parse({
...compareFullyObserved(emptyCapture(), emptyCapture()),
terminal,
interaction,
status: expected,
}),
).toMatchObject({ status: expected });
}
},
);
it("compares normalized terminal facts while retaining different source text for audit", () => {
const capture = emptyCapture();
const frame = { sequence: 0, at_ms: 0, data: "listen:<port> score=20" };
const left = {
...capture,
frames: [{ ...frame, raw_data: "listen:8080 score=20" }],
};
const right = {
...capture,
frames: [{ ...frame, raw_data: "listen:9090 score=20" }],
};
expect(compareFullyObserved(left, right)).toMatchObject({
terminal: "unchanged",
});
expect(
compareFullyObserved(left, {
...right,
frames: [{ ...frame, data: "listen:<port> score=21" }],
}),
).toMatchObject({ terminal: "changed" });
});
it("distinguishes added evidence from unknown observations", () => {
const capture = emptyCapture();
const added = compareFullyObserved(capture, {
...capture,
frames: [{ sequence: 0, at_ms: 0, data: "new" }],
rendered_frames: [
{
sequence: 0,
at_ms: 0,
columns: 3,
rows: 1,
cursor_x: 3,
cursor_y: 0,
active_buffer: "normal",
lines: ["new"],
serialized_state: "new",
},
],
});
expect(added).toMatchObject({ terminal: "added", status: "changed" });
for (const [scope, dimension] of [
["process", "process"],
["interaction", "interaction"],
] as const) {
expect(
compareFullyObserved(
{
...capture,
residual_unknowns: [{ scope, reason: "observation was partial" }],
},
capture,
),
).toMatchObject({
status: "unknown",
[dimension]: "unknown",
terminal: "unchanged",
});
}
});
it("compares raw terminal and process observations while honoring normalization shape", () => {
const capture = emptyCapture();
expect(
compareFullyObserved(capture, {
...capture,
normalization: {
patterns: [],
time_bucket_ms: 10,
ports: true,
pids: true,
paths: true,
},
}),
).toMatchObject({
status: "unchanged",
first_divergence: { status: "none" },
});
const terminal = compareFullyObserved(
{ ...capture, frames: [{ sequence: 0, at_ms: 0, data: "bar" }] },
{ ...capture, frames: [{ sequence: 0, at_ms: 0, data: "foo\rbar" }] },
);
expect(terminal).toMatchObject({
terminal: "changed",
first_divergence: { status: "found", dimension: "terminal" },
});
expect(
compareFullyObserved(
{
...capture,
process_samples: [
{
at_ms: 10,
pid: 1,
parent_pid: 0,
process_group_id: 1,
session_id: 1,
command: "worker",
},
],
},
{
...capture,
process_samples: [
{
at_ms: 20,
pid: 1,
parent_pid: 2,
process_group_id: 1,
session_id: 1,
command: "worker",
},
],
},
),
).toMatchObject({ process: "changed", status: "changed" });
});
it("keeps filesystem comparison unknown without coverage and reports observed changes", () => {
const capture = emptyCapture();
const incomplete = {
...capture,
residual_unknowns: [
{ scope: "filesystem" as const, reason: "watcher unavailable" },
],
};
expect(compareFullyObserved(incomplete, incomplete)).toMatchObject({
filesystem: "unknown",
status: "unknown",
});
const finalFile = {
path: "root_0:created.txt",
type: "file" as const,
mode: 0o644,
size: 4,
sha256: "a".repeat(64),
symlink_target: null,
};
expect(
compareFullyObserved(capture, {
...capture,
filesystem_checkpoints: [
{ name: "before", at_ms: 0, files: [], effects: [], truncated: false },
{
name: "after_settlement",
at_ms: 10,
files: [finalFile],
effects: [
{
path: finalFile.path,
status: "created",
before: null,
after: finalFile,
},
],
truncated: false,
},
],
files_after: [finalFile],
filesystem_effects: [
{
path: finalFile.path,
status: "created",
before: null,
after: finalFile,
},
],
}),
).toMatchObject({ filesystem: "changed", status: "changed" });
});
it.each([
[500, "interaction", 6],
[4, "process", 4],
] as const)(
"orders an interaction difference by its dispatch time (process sample at %i ms)",
(processTime, dimension, at) => {
const capture = (side: "left" | "right") => {
const base = emptyCapture();
const journal = [
{ collection: "filesystem_checkpoints" as const, index: 0 },
...(processTime < 6
? [{ collection: "process_samples" as const, index: 0 }]
: []),
{ collection: "interaction_events" as const, index: 0 },
...(processTime >= 6
? [{ collection: "process_samples" as const, index: 0 }]
: []),
{ collection: "lifecycle" as const, index: 0 },
{ collection: "lifecycle" as const, index: 1 },
{ collection: "filesystem_checkpoints" as const, index: 1 },
].map((entry, capture_order) => ({ ...entry, capture_order }));
return {
...base,
manifest: {
...base.manifest,
completed_at: "2026-01-01T00:00:01.000Z",
},
settlement: { ...base.settlement, elapsed_ms: 1000 },
filesystem_checkpoints: base.filesystem_checkpoints.map(
(checkpoint, index) => ({
...checkpoint,
at_ms: index === 0 ? 0 : 1000,
}),
),
interaction_events: [
{
sequence: 0,
scheduled_at_ms: 5,
dispatched_at_ms: 6,
type: "input" as const,
data: "hello",
outcome:
side === "left" ? ("dispatched" as const) : ("failed" as const),
},
],
process_samples: [
{
at_ms: processTime,
pid: 2,
parent_pid: 1,
command: side === "left" ? "worker-a" : "worker-b",
process_group_id: 1,
session_id: 1,
},
],
event_journal: journal,
};
};
const comparison = compareFullyObserved(capture("left"), capture("right"));
expect(comparison).toMatchObject({
interaction: "changed",
process: "changed",
first_divergence: {
status: "found",
dimension,
left_at_ms: at,
right_at_ms: at,
},
});
},
);
it("refuses mixed terminal observation versions even with PID and port rules disabled", () => {
const base = emptyCapture();
const scenario = processScenarioSchema.parse({
executable: "/usr/bin/true",
normalization: { pids: false, ports: false },
});
const committed = processScenarioCommitment(
scenario,
base.manifest.selected_executable_sha256 ?? undefined,
);
const contract = processComparisonContract(scenario);
expect(contract).not.toHaveProperty("pid_normalization_version");
expect(contract).not.toHaveProperty("port_normalization_version");
expect(contract.terminal_observation_version).toBe(
"deferred-rendered-journal-v1",
);
const capture = parseProcessCapture({
...base,
normalization: scenario.normalization,
manifest: {
...base.manifest,
scenario: committed,
full_scenario_sha256: digestProcessCommitment(committed),
normalization_sha256: digestProcessCommitment(scenario.normalization),
comparison_contract: contract,
comparison_contract_sha256: digestProcessCommitment(contract),
},
});
const legacyContract = { ...contract };
delete legacyContract.terminal_observation_version;
const legacy = parseProcessCapture({
...capture,
manifest: {
...capture.manifest,
comparison_contract: legacyContract,
comparison_contract_sha256: digestProcessCommitment(legacyContract),
},
});
expect(() => compareProcessCaptures(capture, legacy)).toThrow(
expect.objectContaining({
issues: [
expect.objectContaining({ expected: ["terminal_observation_version"] }),
],
}),
);
});