import assert from "node:assert/strict"; import { EventLog, ReplayTimeline, SnapshotStore, fnv1a64, REPLAY_SCHEMA } from "./replay.mjs"; function buildTimeline() { const timeline = new ReplayTimeline(); timeline.appendEvent(1, 0, "SESSION_READY", {}); timeline.appendEvent(2, 5, "SIM_STEP", { cycles: 5 }); timeline.checkpoint(2, 5, { tick: 5, position: 5 }); timeline.appendEvent(3, 9, "SIM_STEP", { cycles: 4 }); const base = timeline.snapshots.latest().snapshot_id; timeline.checkpoint(3, 9, { tick: 9, position: 9 }, base); return timeline; } const first = buildTimeline(); const second = buildTimeline(); assert.deepEqual(first.export(), second.export()); const replay = first.replay({ tick: 0, position: 0 }, (state, event) => ({ tick: event.tick, position: state.position + (event.payload.cycles ?? 0), })); assert.equal(replay.tick, 9); assert.equal(replay.state.position, 9); assert.equal(replay.state_hash, fnv1a64({ tick: 9, position: 9 })); assert.throws(() => { const log = new EventLog(); log.append(2, 0, "GAP", {}); }, /event_sequence_gap/); assert.throws(() => { const snapshots = new SnapshotStore(); snapshots.create(1, 1, { tick: 1 }, "snap:missing"); }, /snapshot_base_missing/); const exported = first.export(); assert.equal(exported.schema, REPLAY_SCHEMA); exported.snapshots.snapshots[0].state_hash = "fnv1a64:0000000000000000"; assert.throws(() => new ReplayTimeline(exported), /snapshot_hash_mismatch/); assert.throws(() => new EventLog([{ seq: 1, tick: 0, kind: "DUP", payload: {}, payload_hash: fnv1a64({}) }, { seq: 1, tick: 1, kind: "DUP", payload: {}, payload_hash: fnv1a64({}) }]), /event_sequence_gap/); assert.throws(() => first.checkpoint(3, 8, { tick: 8 }), /checkpoint_cursor_mismatch/); assert.throws(() => first.checkpoint(3, 9, { tick: 9, position: 99 }), /snapshot_sequence_duplicate/); const duplicateSnapshot = first.export(); duplicateSnapshot.snapshots.snapshots.push({ ...duplicateSnapshot.snapshots.snapshots[1] }); assert.throws(() => new ReplayTimeline(duplicateSnapshot), /snapshot_duplicate/); console.log("replay=passed cursor=" + replay.cursor + " tick=" + replay.tick);