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.
- package/lib/shared-realtime-tunnel.js +18 -4
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
- package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +13 -0
- package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
- package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
- package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
- package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
- package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
- package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +98 -4
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +156 -8
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
- package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
- package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
- package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
- 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 +72 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +185 -55
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
- package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
- package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
- 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: '
|
|
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@
|
|
212
|
-
schemaVersion:
|
|
301
|
+
contract: 'code-project-stream@2',
|
|
302
|
+
schemaVersion: 2,
|
|
213
303
|
operationKind: 'code.publish',
|
|
214
|
-
operation: {
|
|
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) => {
|