@effectstream/runtime 0.100.17 → 0.100.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,197 @@
1
+ /**
2
+ * E2E database-consistency checks over the synthetic `test` chain.
3
+ *
4
+ * The database the engine builds must be a pure function of the source blocks —
5
+ * independent of *how* it got there. Two scenarios:
6
+ *
7
+ * A. Determinism baseline — two from-scratch syncs to the same height produce
8
+ * identical databases. Expected to PASS (proves reproducibility).
9
+ *
10
+ * B. Resume guarantee — a sync that stops mid-chunk and resumes to N must
11
+ * match a clean sync to N. Regression test for block-accurate resume
12
+ * (see @effectstream/sync CLAUDE.md, design idea #5): a restart must
13
+ * re-fetch (committed, chunk_end] instead of silently skipping it.
14
+ *
15
+ * Each node runs in its own subprocess (see harness.ts / node-runner.ts): a
16
+ * restart is a genuine new OS process whose in-memory Deque is gone while the
17
+ * database keeps every committed row — exactly the production restart condition.
18
+ *
19
+ * These tests assert a property of the **production** database path, so they run
20
+ * only against real Postgres — i.e. only when `PGLITE=false` and `postgresql@18`
21
+ * is installed. In the default PGLite mode (or if Postgres isn't installed) they
22
+ * skip rather than run, since PGLite is not the backend whose fidelity these
23
+ * guarantees are about.
24
+ */
25
+ import { expect, test } from "bun:test";
26
+ import { postgresAvailable, setupHarness } from "./harness.ts";
27
+ import {
28
+ compareConsistencySnapshots,
29
+ type ConsistencySnapshot,
30
+ dumpConsistencySnapshot,
31
+ type SnapshotDiff,
32
+ type SnapshotOpts,
33
+ SYNC_BOOKKEEPING_TABLES,
34
+ } from "./consistency-snapshot.ts";
35
+
36
+ // Postgres-only: run only when PGLITE=false and Postgres is present.
37
+ const canRun = process.env.PGLITE === "false" && postgresAvailable();
38
+ const it = canRun ? test : test.skip;
39
+
40
+ /** "<n> tables, <m> rows". */
41
+ function summarize(snap: ConsistencySnapshot): string {
42
+ let rows = 0;
43
+ for (const f of snap.values()) rows += f.rowCount;
44
+ return `${snap.size} tables, ${rows} rows`;
45
+ }
46
+
47
+ /** e.g. "effectstream.primitive_accounting [row-count A=1 B=0]". */
48
+ function formatDiffs(diffs: SnapshotDiff[]): string {
49
+ return diffs
50
+ .map((d) => `${d.table} [${d.reason} A=${d.aRows ?? "—"} B=${d.bRows ?? "—"}]`)
51
+ .join(", ");
52
+ }
53
+
54
+ /** Log the two snapshots and their comparison result. */
55
+ function logComparison(
56
+ tag: string,
57
+ label: string,
58
+ aLabel: string,
59
+ a: ConsistencySnapshot,
60
+ bLabel: string,
61
+ b: ConsistencySnapshot,
62
+ diffs: SnapshotDiff[],
63
+ ): void {
64
+ console.log(`${tag} ${label}`);
65
+ console.log(`${tag} ${aLabel}: ${summarize(a)}`);
66
+ console.log(`${tag} ${bLabel}: ${summarize(b)}`);
67
+ console.log(
68
+ diffs.length === 0
69
+ ? `${tag} → IDENTICAL (0 differences)`
70
+ : `${tag} → ${diffs.length} difference(s): ${formatDiffs(diffs)}`,
71
+ );
72
+ }
73
+
74
+ it("two from-scratch syncs to the same height produce identical databases", async () => {
75
+ const events = [
76
+ { atBlock: 25, marker: "e25" },
77
+ { atBlock: 50, marker: "e50" },
78
+ { atBlock: 75, marker: "e75" },
79
+ ];
80
+
81
+ // A fresh harness == a fresh database == "syncing again from scratch".
82
+ const runFromScratch = async (
83
+ apiPort: number,
84
+ ): Promise<ConsistencySnapshot> => {
85
+ const h = await setupHarness({ backend: "postgres" });
86
+ try {
87
+ await h.runToHeight({
88
+ events,
89
+ tips: { mainClock: 100, parallelP: 200 },
90
+ target: 100,
91
+ apiPort,
92
+ });
93
+ return await dumpConsistencySnapshot(h.pool);
94
+ } finally {
95
+ await h.teardown();
96
+ }
97
+ };
98
+
99
+ const snapA = await runFromScratch(19140);
100
+ const snapB = await runFromScratch(19141);
101
+
102
+ const { diffs } = compareConsistencySnapshots(snapA, snapB);
103
+ logComparison(
104
+ "[consistency A]",
105
+ "two from-scratch syncs to height 100:",
106
+ "run A",
107
+ snapA,
108
+ "run B",
109
+ snapB,
110
+ diffs,
111
+ );
112
+ expect(diffs).toEqual([]);
113
+ }, 120_000);
114
+
115
+ it(
116
+ "a mid-chunk restart resumes to the same database as a clean sync",
117
+ async () => {
118
+ // One event inside the gap the restart will skip: it must fall in
119
+ // (phase-1 committed height 10, parallel page 50] AND be ≤ the phase-2 main
120
+ // tip 30, so a *correct* engine would re-fetch and commit it after resume.
121
+ const events = [{ atBlock: 20, marker: "gap20" }];
122
+ // Large parallel stepSize => the whole tip is fetched as ONE chunk, so its
123
+ // page row persists at page_number = tip (the chunk-granular watermark that
124
+ // Fix D wrongly resumes from).
125
+ const parallelStepSize = 1000;
126
+ // Sync bookkeeping (page queue, version/migration history) legitimately
127
+ // differs after a restart even in a correct engine; compare observable
128
+ // state only.
129
+ const dumpOpts: SnapshotOpts = { excludeTables: SYNC_BOOKKEEPING_TABLES };
130
+
131
+ // Reference: one clean sync straight to height 30. Parallel stays ahead of
132
+ // the main clock so the final main block can finalize (the merge gates block
133
+ // T on the parallel page advancing past T). Contains the block-20 event.
134
+ const ref = await setupHarness({ backend: "postgres" });
135
+ let refSnap: ConsistencySnapshot;
136
+ try {
137
+ await ref.runToHeight({
138
+ events,
139
+ parallelStepSize,
140
+ tips: { mainClock: 30, parallelP: 50 },
141
+ target: 30,
142
+ apiPort: 19150,
143
+ });
144
+ refSnap = await dumpConsistencySnapshot(ref.pool, dumpOpts);
145
+ } finally {
146
+ await ref.teardown();
147
+ }
148
+
149
+ // Resumed: one database, two subprocess runs (= a real restart).
150
+ const resumed = await setupHarness({ backend: "postgres" });
151
+ let resumeSnap: ConsistencySnapshot;
152
+ try {
153
+ // Phase 1: the parallel fetcher (no backpressure) races to its own tip
154
+ // (50) into the in-memory Deque, but the main clock only drives commits to
155
+ // height 10 — so parallel blocks 11..50 (incl. the event at 20) are fetched
156
+ // but never committed, and the in-memory Deque is lost when the process
157
+ // exits. The runtime persists a block-accurate resume marker at the last
158
+ // COMMITTED parallel block (≤ 10), not the fetched-ahead tip.
159
+ await resumed.runToHeight({
160
+ events,
161
+ parallelStepSize,
162
+ tips: { mainClock: 10, parallelP: 50 },
163
+ target: 10,
164
+ apiPort: 19151,
165
+ });
166
+
167
+ // Phase 2 (restart): a new process boots against the same database. Its
168
+ // Deque is empty; restoreState reads the persisted resume marker (the last
169
+ // committed parallel block ≤ 10) and resumes parallel from there. The main
170
+ // clock now advances to 30, so the parallel blocks it needs (11..30,
171
+ // including the event at 20) are re-fetched and committed.
172
+ await resumed.runToHeight({
173
+ events,
174
+ parallelStepSize,
175
+ tips: { mainClock: 30, parallelP: 50 },
176
+ target: 30,
177
+ apiPort: 19152,
178
+ });
179
+ resumeSnap = await dumpConsistencySnapshot(resumed.pool, dumpOpts);
180
+ } finally {
181
+ await resumed.teardown();
182
+ }
183
+
184
+ const { diffs } = compareConsistencySnapshots(refSnap, resumeSnap);
185
+ logComparison(
186
+ "[consistency B]",
187
+ "clean sync vs mid-chunk restart, both to height 30:",
188
+ "clean ref",
189
+ refSnap,
190
+ "resumed ",
191
+ resumeSnap,
192
+ diffs,
193
+ );
194
+ expect(diffs).toEqual([]);
195
+ },
196
+ 120_000,
197
+ );