summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/tests/snapshot-sequencer.test.ts
blob: f2c5b8e3b9e79f829093314166995c2465b9278e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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);
	});
});