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.
- package/README.md +12 -0
- package/lib/cli.js +146 -8
- package/lib/layout.js +89 -4
- package/lib/shared-realtime-tunnel.js +384 -43
- package/lib/supervisor.js +0 -3
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
- package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
- package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
- package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
- package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
- package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
- package/runtime/scripts/amalgm-mcp/index.js +4 -0
- package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
- package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
- package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
- package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
- package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
- package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
- package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
- package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
- package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
- package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
- package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
- package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
- package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
- package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
- package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
- package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
- package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
- package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
- package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
- package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
- package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
- package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
- package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
- package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
- package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
- package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
- package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
- package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
- package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
- package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
- package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
- package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
- package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
- package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
- package/runtime/scripts/local-gateway.js +1 -0
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
- package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
- 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
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
});
|