amalgm 0.1.239 → 0.1.241

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/lib/shared-realtime-tunnel.js +18 -4
  2. package/package.json +1 -1
  3. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
  4. package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
  5. package/runtime/scripts/amalgm-mcp/server/routes/state.js +13 -0
  6. package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
  7. package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
  8. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
  9. package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
  10. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
  11. package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
  12. package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
  13. package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
  14. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
  15. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
  16. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
  17. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
  18. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
  19. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
  20. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
  21. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
  22. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
  23. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
  24. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
  25. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
  26. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
  27. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
  28. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
  29. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
  30. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +98 -4
  31. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +156 -8
  32. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
  33. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
  34. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
  35. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
  36. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
  37. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
  38. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
  39. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
  40. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
  41. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
  42. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
  43. package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
  44. package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
  45. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
  46. package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
  47. package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
  48. package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
  49. package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
  50. package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
  51. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
  52. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  53. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  54. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
  55. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
  56. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
  57. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
  58. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
  59. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
  60. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
  61. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
  63. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
  64. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
  65. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
  66. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
  67. package/runtime/scripts/amalgm-mcp/workspace/objects.js +185 -55
  68. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
  69. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
  70. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
  71. package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
  72. package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
  73. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
  74. package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
  75. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
  76. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
  77. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
  78. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
  79. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
  80. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
  81. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
  82. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
  83. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
  84. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
  85. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
  86. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
  87. package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
@@ -0,0 +1,364 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const crypto = require('node:crypto');
6
+ const fs = require('node:fs');
7
+ const os = require('node:os');
8
+ const path = require('node:path');
9
+ const { execFile, execFileSync, spawn } = require('node:child_process');
10
+ const { promisify } = require('node:util');
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ test('checked-in descriptor-relative transaction binaries match their exact source', async () => {
15
+ const verifier = path.join(
16
+ __dirname,
17
+ '..',
18
+ 'workspace',
19
+ 'native-file-transaction',
20
+ 'verify.mjs',
21
+ );
22
+ const { stdout } = await execFileAsync(process.execPath, [verifier]);
23
+ assert.match(stdout, /verified source [a-f0-9]{64} and 2 binaries/);
24
+ });
25
+
26
+ test('native process lifetime distinguishes live owners from exited PIDs', () => {
27
+ const platform = process.platform === 'darwin' && process.arch === 'arm64'
28
+ ? 'darwin-arm64'
29
+ : process.platform === 'linux' && process.arch === 'x64'
30
+ ? 'linux-x64'
31
+ : null;
32
+ if (platform === null) return;
33
+ const executable = path.join(
34
+ __dirname,
35
+ '..',
36
+ 'workspace',
37
+ 'native-file-transaction',
38
+ 'bin',
39
+ platform,
40
+ 'amalgm-file-transaction',
41
+ );
42
+ const identity = (pid) => JSON.parse(execFileSync(executable, [], {
43
+ input: JSON.stringify({
44
+ action: 'identity',
45
+ backend: 'process-identity',
46
+ pid,
47
+ }),
48
+ encoding: 'utf8',
49
+ }));
50
+
51
+ const current = identity(process.pid);
52
+ assert.equal(current.status, 'identified');
53
+ assert.match(current.identity, /.+/);
54
+ assert.equal(identity(1).status, 'identified');
55
+ assert.deepEqual(identity(2_147_483_647), { status: 'exited' });
56
+ });
57
+
58
+ test('same bytes on a replacement inode do not satisfy an exact document predecessor', () => {
59
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-file-cas-test-'));
60
+ const file = path.join(root, 'document.md');
61
+ fs.writeFileSync(file, 'same bytes\n', 'utf8');
62
+ const docDisk = require('../state/doc-disk');
63
+ const native = require('../workspace/native-file-transaction/client');
64
+ const expected = docDisk.observe(file);
65
+
66
+ const replacement = path.join(root, 'replacement.md');
67
+ fs.writeFileSync(replacement, 'same bytes\n', 'utf8');
68
+ fs.renameSync(replacement, file);
69
+
70
+ const targetContent = Buffer.from('new target\n', 'utf8');
71
+ const transaction = {
72
+ root: expected.root,
73
+ rootDev: expected.rootDev,
74
+ rootIno: expected.rootIno,
75
+ path: expected.relativePath,
76
+ id: crypto.createHash('sha256').update('same-byte-inode-cas').digest('hex'),
77
+ operation: 'same-byte-inode-cas',
78
+ expected: expected.identity,
79
+ target: docDisk.byteIdentity(targetContent),
80
+ };
81
+ try {
82
+ const result = native.run(transaction, 'apply', targetContent);
83
+ assert.equal(result.status, 'changed');
84
+ assert.equal(fs.readFileSync(file, 'utf8'), 'same bytes\n');
85
+ } finally {
86
+ fs.rmSync(root, { recursive: true, force: true });
87
+ }
88
+ });
89
+
90
+ test('document observation stays with one physical parent across rename and replacement', () => {
91
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-file-observe-test-'));
92
+ const parent = path.join(root, 'registered-parent');
93
+ const renamed = path.join(root, 'uuid-owned-parent');
94
+ const file = path.join(parent, 'document.md');
95
+ fs.mkdirSync(parent);
96
+ fs.writeFileSync(file, 'owned bytes\n', 'utf8');
97
+ const originalParent = fs.statSync(parent, { bigint: true });
98
+ const child = spawn(process.execPath, [
99
+ '-e',
100
+ 'const fs=require("fs");'
101
+ + `setTimeout(()=>{fs.renameSync(${JSON.stringify(parent)},${JSON.stringify(renamed)});`
102
+ + `fs.mkdirSync(${JSON.stringify(parent)});`
103
+ + `fs.writeFileSync(${JSON.stringify(file)},"unrelated bytes\\n","utf8");},75);`,
104
+ ], { stdio: 'ignore' });
105
+
106
+ try {
107
+ const observed = require('../state/doc-disk').observe(file, {
108
+ pauseAfterParentOpen: 350,
109
+ });
110
+ assert.equal(observed.text, 'owned bytes\n');
111
+ assert.equal(observed.rootDev, originalParent.dev.toString());
112
+ assert.equal(observed.rootIno, originalParent.ino.toString());
113
+ assert.notEqual(observed.text, fs.readFileSync(file, 'utf8'));
114
+ } finally {
115
+ child.unref();
116
+ fs.rmSync(root, { recursive: true, force: true });
117
+ }
118
+ });
119
+
120
+ test('receipt release keeps proof when its physical parent moved', () => {
121
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-receipt-move-test-'));
122
+ const parent = path.join(root, 'original-parent');
123
+ const movedParent = path.join(root, 'moved-parent');
124
+ const file = path.join(parent, 'document.md');
125
+ fs.mkdirSync(parent);
126
+ fs.writeFileSync(file, 'before\n', 'utf8');
127
+ const docDisk = require('../state/doc-disk');
128
+ const native = require('../workspace/native-file-transaction/client');
129
+ const expected = docDisk.observe(file);
130
+ const targetContent = Buffer.from('after\n', 'utf8');
131
+ const transaction = {
132
+ root: expected.root,
133
+ rootDev: expected.rootDev,
134
+ rootIno: expected.rootIno,
135
+ path: expected.relativePath,
136
+ id: crypto.createHash('sha256').update('moved-receipt-parent').digest('hex'),
137
+ operation: 'moved-receipt-parent',
138
+ expected: expected.identity,
139
+ target: docDisk.byteIdentity(targetContent),
140
+ };
141
+
142
+ try {
143
+ assert.equal(
144
+ native.run(transaction, 'apply', targetContent).status,
145
+ 'applied',
146
+ );
147
+ fs.renameSync(parent, movedParent);
148
+
149
+ const retained = native.run(transaction, 'release');
150
+ assert.equal(retained.status, 'pending');
151
+ assert.match(retained.reason, /receipt parent is unavailable/);
152
+ assert.equal(
153
+ fs.existsSync(path.join(
154
+ movedParent,
155
+ `.amalgm-apply-${transaction.id}`,
156
+ )),
157
+ true,
158
+ );
159
+
160
+ const rebound = native.run({
161
+ ...transaction,
162
+ root: fs.realpathSync(movedParent),
163
+ }, 'release');
164
+ assert.equal(rebound.status, 'released');
165
+ assert.equal(
166
+ fs.existsSync(path.join(
167
+ movedParent,
168
+ `.amalgm-apply-${transaction.id}`,
169
+ )),
170
+ false,
171
+ );
172
+ } finally {
173
+ fs.rmSync(root, { recursive: true, force: true });
174
+ }
175
+ });
176
+
177
+ test('exclusive tree moves preserve a destination created during the rename window', () => {
178
+ const native = require('../workspace/native-file-transaction/client');
179
+ for (const kind of ['file', 'directory']) {
180
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), `amalgm-tree-cas-${kind}-`));
181
+ const source = path.join(root, `source-${kind}`);
182
+ const destination = path.join(root, `destination-${kind}`);
183
+ if (kind === 'directory') {
184
+ fs.mkdirSync(source);
185
+ fs.writeFileSync(path.join(source, 'value.txt'), 'source directory\n', 'utf8');
186
+ } else {
187
+ fs.writeFileSync(source, 'source file\n', 'utf8');
188
+ }
189
+ const rootStats = fs.lstatSync(root, { bigint: true });
190
+ const sourceStats = fs.lstatSync(source, { bigint: true });
191
+ const child = spawn(process.execPath, [
192
+ '-e',
193
+ 'const fs=require("fs");'
194
+ + `setTimeout(()=>{`
195
+ + (kind === 'directory'
196
+ ? `fs.mkdirSync(${JSON.stringify(destination)});`
197
+ + `fs.writeFileSync(${JSON.stringify(path.join(destination, 'value.txt'))},"racing directory\\n","utf8");`
198
+ : `fs.writeFileSync(${JSON.stringify(destination)},"racing file\\n","utf8");`)
199
+ + '},75);',
200
+ ], { stdio: 'ignore' });
201
+
202
+ try {
203
+ const result = native.moveNoReplace({
204
+ root: fs.realpathSync(root),
205
+ rootDev: rootStats.dev.toString(),
206
+ rootIno: rootStats.ino.toString(),
207
+ source: path.basename(source),
208
+ sourceIdentity: {
209
+ kind: kind === 'directory' ? 'folder' : kind,
210
+ dev: sourceStats.dev.toString(),
211
+ ino: sourceStats.ino.toString(),
212
+ },
213
+ sourceParentIdentity: {
214
+ kind: 'folder',
215
+ dev: rootStats.dev.toString(),
216
+ ino: rootStats.ino.toString(),
217
+ },
218
+ targetParentIdentity: {
219
+ kind: 'folder',
220
+ dev: rootStats.dev.toString(),
221
+ ino: rootStats.ino.toString(),
222
+ },
223
+ path: path.basename(destination),
224
+ id: crypto.createHash('sha256').update(`tree-cas-${kind}`).digest('hex'),
225
+ operation: `tree-cas-${kind}`,
226
+ }, {
227
+ pauseBeforeInstall: 300,
228
+ });
229
+ assert.equal(result.status, 'changed');
230
+ assert.match(result.reason, /destination changed/);
231
+ if (kind === 'directory') {
232
+ assert.equal(
233
+ fs.readFileSync(path.join(source, 'value.txt'), 'utf8'),
234
+ 'source directory\n',
235
+ );
236
+ assert.equal(
237
+ fs.readFileSync(path.join(destination, 'value.txt'), 'utf8'),
238
+ 'racing directory\n',
239
+ );
240
+ } else {
241
+ assert.equal(fs.readFileSync(source, 'utf8'), 'source file\n');
242
+ assert.equal(fs.readFileSync(destination, 'utf8'), 'racing file\n');
243
+ }
244
+ } finally {
245
+ child.unref();
246
+ fs.rmSync(root, { recursive: true, force: true });
247
+ }
248
+ }
249
+ });
250
+
251
+ test('exclusive tree moves restore a source name swapped during the rename window', () => {
252
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-tree-source-cas-'));
253
+ const source = path.join(root, 'source.txt');
254
+ const movedOriginal = path.join(root, 'moved-original.txt');
255
+ const destination = path.join(root, 'destination.txt');
256
+ fs.writeFileSync(source, 'original source\n', 'utf8');
257
+ const rootStats = fs.lstatSync(root, { bigint: true });
258
+ const sourceStats = fs.lstatSync(source, { bigint: true });
259
+ const child = spawn(process.execPath, [
260
+ '-e',
261
+ 'const fs=require("fs");'
262
+ + `setTimeout(()=>{`
263
+ + `fs.renameSync(${JSON.stringify(source)},${JSON.stringify(movedOriginal)});`
264
+ + `fs.writeFileSync(${JSON.stringify(source)},"replacement source\\n","utf8");`
265
+ + '},75);',
266
+ ], { stdio: 'ignore' });
267
+
268
+ try {
269
+ const result = require('../workspace/native-file-transaction/client')
270
+ .moveNoReplace({
271
+ root: fs.realpathSync(root),
272
+ rootDev: rootStats.dev.toString(),
273
+ rootIno: rootStats.ino.toString(),
274
+ source: path.basename(source),
275
+ sourceIdentity: {
276
+ kind: 'file',
277
+ dev: sourceStats.dev.toString(),
278
+ ino: sourceStats.ino.toString(),
279
+ },
280
+ sourceParentIdentity: {
281
+ kind: 'folder',
282
+ dev: rootStats.dev.toString(),
283
+ ino: rootStats.ino.toString(),
284
+ },
285
+ targetParentIdentity: {
286
+ kind: 'folder',
287
+ dev: rootStats.dev.toString(),
288
+ ino: rootStats.ino.toString(),
289
+ },
290
+ path: path.basename(destination),
291
+ id: crypto.createHash('sha256').update('tree-source-cas').digest('hex'),
292
+ operation: 'tree-source-cas',
293
+ }, {
294
+ pauseBeforeInstall: 300,
295
+ });
296
+ assert.equal(result.status, 'changed');
297
+ assert.match(result.reason, /source changed during exclusive move and was restored/);
298
+ assert.equal(fs.existsSync(destination), false);
299
+ assert.equal(fs.readFileSync(source, 'utf8'), 'replacement source\n');
300
+ assert.equal(fs.readFileSync(movedOriginal, 'utf8'), 'original source\n');
301
+ } finally {
302
+ child.unref();
303
+ fs.rmSync(root, { recursive: true, force: true });
304
+ }
305
+ });
306
+
307
+ test('exclusive tree moves retain both names when a second source wins the undo race', () => {
308
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-tree-source-both-'));
309
+ const source = path.join(root, 'source.txt');
310
+ const movedOriginal = path.join(root, 'moved-original.txt');
311
+ const destination = path.join(root, 'destination.txt');
312
+ fs.writeFileSync(source, 'original source\n', 'utf8');
313
+ const rootStats = fs.lstatSync(root, { bigint: true });
314
+ const sourceStats = fs.lstatSync(source, { bigint: true });
315
+ const child = spawn(process.execPath, [
316
+ '-e',
317
+ 'const fs=require("fs");'
318
+ + `setTimeout(()=>{`
319
+ + `fs.renameSync(${JSON.stringify(source)},${JSON.stringify(movedOriginal)});`
320
+ + `fs.writeFileSync(${JSON.stringify(source)},"first replacement\\n","utf8");`
321
+ + '},250);'
322
+ + `setTimeout(()=>{`
323
+ + `fs.writeFileSync(${JSON.stringify(source)},"second replacement\\n","utf8");`
324
+ + '},900);',
325
+ ], { stdio: 'ignore' });
326
+ try {
327
+ const result = require('../workspace/native-file-transaction/client')
328
+ .moveNoReplace({
329
+ root: fs.realpathSync(root),
330
+ rootDev: rootStats.dev.toString(),
331
+ rootIno: rootStats.ino.toString(),
332
+ source: path.basename(source),
333
+ sourceIdentity: {
334
+ kind: 'file',
335
+ dev: sourceStats.dev.toString(),
336
+ ino: sourceStats.ino.toString(),
337
+ },
338
+ sourceParentIdentity: {
339
+ kind: 'folder',
340
+ dev: rootStats.dev.toString(),
341
+ ino: rootStats.ino.toString(),
342
+ },
343
+ targetParentIdentity: {
344
+ kind: 'folder',
345
+ dev: rootStats.dev.toString(),
346
+ ino: rootStats.ino.toString(),
347
+ },
348
+ path: path.basename(destination),
349
+ id: crypto.createHash('sha256').update('tree-source-both').digest('hex'),
350
+ operation: 'tree-source-both',
351
+ }, {
352
+ pauseBeforeInstall: 600,
353
+ pauseAfterTreeMove: 600,
354
+ });
355
+ assert.equal(result.status, 'changed');
356
+ assert.match(result.reason, /both entries were retained/);
357
+ assert.equal(fs.readFileSync(movedOriginal, 'utf8'), 'original source\n');
358
+ assert.equal(fs.readFileSync(destination, 'utf8'), 'first replacement\n');
359
+ assert.equal(fs.readFileSync(source, 'utf8'), 'second replacement\n');
360
+ } finally {
361
+ child.unref();
362
+ fs.rmSync(root, { recursive: true, force: true });
363
+ }
364
+ });
@@ -201,17 +201,111 @@ test('repo object chunks retry after the tunnel reconnects', async () => {
201
201
  }
202
202
  });
203
203
 
204
+ test('a rejected stale object yields the tunnel to the next upload', async () => {
205
+ const staleBytes = Buffer.from('stale denied object');
206
+ const staleSha = crypto.createHash('sha256').update(staleBytes).digest('hex');
207
+ const currentBytes = Buffer.from('current allowed object');
208
+ const currentSha = crypto.createHash('sha256').update(currentBytes).digest('hex');
209
+ const resourceId = 'shr_cp1abcdefghijklmnopqrstuvwx';
210
+ let failed = false;
211
+ const calls = [];
212
+ const server = http.createServer((req, res) => {
213
+ let raw = '';
214
+ req.on('data', (chunk) => { raw += chunk; });
215
+ req.on('end', () => {
216
+ const body = raw ? JSON.parse(raw) : null;
217
+ calls.push({ path: req.url, body });
218
+ res.setHeader('content-type', 'application/json');
219
+ if (req.url === '/state/shared/replicas') {
220
+ res.end('{"replicas":[]}');
221
+ } else if (req.url.startsWith('/state/stream')) {
222
+ res.writeHead(200, { 'content-type': 'text/event-stream' });
223
+ res.write('\n');
224
+ } else if (req.url === '/state/repo-objects/uploads/claim') {
225
+ const bytes = failed ? currentBytes : staleBytes;
226
+ const sha256 = failed ? currentSha : staleSha;
227
+ res.end(JSON.stringify({
228
+ upload: {
229
+ resourceId,
230
+ sha256,
231
+ bytes: bytes.length,
232
+ dataBase64: bytes.toString('base64'),
233
+ },
234
+ }));
235
+ } else if (req.url === '/state/repo-objects/uploads/fail') {
236
+ failed = true;
237
+ res.end('{"changed":true,"terminal":true}');
238
+ } else if (req.url.endsWith('/claim')) {
239
+ res.end('{}');
240
+ } else {
241
+ res.end('{}');
242
+ }
243
+ });
244
+ });
245
+ await new Promise((resolve) => { server.listen(0, '127.0.0.1', resolve); });
246
+
247
+ const frames = [];
248
+ const tunnel = createSharedRealtimeTunnel({
249
+ send: (frame) => { frames.push(frame); return true; },
250
+ sendLarge: (frame) => { frames.push(frame); return true; },
251
+ localPort: () => server.address().port,
252
+ localHeaders: (headers) => headers,
253
+ warn: () => {},
254
+ });
255
+
256
+ try {
257
+ tunnel.enable(['shared.mutation.v1']);
258
+ await until(
259
+ () => frames.some((frame) => frame.type === 'repo.object.put'),
260
+ 'the stale upload',
261
+ );
262
+ const stale = frames.find((frame) => frame.type === 'repo.object.put');
263
+ assert.equal(stale.sha256, staleSha);
264
+ tunnel.handleFrame({
265
+ type: 'shared.error',
266
+ request_id: stale.request_id,
267
+ resource_id: resourceId,
268
+ code: 'shared_resource_read_forbidden',
269
+ message: 'stale rail is no longer readable',
270
+ retryable: false,
271
+ });
272
+ await until(
273
+ () => frames.some((frame) => (
274
+ frame.type === 'repo.object.put' && frame.sha256 === currentSha
275
+ )),
276
+ 'the current upload behind the stale one',
277
+ );
278
+ assert.deepEqual(
279
+ calls.find((call) => call.path === '/state/repo-objects/uploads/fail').body,
280
+ {
281
+ resourceId,
282
+ sha256: staleSha,
283
+ retryable: false,
284
+ code: 'shared_resource_read_forbidden',
285
+ message: 'stale rail is no longer readable',
286
+ },
287
+ );
288
+ } finally {
289
+ tunnel.stop();
290
+ server.close();
291
+ }
292
+ });
293
+
204
294
  test('a healthy tunnel sends one request for one durable mutation', async () => {
205
295
  const envelope = {
206
- resourceId: 'shr_cp1abcdefghijklmnopqrstuvwx',
296
+ resourceId: '018f0000-0000-7000-8000-000000000050',
207
297
  authorityEpoch: 1,
208
298
  mutationId: 'mut_one_ordered_request',
209
299
  deviceId: 'machine-a',
210
300
  baseVersion: 0,
211
- contract: 'code-project-stream@1',
212
- schemaVersion: 1,
301
+ contract: 'code-project-stream@2',
302
+ schemaVersion: 2,
213
303
  operationKind: 'code.publish',
214
- operation: { kind: 'code.publish', record: { value: 'one change' } },
304
+ operation: {
305
+ kind: 'code.publish',
306
+ project: '018f0000-0000-7000-8000-000000000050',
307
+ delta: { kind: 'checkpoint.set' },
308
+ },
215
309
  };
216
310
  let wakeResponse = null;
217
311
  const server = http.createServer((req, res) => {