amalgm 0.1.245 → 0.1.246

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 (157) hide show
  1. package/README.md +12 -0
  2. package/lib/cli.js +146 -8
  3. package/lib/layout.js +89 -4
  4. package/lib/shared-realtime-tunnel.js +384 -43
  5. package/lib/supervisor.js +0 -3
  6. package/package.json +2 -2
  7. package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
  8. package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
  9. package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
  10. package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
  11. package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
  12. package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
  13. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
  14. package/runtime/scripts/amalgm-mcp/index.js +4 -0
  15. package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
  16. package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
  17. package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
  18. package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
  19. package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
  20. package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
  21. package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
  22. package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
  23. package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
  24. package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
  25. package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
  26. package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
  27. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
  28. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
  29. package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
  30. package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
  31. package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
  32. package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
  33. package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
  34. package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
  35. package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
  36. package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
  37. package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
  38. package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
  39. package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
  40. package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
  41. package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
  42. package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
  43. package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
  44. package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
  45. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  46. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
  47. package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
  48. package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
  49. package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
  50. package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
  51. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
  52. package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
  53. package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
  54. package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
  55. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
  56. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
  57. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
  58. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
  59. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
  60. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
  61. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
  62. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
  63. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
  64. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
  65. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
  66. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
  67. package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
  68. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
  69. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
  70. package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
  71. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
  72. package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
  73. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
  74. package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
  75. package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
  76. package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
  77. package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
  78. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
  79. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
  80. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
  81. package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
  82. package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
  83. package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
  84. package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
  85. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
  86. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
  87. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
  88. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
  89. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
  90. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
  91. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
  92. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
  93. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
  94. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
  95. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
  96. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
  97. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
  98. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
  99. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
  100. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
  101. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
  102. package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
  103. package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
  104. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
  105. package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
  106. package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
  107. package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
  108. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
  109. package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
  110. package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
  111. package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
  112. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
  113. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
  114. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
  115. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
  116. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
  117. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
  118. package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
  119. package/runtime/scripts/local-gateway.js +1 -0
  120. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
  121. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
  122. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
  123. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
  124. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
  125. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
  126. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
  127. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
  128. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
  129. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
  130. package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
  131. package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
  132. package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
  133. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
  134. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  135. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  136. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
  137. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
  138. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
  139. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
  140. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
  141. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
  142. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
  143. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
  144. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
  145. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
  146. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
  147. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
  148. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
  149. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
  150. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
  151. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
  152. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
  153. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
  154. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
  155. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
  156. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
  157. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
@@ -5,7 +5,6 @@ const assert = require('node:assert/strict');
5
5
  const fs = require('fs');
6
6
  const os = require('os');
7
7
  const path = require('path');
8
- const { spawn } = require('node:child_process');
9
8
 
10
9
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-shared-replica-test-'));
11
10
  process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
@@ -14,14 +13,13 @@ process.env.AMALGM_DEVICE_ID = 'device-shared-replica-test';
14
13
  process.env.AMALGM_EVENT_LOG_PRUNE_INTERVAL_MS = '0';
15
14
 
16
15
  const Y = require('yjs');
17
- const { closeLocalDb, openLocalDb } = require('../state/db');
16
+ const { closeLocalDb } = require('../state/db');
18
17
  const docs = require('../state/docs');
19
18
  const { listEventsAfter, subscribeStateEvents } = require('../state/events');
20
19
  const {
21
20
  acknowledgeCloudMutation,
22
21
  claimCloudMutations,
23
22
  getMutation,
24
- journalCloudMutation,
25
23
  journalMutation,
26
24
  listMutations,
27
25
  } = require('../state/mutations');
@@ -63,27 +61,6 @@ function captureEdit(doc, mutate) {
63
61
  return Buffer.from(captured).toString('base64');
64
62
  }
65
63
 
66
- function acknowledgeAndApplyClaim(claim, version, committedAt) {
67
- acknowledgeCloudMutation(claim.envelope.resourceId, claim.envelope.mutationId, {
68
- version,
69
- committedAt,
70
- });
71
- return docs.applyCloudDocMutation({
72
- envelope: claim.envelope,
73
- version,
74
- committedAt,
75
- });
76
- }
77
-
78
- function writeFromAnotherProcessAfter(file, text, delayMs) {
79
- return spawn(process.execPath, [
80
- '-e',
81
- 'const fs=require("fs");'
82
- + `setTimeout(()=>fs.writeFileSync(${JSON.stringify(file)},`
83
- + `${JSON.stringify(text)},"utf8"),${delayMs});`,
84
- ], { stdio: 'ignore' });
85
- }
86
-
87
64
  test.after(() => {
88
65
  docs.closeAllDocs();
89
66
  closeLocalDb();
@@ -174,25 +151,10 @@ test('batched surface edits retain separate IDs/versions and recipient file prec
174
151
  version: 1,
175
152
  committedAt: '2026-07-14T20:00:00.000Z',
176
153
  });
177
- assert.equal(
178
- getReplicaByResourceId(resourceId).appliedVersion,
179
- 0,
180
- 'an acknowledgement records order; the official inbox advances it',
181
- );
182
- docs.applyCloudDocMutation({
183
- envelope: claims[0].envelope,
184
- version: 1,
185
- committedAt: '2026-07-14T20:00:00.000Z',
186
- });
187
154
  acknowledgeCloudMutation(resourceId, 'mut_shared_batch_002', {
188
155
  version: 2,
189
156
  committedAt: '2026-07-14T20:00:01.000Z',
190
157
  });
191
- docs.applyCloudDocMutation({
192
- envelope: claims[1].envelope,
193
- version: 2,
194
- committedAt: '2026-07-14T20:00:01.000Z',
195
- });
196
158
  assert.equal(getReplicaByResourceId(resourceId).appliedVersion, 2);
197
159
 
198
160
  const remoteUpdate = captureEdit(client, (text) => text.insert(text.length, 'Remote\n'));
@@ -291,153 +253,10 @@ test('reopen recovers a journaled shared edit that died before materialization',
291
253
 
292
254
  // Drain the outbox so later tests observe only their own deliveries.
293
255
  const [claim] = claimCloudMutations().filter((item) => item.envelope.mutationId === mutationId);
294
- acknowledgeAndApplyClaim(claim, 4, '2026-07-14T20:00:04.000Z');
295
- });
296
-
297
- test('restart merges a downtime disk edit over the last proven base before official recovery', () => {
298
- const restartResourceId = 'shr_restartbasefold1';
299
- const restartPath = path.join(docDir, 'restart-base-fold.md');
300
- fs.writeFileSync(restartPath, '', 'utf8');
301
- const cloud = new Y.Doc();
302
- cloud.getText('content').insert(0, 'left\nright\n');
303
- const snapshot = Buffer.from(Y.encodeStateAsUpdate(cloud));
304
- const checksum = require('crypto').createHash('sha256').update(snapshot).digest('hex');
305
- docs.installCloudDocSnapshot(restartPath, {
306
- resourceId: restartResourceId,
307
- authorityEpoch: 1,
308
- headVersion: 0,
309
- snapshotVersion: 0,
310
- snapshotChecksum: checksum,
311
- snapshotBase64: snapshot.toString('base64'),
312
- contract: 'text-yjs@1',
313
- schemaVersion: 1,
256
+ acknowledgeCloudMutation(resourceId, claim.envelope.mutationId, {
257
+ version: 4,
258
+ committedAt: '2026-07-14T20:00:04.000Z',
314
259
  });
315
- const opened = docs.openDoc(restartPath);
316
- const remote = clientFromState(opened.state);
317
- const remoteUpdate = captureEdit(remote, (text) => {
318
- text.insert('left\nright'.length, ' cloud');
319
- });
320
- docs.closeAllDocs();
321
-
322
- const base = require('../state/doc-disk').observe(restartPath);
323
- journalCloudMutation({
324
- envelope: {
325
- resourceId: restartResourceId,
326
- authorityEpoch: 1,
327
- mutationId: 'mut_restart_base_cloud_001',
328
- deviceId: 'device-remote-restart-base',
329
- baseVersion: 0,
330
- contract: 'text-yjs@1',
331
- schemaVersion: 1,
332
- operationKind: 'yjs.update',
333
- operation: { kind: 'yjs.update', update: remoteUpdate },
334
- },
335
- version: 1,
336
- committedAt: '2026-07-28T10:30:00.000Z',
337
- localResource: opened.resource,
338
- }, {
339
- persist(database, mutation) {
340
- require('../state/doc-disk').insertBase(database, mutation, base, 'doc:cloud');
341
- },
342
- });
343
-
344
- // Model the later crash window too: the desired official Y.Doc target was
345
- // persisted, but its native file transaction never crossed. The persisted
346
- // hash still names the old disk base, so recovery needs the exact base text
347
- // sealed in the physical plan rather than trying to recover it from the
348
- // newer Y.Doc state.
349
- const persistedTarget = clientFromState(opened.state);
350
- Y.applyUpdate(persistedTarget, new Uint8Array(Buffer.from(remoteUpdate, 'base64')));
351
- openLocalDb().prepare('UPDATE doc_states SET state = ? WHERE path = ?').run(
352
- Buffer.from(Y.encodeStateAsUpdate(persistedTarget)),
353
- restartPath,
354
- );
355
-
356
- // This save is born while the process is down and changes only the other
357
- // line. Recovery must merge both deltas over the persisted physical base.
358
- fs.writeFileSync(restartPath, 'left local\nright\n', 'utf8');
359
- const recovered = docs.openDoc(restartPath);
360
- const finalText = fs.readFileSync(restartPath, 'utf8');
361
- assert.equal(finalText, 'left local\nright cloud\n');
362
- assert.equal(clientFromState(recovered.state).getText('content').toString(), finalText);
363
- assert.equal(getReplicaByResourceId(restartResourceId).appliedVersion, 1);
364
-
365
- const rows = listMutations({ localResource: opened.resource });
366
- const cloudRow = rows.find(
367
- (mutation) => mutation.mutationId === 'mut_restart_base_cloud_001',
368
- );
369
- assert.equal(cloudRow.state, 'synced');
370
- const localRows = rows.filter((mutation) => (
371
- mutation.origin === 'local'
372
- && mutation.mutationId !== 'mut_restart_base_cloud_001'
373
- ));
374
- assert.equal(localRows.length, 1, 'one downtime disk save receives one local mutation ID');
375
- assert.ok(
376
- claimCloudMutations().some((claim) => (
377
- claim.envelope.resourceId === restartResourceId
378
- && claim.envelope.mutationId === localRows[0].mutationId
379
- )),
380
- 'the merged downtime save remains in the outbox',
381
- );
382
- });
383
-
384
- test('a held viewer disk save blocks official materialization instead of becoming permission', () => {
385
- const heldResourceId = 'shr_viewerheldsave01';
386
- const heldPath = path.join(docDir, 'viewer-held-save.md');
387
- fs.writeFileSync(heldPath, '', 'utf8');
388
- const cloud = new Y.Doc();
389
- cloud.getText('content').insert(0, 'official base\n');
390
- const snapshot = Buffer.from(Y.encodeStateAsUpdate(cloud));
391
- const checksum = require('crypto').createHash('sha256').update(snapshot).digest('hex');
392
- docs.installCloudDocSnapshot(heldPath, {
393
- resourceId: heldResourceId,
394
- authorityEpoch: 1,
395
- headVersion: 0,
396
- snapshotVersion: 0,
397
- snapshotChecksum: checksum,
398
- snapshotBase64: snapshot.toString('base64'),
399
- contract: 'text-yjs@1',
400
- schemaVersion: 1,
401
- });
402
- const opened = docs.openDoc(heldPath);
403
- const remote = clientFromState(opened.state);
404
- const remoteUpdate = captureEdit(remote, (text) => text.insert(text.length, 'cloud edit\n'));
405
- const entry = docs._private.openDocs.get(opened.path);
406
- const accessStore = require('../workspace/access-store');
407
- const originalCurrentUserRole = accessStore.currentUserRole;
408
- entry.nodeId = '33333333-3333-4333-8333-333333333333';
409
- accessStore.currentUserRole = () => 'viewer';
410
- fs.writeFileSync(heldPath, 'LOCAL SAVE\n', 'utf8');
411
-
412
- try {
413
- assert.throws(() => docs.applyCloudDocMutation({
414
- envelope: {
415
- resourceId: heldResourceId,
416
- authorityEpoch: 1,
417
- mutationId: 'mut_cloud_blocked_by_viewer_save',
418
- deviceId: 'device-remote-viewer-held',
419
- baseVersion: 0,
420
- contract: 'text-yjs@1',
421
- schemaVersion: 1,
422
- operationKind: 'yjs.update',
423
- operation: { kind: 'yjs.update', update: remoteUpdate },
424
- },
425
- version: 1,
426
- committedAt: '2026-07-28T11:00:00.000Z',
427
- }), /disk change is held: forbidden/);
428
- assert.equal(fs.readFileSync(heldPath, 'utf8'), 'LOCAL SAVE\n');
429
- assert.equal(getReplicaByResourceId(heldResourceId).appliedVersion, 0);
430
- assert.equal(
431
- getMutation(`resource:${heldResourceId}`, 'mut_cloud_blocked_by_viewer_save'),
432
- null,
433
- 'the official cursor row is not journaled before held local bytes are resolved',
434
- );
435
- } finally {
436
- accessStore.currentUserRole = originalCurrentUserRole;
437
- entry.nodeId = null;
438
- fs.writeFileSync(heldPath, 'official base\n', 'utf8');
439
- docs._private.verifyOpenDocs();
440
- }
441
260
  });
442
261
 
443
262
  test('authorized snapshot attachment creates the recipient file before its UI event', () => {
@@ -491,11 +310,11 @@ function sha256(text) {
491
310
 
492
311
  // One door: an agent writing the shared file directly (no doc API at all)
493
312
  // must produce a cloud-deliverable mutation, not just a local event.
494
- test('bounded verification puts a dropped external disk write in the cloud outbox', () => {
313
+ test('an external disk write on a shared doc enters the cloud outbox', async () => {
495
314
  docs.openDoc(target);
496
315
  const beforeText = fs.readFileSync(target, 'utf8');
497
316
  fs.writeFileSync(target, `${beforeText}From vim\n`, 'utf8');
498
- docs._private.verifyOpenDocs();
317
+ await sleep(700);
499
318
 
500
319
  assert.equal(docs.getDocText(target), `${beforeText}From vim\n`);
501
320
  const claims = claimCloudMutations().filter((claim) => claim.envelope.resourceId === resourceId);
@@ -503,11 +322,10 @@ test('bounded verification puts a dropped external disk write in the cloud outbo
503
322
  assert.equal(claims[0].envelope.operationKind, 'yjs.update');
504
323
  const event = listEventsAfter(0).find((e) => e.mutationId === claims[0].envelope.mutationId);
505
324
  assert.equal(event.source, 'doc:disk');
506
- acknowledgeAndApplyClaim(
507
- claims[0],
508
- getReplicaByResourceId(resourceId).appliedVersion + 1,
509
- '2026-07-15T20:00:00.000Z',
510
- );
325
+ acknowledgeCloudMutation(resourceId, claims[0].envelope.mutationId, {
326
+ version: getReplicaByResourceId(resourceId).appliedVersion + 1,
327
+ committedAt: '2026-07-15T20:00:00.000Z',
328
+ });
511
329
  });
512
330
 
513
331
  // One door: the whole-text write API (agents/tools) on a shared doc.
@@ -520,11 +338,10 @@ test('a versioned whole-text write on a shared doc enters the cloud outbox', ()
520
338
  assert.equal(claims.length, 1, 'the API write must be claimable for cloud delivery');
521
339
  const event = listEventsAfter(0).find((e) => e.mutationId === claims[0].envelope.mutationId);
522
340
  assert.equal(event.source, 'doc:write');
523
- acknowledgeAndApplyClaim(
524
- claims[0],
525
- getReplicaByResourceId(resourceId).appliedVersion + 1,
526
- '2026-07-15T20:00:01.000Z',
527
- );
341
+ acknowledgeCloudMutation(resourceId, claims[0].envelope.mutationId, {
342
+ version: getReplicaByResourceId(resourceId).appliedVersion + 1,
343
+ committedAt: '2026-07-15T20:00:01.000Z',
344
+ });
528
345
  });
529
346
 
530
347
  // Promotion fence: edits made while sharing is being set up may not strand.
@@ -571,165 +388,6 @@ test('edits journaled during the promotion window are re-homed into the outbox',
571
388
  assert.deepEqual(claims.map((claim) => claim.envelope.mutationId), ['mut_during_promotion1']);
572
389
  });
573
390
 
574
- test('promotion rehomes a saving mutation and its physical plan as one identity', () => {
575
- const planResourceId = 'shr_planrehomecrash1';
576
- const planPath = path.join(docDir, 'promotion-plan-rehome.md');
577
- fs.writeFileSync(planPath, 'promotion base\n', 'utf8');
578
- const opened = docs.openDoc(planPath);
579
- const queued = enqueuePromotion({
580
- promotionId: 'prm_plan_rehome_crash_01',
581
- resourceId: planResourceId,
582
- localResource: opened.resource,
583
- localPath: opened.path,
584
- contract: 'text-yjs@1',
585
- schemaVersion: 1,
586
- snapshotBase64: opened.state,
587
- contentBytes: fs.statSync(planPath).size,
588
- }).promotion;
589
- const client = clientFromState(opened.state);
590
- const update = captureEdit(client, (text) => text.insert(text.length, 'survives crash\n'));
591
- const mutationId = 'mut_promotion_plan_rehome_01';
592
- docs._private.setDiskTransactionTestHooks({ crashAfterInstall: true });
593
- try {
594
- assert.throws(
595
- () => docs.applyDocMutations(planPath, [{ mutationId, update }]),
596
- /native file transaction failed/,
597
- );
598
- } finally {
599
- docs._private.setDiskTransactionTestHooks();
600
- }
601
- const docDisk = require('../state/doc-disk');
602
- assert.ok(docDisk.get(opened.resource, mutationId));
603
-
604
- acknowledgePromotion(queued.promotionId, {
605
- resourceId: planResourceId,
606
- authorityEpoch: 1,
607
- headVersion: 0,
608
- snapshotVersion: 0,
609
- snapshotChecksum: queued.snapshotChecksum,
610
- snapshotKey: `shared/${planResourceId}/snapshot.bin`,
611
- });
612
- const sharedChannel = `resource:${planResourceId}`;
613
- assert.equal(docDisk.get(opened.resource, mutationId), null);
614
- assert.ok(docDisk.get(sharedChannel, mutationId), 'the same physical plan follows the journal row');
615
- assert.equal(getMutation(sharedChannel, mutationId).state, 'saving-local');
616
-
617
- assert.doesNotThrow(() => docs.closeAllDocs());
618
- const reopened = docs.openDoc(planPath);
619
- assert.match(clientFromState(reopened.state).getText('content').toString(), /survives crash\n/);
620
- assert.match(fs.readFileSync(planPath, 'utf8'), /survives crash\n/);
621
- assert.equal(getMutation(sharedChannel, mutationId).state, 'saved-local');
622
- });
623
-
624
- test('an edit during first attachment stays overlay and joins after the snapshot', () => {
625
- const attachedResourceId = 'shr_attachfenceedit1';
626
- const attachedPath = path.join(docDir, 'attachment-fence-edit.md');
627
- fs.writeFileSync(attachedPath, '', 'utf8');
628
- const queued = enqueueAttachment({
629
- attachmentId: 'att_first_attachment_fence_01',
630
- resourceId: attachedResourceId,
631
- localPath: attachedPath,
632
- }).attachment;
633
- const local = docs.writeDocText(attachedPath, 'local during request\n', {
634
- baseHash: sha256(''),
635
- });
636
- const cloud = new Y.Doc();
637
- cloud.getText('content').insert(0, 'official snapshot\n');
638
- const snapshot = Buffer.from(Y.encodeStateAsUpdate(cloud));
639
- const checksum = require('crypto').createHash('sha256').update(snapshot).digest('hex');
640
-
641
- acknowledgeAttachment(queued.attachmentId, {
642
- resourceId: attachedResourceId,
643
- authorityEpoch: 1,
644
- headVersion: 0,
645
- snapshotVersion: 0,
646
- snapshotChecksum: checksum,
647
- snapshotBase64: snapshot.toString('base64'),
648
- contract: 'text-yjs@1',
649
- schemaVersion: 1,
650
- });
651
- const finalText = fs.readFileSync(attachedPath, 'utf8');
652
- assert.match(finalText, /official snapshot\n/);
653
- assert.match(finalText, /local during request\n/);
654
- const rehomed = getMutation(`resource:${attachedResourceId}`, local.mutationId);
655
- assert.ok(rehomed);
656
- assert.equal(rehomed.sharedResourceId, attachedResourceId);
657
- const claim = claimCloudMutations().find(
658
- (item) => item.envelope.mutationId === local.mutationId,
659
- );
660
- assert.ok(claim, 'post-request local work joins the new cloud outbox');
661
- acknowledgeAndApplyClaim(claim, 1, '2026-07-28T11:10:00.000Z');
662
- });
663
-
664
- test('a legacy nullable attachment fence migrates to durable zero', () => {
665
- const Database = require('better-sqlite3');
666
- const legacy = new Database(':memory:');
667
- try {
668
- legacy.exec(`
669
- CREATE TABLE shared_resource_attachments (
670
- attachment_id TEXT PRIMARY KEY,
671
- resource_id TEXT NOT NULL UNIQUE,
672
- local_path TEXT NOT NULL UNIQUE,
673
- attachment_state TEXT NOT NULL DEFAULT 'pending',
674
- attempt_count INTEGER NOT NULL DEFAULT 0,
675
- next_attempt_at TEXT,
676
- last_error TEXT,
677
- journal_cursor INTEGER,
678
- created_at TEXT NOT NULL,
679
- updated_at TEXT NOT NULL
680
- );
681
- INSERT INTO shared_resource_attachments (
682
- attachment_id,
683
- resource_id,
684
- local_path,
685
- journal_cursor,
686
- created_at,
687
- updated_at
688
- ) VALUES (
689
- 'att_legacy_nullable_fence_01',
690
- 'shr_legacyfence0001',
691
- '/tmp/legacy-fence.md',
692
- NULL,
693
- '2026-07-28T00:00:00.000Z',
694
- '2026-07-28T00:00:00.000Z'
695
- );
696
- `);
697
- require('../state/db')._private.migrate(legacy);
698
-
699
- const cursor = legacy.prepare(
700
- 'PRAGMA table_info(shared_resource_attachments)',
701
- ).all().find((column) => column.name === 'journal_cursor');
702
- assert.equal(cursor.notnull, 1);
703
- assert.equal(String(cursor.dflt_value), '0');
704
- assert.equal(
705
- legacy.prepare(`
706
- SELECT journal_cursor AS cursor
707
- FROM shared_resource_attachments
708
- WHERE attachment_id = 'att_legacy_nullable_fence_01'
709
- `).get().cursor,
710
- 0,
711
- );
712
- assert.throws(() => legacy.prepare(`
713
- INSERT INTO shared_resource_attachments (
714
- attachment_id,
715
- resource_id,
716
- local_path,
717
- journal_cursor,
718
- created_at,
719
- updated_at
720
- ) VALUES (?, ?, ?, NULL, ?, ?)
721
- `).run(
722
- 'att_legacy_null_rejected_01',
723
- 'shr_legacynull00001',
724
- '/tmp/legacy-null-rejected.md',
725
- '2026-07-28T00:00:00.000Z',
726
- '2026-07-28T00:00:00.000Z',
727
- ), /NOT NULL/);
728
- } finally {
729
- legacy.close();
730
- }
731
- });
732
-
733
391
  // Truthful saves: a failed file write must fail the edit — no journal
734
392
  // completion, no event, no acknowledgement — and recover once disk works.
735
393
  test('a failed shared-file write reports failure and recovers on reopen', async () => {
@@ -741,7 +399,7 @@ test('a failed shared-file write reports failure and recovers on reopen', async
741
399
  try {
742
400
  assert.throws(
743
401
  () => docs.applyDocMutations(target, [{ mutationId: 'mut_unwritable_00001', update }]),
744
- /EACCES|EPERM|permission denied/,
402
+ /EACCES|EPERM/,
745
403
  );
746
404
  } finally {
747
405
  fs.chmodSync(docDir, 0o755);
@@ -749,7 +407,7 @@ test('a failed shared-file write reports failure and recovers on reopen', async
749
407
  assert.equal(listEventsAfter(0).length, beforeEvents, 'a failed save must not project an event');
750
408
  const stalled = getMutation(`resource:${resourceId}`, 'mut_unwritable_00001');
751
409
  assert.equal(stalled.state, 'saving-local');
752
- assert.match(stalled.lastError, /EACCES|EPERM|permission denied/);
410
+ assert.match(stalled.lastError, /EACCES|EPERM/);
753
411
 
754
412
  docs.closeAllDocs();
755
413
  docs.openDoc(target);
@@ -757,14 +415,11 @@ test('a failed shared-file write reports failure and recovers on reopen', async
757
415
  assert.match(fs.readFileSync(target, 'utf8'), /Unwritable\n$/);
758
416
 
759
417
  // Drain the outbox so later tests observe only their own deliveries.
760
- const [claim] = claimCloudMutations().filter(
761
- (item) => item.envelope.mutationId === 'mut_unwritable_00001',
762
- );
763
- acknowledgeAndApplyClaim(
764
- claim,
765
- getReplicaByResourceId(resourceId).appliedVersion + 1,
766
- '2026-07-15T20:00:01.500Z',
767
- );
418
+ claimCloudMutations();
419
+ acknowledgeCloudMutation(resourceId, 'mut_unwritable_00001', {
420
+ version: getReplicaByResourceId(resourceId).appliedVersion + 1,
421
+ committedAt: '2026-07-15T20:00:01.500Z',
422
+ });
768
423
  });
769
424
 
770
425
  // Truthful saves, the retry-while-open half: after a failed flush the desired
@@ -779,10 +434,7 @@ test('retrying a failed whole-text write settles the original mutation: disk, jo
779
434
 
780
435
  fs.chmodSync(docDir, 0o555);
781
436
  try {
782
- assert.throws(
783
- () => docs.writeDocText(target, nextText, { baseHash }),
784
- /EACCES|EPERM|permission denied/,
785
- );
437
+ assert.throws(() => docs.writeDocText(target, nextText, { baseHash }), /EACCES|EPERM/);
786
438
  } finally {
787
439
  fs.chmodSync(docDir, 0o755);
788
440
  }
@@ -801,11 +453,10 @@ test('retrying a failed whole-text write settles the original mutation: disk, jo
801
453
  );
802
454
  const claims = claimCloudMutations().filter((claim) => claim.envelope.resourceId === resourceId);
803
455
  assert.deepEqual(claims.map((claim) => claim.envelope.mutationId), [stuck.mutationId]);
804
- acknowledgeAndApplyClaim(
805
- claims[0],
806
- getReplicaByResourceId(resourceId).appliedVersion + 1,
807
- '2026-07-15T20:00:02.000Z',
808
- );
456
+ acknowledgeCloudMutation(resourceId, stuck.mutationId, {
457
+ version: getReplicaByResourceId(resourceId).appliedVersion + 1,
458
+ committedAt: '2026-07-15T20:00:02.000Z',
459
+ });
809
460
  });
810
461
 
811
462
  // A live edit must not strip the file's permission bits: replace-by-rename
@@ -817,11 +468,10 @@ test('an executable file stays executable through a live write', () => {
817
468
  assert.equal(fs.statSync(target).mode & 0o777, 0o755);
818
469
 
819
470
  const claims = claimCloudMutations().filter((claim) => claim.envelope.resourceId === resourceId);
820
- acknowledgeAndApplyClaim(
821
- claims[0],
822
- getReplicaByResourceId(resourceId).appliedVersion + 1,
823
- '2026-07-15T20:00:03.000Z',
824
- );
471
+ acknowledgeCloudMutation(resourceId, claims[0].envelope.mutationId, {
472
+ version: getReplicaByResourceId(resourceId).appliedVersion + 1,
473
+ committedAt: '2026-07-15T20:00:03.000Z',
474
+ });
825
475
  });
826
476
 
827
477
  // Compaction on the authority prunes official rows the moment a verified
@@ -880,71 +530,6 @@ test('a pruned-tail replica rehydrates: snapshot merge keeps unsynced local edit
880
530
  assert.notEqual(pending.state, 'synced');
881
531
  });
882
532
 
883
- test('equal snapshot version with different bytes fails before state or cursor movement', () => {
884
- const equalResourceId = 'shr_equalsnapshot001';
885
- const equalPath = path.join(docDir, 'equal-snapshot.md');
886
- fs.writeFileSync(equalPath, '', 'utf8');
887
- const first = new Y.Doc();
888
- first.getText('content').insert(0, 'state A\n');
889
- const firstBytes = Buffer.from(Y.encodeStateAsUpdate(first));
890
- const firstChecksum = require('crypto').createHash('sha256').update(firstBytes).digest('hex');
891
- docs.installCloudDocSnapshot(equalPath, {
892
- resourceId: equalResourceId,
893
- authorityEpoch: 1,
894
- headVersion: 1,
895
- snapshotVersion: 1,
896
- snapshotChecksum: firstChecksum,
897
- snapshotBase64: firstBytes.toString('base64'),
898
- contract: 'text-yjs@1',
899
- schemaVersion: 1,
900
- });
901
- const beforeEvents = listEventsAfter(0).length;
902
- const second = new Y.Doc();
903
- second.getText('content').insert(0, 'state B\n');
904
- const secondBytes = Buffer.from(Y.encodeStateAsUpdate(second));
905
- const secondChecksum = require('crypto').createHash('sha256').update(secondBytes).digest('hex');
906
-
907
- assert.throws(() => docs.installCloudDocSnapshot(equalPath, {
908
- resourceId: equalResourceId,
909
- authorityEpoch: 1,
910
- headVersion: 1,
911
- snapshotVersion: 1,
912
- snapshotChecksum: secondChecksum,
913
- snapshotBase64: secondBytes.toString('base64'),
914
- contract: 'text-yjs@1',
915
- schemaVersion: 1,
916
- }), /unequal state at version 1/);
917
- assert.equal(fs.readFileSync(equalPath, 'utf8'), 'state A\n');
918
- assert.equal(getReplicaByResourceId(equalResourceId).snapshotChecksum, firstChecksum);
919
- assert.equal(listEventsAfter(0).length, beforeEvents);
920
-
921
- openLocalDb().prepare(`
922
- UPDATE shared_resource_replicas SET applied_version = 3 WHERE resource_id = ?
923
- `).run(equalResourceId);
924
- assert.throws(() => docs.installCloudDocSnapshot(equalPath, {
925
- resourceId: equalResourceId,
926
- authorityEpoch: 1,
927
- headVersion: 3,
928
- snapshotVersion: 1,
929
- snapshotChecksum: secondChecksum,
930
- snapshotBase64: secondBytes.toString('base64'),
931
- contract: 'text-yjs@1',
932
- schemaVersion: 1,
933
- }), /unequal state at version 1/);
934
- assert.throws(() => docs.installCloudDocSnapshot(equalPath, {
935
- resourceId: equalResourceId,
936
- authorityEpoch: 1,
937
- headVersion: 3,
938
- snapshotVersion: 4,
939
- snapshotChecksum: secondChecksum,
940
- snapshotBase64: secondBytes.toString('base64'),
941
- contract: 'text-yjs@1',
942
- schemaVersion: 1,
943
- }), /cannot exceed headVersion/);
944
- assert.equal(fs.readFileSync(equalPath, 'utf8'), 'state A\n');
945
- assert.equal(getReplicaByResourceId(equalResourceId).appliedVersion, 3);
946
- });
947
-
948
533
  test('a promotion-born replica (which never attached) gets an attachment row for rehydration', () => {
949
534
  const { requestRehydration } = require('../state/attachments');
950
535
  const requested = requestRehydration({ resourceId });
@@ -987,316 +572,3 @@ test('revoked authority pauses the outbox and a successful rejoin resumes it', (
987
572
  'the same durable edit resumes after authority is restored',
988
573
  );
989
574
  });
990
-
991
- test('cloud apply first captures an unsent disk save and sends no machine-local metadata', () => {
992
- const opened = docs.openDoc(target);
993
- const beforeText = fs.readFileSync(target, 'utf8');
994
- const priorEvents = listEventsAfter(0);
995
- const beforeSeq = priorEvents.length ? priorEvents[priorEvents.length - 1].seq : 0;
996
- fs.writeFileSync(target, `disk-before-cloud\n${beforeText}`, 'utf8');
997
-
998
- const mutations = require('../state/mutations');
999
- const originalCommit = mutations.commitLocalMutation;
1000
- mutations.commitLocalMutation = () => {
1001
- throw new Error('simulated local journal outage');
1002
- };
1003
- try {
1004
- docs._private.verifyOpenDocs();
1005
- } finally {
1006
- mutations.commitLocalMutation = originalCommit;
1007
- }
1008
- assert.equal(docs.getDocText(target), beforeText);
1009
-
1010
- const remote = clientFromState(opened.state);
1011
- const update = captureEdit(remote, (text) => text.insert(text.length, 'cloud-after-disk\n'));
1012
- const beforeVersion = getReplicaByResourceId(resourceId).appliedVersion;
1013
- docs.applyCloudDocMutation({
1014
- envelope: {
1015
- resourceId,
1016
- authorityEpoch: getReplicaByResourceId(resourceId).authorityEpoch,
1017
- mutationId: 'mut_cloud_after_disk_1',
1018
- deviceId: 'device-remote-disk-capture',
1019
- baseVersion: beforeVersion,
1020
- contract: 'text-yjs@1',
1021
- schemaVersion: 1,
1022
- operationKind: 'yjs.update',
1023
- operation: { kind: 'yjs.update', update },
1024
- },
1025
- version: beforeVersion + 1,
1026
- committedAt: '2026-07-28T08:00:00.000Z',
1027
- });
1028
-
1029
- const finalText = fs.readFileSync(target, 'utf8');
1030
- assert.match(finalText, /^disk-before-cloud\n/);
1031
- assert.match(finalText, /cloud-after-disk\n/);
1032
- assert.equal(getReplicaByResourceId(resourceId).appliedVersion, beforeVersion + 1);
1033
- const diskEvent = listEventsAfter(beforeSeq).find((event) => (
1034
- event.resource === opened.resource && event.source === 'doc:disk'
1035
- ));
1036
- assert.ok(diskEvent, 'the disk save received its own durable mutation identity');
1037
- const diskClaim = claimCloudMutations().find(
1038
- (claim) => claim.envelope.mutationId === diskEvent.mutationId,
1039
- );
1040
- assert.ok(diskClaim, 'the formerly unsent disk save is now in the shared outbox');
1041
- assert.deepEqual(Object.keys(diskClaim.envelope.operation).sort(), ['kind', 'update']);
1042
- });
1043
-
1044
- test('cloud materialization CAS keeps a disk save that lands at the install boundary', () => {
1045
- const opened = docs.openDoc(target);
1046
- const beforeText = fs.readFileSync(target, 'utf8');
1047
- const remote = clientFromState(opened.state);
1048
- const update = captureEdit(remote, (text) => text.insert(text.length, 'cloud-cas\n'));
1049
- const beforeVersion = getReplicaByResourceId(resourceId).appliedVersion;
1050
- const child = writeFromAnotherProcessAfter(target, `disk-cas\n${beforeText}`, 75);
1051
- docs._private.setDiskTransactionTestHooks({ pauseBeforeInstall: 350 });
1052
- try {
1053
- docs.applyCloudDocMutation({
1054
- envelope: {
1055
- resourceId,
1056
- authorityEpoch: getReplicaByResourceId(resourceId).authorityEpoch,
1057
- mutationId: 'mut_cloud_cas_race_1',
1058
- deviceId: 'device-remote-cas',
1059
- baseVersion: beforeVersion,
1060
- contract: 'text-yjs@1',
1061
- schemaVersion: 1,
1062
- operationKind: 'yjs.update',
1063
- operation: { kind: 'yjs.update', update },
1064
- },
1065
- version: beforeVersion + 1,
1066
- committedAt: '2026-07-28T08:00:01.000Z',
1067
- });
1068
- } finally {
1069
- docs._private.setDiskTransactionTestHooks();
1070
- }
1071
- child.unref();
1072
- const finalText = fs.readFileSync(target, 'utf8');
1073
- assert.match(finalText, /^disk-cas\n/);
1074
- assert.match(finalText, /cloud-cas\n/);
1075
- assert.equal(getReplicaByResourceId(resourceId).appliedVersion, beforeVersion + 1);
1076
- });
1077
-
1078
- test('snapshot CAS preserves and re-homes a local save racing first attachment', () => {
1079
- const recipientResourceId = 'shr_ponmlkjihgfedcba';
1080
- const recipientPath = path.join(docDir, 'snapshot-cas-recipient.md');
1081
- fs.writeFileSync(recipientPath, '', 'utf8');
1082
- const cloud = new Y.Doc();
1083
- cloud.getText('content').insert(0, 'official snapshot\n');
1084
- const snapshot = Buffer.from(Y.encodeStateAsUpdate(cloud));
1085
- const checksum = require('crypto').createHash('sha256').update(snapshot).digest('hex');
1086
- const child = writeFromAnotherProcessAfter(recipientPath, 'local during snapshot\n', 75);
1087
-
1088
- docs._private.setDiskTransactionTestHooks({ pauseBeforeInstall: 350 });
1089
- try {
1090
- docs.installCloudDocSnapshot(recipientPath, {
1091
- resourceId: recipientResourceId,
1092
- authorityEpoch: 1,
1093
- headVersion: 8,
1094
- snapshotVersion: 8,
1095
- snapshotChecksum: checksum,
1096
- snapshotBase64: snapshot.toString('base64'),
1097
- contract: 'text-yjs@1',
1098
- schemaVersion: 1,
1099
- });
1100
- } finally {
1101
- docs._private.setDiskTransactionTestHooks();
1102
- }
1103
- child.unref();
1104
- const finalText = fs.readFileSync(recipientPath, 'utf8');
1105
- assert.match(finalText, /official snapshot\n/);
1106
- assert.match(finalText, /local during snapshot\n/);
1107
- assert.equal(getReplicaByResourceId(recipientResourceId).appliedVersion, 8);
1108
- assert.ok(
1109
- claimCloudMutations().some((claim) => (
1110
- claim.envelope.resourceId === recipientResourceId
1111
- && claim.envelope.operationKind === 'yjs.update'
1112
- )),
1113
- 'the local save born during attachment is re-homed into the new outbox',
1114
- );
1115
- });
1116
-
1117
- test('snapshot plan pins the captured file instead of adopting a later save', () => {
1118
- const recipientResourceId = 'shr_captureplanrace1';
1119
- const recipientPath = path.join(docDir, 'snapshot-capture-plan-recipient.md');
1120
- fs.writeFileSync(recipientPath, '', 'utf8');
1121
- const cloud = new Y.Doc();
1122
- cloud.getText('content').insert(0, 'official snapshot\n');
1123
- const snapshot = Buffer.from(Y.encodeStateAsUpdate(cloud));
1124
- const checksum = require('crypto').createHash('sha256').update(snapshot).digest('hex');
1125
- const child = writeFromAnotherProcessAfter(
1126
- recipientPath,
1127
- 'local after snapshot capture\n',
1128
- 75,
1129
- );
1130
-
1131
- docs._private.setDiskTransactionTestHooks({ pauseAfterSnapshotCapture: 350 });
1132
- try {
1133
- docs.installCloudDocSnapshot(recipientPath, {
1134
- resourceId: recipientResourceId,
1135
- authorityEpoch: 1,
1136
- headVersion: 9,
1137
- snapshotVersion: 9,
1138
- snapshotChecksum: checksum,
1139
- snapshotBase64: snapshot.toString('base64'),
1140
- contract: 'text-yjs@1',
1141
- schemaVersion: 1,
1142
- });
1143
- } finally {
1144
- docs._private.setDiskTransactionTestHooks();
1145
- }
1146
- child.unref();
1147
-
1148
- const finalText = fs.readFileSync(recipientPath, 'utf8');
1149
- assert.match(finalText, /official snapshot\n/);
1150
- assert.match(finalText, /local after snapshot capture\n/);
1151
- assert.equal(getReplicaByResourceId(recipientResourceId).appliedVersion, 9);
1152
- assert.ok(
1153
- claimCloudMutations().some((claim) => (
1154
- claim.envelope.resourceId === recipientResourceId
1155
- && claim.envelope.operationKind === 'yjs.update'
1156
- )),
1157
- 'the save after capture keeps one durable local mutation identity',
1158
- );
1159
- });
1160
-
1161
- test('snapshot replacement retries share one finite budget', () => {
1162
- const boundedResourceId = 'shr_snapshotbounded1';
1163
- const boundedPath = path.join(docDir, 'snapshot-bounded.md');
1164
- fs.writeFileSync(boundedPath, '', 'utf8');
1165
- const cloud = new Y.Doc();
1166
- cloud.getText('content').insert(0, 'never installs\n');
1167
- const snapshot = Buffer.from(Y.encodeStateAsUpdate(cloud));
1168
- const checksum = require('crypto').createHash('sha256').update(snapshot).digest('hex');
1169
- const docDisk = require('../state/doc-disk');
1170
- const originalCross = docDisk.cross;
1171
- let attempts = 0;
1172
- docDisk.cross = () => {
1173
- attempts += 1;
1174
- return { status: 'changed', reason: 'simulated continuous replacer' };
1175
- };
1176
- try {
1177
- assert.throws(() => docs.installCloudDocSnapshot(boundedPath, {
1178
- resourceId: boundedResourceId,
1179
- authorityEpoch: 1,
1180
- headVersion: 1,
1181
- snapshotVersion: 1,
1182
- snapshotChecksum: checksum,
1183
- snapshotBase64: snapshot.toString('base64'),
1184
- contract: 'text-yjs@1',
1185
- schemaVersion: 1,
1186
- }), /kept changing during snapshot install/);
1187
- } finally {
1188
- docDisk.cross = originalCross;
1189
- }
1190
- assert.equal(attempts, 8);
1191
- assert.equal(docDisk.listSnapshots(docs.docResourceName(boundedPath)).length, 0);
1192
- assert.equal(fs.readFileSync(boundedPath, 'utf8'), '');
1193
- });
1194
-
1195
- test('snapshot install recovers exactly once after the native helper dies post-install', () => {
1196
- const recipientResourceId = 'shr_crashrecoverabcd';
1197
- const recipientPath = path.join(docDir, 'snapshot-crash-recipient.md');
1198
- fs.writeFileSync(recipientPath, '', 'utf8');
1199
- const cloud = new Y.Doc();
1200
- cloud.getText('content').insert(0, 'snapshot survives helper crash\n');
1201
- const snapshot = Buffer.from(Y.encodeStateAsUpdate(cloud));
1202
- const checksum = require('crypto').createHash('sha256').update(snapshot).digest('hex');
1203
- const input = {
1204
- resourceId: recipientResourceId,
1205
- authorityEpoch: 1,
1206
- headVersion: 3,
1207
- snapshotVersion: 3,
1208
- snapshotChecksum: checksum,
1209
- snapshotBase64: snapshot.toString('base64'),
1210
- contract: 'text-yjs@1',
1211
- schemaVersion: 1,
1212
- };
1213
-
1214
- docs._private.setDiskTransactionTestHooks({ crashAfterInstall: true });
1215
- try {
1216
- assert.throws(
1217
- () => docs.installCloudDocSnapshot(recipientPath, input),
1218
- /native file transaction failed/,
1219
- );
1220
- } finally {
1221
- docs._private.setDiskTransactionTestHooks();
1222
- }
1223
- assert.equal(fs.readFileSync(recipientPath, 'utf8'), 'snapshot survives helper crash\n');
1224
- assert.equal(getReplicaByResourceId(recipientResourceId), null);
1225
-
1226
- const snapshotText = fs.readFileSync(recipientPath, 'utf8');
1227
- docs.writeDocText(
1228
- recipientPath,
1229
- `${snapshotText}local write after helper crash\n`,
1230
- {
1231
- baseHash: require('crypto').createHash('sha256').update(snapshotText).digest('hex'),
1232
- mutationId: 'mut_snapshot_crash_local_01',
1233
- },
1234
- );
1235
- const retried = docs.installCloudDocSnapshot(recipientPath, input);
1236
- assert.equal(retried.duplicate, true);
1237
- assert.equal(getReplicaByResourceId(recipientResourceId).appliedVersion, 3);
1238
- assert.equal(
1239
- fs.readFileSync(recipientPath, 'utf8'),
1240
- 'snapshot survives helper crash\nlocal write after helper crash\n',
1241
- );
1242
- assert.equal(
1243
- getMutation(`resource:${recipientResourceId}`, 'mut_snapshot_crash_local_01')?.mutationId,
1244
- 'mut_snapshot_crash_local_01',
1245
- );
1246
- assert.equal(
1247
- listEventsAfter(0).filter((event) => (
1248
- event.sharedResourceId === recipientResourceId
1249
- && event.source === 'doc:cloud-snapshot'
1250
- )).length,
1251
- 1,
1252
- );
1253
- });
1254
-
1255
- test('rebasing a physical predecessor also rebases its durable semantic base', () => {
1256
- const docDisk = require('../state/doc-disk');
1257
- const file = path.join(docDir, 'rebase-semantic-base.md');
1258
- fs.writeFileSync(file, 'A\n', 'utf8');
1259
- const mutation = {
1260
- channelId: 'doc:rebase-semantic-base',
1261
- mutationId: 'mut_rebase_semantic_01',
1262
- localResource: 'doc:rebase-semantic-base',
1263
- };
1264
- docDisk.insertBase(openLocalDb(), mutation, docDisk.observe(file), 'doc:write');
1265
- docDisk.prepare(mutation.channelId, mutation.mutationId, 'B\n');
1266
- fs.writeFileSync(file, 'B\n', 'utf8');
1267
- const rebased = docDisk.rebaseExpected(
1268
- mutation.channelId,
1269
- mutation.mutationId,
1270
- docDisk.observe(file),
1271
- );
1272
- assert.equal(rebased.baseText, 'B\n');
1273
- docDisk.cancel(mutation.channelId, mutation.mutationId);
1274
- });
1275
-
1276
- test('legacy restart recovery keeps a newer disk successor over an older saving row', () => {
1277
- const restartPath = path.join(docDir, 'restart-successor.md');
1278
- fs.writeFileSync(restartPath, 'base\n', 'utf8');
1279
- const opened = docs.openDoc(restartPath);
1280
- const client = clientFromState(opened.state);
1281
- const update = captureEdit(client, (text) => text.insert(text.length, 'older journal\n'));
1282
- docs.closeAllDocs();
1283
-
1284
- journalMutation({
1285
- channelId: opened.resource,
1286
- localResource: opened.resource,
1287
- mutationId: 'mut_restart_successor_1',
1288
- authorityEpoch: 0,
1289
- baseCloudVersion: 0,
1290
- contract: 'text-yjs@1',
1291
- schemaVersion: 1,
1292
- operationKind: 'yjs.update',
1293
- operation: { kind: 'yjs.update', update },
1294
- });
1295
- fs.writeFileSync(restartPath, 'newer disk\nbase\n', 'utf8');
1296
-
1297
- docs.openDoc(restartPath);
1298
- const recovered = fs.readFileSync(restartPath, 'utf8');
1299
- assert.match(recovered, /^newer disk\n/);
1300
- assert.match(recovered, /older journal\n/);
1301
- assert.equal(getMutation(opened.resource, 'mut_restart_successor_1').state, 'saved-local');
1302
- });