summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/tests')
-rw-r--r--packages/frontend/tests/snapshot-sequencer.test.ts92
1 files changed, 92 insertions, 0 deletions
diff --git a/packages/frontend/tests/snapshot-sequencer.test.ts b/packages/frontend/tests/snapshot-sequencer.test.ts
new file mode 100644
index 0000000..f2c5b8e
--- /dev/null
+++ b/packages/frontend/tests/snapshot-sequencer.test.ts
@@ -0,0 +1,92 @@
+import { describe, expect, it } from "vitest";
+import { SnapshotSequencer } from "../src/lib/snapshot-sequencer.js";
+
+describe("SnapshotSequencer", () => {
+ it("accepts the first response unconditionally", () => {
+ const s = new SnapshotSequencer();
+ const seq = s.begin();
+ expect(s.accept(seq)).toBe(true);
+ });
+
+ it("accepts responses in send order", () => {
+ const s = new SnapshotSequencer();
+ const a = s.begin();
+ const b = s.begin();
+ const c = s.begin();
+ expect(s.accept(a)).toBe(true);
+ expect(s.accept(b)).toBe(true);
+ expect(s.accept(c)).toBe(true);
+ });
+
+ it("rejects an older response that arrives AFTER a newer one (the core race)", () => {
+ // Sequence: user clicks hour 9 (A), then hour 10 (B). B arrives first.
+ const s = new SnapshotSequencer();
+ const a = s.begin(); // toggle hour 9
+ const b = s.begin(); // toggle hour 10
+
+ // B arrives first — applied.
+ expect(s.accept(b)).toBe(true);
+
+ // A arrives later — must be dropped, else the snapshot from B (which
+ // knows about both 9 and 10) gets overwritten by A's stale snapshot
+ // (which only knows about 9), and hour 10 vanishes from the UI.
+ expect(s.accept(a)).toBe(false);
+ });
+
+ it("rejects ALL straggler responses once a newer one wins", () => {
+ const s = new SnapshotSequencer();
+ const a = s.begin();
+ const b = s.begin();
+ const c = s.begin();
+ expect(s.accept(c)).toBe(true);
+ expect(s.accept(b)).toBe(false);
+ expect(s.accept(a)).toBe(false);
+ });
+
+ it("handles the initial-load vs first-click race", () => {
+ // On mount: $effect fires loadFromServer (seq=1).
+ // Before it lands, user clicks a hour (seq=2).
+ const s = new SnapshotSequencer();
+ const initial = s.begin();
+ const click = s.begin();
+
+ // Click response arrives first — applied.
+ expect(s.accept(click)).toBe(true);
+ // Initial load straggles in — must be dropped (it pre-dates the click).
+ expect(s.accept(initial)).toBe(false);
+ });
+
+ it("treats an equal seq as accept (idempotent re-arrival of the winner)", () => {
+ const s = new SnapshotSequencer();
+ const a = s.begin();
+ expect(s.accept(a)).toBe(true);
+ // Defensive: same seq accepted again (shouldn't happen in practice
+ // but the semantics must be 'no-op accept', not 'reject').
+ expect(s.accept(a)).toBe(true);
+ });
+
+ it("seq numbers are monotonic and unique across begin() calls", () => {
+ const s = new SnapshotSequencer();
+ const seen = new Set<number>();
+ let prev = 0;
+ for (let i = 0; i < 100; i++) {
+ const seq = s.begin();
+ expect(seq).toBeGreaterThan(prev);
+ expect(seen.has(seq)).toBe(false);
+ seen.add(seq);
+ prev = seq;
+ }
+ });
+
+ it("state inspector reflects last-applied watermark", () => {
+ const s = new SnapshotSequencer();
+ expect(s.state).toEqual({ nextSeq: 0, latestApplied: 0 });
+ const a = s.begin();
+ const b = s.begin();
+ s.accept(b);
+ expect(s.state).toEqual({ nextSeq: 2, latestApplied: b });
+ // A is too old now — accept() returns false and watermark doesn't move back.
+ expect(s.accept(a)).toBe(false);
+ expect(s.state.latestApplied).toBe(b);
+ });
+});