mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 16:26:36 +00:00
241 lines
6.5 KiB
TypeScript
241 lines
6.5 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import {
|
|
digestProcessCommitment,
|
|
parseProcessCapture,
|
|
type UnverifiedProcessCapture,
|
|
} from "./processCapture.js";
|
|
import {
|
|
compareUnverifiedProcessCaptures as compareProcessCaptures,
|
|
emptyUnverifiedProcessCapture as emptyCapture,
|
|
processCaptureIssues,
|
|
} from "./processCapture.fixture.js";
|
|
|
|
it("never considers truncated captures equivalent", () => {
|
|
const capture = {
|
|
manifest: emptyCapture().manifest,
|
|
settlement: emptyCapture().settlement,
|
|
normalization: {
|
|
paths: true,
|
|
pids: true,
|
|
ports: true,
|
|
time_bucket_ms: 10,
|
|
patterns: [],
|
|
},
|
|
frames: [],
|
|
rendered_frames: [],
|
|
interaction_events: [],
|
|
exit: { code: 0, signal: null, reason: "exited" as const },
|
|
process_samples: [],
|
|
filesystem_checkpoints: emptyCapture().filesystem_checkpoints,
|
|
shim_events: [],
|
|
protocol_events: [],
|
|
replay_transitions: [],
|
|
reactive_run: null,
|
|
files_before: [],
|
|
files_after: [],
|
|
filesystem_effects: [],
|
|
truncated: true,
|
|
limitations: [],
|
|
residual_unknowns: [],
|
|
cleanup: {
|
|
owned_process_group: "verified" as const,
|
|
temporary_root: "removed" as const,
|
|
},
|
|
};
|
|
expect(compareProcessCaptures(capture, capture).status).toBe("truncated");
|
|
});
|
|
|
|
it("rejects altered v4 commitments and accepts canonical key reordering", () => {
|
|
const capture = emptyCapture();
|
|
expect(parseProcessCapture(capture)).toEqual(capture);
|
|
expect(digestProcessCommitment({ second: 2, first: 1 })).toBe(
|
|
digestProcessCommitment({ first: 1, second: 2 }),
|
|
);
|
|
expect(() =>
|
|
parseProcessCapture({
|
|
...capture,
|
|
manifest: {
|
|
...capture.manifest,
|
|
normalization_sha256: "f".repeat(64),
|
|
},
|
|
}),
|
|
).toThrow("normalization_sha256");
|
|
expect(() =>
|
|
parseProcessCapture({
|
|
...capture,
|
|
manifest: {
|
|
...capture.manifest,
|
|
executable_sha256: "f".repeat(64),
|
|
},
|
|
}),
|
|
).toThrow("executable_sha256");
|
|
});
|
|
|
|
it("rejects settlement and cleanup combinations that cannot occur", () => {
|
|
const capture = emptyCapture();
|
|
expect(() =>
|
|
parseProcessCapture({
|
|
...capture,
|
|
settlement: {
|
|
state: "quiesced",
|
|
elapsed_ms: 0,
|
|
cleanup_outcome: "cleaned",
|
|
},
|
|
}),
|
|
).toThrow("cleanup_outcome");
|
|
expect(() =>
|
|
parseProcessCapture({
|
|
...capture,
|
|
settlement: {
|
|
state: "alive_at_deadline",
|
|
elapsed_ms: 1,
|
|
cleanup_outcome: "not_required",
|
|
},
|
|
}),
|
|
).toThrow("cleanup_outcome");
|
|
});
|
|
|
|
it("accepts old captures without a journal and validates complete journals", () => {
|
|
const capture = emptyCapture();
|
|
const { event_journal: _eventJournal, ...oldCapture } = capture;
|
|
expect(parseProcessCapture(oldCapture).event_journal).toEqual([]);
|
|
|
|
const eventJournal = [
|
|
{ capture_order: 0, collection: "filesystem_checkpoints", index: 0 },
|
|
{ capture_order: 1, collection: "lifecycle", index: 0 },
|
|
{ capture_order: 2, collection: "lifecycle", index: 1 },
|
|
{ capture_order: 3, collection: "filesystem_checkpoints", index: 1 },
|
|
] as const;
|
|
expect(
|
|
processCaptureIssues({ ...capture, event_journal: eventJournal }),
|
|
).toEqual([]);
|
|
|
|
for (const [candidate, message] of [
|
|
[
|
|
eventJournal.map((entry, index) =>
|
|
index === 1 ? { ...entry, capture_order: 2 } : entry,
|
|
),
|
|
"contiguous",
|
|
],
|
|
[
|
|
eventJournal.map((entry, index) =>
|
|
index === 3
|
|
? { ...entry, collection: "lifecycle" as const, index: 1 }
|
|
: entry,
|
|
),
|
|
"unique",
|
|
],
|
|
[eventJournal.slice(0, 3), "every captured observation"],
|
|
[
|
|
eventJournal.map((entry, index) =>
|
|
index === 3 ? { ...entry, index: 2 } : entry,
|
|
),
|
|
"outside",
|
|
],
|
|
] as const) {
|
|
expect(
|
|
processCaptureIssues({ ...capture, event_journal: candidate }),
|
|
).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
message: expect.stringContaining(message),
|
|
}),
|
|
]),
|
|
);
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
["machine limit", 1, 5, 10],
|
|
["transition use limit", 5, 1, 10],
|
|
["state visit limit", 5, 5, 1],
|
|
])(
|
|
"rejects replay journals that exceed the declared %s",
|
|
(_name, maxTransitions, maxUses, maxVisits) => {
|
|
const replayPlan = {
|
|
machine: {
|
|
initial_state: "active",
|
|
states: [
|
|
{ name: "active", max_visits: maxVisits },
|
|
{ name: "complete", terminal: true },
|
|
],
|
|
transitions: [
|
|
{
|
|
id: "again",
|
|
from: "active",
|
|
to: "active",
|
|
trigger: {
|
|
protocol: "websocket_message",
|
|
path: "/ws",
|
|
body: "again",
|
|
},
|
|
actions: [{ type: "websocket_send", data: "again" }],
|
|
max_uses: maxUses,
|
|
},
|
|
{
|
|
id: "finish",
|
|
from: "active",
|
|
to: "complete",
|
|
trigger: {
|
|
protocol: "websocket_message",
|
|
path: "/ws",
|
|
body: "finish",
|
|
},
|
|
actions: [{ type: "websocket_send", data: "done" }],
|
|
max_uses: 1,
|
|
},
|
|
],
|
|
max_transitions: maxTransitions,
|
|
},
|
|
};
|
|
const capture = emptyCapture();
|
|
const candidate: UnverifiedProcessCapture = {
|
|
...capture,
|
|
manifest: {
|
|
...capture.manifest,
|
|
replay_plan: replayPlan,
|
|
replay_plan_sha256: digestProcessCommitment(replayPlan),
|
|
},
|
|
replay_transitions: [0, 1].map((sequence) => ({
|
|
sequence,
|
|
at_ms: sequence,
|
|
transition_id: "again",
|
|
state_before: "active",
|
|
state_after: "active",
|
|
sensitive_aliases: [],
|
|
})),
|
|
};
|
|
|
|
expect(processCaptureIssues(candidate)).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
message: expect.stringContaining("limit"),
|
|
}),
|
|
]),
|
|
);
|
|
},
|
|
);
|
|
|
|
it("requires compatible contracts and enforces capture age through a clock seam", () => {
|
|
const capture = emptyCapture();
|
|
expect(() =>
|
|
compareProcessCaptures(capture, {
|
|
...capture,
|
|
manifest: {
|
|
...capture.manifest,
|
|
comparison_contract: { changed: true },
|
|
comparison_contract_sha256: digestProcessCommitment({
|
|
changed: true,
|
|
}),
|
|
},
|
|
}),
|
|
).toThrow("incompatible comparison contracts");
|
|
expect(() =>
|
|
compareProcessCaptures(capture, capture, {
|
|
maxCaptureAgeMs: 1,
|
|
now: () => Date.parse("2026-01-01T00:00:01.000Z"),
|
|
}),
|
|
).toThrow("max_capture_age_ms");
|
|
});
|