amalgm 0.1.238 → 0.1.240

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.
Files changed (86) hide show
  1. package/lib/shared-realtime-tunnel.js +22 -14
  2. package/package.json +1 -1
  3. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
  4. package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
  5. package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
  6. package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
  7. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
  8. package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
  9. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
  10. package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
  11. package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
  12. package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
  13. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
  14. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
  15. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
  16. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
  17. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
  18. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
  19. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
  20. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
  21. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
  22. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
  23. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
  24. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
  25. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
  26. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
  27. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
  28. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
  29. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +81 -3
  30. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +114 -8
  31. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
  32. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
  33. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
  34. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
  35. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
  36. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
  37. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
  38. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
  39. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
  40. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
  41. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
  42. package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
  43. package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
  44. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
  45. package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
  46. package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
  47. package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
  48. package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
  49. package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
  50. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
  51. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  52. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  53. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
  54. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
  55. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
  56. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
  57. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
  58. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
  59. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
  60. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
  61. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
  63. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
  64. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
  65. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
  66. package/runtime/scripts/amalgm-mcp/workspace/objects.js +119 -54
  67. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
  68. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
  69. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
  70. package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
  71. package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
  72. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
  73. package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
  74. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
  75. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
  76. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
  77. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
  78. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
  79. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
  80. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
  81. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
  82. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
  83. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
  84. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
  85. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
  86. package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
@@ -2,9 +2,11 @@
2
2
 
3
3
  const test = require('node:test');
4
4
  const assert = require('node:assert/strict');
5
+ const crypto = require('node:crypto');
5
6
  const fs = require('fs');
6
7
  const os = require('os');
7
8
  const path = require('path');
9
+ const { spawn } = require('node:child_process');
8
10
 
9
11
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-live-docs-test-'));
10
12
  process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
@@ -28,6 +30,19 @@ function sleep(ms) {
28
30
  return new Promise((resolve) => setTimeout(resolve, ms));
29
31
  }
30
32
 
33
+ function sha256(text) {
34
+ return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
35
+ }
36
+
37
+ function writeFromAnotherProcessAfter(file, text, delayMs) {
38
+ return spawn(process.execPath, [
39
+ '-e',
40
+ 'const fs=require("fs");'
41
+ + `setTimeout(()=>fs.writeFileSync(${JSON.stringify(file)},`
42
+ + `${JSON.stringify(text)},"utf8"),${delayMs});`,
43
+ ], { stdio: 'ignore' });
44
+ }
45
+
31
46
  function clientFromState(stateBase64) {
32
47
  const doc = new Y.Doc();
33
48
  Y.applyUpdate(doc, new Uint8Array(Buffer.from(stateBase64, 'base64')));
@@ -70,7 +85,7 @@ test('open seeds the document from disk and returns usable state', () => {
70
85
  assert.equal(client.getText('content').toString(), '# hello\n');
71
86
  });
72
87
 
73
- test('client updates apply, log as patch events, and flush back to disk', async () => {
88
+ test('client updates apply, log as patch events, and materialize to disk', async () => {
74
89
  const target = writeDoc('edit.md', 'hello world\n');
75
90
  const opened = docs.openDoc(target);
76
91
  const client = clientFromState(opened.state);
@@ -89,7 +104,7 @@ test('client updates apply, log as patch events, and flush back to disk', async
89
104
  assert.ok(events[0].patch?.yjs);
90
105
  assert.equal(events[0].value, undefined);
91
106
 
92
- // Debounced write-back lands on disk.
107
+ // The saved-local response already stands on a crossed file transaction.
93
108
  await sleep(500);
94
109
  assert.equal(fs.readFileSync(target, 'utf8'), 'hello, world\n');
95
110
  });
@@ -119,14 +134,14 @@ test('two clients editing concurrently converge to the same text', () => {
119
134
  assert.equal(clientB.getText('content').toString(), engineText);
120
135
  });
121
136
 
122
- test('external disk edits reconcile into the live doc via the watcher', async () => {
137
+ test('bounded verification reconciles a dropped external disk notification', () => {
123
138
  const target = writeDoc('agent.md', 'agent line 1\n');
124
139
  const opened = docs.openDoc(target);
125
140
  const before = currentSeq();
126
141
 
127
142
  // An "agent" writes the file directly, bypassing the doc API.
128
143
  fs.writeFileSync(target, 'agent line 1\nagent line 2\n', 'utf8');
129
- await sleep(700);
144
+ docs._private.verifyOpenDocs();
130
145
 
131
146
  assert.equal(docs.getDocText(target), 'agent line 1\nagent line 2\n');
132
147
  const events = listEventsAfter(before).filter((e) => e.resource === opened.resource);
@@ -134,6 +149,243 @@ test('external disk edits reconcile into the live doc via the watcher', async ()
134
149
  assert.equal(events[events.length - 1].source, 'doc:disk');
135
150
  });
136
151
 
152
+ test('a dropped watcher cannot let a later versioned API write erase the disk save', () => {
153
+ const base = 'left\nright\n';
154
+ const target = writeDoc('dropped-watcher-api.md', base);
155
+ docs.openDoc(target);
156
+
157
+ fs.writeFileSync(target, 'left from disk\nright\n', 'utf8');
158
+ const written = docs.writeDocText(target, 'left\nright from api\n', {
159
+ baseHash: sha256(base),
160
+ mutationId: 'dropped_watcher_api_001',
161
+ });
162
+
163
+ assert.equal(written.merge, 'merged');
164
+ assert.equal(docs.getDocText(target), 'left from disk\nright from api\n');
165
+ assert.equal(fs.readFileSync(target, 'utf8'), 'left from disk\nright from api\n');
166
+ });
167
+
168
+ test('the final native CAS retains a save that lands during an API materialization', () => {
169
+ const base = 'first\nsecond\n';
170
+ const target = writeDoc('api-final-cas.md', base);
171
+ docs.openDoc(target);
172
+ const child = writeFromAnotherProcessAfter(
173
+ target,
174
+ 'first from disk\nsecond\n',
175
+ 75,
176
+ );
177
+ docs._private.setDiskTransactionTestHooks({ pauseBeforeInstall: 350 });
178
+ try {
179
+ docs.writeDocText(target, 'first\nsecond from api\n', {
180
+ baseHash: sha256(base),
181
+ mutationId: 'api_final_cas_race_001',
182
+ });
183
+ } finally {
184
+ docs._private.setDiskTransactionTestHooks();
185
+ }
186
+ child.unref();
187
+ assert.equal(docs.getDocText(target), 'first from disk\nsecond from api\n');
188
+ assert.equal(fs.readFileSync(target, 'utf8'), 'first from disk\nsecond from api\n');
189
+ });
190
+
191
+ test('the API plan stays pinned when disk changes after merge derivation', () => {
192
+ const base = 'first\nsecond\n';
193
+ const target = writeDoc('api-derived-base-cas.md', base);
194
+ docs.openDoc(target);
195
+ const child = writeFromAnotherProcessAfter(
196
+ target,
197
+ 'first from disk\nsecond\n',
198
+ 75,
199
+ );
200
+ docs._private.setDiskTransactionTestHooks({ pauseAfterEditBeforeJournal: 350 });
201
+ let written;
202
+ try {
203
+ written = docs.writeDocText(target, 'first\nsecond from api\n', {
204
+ baseHash: sha256(base),
205
+ mutationId: 'api_derived_base_cas_001',
206
+ });
207
+ } finally {
208
+ docs._private.setDiskTransactionTestHooks();
209
+ }
210
+ child.unref();
211
+ assert.equal(docs.getDocText(target), 'first from disk\nsecond from api\n');
212
+ assert.equal(fs.readFileSync(target, 'utf8'), 'first from disk\nsecond from api\n');
213
+ assert.equal(written.sha256, sha256('first from disk\nsecond from api\n'));
214
+ });
215
+
216
+ test('a save after native install becomes the successor during receipt recovery', () => {
217
+ const base = 'first\nsecond\n';
218
+ const target = writeDoc('api-post-install-successor.md', base);
219
+ docs.openDoc(target);
220
+
221
+ docs._private.setDiskTransactionTestHooks({ crashAfterInstall: true });
222
+ try {
223
+ assert.throws(
224
+ () => docs.writeDocText(target, 'first\nsecond from api\n', {
225
+ baseHash: sha256(base),
226
+ mutationId: 'api_post_install_successor_001',
227
+ }),
228
+ /native file transaction failed/,
229
+ );
230
+ } finally {
231
+ docs._private.setDiskTransactionTestHooks();
232
+ }
233
+ assert.equal(fs.readFileSync(target, 'utf8'), 'first\nsecond from api\n');
234
+
235
+ // The helper's receipt proves the API target crossed. This later save is B,
236
+ // not evidence that A failed, and must become a durable successor.
237
+ fs.writeFileSync(target, 'first from disk\nsecond\n', 'utf8');
238
+ docs._private.verifyOpenDocs();
239
+
240
+ assert.equal(docs.getDocText(target), 'first from disk\nsecond from api\n');
241
+ assert.equal(fs.readFileSync(target, 'utf8'), 'first from disk\nsecond from api\n');
242
+ });
243
+
244
+ test('UUID ownership recovers a crossed edit after its parent folder is renamed', () => {
245
+ const beforeDir = path.join(docDir, 'rename-before');
246
+ const afterDir = path.join(docDir, 'rename-after');
247
+ fs.mkdirSync(beforeDir);
248
+ const beforePath = path.join(beforeDir, 'owned.md');
249
+ const afterPath = path.join(afterDir, 'owned.md');
250
+ fs.writeFileSync(beforePath, 'before\n', 'utf8');
251
+ docs.openDoc(beforePath);
252
+
253
+ docs._private.setDiskTransactionTestHooks({ crashAfterInstall: true });
254
+ try {
255
+ assert.throws(
256
+ () => docs.writeDocText(beforePath, 'after API\n', {
257
+ baseHash: sha256('before\n'),
258
+ mutationId: 'uuid_parent_rename_recovery_001',
259
+ }),
260
+ /native file transaction failed/,
261
+ );
262
+ } finally {
263
+ docs._private.setDiskTransactionTestHooks();
264
+ }
265
+
266
+ fs.renameSync(beforeDir, afterDir);
267
+ docs.rebindNodePaths([{
268
+ nodeId: '11111111-1111-4111-8111-111111111111',
269
+ oldPath: beforePath,
270
+ newPath: afterPath,
271
+ }]);
272
+ docs._private.verifyOpenDocs();
273
+
274
+ assert.equal(docs.getDocText(afterPath), 'after API\n');
275
+ assert.equal(fs.readFileSync(afterPath, 'utf8'), 'after API\n');
276
+ });
277
+
278
+ test('the bounded verifier schedule reconciles a file whose watcher failed', async () => {
279
+ const isolatedDir = path.join(docDir, 'scheduled-verifier');
280
+ fs.mkdirSync(isolatedDir, { recursive: true });
281
+ const target = path.join(isolatedDir, 'agent.md');
282
+ fs.writeFileSync(target, 'before\n', 'utf8');
283
+ const originalWatch = fs.watch;
284
+ let watchAttempts = 0;
285
+ fs.watch = (...args) => {
286
+ watchAttempts += 1;
287
+ if (watchAttempts === 1) {
288
+ const error = new Error('simulated dropped watcher');
289
+ error.code = 'EMFILE';
290
+ throw error;
291
+ }
292
+ return originalWatch.apply(fs, args);
293
+ };
294
+
295
+ try {
296
+ const scheduledBefore = docs._private.watchVerificationStats();
297
+ const opened = docs.openDoc(target);
298
+ const before = currentSeq();
299
+ fs.writeFileSync(target, 'after\n', 'utf8');
300
+
301
+ const deadline = Date.now() + 7_000;
302
+ while (docs.getDocText(target) !== 'after\n' && Date.now() < deadline) {
303
+ await sleep(50);
304
+ }
305
+ assert.equal(
306
+ docs.getDocText(target),
307
+ 'after\n',
308
+ JSON.stringify({
309
+ scheduledBefore,
310
+ scheduledAfter: docs._private.watchVerificationStats(),
311
+ watchAttempts,
312
+ disk: fs.readFileSync(target, 'utf8'),
313
+ }),
314
+ );
315
+ const events = listEventsAfter(before).filter((event) => event.resource === opened.resource);
316
+ assert.equal(events.length, 1);
317
+ assert.equal(events[0].source, 'doc:disk');
318
+ assert.ok(watchAttempts >= 2, 'the scheduled pass retried the failed watcher');
319
+ const scheduledAfter = docs._private.watchVerificationStats();
320
+ assert.equal(scheduledAfter.active, true);
321
+ assert.ok(scheduledAfter.passes > scheduledBefore.passes);
322
+ } finally {
323
+ fs.watch = originalWatch;
324
+ }
325
+ });
326
+
327
+ test('a transient journal failure leaves the exact disk observation retryable', () => {
328
+ const target = writeDoc('agent-retry.md', 'before\n');
329
+ const opened = docs.openDoc(target);
330
+ const before = currentSeq();
331
+ const mutations = require('../state/mutations');
332
+ const commitLocalMutation = mutations.commitLocalMutation;
333
+ fs.writeFileSync(target, 'after\n', 'utf8');
334
+ mutations.commitLocalMutation = () => {
335
+ throw new Error('simulated journal outage');
336
+ };
337
+ try {
338
+ docs._private.verifyOpenDocs();
339
+ } finally {
340
+ mutations.commitLocalMutation = commitLocalMutation;
341
+ }
342
+
343
+ assert.equal(docs.getDocText(target), 'before\n');
344
+ assert.equal(
345
+ listEventsAfter(before).filter((event) => event.resource === opened.resource).length,
346
+ 0,
347
+ );
348
+
349
+ docs._private.verifyOpenDocs();
350
+ assert.equal(docs.getDocText(target), 'after\n');
351
+ assert.equal(fs.readFileSync(target, 'utf8'), 'after\n');
352
+ const events = listEventsAfter(before).filter((event) => event.resource === opened.resource);
353
+ assert.equal(events.length, 1);
354
+ assert.ok(events.every((event) => event.source === 'doc:disk'));
355
+ });
356
+
357
+ test('a transient journal failure cannot consume a whole-text API write', () => {
358
+ const target = writeDoc('api-retry.md', 'before\n');
359
+ const opened = docs.openDoc(target);
360
+ const before = currentSeq();
361
+ const mutations = require('../state/mutations');
362
+ const commitLocalMutation = mutations.commitLocalMutation;
363
+ mutations.commitLocalMutation = () => {
364
+ throw new Error('simulated journal outage');
365
+ };
366
+ try {
367
+ assert.throws(
368
+ () => docs.writeDocText(target, 'after\n', { mutationId: 'api_retry_write_0001' }),
369
+ /simulated journal outage/,
370
+ );
371
+ } finally {
372
+ mutations.commitLocalMutation = commitLocalMutation;
373
+ }
374
+
375
+ assert.equal(docs.getDocText(target), 'before\n');
376
+ assert.equal(fs.readFileSync(target, 'utf8'), 'before\n');
377
+ assert.equal(
378
+ listEventsAfter(before).filter((event) => event.resource === opened.resource).length,
379
+ 0,
380
+ );
381
+
382
+ docs.writeDocText(target, 'after\n', { mutationId: 'api_retry_write_0001' });
383
+ assert.equal(docs.getDocText(target), 'after\n');
384
+ const events = listEventsAfter(before).filter((event) => event.resource === opened.resource);
385
+ assert.equal(events.length, 1);
386
+ assert.equal(events[0].source, 'doc:write');
387
+ });
388
+
137
389
  test('internal protocol documents never treat their own disk flush as another writer', async () => {
138
390
  const checkpointDir = path.join(process.env.AMALGM_DIR, 'state', 'repo-checkpoints');
139
391
  const projectDir = path.join(process.env.AMALGM_DIR, 'state', 'code-projects');
@@ -185,7 +437,7 @@ test('documents survive close: persisted state + disk agree on reopen', () => {
185
437
  const update = encodeLocalEdit(client, (ytext) => ytext.insert(0, 'v2 over '));
186
438
  docs.applyDocUpdates(target, [update]);
187
439
 
188
- docs.closeAllDocs(); // flushes disk + state
440
+ docs.closeAllDocs(); // captures final disk observations and releases memory
189
441
 
190
442
  const reopened = docs.openDoc(target);
191
443
  const fresh = clientFromState(reopened.state);
@@ -224,8 +476,33 @@ test('guards: binary, missing, oversized, and malformed inputs are rejected', ()
224
476
  assert.throws(() => docs.applyDocUpdates(target, ['aGVsbG8=']), /invalid Yjs update/);
225
477
  });
226
478
 
479
+ test('all managed document writes cross the one native materialization boundary', () => {
480
+ const source = fs.readFileSync(require.resolve('../state/docs'), 'utf8');
481
+ const rawWriteTargets = Array.from(
482
+ source.matchAll(/fs\.writeFileSync\(\s*([^,\n]+)/g),
483
+ (match) => match[1].trim(),
484
+ );
485
+
486
+ assert.deepEqual(
487
+ rawWriteTargets,
488
+ ['sidecarPath'],
489
+ 'only the independent metadata sidecar may bypass document materialization',
490
+ );
491
+ assert.doesNotMatch(source, /\bflushDocToDisk\b|\bwriteFileAtomic\b|fs\.renameSync\(/);
492
+ assert.match(source, /docDisk\.cross\(/);
493
+ assert.equal(
494
+ (source.match(/docDisk\.insertSnapshotBase\(/g) || []).length,
495
+ 1,
496
+ 'every snapshot attempt must acquire its physical base through one capture-and-pin helper',
497
+ );
498
+ assert.match(
499
+ source,
500
+ /function planCloudDocSnapshot[\s\S]*?captureDiskBeforeMutation[\s\S]*?docDisk\.insertSnapshotBase/,
501
+ );
502
+ });
503
+
227
504
  // ---------------------------------------------------------------------------
228
- // Durability: a crash (SIGKILL — no flush, no graceful close) must never lose
505
+ // Durability: a crash (SIGKILL — no graceful close) must never lose
229
506
  // an acknowledged edit. The contract is per acknowledgment, not per update:
230
507
  // state is persisted before applyDocUpdates returns (before the POST response
231
508
  // acknowledges); the recorded last-disk hash lets reopen tell "disk is merely
@@ -234,20 +511,20 @@ test('guards: binary, missing, oversized, and malformed inputs are rejected', ()
234
511
 
235
512
  function crashAndReloadDocs() {
236
513
  // Simulate SIGKILL: drop the module (and its open docs, timers untouched)
237
- // without any flush. The reloaded module sees only what was persisted.
514
+ // without graceful cleanup. The reloaded module sees only durable state.
238
515
  delete require.cache[require.resolve('../state/docs')];
239
516
  return require('../state/docs');
240
517
  }
241
518
 
242
- test('a kill inside the disk-write debounce loses nothing: reopen restores the edit', async () => {
519
+ test('an acknowledged edit is on disk before a kill and remains current on reopen', () => {
243
520
  const target = writeDoc('crash.md', 'before the crash\n');
244
521
  const opened = docs.openDoc(target);
245
522
  const client = clientFromState(opened.state);
246
523
  const update = encodeLocalEdit(client, (ytext) => ytext.insert(0, 'acknowledged edit / '));
247
524
  docs.applyDocUpdates(target, [update]);
248
525
 
249
- // Crash immediately — the 300ms disk write has NOT run; disk is stale.
250
- assert.equal(fs.readFileSync(target, 'utf8'), 'before the crash\n');
526
+ // Acknowledgment is earned only after the native file transaction crossed.
527
+ assert.equal(fs.readFileSync(target, 'utf8'), 'acknowledged edit / before the crash\n');
251
528
  const reloaded = crashAndReloadDocs();
252
529
 
253
530
  const reopened = reloaded.openDoc(target);
@@ -258,8 +535,7 @@ test('a kill inside the disk-write debounce loses nothing: reopen restores the e
258
535
  'persisted state must win over a disk that is merely behind',
259
536
  );
260
537
 
261
- // The stale disk artifact catches up via the rescheduled write-back.
262
- await sleep(500);
538
+ // No timer or later process lifetime is needed to make the save real.
263
539
  assert.equal(fs.readFileSync(target, 'utf8'), 'acknowledged edit / before the crash\n');
264
540
  reloaded.closeAllDocs();
265
541
  });
@@ -277,7 +553,7 @@ test('a multi-update batch is fully persisted by the time it returns', () => {
277
553
  const result = docs.applyDocUpdates(target, updates);
278
554
  assert.equal(result.applied, 3);
279
555
 
280
- // Crash with no flush: the batch persist alone must carry all three
556
+ // Crash with no graceful close: the journal and crossed files carry all three
281
557
  // updates into doc_states.
282
558
  const reloaded = crashAndReloadDocs();
283
559
  const reopened = reloaded.openDoc(target);
@@ -306,3 +582,90 @@ test('an external edit made while closed still folds in on reopen', () => {
306
582
  'a genuinely changed disk is folded in, not overwritten',
307
583
  );
308
584
  });
585
+
586
+ test('restart reuses a crossed successor instead of inventing another mutation', () => {
587
+ const base = 'first\nsecond\n';
588
+ const target = writeDoc('crossed-successor-restart.md', base);
589
+ const opened = docs.openDoc(target);
590
+ const before = currentSeq();
591
+ const mutations = require('../state/mutations');
592
+ const baselineMutationIds = new Set(
593
+ mutations.listMutations({ localResource: opened.resource })
594
+ .map((mutation) => mutation.mutationId),
595
+ );
596
+
597
+ docs._private.setDiskTransactionTestHooks({ crashAfterInstall: true });
598
+ try {
599
+ assert.throws(
600
+ () => docs.writeDocText(target, 'first\nsecond from api\n', {
601
+ baseHash: sha256(base),
602
+ mutationId: 'restart_crossed_original_001',
603
+ }),
604
+ /native file transaction failed/,
605
+ );
606
+ } finally {
607
+ docs._private.setDiskTransactionTestHooks();
608
+ }
609
+
610
+ fs.writeFileSync(target, 'first from disk\nsecond\n', 'utf8');
611
+ const completeMaterializedMutation = mutations.completeMaterializedMutation;
612
+ let interrupted = false;
613
+ mutations.completeMaterializedMutation = (...args) => {
614
+ if (!interrupted) {
615
+ interrupted = true;
616
+ throw new Error('simulated process death before predecessor completion');
617
+ }
618
+ return completeMaterializedMutation(...args);
619
+ };
620
+ try {
621
+ assert.throws(
622
+ () => docs.writeDocText(target, 'unreached later API\n'),
623
+ /simulated process death before predecessor completion/,
624
+ );
625
+ } finally {
626
+ mutations.completeMaterializedMutation = completeMaterializedMutation;
627
+ }
628
+
629
+ const beforeRestart = mutations.listMutations({ localResource: opened.resource });
630
+ assert.equal(
631
+ beforeRestart.length,
632
+ baselineMutationIds.size + 2,
633
+ `one API edit and one disk successor are durable: ${JSON.stringify(
634
+ beforeRestart.map((mutation) => ({
635
+ mutationId: mutation.mutationId,
636
+ state: mutation.state,
637
+ operation: mutation.operation,
638
+ })),
639
+ )}`,
640
+ );
641
+ assert.ok(
642
+ beforeRestart
643
+ .filter((mutation) => !baselineMutationIds.has(mutation.mutationId))
644
+ .every((mutation) => mutation.state === 'saving-local'),
645
+ );
646
+
647
+ const reloaded = crashAndReloadDocs();
648
+ const reopened = reloaded.openDoc(target);
649
+ const finalText = fs.readFileSync(target, 'utf8');
650
+ assert.match(finalText, /^first from disk\n/);
651
+ assert.match(finalText, /second from api\n/);
652
+ assert.equal(clientFromState(reopened.state).getText('content').toString(), finalText);
653
+
654
+ const afterRestart = mutations.listMutations({ localResource: opened.resource });
655
+ assert.equal(
656
+ afterRestart.length,
657
+ baselineMutationIds.size + 2,
658
+ 'restart must not assign the successor a second ID',
659
+ );
660
+ assert.ok(
661
+ afterRestart
662
+ .filter((mutation) => !baselineMutationIds.has(mutation.mutationId))
663
+ .every((mutation) => mutation.state === 'saved-local'),
664
+ );
665
+ assert.equal(
666
+ listEventsAfter(before).filter((event) => event.resource === opened.resource).length,
667
+ 2,
668
+ 'one real API edit plus one real disk save produce exactly two events',
669
+ );
670
+ reloaded.closeAllDocs();
671
+ });