107 lines
3 KiB
TypeScript
107 lines
3 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* CI replay-determinism verifier.
|
|
*
|
|
* Drives a handful of canonical event sequences through a live Session
|
|
* (capturing to an EventLog) and re-plays each log on a fresh Session,
|
|
* asserting that the state hashes match byte-for-byte. See
|
|
* packages/rete/SPEC.md §Replay Determinism and P3.3 in docs/PHASES.md.
|
|
*
|
|
* Intentionally imports engine internals via relative paths so the
|
|
* script is runnable without first building the package — Bun executes
|
|
* TypeScript sources directly.
|
|
*
|
|
* Usage:
|
|
* bun run scripts/replay-determinism.ts
|
|
*
|
|
* Exit status:
|
|
* 0 — all sequences produced matching hashes
|
|
* 1 — at least one mismatch (replay is non-deterministic)
|
|
*/
|
|
import { Session } from "../packages/rete/src/session.js";
|
|
import { EventLog } from "../packages/rete/src/eventlog.js";
|
|
import { stateHash, replayFromLog } from "../packages/rete/src/replay.js";
|
|
import type { EntityId } from "../packages/rete/src/schema.js";
|
|
|
|
type Op =
|
|
| { op: "insert"; id: number; attr: string; value: unknown }
|
|
| { op: "retract"; id: number; attr: string };
|
|
|
|
interface TestCase {
|
|
name: string;
|
|
operations: Op[];
|
|
}
|
|
|
|
const TEST_CASES: TestCase[] = [
|
|
{
|
|
name: "basic-inserts",
|
|
operations: [
|
|
{ op: "insert", id: 1, attr: "X", value: 42 },
|
|
{ op: "insert", id: 2, attr: "Y", value: "hello" },
|
|
{ op: "insert", id: 3, attr: "Z", value: true },
|
|
],
|
|
},
|
|
{
|
|
name: "with-retracts",
|
|
operations: [
|
|
{ op: "insert", id: 1, attr: "X", value: 100 },
|
|
{ op: "insert", id: 2, attr: "Y", value: 200 },
|
|
{ op: "retract", id: 1, attr: "X" },
|
|
{ op: "insert", id: 1, attr: "X", value: 300 },
|
|
],
|
|
},
|
|
{
|
|
name: "nested-values",
|
|
operations: [
|
|
{ op: "insert", id: 1, attr: "pos", value: { x: 1, y: 2 } },
|
|
{ op: "insert", id: 2, attr: "tags", value: ["a", "b", "c"] },
|
|
],
|
|
},
|
|
{
|
|
name: "large-set",
|
|
operations: Array.from({ length: 100 }, (_, i) => ({
|
|
op: "insert" as const,
|
|
id: i + 1,
|
|
attr: "N",
|
|
value: i * 7,
|
|
})),
|
|
},
|
|
];
|
|
|
|
function runCase(tc: TestCase): boolean {
|
|
const log = new EventLog();
|
|
const original = new Session({ autoFire: false, eventLog: log });
|
|
|
|
for (const op of tc.operations) {
|
|
const id = op.id as unknown as EntityId;
|
|
if (op.op === "insert") {
|
|
original.insert(id, op.attr, op.value);
|
|
} else {
|
|
original.retract(id, op.attr);
|
|
}
|
|
}
|
|
|
|
const originalHash = stateHash(original);
|
|
const replayed = replayFromLog(log.getAll());
|
|
const replayedHash = stateHash(replayed);
|
|
|
|
const match = originalHash === replayedHash;
|
|
const status = match ? "MATCH " : "MISMATCH";
|
|
console.log(
|
|
`${status} [${tc.name}] original=${originalHash} replayed=${replayedHash} events=${log.length}`,
|
|
);
|
|
return match;
|
|
}
|
|
|
|
let allMatch = true;
|
|
for (const tc of TEST_CASES) {
|
|
if (!runCase(tc)) allMatch = false;
|
|
}
|
|
|
|
if (allMatch) {
|
|
console.log("\nAll replay hashes match.");
|
|
process.exit(0);
|
|
} else {
|
|
console.error("\nOne or more replay hashes diverged — replay is non-deterministic.");
|
|
process.exit(1);
|
|
}
|