amalgm 0.1.152 → 0.1.153

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,394 @@
1
+ 'use strict';
2
+
3
+ // Versioned-write contract + unversioned-overwrite preservation.
4
+ //
5
+ // The silent-loss bug this file exists for: reconcileFromDisk's merge gate
6
+ // (`ours !== base`) only protects live edits still inside the write-back
7
+ // debounce window. Once a human edit FLUSHES (live == lastDiskText == B), an
8
+ // external writer holding a stale read A can write C and the gate sees no
9
+ // divergence — C applies wholesale and B's edits vanish with no merge, no
10
+ // sidecar, no event. The tests here pin the fix: flushed live-channel edits
11
+ // are either preserved in the resulting text or recoverably preserved in a
12
+ // conflict sidecar and surfaced on the doc event.
13
+ //
14
+ // It also pins the versioned write path (writeDocText / POST
15
+ // /state/docs/write): a writer that declares the base it read (baseHash /
16
+ // baseSeq) gets a true three-way merge against the retained disk-sync
17
+ // history; an unknown base degrades conservatively (preserve both, surface a
18
+ // conflict), never to a silent winner.
19
+
20
+ const test = require('node:test');
21
+ const assert = require('node:assert/strict');
22
+ const crypto = require('crypto');
23
+ const fs = require('fs');
24
+ const os = require('os');
25
+ const path = require('path');
26
+
27
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-docs-versioning-test-'));
28
+ process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
29
+
30
+ const Y = require('yjs');
31
+ const { closeLocalDb } = require('../state/db');
32
+ const docs = require('../state/docs');
33
+ const { listEventsAfter, currentSeq } = require('../state/events');
34
+
35
+ const docDir = path.join(tempRoot, 'workspace');
36
+ fs.mkdirSync(docDir, { recursive: true });
37
+
38
+ function writeDoc(name, text) {
39
+ const target = path.join(docDir, name);
40
+ fs.writeFileSync(target, text, 'utf8');
41
+ return target;
42
+ }
43
+
44
+ function sleep(ms) {
45
+ return new Promise((resolve) => setTimeout(resolve, ms));
46
+ }
47
+
48
+ function sha256(text) {
49
+ return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
50
+ }
51
+
52
+ function clientFromState(stateBase64) {
53
+ const doc = new Y.Doc();
54
+ Y.applyUpdate(doc, new Uint8Array(Buffer.from(stateBase64, 'base64')));
55
+ return doc;
56
+ }
57
+
58
+ function encodeLocalEdit(doc, mutate) {
59
+ let captured = null;
60
+ const listener = (update) => { captured = update; };
61
+ doc.on('update', listener);
62
+ mutate(doc.getText('content'));
63
+ doc.off('update', listener);
64
+ assert.ok(captured, 'edit should produce an update');
65
+ return Buffer.from(captured).toString('base64');
66
+ }
67
+
68
+ function sidecarsFor(target) {
69
+ const ext = path.extname(target);
70
+ const stem = path.basename(target, ext);
71
+ const pattern = new RegExp(
72
+ `^${stem}\\.conflicted-\\d{8}-\\d{6}(?:-\\d+)?${ext.replace('.', '\\.')}$`,
73
+ );
74
+ return fs.readdirSync(path.dirname(target))
75
+ .filter((name) => pattern.test(name))
76
+ .map((name) => path.join(path.dirname(target), name));
77
+ }
78
+
79
+ function conflictEventsFor(resource, afterSeq) {
80
+ return listEventsAfter(afterSeq).filter((e) => e.resource === resource && e.patch?.conflict);
81
+ }
82
+
83
+ test.after(() => {
84
+ docs.closeAllDocs();
85
+ closeLocalDb();
86
+ fs.rmSync(tempRoot, { recursive: true, force: true });
87
+ });
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // The confirmed silent-loss sequence (step 1: this test MUST fail against the
91
+ // pre-fix code).
92
+ // ---------------------------------------------------------------------------
93
+
94
+ test('flushed human edits survive a stale external rewrite (preserved or sidecar + event)', async () => {
95
+ const versionA = 'l1\nl2\nl3\n';
96
+ const target = writeDoc('stale-rewrite.md', versionA);
97
+ const opened = docs.openDoc(target);
98
+ const client = clientFromState(opened.state);
99
+
100
+ // Human edits l2 through the live channel…
101
+ docs.applyDocUpdates(target, [
102
+ encodeLocalEdit(client, (ytext) => ytext.insert('l1\nl2'.length, ' human')),
103
+ ]);
104
+ // …and the edit FLUSHES: disk == live == lastDiskText == B. This is the
105
+ // window the old merge gate does not protect.
106
+ await sleep(500);
107
+ const versionB = 'l1\nl2 human\nl3\n';
108
+ assert.equal(fs.readFileSync(target, 'utf8'), versionB, 'precondition: human edit flushed to disk');
109
+
110
+ const before = currentSeq();
111
+ // An agent that read version A (stale) writes C — derived from A, without
112
+ // the human's l2 edit — directly to disk.
113
+ const versionC = 'l1\nl2\nl3 agent\n';
114
+ fs.writeFileSync(target, versionC, 'utf8');
115
+ await sleep(700);
116
+
117
+ const text = docs.getDocText(target);
118
+ const humanEditInText = text.includes('l2 human');
119
+ const sidecars = sidecarsFor(target);
120
+ const humanEditInSidecar = sidecars.some((p) => fs.readFileSync(p, 'utf8').includes('l2 human'));
121
+ const conflicts = conflictEventsFor(opened.resource, before);
122
+
123
+ assert.ok(
124
+ humanEditInText || (humanEditInSidecar && conflicts.length >= 1),
125
+ 'the human\'s flushed edit must be preserved in the text, or recoverably preserved '
126
+ + `in a sidecar AND surfaced as a conflict event (text=${JSON.stringify(text)}, `
127
+ + `sidecars=${sidecars.length}, conflictEvents=${conflicts.length})`,
128
+ );
129
+ });
130
+
131
+ test('unversioned overwrite of flushed live edits: incoming applies, replaced text sidecar\'d, event flagged', async () => {
132
+ const versionA = 'a1\na2\na3\n';
133
+ const target = writeDoc('overwrite-flagged.md', versionA);
134
+ const opened = docs.openDoc(target);
135
+ const client = clientFromState(opened.state);
136
+
137
+ docs.applyDocUpdates(target, [
138
+ encodeLocalEdit(client, (ytext) => ytext.insert('a1\na2'.length, ' human')),
139
+ ]);
140
+ await sleep(500);
141
+ const versionB = 'a1\na2 human\na3\n';
142
+ assert.equal(fs.readFileSync(target, 'utf8'), versionB);
143
+
144
+ const before = currentSeq();
145
+ const versionC = 'rewritten\nwholesale\n';
146
+ fs.writeFileSync(target, versionC, 'utf8');
147
+ await sleep(700);
148
+
149
+ // Raw fs writers keep the freedom to rewrite files: the incoming text wins…
150
+ assert.equal(docs.getDocText(target), versionC);
151
+ // …but the replaced live-channel text is preserved…
152
+ const sidecars = sidecarsFor(target);
153
+ assert.equal(sidecars.length, 1, 'exactly one sidecar for the replaced text');
154
+ assert.equal(fs.readFileSync(sidecars[0], 'utf8'), versionB, 'sidecar holds the replaced flushed text');
155
+ // …and the overwrite is surfaced, not silent.
156
+ const conflicts = conflictEventsFor(opened.resource, before);
157
+ assert.equal(conflicts.length, 1);
158
+ assert.equal(conflicts[0].patch.conflict.reason, 'unversioned-overwrite');
159
+ assert.equal(conflicts[0].patch.conflict.sidecarPath, sidecars[0]);
160
+ });
161
+
162
+ test('agent rewrites with no live edits since last sync stay silent: no sidecar, no conflict', async () => {
163
+ const target = writeDoc('agent-rewrites.md', 'r1\n');
164
+ const opened = docs.openDoc(target);
165
+ const before = currentSeq();
166
+
167
+ // Two successive raw rewrites with nothing arriving through the live
168
+ // channel in between — the overwhelmingly common agent case.
169
+ fs.writeFileSync(target, 'r1\nr2\n', 'utf8');
170
+ await sleep(700);
171
+ fs.writeFileSync(target, 'r1\nr2\nr3\n', 'utf8');
172
+ await sleep(700);
173
+
174
+ assert.equal(docs.getDocText(target), 'r1\nr2\nr3\n');
175
+ assert.equal(sidecarsFor(target).length, 0, 'no sidecar spam for plain agent rewrites');
176
+ assert.equal(conflictEventsFor(opened.resource, before).length, 0, 'no conflict events either');
177
+ });
178
+
179
+ test('unflushed live edits still merge exactly as before (step-5 regression guard)', async () => {
180
+ const base = 'k1\nk2\nk3\nk4\nk5\n';
181
+ const target = writeDoc('unflushed-window.md', base);
182
+ const opened = docs.openDoc(target);
183
+ const client = clientFromState(opened.state);
184
+
185
+ // Live edit NOT yet flushed (inside the 300ms debounce)…
186
+ docs.applyDocUpdates(target, [
187
+ encodeLocalEdit(client, (ytext) => ytext.insert('k1'.length, ' human')),
188
+ ]);
189
+ // …and a disjoint external write lands immediately.
190
+ fs.writeFileSync(target, 'k1\nk2\nk3\nk4\nk5 agent\n', 'utf8');
191
+ await sleep(900);
192
+
193
+ assert.equal(docs.getDocText(target), 'k1 human\nk2\nk3\nk4\nk5 agent\n');
194
+ assert.equal(sidecarsFor(target).length, 0);
195
+ });
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Versioned writes: the SDK contract (writeDocText / POST /state/docs/write).
199
+ // ---------------------------------------------------------------------------
200
+
201
+ test('open returns the sha256 a versioned writer sends back as baseHash', () => {
202
+ const target = writeDoc('hash-handshake.md', 'h1\n');
203
+ const opened = docs.openDoc(target);
204
+ assert.equal(opened.sha256, sha256('h1\n'));
205
+ });
206
+
207
+ test('versioned write whose base matches the live text applies as-is (fast path)', () => {
208
+ const target = writeDoc('fastpath.md', 'f1\nf2\n');
209
+ docs.openDoc(target);
210
+ const result = docs.writeDocText(target, 'f1\nf2\nf3\n', { baseHash: sha256('f1\nf2\n') });
211
+ assert.equal(result.merge, 'clean');
212
+ assert.equal(result.conflict, null);
213
+ assert.equal(result.sha256, sha256('f1\nf2\nf3\n'));
214
+ assert.equal(docs.getDocText(target), 'f1\nf2\nf3\n');
215
+ assert.equal(sidecarsFor(target).length, 0);
216
+ });
217
+
218
+ test('versioned write from a stale base three-way merges: non-overlapping edits both land', async () => {
219
+ const versionA = 'v1\nv2\nv3\nv4\nv5\n';
220
+ const target = writeDoc('versioned-merge.md', versionA);
221
+ const opened = docs.openDoc(target);
222
+ const client = clientFromState(opened.state);
223
+ const baseHash = opened.sha256;
224
+
225
+ // Human edits line 1 and the edit flushes — the exact window the raw-disk
226
+ // path could not protect.
227
+ docs.applyDocUpdates(target, [
228
+ encodeLocalEdit(client, (ytext) => ytext.insert('v1'.length, ' human')),
229
+ ]);
230
+ await sleep(500);
231
+
232
+ const before = currentSeq();
233
+ // The writer read version A; its rewrite touches only line 5.
234
+ const result = docs.writeDocText(target, 'v1\nv2\nv3\nv4\nv5 agent\n', { baseHash });
235
+
236
+ assert.equal(result.merge, 'merged');
237
+ assert.equal(result.conflict, null);
238
+ assert.equal(docs.getDocText(target), 'v1 human\nv2\nv3\nv4\nv5 agent\n');
239
+ assert.equal(result.sha256, sha256('v1 human\nv2\nv3\nv4\nv5 agent\n'));
240
+ assert.equal(sidecarsFor(target).length, 0, 'clean merge writes no sidecar');
241
+ assert.equal(conflictEventsFor(opened.resource, before).length, 0);
242
+
243
+ await sleep(500);
244
+ assert.equal(fs.readFileSync(target, 'utf8'), 'v1 human\nv2\nv3\nv4\nv5 agent\n');
245
+ });
246
+
247
+ test('versioned write with overlapping edits conflicts: OURS kept, THEIRS sidecar\'d, event published', async () => {
248
+ const versionA = 'c1\nc2\nc3\n';
249
+ const target = writeDoc('versioned-conflict.md', versionA);
250
+ const opened = docs.openDoc(target);
251
+ const client = clientFromState(opened.state);
252
+ const baseHash = opened.sha256;
253
+
254
+ docs.applyDocUpdates(target, [
255
+ encodeLocalEdit(client, (ytext) => ytext.insert('c1\nc2'.length, ' human')),
256
+ ]);
257
+ await sleep(500);
258
+
259
+ const before = currentSeq();
260
+ const theirs = 'c1\nc2 agent\nc3\n';
261
+ const result = docs.writeDocText(target, theirs, { baseHash });
262
+
263
+ assert.equal(result.merge, 'conflict');
264
+ assert.equal(result.conflict.reason, 'overlapping-hunks');
265
+ assert.equal(result.conflict.hunks, 1);
266
+ // Both versions present: OURS in the live text, THEIRS in the sidecar.
267
+ assert.equal(docs.getDocText(target), 'c1\nc2 human\nc3\n');
268
+ const sidecars = sidecarsFor(target);
269
+ assert.equal(sidecars.length, 1);
270
+ assert.equal(fs.readFileSync(sidecars[0], 'utf8'), theirs);
271
+ assert.equal(result.conflict.sidecarPath, sidecars[0]);
272
+
273
+ const conflicts = conflictEventsFor(opened.resource, before);
274
+ assert.equal(conflicts.length, 1);
275
+ assert.equal(conflicts[0].patch.conflict.reason, 'overlapping-hunks');
276
+ });
277
+
278
+ test('versioned write with an unknown base is conservative: live text sidecar\'d, write applies, base-unknown surfaced', async () => {
279
+ const target = writeDoc('base-unknown.md', 'u1\nu2\n');
280
+ const opened = docs.openDoc(target);
281
+ const client = clientFromState(opened.state);
282
+
283
+ docs.applyDocUpdates(target, [
284
+ encodeLocalEdit(client, (ytext) => ytext.insert('u1'.length, ' human')),
285
+ ]);
286
+ await sleep(500);
287
+ const live = 'u1 human\nu2\n';
288
+
289
+ const before = currentSeq();
290
+ const result = docs.writeDocText(target, 'incoming rewrite\n', {
291
+ baseHash: sha256('some text this doc never contained\n'),
292
+ });
293
+
294
+ assert.equal(result.merge, 'base-unknown');
295
+ assert.equal(result.conflict.reason, 'base-unknown');
296
+ // Writer keeps the freedom to rewrite; the live text is recoverable.
297
+ assert.equal(docs.getDocText(target), 'incoming rewrite\n');
298
+ const sidecars = sidecarsFor(target);
299
+ assert.equal(sidecars.length, 1);
300
+ assert.equal(fs.readFileSync(sidecars[0], 'utf8'), live, 'sidecar preserves the replaced live text');
301
+ const conflicts = conflictEventsFor(opened.resource, before);
302
+ assert.equal(conflicts.length, 1);
303
+ assert.equal(conflicts[0].patch.conflict.reason, 'base-unknown');
304
+ });
305
+
306
+ test('versioned write via baseSeq resolves against the retained history', async () => {
307
+ const versionA = 's1\ns2\ns3\n';
308
+ const target = writeDoc('base-seq.md', versionA);
309
+ const opened = docs.openDoc(target);
310
+ const client = clientFromState(opened.state);
311
+ const entry = docs._private.openDocs.get(opened.path);
312
+ const baseSeq = entry.diskHistory[entry.diskHistory.length - 1].seq;
313
+
314
+ docs.applyDocUpdates(target, [
315
+ encodeLocalEdit(client, (ytext) => ytext.insert('s1'.length, ' human')),
316
+ ]);
317
+ await sleep(500);
318
+
319
+ const result = docs.writeDocText(target, 's1\ns2\ns3 agent\n', { baseSeq });
320
+ assert.equal(result.merge, 'merged');
321
+ assert.equal(docs.getDocText(target), 's1 human\ns2\ns3 agent\n');
322
+ });
323
+
324
+ test('unversioned API write over live edits preserves them; over synced text applies silently', async () => {
325
+ const target = writeDoc('no-base.md', 'n1\n');
326
+ const opened = docs.openDoc(target);
327
+ const client = clientFromState(opened.state);
328
+
329
+ // No live edits since last sync: applies silently, like an agent rewrite.
330
+ const beforeQuiet = currentSeq();
331
+ const quiet = docs.writeDocText(target, 'n1\nn2\n', {});
332
+ assert.equal(quiet.merge, 'overwrite');
333
+ assert.equal(quiet.conflict, null);
334
+ assert.equal(sidecarsFor(target).length, 0);
335
+ assert.equal(conflictEventsFor(opened.resource, beforeQuiet).length, 0);
336
+
337
+ // Live edit flushed, then a no-base write replaces it: preserve + surface.
338
+ docs.applyDocUpdates(target, [
339
+ encodeLocalEdit(client, (ytext) => ytext.insert('n1'.length, ' human')),
340
+ ]);
341
+ await sleep(500);
342
+ const live = 'n1 human\nn2\n';
343
+ const before = currentSeq();
344
+ const result = docs.writeDocText(target, 'replaced\n', {});
345
+ assert.equal(result.merge, 'overwrite');
346
+ assert.equal(result.conflict.reason, 'unversioned-overwrite');
347
+ assert.equal(docs.getDocText(target), 'replaced\n');
348
+ const sidecars = sidecarsFor(target);
349
+ assert.equal(sidecars.length, 1);
350
+ assert.equal(fs.readFileSync(sidecars[0], 'utf8'), live);
351
+ assert.equal(conflictEventsFor(opened.resource, before).length, 1);
352
+ });
353
+
354
+ test('write validation is shape-explicit with example payloads', () => {
355
+ const target = writeDoc('validation.md', 'x\n');
356
+ docs.openDoc(target);
357
+ assert.throws(() => docs.writeDocText(target, 42, {}), /"text" must be[\s\S]*e\.g\./);
358
+ assert.throws(
359
+ () => docs.writeDocText(target, 'ok\n', { baseHash: 'not-a-hash' }),
360
+ /"baseHash" must be[\s\S]*sha256[\s\S]*e\.g\./,
361
+ );
362
+ assert.throws(
363
+ () => docs.writeDocText(target, 'ok\n', { baseSeq: -3 }),
364
+ /"baseSeq" must be[\s\S]*e\.g\./,
365
+ );
366
+ assert.throws(
367
+ () => docs.writeDocText(target, 'ok\n', { baseSeq: 1.5 }),
368
+ /"baseSeq" must be/,
369
+ );
370
+ });
371
+
372
+ test('writing identical text is a no-op: no sidecar, no event, no disk churn', async () => {
373
+ const target = writeDoc('idempotent.md', 'same\n');
374
+ const opened = docs.openDoc(target);
375
+ const before = currentSeq();
376
+ const result = docs.writeDocText(target, 'same\n', { baseHash: sha256('same\n') });
377
+ assert.equal(result.merge, 'noop');
378
+ await sleep(700);
379
+ assert.equal(listEventsAfter(before).filter((e) => e.resource === opened.resource).length, 0);
380
+ assert.equal(sidecarsFor(target).length, 0);
381
+ });
382
+
383
+ test('disk-sync history is bounded', async () => {
384
+ const target = writeDoc('bounded.md', 'gen 0\n');
385
+ const opened = docs.openDoc(target);
386
+ const entry = docs._private.openDocs.get(opened.path);
387
+ for (let i = 1; i <= 12; i += 1) {
388
+ docs.writeDocText(target, `gen ${i}\n`, { baseHash: sha256(`gen ${i - 1}\n`) });
389
+ await sleep(400); // let each write flush so history records each state
390
+ }
391
+ assert.ok(entry.diskHistory.length <= 8, `history stays bounded (got ${entry.diskHistory.length})`);
392
+ const head = entry.diskHistory[entry.diskHistory.length - 1];
393
+ assert.equal(head.text, 'gen 12\n', 'head is always the current disk-sync state');
394
+ });
@@ -0,0 +1,223 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const { EventEmitter } = require('events');
9
+
10
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-stream-bootstrap-test-'));
11
+ process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
12
+
13
+ const { closeLocalDb } = require('../state/db');
14
+ const { appendStateEvent, currentSeq } = require('../state/events');
15
+ const registry = require('../state/registry');
16
+ const rest = require('../state/rest');
17
+
18
+ test.after(() => {
19
+ closeLocalDb();
20
+ fs.rmSync(tempRoot, { recursive: true, force: true });
21
+ });
22
+
23
+ // Test resources. `midBuildEmits` makes reads append state events while the
24
+ // snapshot is being built — the "write lands mid-read" scenario the snapshot
25
+ // retry loop exists for, and the only way anything can interleave with the
26
+ // synchronous build.
27
+ let midBuildEmits = 0;
28
+ registry.registerResource('boot:a', { read: () => [{ id: 'a' }] });
29
+ registry.registerResource('boot:b', { read: () => [{ id: 'b' }] });
30
+ registry.registerResource('boot:mid', {
31
+ read: () => {
32
+ if (midBuildEmits > 0) {
33
+ midBuildEmits -= 1;
34
+ appendStateEvent({ resource: 'boot:other', op: 'replace', value: [], source: 'mid-build' });
35
+ }
36
+ return [{ id: 'mid' }];
37
+ },
38
+ });
39
+ registry.registerResource('boot:throw', {
40
+ read: () => {
41
+ throw new Error('kaboom');
42
+ },
43
+ });
44
+
45
+ function fakeRequest() {
46
+ return new EventEmitter();
47
+ }
48
+
49
+ function fakeResponse() {
50
+ const res = new EventEmitter();
51
+ res.writes = [];
52
+ res.destroyed = false;
53
+ res.writableEnded = false;
54
+ res.writeHead = () => res;
55
+ res.write = (chunk) => {
56
+ res.writes.push(String(chunk));
57
+ return true;
58
+ };
59
+ res.end = () => {
60
+ res.writableEnded = true;
61
+ };
62
+ return res;
63
+ }
64
+
65
+ // SSE frames are delimited by a blank line; a frame may span writes (control
66
+ // frames are two writes), so parse the concatenated output.
67
+ function framesOf(res) {
68
+ return res.writes
69
+ .join('')
70
+ .split('\n\n')
71
+ .filter(Boolean)
72
+ .map((raw) => {
73
+ const frame = { raw };
74
+ for (const line of raw.split('\n')) {
75
+ if (line.startsWith(':')) frame.comment = line.slice(1).trim();
76
+ else if (line.startsWith('event: ')) frame.event = line.slice('event: '.length);
77
+ else if (line.startsWith('id: ')) frame.id = Number(line.slice('id: '.length));
78
+ else if (line.startsWith('data: ')) frame.data = JSON.parse(line.slice('data: '.length));
79
+ }
80
+ return frame;
81
+ });
82
+ }
83
+
84
+ function dataFramesOf(res) {
85
+ return framesOf(res).filter((frame) => frame.data !== undefined);
86
+ }
87
+
88
+ function stateFramesOf(res) {
89
+ return framesOf(res).filter((frame) => frame.event === 'state');
90
+ }
91
+
92
+ test('bootstrap=1: snapshot is the first data frame, correct seq, requested resources only', () => {
93
+ // Backlog exists; legacy would replay it. Bootstrap must not.
94
+ appendStateEvent({ resource: 'boot:other', op: 'replace', value: [], source: 'test' });
95
+ appendStateEvent({ resource: 'boot:other', op: 'replace', value: [], source: 'test' });
96
+
97
+ const req = fakeRequest();
98
+ const res = fakeResponse();
99
+ rest.handleStream(req, res, { bootstrap: '1', resources: 'boot:a' });
100
+
101
+ const frames = framesOf(res);
102
+ assert.equal(frames[0].comment, 'local-live-store connected', 'hello comment first');
103
+ assert.equal(frames[0].data, undefined);
104
+
105
+ const data = dataFramesOf(res);
106
+ assert.equal(data[0].event, 'snapshot', 'snapshot is the first data frame');
107
+ assert.equal(data[0].data.seq, currentSeq(), 'snapshot seq is the current seq');
108
+ assert.deepEqual(data[0].data.resources, { 'boot:a': [{ id: 'a' }] }, 'only the requested resource');
109
+ assert.equal(stateFramesOf(res).length, 0, 'no backlog replay in bootstrap mode');
110
+
111
+ const live = appendStateEvent({ resource: 'boot:other', op: 'replace', value: [], source: 'test' });
112
+ const states = stateFramesOf(res);
113
+ assert.equal(states.length, 1, 'live events flow after the snapshot');
114
+ assert.equal(states[0].id, live.seq);
115
+
116
+ req.emit('close');
117
+ });
118
+
119
+ test('an event arriving mid-build is buffered and flushed after the snapshot frame, exactly once, in seq order', () => {
120
+ const before = currentSeq();
121
+ // Every snapshot attempt sees a write land mid-read: three attempts append
122
+ // seq before+1..before+3 and the build serves best-effort at before+2
123
+ // (attempt 3's start). before+3 is newer than the snapshot and must be
124
+ // flushed after it; before+1/+2 are inside it and must not.
125
+ midBuildEmits = 3;
126
+
127
+ const req = fakeRequest();
128
+ const res = fakeResponse();
129
+ rest.handleStream(req, res, { bootstrap: '1', resources: 'boot:mid' });
130
+
131
+ assert.equal(midBuildEmits, 0, 'the build consumed all three mid-read writes');
132
+ const data = dataFramesOf(res);
133
+ assert.equal(data[0].event, 'snapshot', 'snapshot frame still first');
134
+ assert.equal(data[0].data.seq, before + 2);
135
+
136
+ const states = stateFramesOf(res);
137
+ assert.deepEqual(states.map((frame) => frame.id), [before + 3], 'exactly the one post-snapshot event, exactly once');
138
+
139
+ const live = appendStateEvent({ resource: 'boot:other', op: 'replace', value: [], source: 'test' });
140
+ assert.deepEqual(
141
+ stateFramesOf(res).map((frame) => frame.id),
142
+ [before + 3, live.seq],
143
+ 'live events continue in seq order after the flush',
144
+ );
145
+
146
+ req.emit('close');
147
+ });
148
+
149
+ test('events at or below snapshot.seq are not re-sent', () => {
150
+ const before = currentSeq();
151
+ // One mid-read write: attempt 1 is unstable, attempt 2 stabilizes at
152
+ // before+1 — the buffered event is inside the snapshot, so nothing flushes.
153
+ midBuildEmits = 1;
154
+
155
+ const req = fakeRequest();
156
+ const res = fakeResponse();
157
+ rest.handleStream(req, res, { bootstrap: '1', resources: 'boot:mid' });
158
+
159
+ const data = dataFramesOf(res);
160
+ assert.equal(data[0].event, 'snapshot');
161
+ assert.equal(data[0].data.seq, before + 1);
162
+ assert.equal(data[0].data.stable, true);
163
+ assert.equal(stateFramesOf(res).length, 0, 'the mid-build event is already inside the snapshot');
164
+
165
+ req.emit('close');
166
+ });
167
+
168
+ test('legacy mode is byte-identical: no snapshot frame, after= replay unchanged', () => {
169
+ appendStateEvent({ resource: 'boot:other', op: 'replace', value: [], source: 'test' });
170
+ const e1 = appendStateEvent({ resource: 'boot:other', op: 'replace', value: [{ id: 'e1' }], source: 'test' });
171
+ const e2 = appendStateEvent({ resource: 'boot:other', op: 'replace', value: [{ id: 'e2' }], source: 'test' });
172
+
173
+ const req = fakeRequest();
174
+ const res = fakeResponse();
175
+ rest.handleStream(req, res, { after: String(e1.seq - 1) });
176
+
177
+ assert.deepEqual(res.writes, [
178
+ ': local-live-store connected\n\n',
179
+ `id: ${e1.seq}\nevent: state\ndata: ${JSON.stringify(e1)}\n\n`,
180
+ `id: ${e2.seq}\nevent: state\ndata: ${JSON.stringify(e2)}\n\n`,
181
+ ]);
182
+
183
+ const e3 = appendStateEvent({ resource: 'boot:other', op: 'replace', value: [{ id: 'e3' }], source: 'test' });
184
+ assert.equal(res.writes.length, 4, 'live event is one write');
185
+ assert.equal(res.writes[3], `id: ${e3.seq}\nevent: state\ndata: ${JSON.stringify(e3)}\n\n`);
186
+ assert.ok(!res.writes.join('').includes('event: snapshot'), 'legacy stream never sends a snapshot frame');
187
+
188
+ req.emit('close');
189
+ });
190
+
191
+ test('a throwing resource read produces a clean stream error and terminates', () => {
192
+ const req = fakeRequest();
193
+ const res = fakeResponse();
194
+ rest.handleStream(req, res, { bootstrap: '1', resources: 'boot:throw' });
195
+
196
+ const data = dataFramesOf(res);
197
+ assert.equal(data.length, 1, 'exactly one data frame');
198
+ assert.equal(data[0].event, 'error');
199
+ assert.deepEqual(data[0].data, { status: 500, error: 'kaboom' });
200
+ assert.equal(res.writableEnded, true, 'the stream is ended, not left hanging');
201
+ assert.ok(!res.writes.join('').includes('event: snapshot'), 'no snapshot frame on failure');
202
+
203
+ // The subscriber must be released: later events write nothing.
204
+ const writesAfterDeath = res.writes.length;
205
+ appendStateEvent({ resource: 'boot:other', op: 'replace', value: [], source: 'test' });
206
+ assert.equal(res.writes.length, writesAfterDeath, 'torn-down stream receives no events');
207
+ });
208
+
209
+ test('unknown resource names: snapshot contains what it can, no crash', () => {
210
+ const req = fakeRequest();
211
+ const res = fakeResponse();
212
+ rest.handleStream(req, res, { bootstrap: '1', resources: 'boot:nope,boot:a' });
213
+
214
+ const data = dataFramesOf(res);
215
+ assert.equal(data[0].event, 'snapshot');
216
+ assert.deepEqual(data[0].data.resources, { 'boot:a': [{ id: 'a' }] }, 'unknown names are simply absent');
217
+ assert.equal(data[0].data.seq, currentSeq());
218
+
219
+ const live = appendStateEvent({ resource: 'boot:other', op: 'replace', value: [], source: 'test' });
220
+ assert.equal(stateFramesOf(res).at(-1).id, live.seq, 'stream stays live');
221
+
222
+ req.emit('close');
223
+ });