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