amalgm 0.1.238 → 0.1.240
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 +22 -14
- 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/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 +81 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +114 -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 +119 -54
- 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
|
+
});
|
|
@@ -123,7 +123,7 @@ test('repo object chunks travel through the shared tunnel one at a time', async
|
|
|
123
123
|
}
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
test('repo object chunks retry
|
|
126
|
+
test('repo object chunks retry after the tunnel reconnects', async () => {
|
|
127
127
|
const uploadBytes = Buffer.from('retry this content-addressed chunk');
|
|
128
128
|
const uploadSha = crypto.createHash('sha256').update(uploadBytes).digest('hex');
|
|
129
129
|
const resourceId = 'shr_cp1abcdefghijklmnopqrstuvwx';
|
|
@@ -165,14 +165,25 @@ test('repo object chunks retry when an acknowledgement is lost', async () => {
|
|
|
165
165
|
localPort: () => server.address().port,
|
|
166
166
|
localHeaders: (headers) => headers,
|
|
167
167
|
warn: () => {},
|
|
168
|
-
repoObjectRequestTimeoutMs: 25,
|
|
169
168
|
});
|
|
170
169
|
|
|
171
170
|
try {
|
|
171
|
+
tunnel.enable(['shared.mutation.v1']);
|
|
172
|
+
await until(
|
|
173
|
+
() => frames.filter((frame) => frame.type === 'repo.object.put').length === 1,
|
|
174
|
+
'the original upload',
|
|
175
|
+
);
|
|
176
|
+
await new Promise((resolve) => { setTimeout(resolve, 50); });
|
|
177
|
+
assert.equal(
|
|
178
|
+
frames.filter((frame) => frame.type === 'repo.object.put').length,
|
|
179
|
+
1,
|
|
180
|
+
'a healthy ordered tunnel never duplicates an in-flight upload',
|
|
181
|
+
);
|
|
182
|
+
tunnel.stop();
|
|
172
183
|
tunnel.enable(['shared.mutation.v1']);
|
|
173
184
|
await until(
|
|
174
185
|
() => frames.filter((frame) => frame.type === 'repo.object.put').length >= 2,
|
|
175
|
-
'the
|
|
186
|
+
'the upload after reconnect',
|
|
176
187
|
);
|
|
177
188
|
const puts = frames.filter((frame) => frame.type === 'repo.object.put');
|
|
178
189
|
assert.notEqual(puts[0].request_id, puts[1].request_id);
|
|
@@ -189,3 +200,70 @@ test('repo object chunks retry when an acknowledgement is lost', async () => {
|
|
|
189
200
|
server.close();
|
|
190
201
|
}
|
|
191
202
|
});
|
|
203
|
+
|
|
204
|
+
test('a healthy tunnel sends one request for one durable mutation', async () => {
|
|
205
|
+
const envelope = {
|
|
206
|
+
resourceId: '018f0000-0000-7000-8000-000000000050',
|
|
207
|
+
authorityEpoch: 1,
|
|
208
|
+
mutationId: 'mut_one_ordered_request',
|
|
209
|
+
deviceId: 'machine-a',
|
|
210
|
+
baseVersion: 0,
|
|
211
|
+
contract: 'code-project-stream@2',
|
|
212
|
+
schemaVersion: 2,
|
|
213
|
+
operationKind: 'code.publish',
|
|
214
|
+
operation: {
|
|
215
|
+
kind: 'code.publish',
|
|
216
|
+
project: '018f0000-0000-7000-8000-000000000050',
|
|
217
|
+
delta: { kind: 'checkpoint.set' },
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
let wakeResponse = null;
|
|
221
|
+
const server = http.createServer((req, res) => {
|
|
222
|
+
req.resume();
|
|
223
|
+
req.on('end', () => {
|
|
224
|
+
res.setHeader('content-type', 'application/json');
|
|
225
|
+
if (req.url === '/state/shared/replicas') {
|
|
226
|
+
res.end('{"replicas":[]}');
|
|
227
|
+
} else if (req.url.startsWith('/state/stream')) {
|
|
228
|
+
res.writeHead(200, { 'content-type': 'text/event-stream' });
|
|
229
|
+
res.write('\n');
|
|
230
|
+
wakeResponse = res;
|
|
231
|
+
} else if (req.url === '/state/mutations/claim') {
|
|
232
|
+
res.end(JSON.stringify({ mutations: [{ envelope }] }));
|
|
233
|
+
} else if (req.url.endsWith('/claim')) {
|
|
234
|
+
res.end('{}');
|
|
235
|
+
} else {
|
|
236
|
+
res.end('{}');
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
await new Promise((resolve) => { server.listen(0, '127.0.0.1', resolve); });
|
|
241
|
+
|
|
242
|
+
const frames = [];
|
|
243
|
+
const tunnel = createSharedRealtimeTunnel({
|
|
244
|
+
send: (frame) => { frames.push(frame); return true; },
|
|
245
|
+
sendLarge: (frame) => { frames.push(frame); return true; },
|
|
246
|
+
localPort: () => server.address().port,
|
|
247
|
+
localHeaders: (headers) => headers,
|
|
248
|
+
warn: () => {},
|
|
249
|
+
});
|
|
250
|
+
const submits = () => frames.filter((frame) => frame.type === 'shared.mutation.submit');
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
tunnel.enable(['shared.mutation.v1']);
|
|
254
|
+
await until(() => submits().length === 1 && wakeResponse, 'the original mutation request');
|
|
255
|
+
for (let index = 0; index < 3; index += 1) {
|
|
256
|
+
wakeResponse.write('event: state\ndata: {"sharedResourceId":"shr_cp1abcdefghijklmnopqrstuvwx"}\n\n');
|
|
257
|
+
await new Promise((resolve) => { setTimeout(resolve, 20); });
|
|
258
|
+
}
|
|
259
|
+
assert.equal(submits().length, 1, 'wake signals never duplicate an in-flight mutation');
|
|
260
|
+
|
|
261
|
+
tunnel.stop();
|
|
262
|
+
tunnel.enable(['shared.mutation.v1']);
|
|
263
|
+
await until(() => submits().length === 2, 'the mutation after reconnect');
|
|
264
|
+
assert.notEqual(submits()[0].request_id, submits()[1].request_id);
|
|
265
|
+
} finally {
|
|
266
|
+
tunnel.stop();
|
|
267
|
+
server.close();
|
|
268
|
+
}
|
|
269
|
+
});
|
|
@@ -25,16 +25,37 @@ fs.writeFileSync(
|
|
|
25
25
|
const { closeLocalDb } = require('../state/db');
|
|
26
26
|
const { hasCommit, headOid } = require('../workspace/git');
|
|
27
27
|
const {
|
|
28
|
+
_test: { locatorChecksum },
|
|
28
29
|
acknowledgeUpload,
|
|
29
30
|
claimDownload,
|
|
30
31
|
claimUpload,
|
|
31
32
|
importFromLocator,
|
|
32
33
|
objectSourceFor,
|
|
34
|
+
prepareBundleImport,
|
|
33
35
|
publishPayload,
|
|
34
36
|
readPayload,
|
|
35
37
|
receiveDownload,
|
|
36
38
|
} = require('../workspace/objects');
|
|
37
39
|
|
|
40
|
+
const PROJECT = '018f0000-0000-7000-8000-000000000030';
|
|
41
|
+
const PROJECT_BINDING = require('../workspace/wall').bindProject(
|
|
42
|
+
PROJECT,
|
|
43
|
+
'11111111-1111-4111-8111-111111111111',
|
|
44
|
+
tempRoot,
|
|
45
|
+
);
|
|
46
|
+
const REPO_KEY = PROJECT_BINDING.repoKey;
|
|
47
|
+
|
|
48
|
+
test('a locator checksum binds the exact ordered chunk list', () => {
|
|
49
|
+
const first = { sha256: 'a'.repeat(64), bytes: 3 };
|
|
50
|
+
const second = { sha256: 'b'.repeat(64), bytes: 4 };
|
|
51
|
+
assert.equal(locatorChecksum([first, second]), locatorChecksum([first, second]));
|
|
52
|
+
assert.notEqual(locatorChecksum([first, second]), locatorChecksum([second, first]));
|
|
53
|
+
assert.notEqual(
|
|
54
|
+
locatorChecksum([first, second]),
|
|
55
|
+
locatorChecksum([first, { ...second, bytes: 5 }]),
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
38
59
|
test.after(() => {
|
|
39
60
|
closeLocalDb();
|
|
40
61
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
@@ -49,7 +70,7 @@ async function git(cwd, ...args) {
|
|
|
49
70
|
return stdout;
|
|
50
71
|
}
|
|
51
72
|
|
|
52
|
-
test('
|
|
73
|
+
test('every committed card travels as project-owned chunks and imports without moving a ref', async () => {
|
|
53
74
|
const origin = path.join(tempRoot, 'origin.git');
|
|
54
75
|
await git(tempRoot, 'init', '--bare', '-b', 'main', origin);
|
|
55
76
|
const source = path.join(tempRoot, 'source');
|
|
@@ -61,12 +82,12 @@ test('an unpushed commit travels as card-owned chunks and imports without moving
|
|
|
61
82
|
|
|
62
83
|
const destination = path.join(tempRoot, 'destination');
|
|
63
84
|
await git(tempRoot, 'clone', '-q', origin, destination);
|
|
64
|
-
assert.equal((await objectSourceFor(source,
|
|
85
|
+
assert.equal((await objectSourceFor(source, REPO_KEY, await headOid(source))).kind, 'pending');
|
|
65
86
|
|
|
66
87
|
fs.appendFileSync(path.join(source, 'app.js'), 'two\n');
|
|
67
88
|
await git(source, 'commit', '-am', 'two');
|
|
68
89
|
const unpushedOid = await headOid(source);
|
|
69
|
-
assert.equal((await objectSourceFor(source,
|
|
90
|
+
assert.equal((await objectSourceFor(source, REPO_KEY, unpushedOid)).kind, 'pending');
|
|
70
91
|
|
|
71
92
|
const uploaded = new Map();
|
|
72
93
|
const deadline = Date.now() + 10_000;
|
|
@@ -78,10 +99,10 @@ test('an unpushed commit travels as card-owned chunks and imports without moving
|
|
|
78
99
|
}
|
|
79
100
|
uploaded.set(upload.sha256, upload.dataBase64);
|
|
80
101
|
acknowledgeUpload(upload.resourceId, upload.sha256);
|
|
81
|
-
const sourceState = await objectSourceFor(source,
|
|
102
|
+
const sourceState = await objectSourceFor(source, REPO_KEY, unpushedOid);
|
|
82
103
|
if (sourceState.kind === 'bundle') break;
|
|
83
104
|
}
|
|
84
|
-
const locator = await objectSourceFor(source,
|
|
105
|
+
const locator = await objectSourceFor(source, REPO_KEY, unpushedOid);
|
|
85
106
|
assert.equal(locator.kind, 'bundle');
|
|
86
107
|
assert.ok(locator.chunks.length > 0);
|
|
87
108
|
|
|
@@ -103,7 +124,7 @@ test('an unpushed commit travels as card-owned chunks and imports without moving
|
|
|
103
124
|
|
|
104
125
|
test('a large checkpoint uses the same verified chunk lane', () => {
|
|
105
126
|
const content = Buffer.alloc(1_300_000, 0x61);
|
|
106
|
-
assert.equal(publishPayload('large-checkpoint', content,
|
|
127
|
+
assert.equal(publishPayload('large-checkpoint', content, REPO_KEY).kind, 'pending');
|
|
107
128
|
|
|
108
129
|
const uploaded = new Map();
|
|
109
130
|
while (true) {
|
|
@@ -112,7 +133,7 @@ test('a large checkpoint uses the same verified chunk lane', () => {
|
|
|
112
133
|
uploaded.set(upload.sha256, upload.dataBase64);
|
|
113
134
|
acknowledgeUpload(upload.resourceId, upload.sha256);
|
|
114
135
|
}
|
|
115
|
-
const locator = publishPayload('large-checkpoint', content,
|
|
136
|
+
const locator = publishPayload('large-checkpoint', content, REPO_KEY);
|
|
116
137
|
assert.equal(locator.kind, 'chunks');
|
|
117
138
|
assert.ok(locator.chunks.length >= 3);
|
|
118
139
|
|
|
@@ -130,9 +151,94 @@ test('a large checkpoint uses the same verified chunk lane', () => {
|
|
|
130
151
|
assert.equal(received.state, 'ready');
|
|
131
152
|
assert.deepEqual(received.bytes, content);
|
|
132
153
|
assert.equal(
|
|
133
|
-
publishPayload('same-content-after-download', content,
|
|
154
|
+
publishPayload('same-content-after-download', content, REPO_KEY).kind,
|
|
134
155
|
'chunks',
|
|
135
156
|
'verified downloaded content is immediately reusable by a later checkpoint',
|
|
136
157
|
);
|
|
137
158
|
assert.equal(claimUpload(), null);
|
|
138
159
|
});
|
|
160
|
+
|
|
161
|
+
test('zero-byte chunks are not legal transport work', () => {
|
|
162
|
+
const chunks = [
|
|
163
|
+
{ sha256: '0'.repeat(64), bytes: 0 },
|
|
164
|
+
{ sha256: '1'.repeat(64), bytes: 1 },
|
|
165
|
+
];
|
|
166
|
+
const common = {
|
|
167
|
+
resourceId: PROJECT,
|
|
168
|
+
checksum: locatorChecksum(chunks),
|
|
169
|
+
bytes: 1,
|
|
170
|
+
chunks,
|
|
171
|
+
};
|
|
172
|
+
assert.throws(
|
|
173
|
+
() => readPayload({
|
|
174
|
+
kind: 'chunks',
|
|
175
|
+
contract: 'opaque-chunks@2',
|
|
176
|
+
...common,
|
|
177
|
+
}),
|
|
178
|
+
/invalid checkpoint payload locator/,
|
|
179
|
+
);
|
|
180
|
+
assert.throws(
|
|
181
|
+
() => prepareBundleImport({
|
|
182
|
+
kind: 'bundle',
|
|
183
|
+
contract: 'git-bundle-chunks@2',
|
|
184
|
+
oid: 'a'.repeat(40),
|
|
185
|
+
ref: `refs/amalgm/exports/${'a'.repeat(40)}`,
|
|
186
|
+
...common,
|
|
187
|
+
}),
|
|
188
|
+
/invalid git bundle locator/,
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('a corrupt pending upload is retired and rebuilt from the writer', () => {
|
|
193
|
+
const content = Buffer.from('rebuild this upload');
|
|
194
|
+
assert.equal(publishPayload('corrupt-upload', content, REPO_KEY).kind, 'pending');
|
|
195
|
+
const first = claimUpload();
|
|
196
|
+
assert.ok(first);
|
|
197
|
+
const chunkFile = path.join(
|
|
198
|
+
process.env.AMALGM_DIR,
|
|
199
|
+
'state',
|
|
200
|
+
'repo-objects',
|
|
201
|
+
'chunks',
|
|
202
|
+
`${first.sha256}.bin`,
|
|
203
|
+
);
|
|
204
|
+
fs.writeFileSync(chunkFile, 'corrupt');
|
|
205
|
+
assert.equal(claimUpload(), null, 'the corrupt manifest is retired instead of retrying forever');
|
|
206
|
+
|
|
207
|
+
assert.equal(publishPayload('corrupt-upload', content, REPO_KEY).kind, 'pending');
|
|
208
|
+
const rebuilt = claimUpload();
|
|
209
|
+
assert.equal(rebuilt.sha256, first.sha256);
|
|
210
|
+
assert.equal(Buffer.from(rebuilt.dataBase64, 'base64').toString(), content.toString());
|
|
211
|
+
acknowledgeUpload(rebuilt.resourceId, rebuilt.sha256);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('one malformed download request cannot block valid requests behind it', () => {
|
|
215
|
+
const content = Buffer.from('download me');
|
|
216
|
+
const chunks = [{
|
|
217
|
+
sha256: require('crypto').createHash('sha256').update(content).digest('hex'),
|
|
218
|
+
bytes: content.length,
|
|
219
|
+
}];
|
|
220
|
+
const locator = {
|
|
221
|
+
kind: 'chunks',
|
|
222
|
+
contract: 'opaque-chunks@2',
|
|
223
|
+
resourceId: PROJECT,
|
|
224
|
+
checksum: locatorChecksum(chunks),
|
|
225
|
+
bytes: content.length,
|
|
226
|
+
chunks,
|
|
227
|
+
};
|
|
228
|
+
fs.rmSync(path.join(
|
|
229
|
+
process.env.AMALGM_DIR,
|
|
230
|
+
'state',
|
|
231
|
+
'repo-objects',
|
|
232
|
+
'chunks',
|
|
233
|
+
`${chunks[0].sha256}.bin`,
|
|
234
|
+
), { force: true });
|
|
235
|
+
assert.equal(readPayload(locator).state, 'downloading');
|
|
236
|
+
const requests = path.join(process.env.AMALGM_DIR, 'state', 'repo-objects', 'requests');
|
|
237
|
+
fs.writeFileSync(path.join(requests, '000-malformed.json'), '{');
|
|
238
|
+
|
|
239
|
+
const claimed = claimDownload();
|
|
240
|
+
assert.equal(claimed.resourceId, PROJECT);
|
|
241
|
+
assert.equal(claimed.sha256, chunks[0].sha256);
|
|
242
|
+
assert.equal(fs.existsSync(path.join(requests, '000-malformed.json')), false);
|
|
243
|
+
receiveDownload(PROJECT, chunks[0].sha256, content.toString('base64'));
|
|
244
|
+
});
|