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
|
@@ -5,7 +5,7 @@ const assert = require('node:assert/strict');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
|
-
const { execFile } = require('child_process');
|
|
8
|
+
const { execFile, spawn } = require('child_process');
|
|
9
9
|
const { promisify } = require('util');
|
|
10
10
|
|
|
11
11
|
const execFileAsync = promisify(execFile);
|
|
@@ -29,7 +29,7 @@ const {
|
|
|
29
29
|
captureWorktreeDiff,
|
|
30
30
|
} = require('../workspace/git');
|
|
31
31
|
const merge = require('../workspace/merge');
|
|
32
|
-
const {
|
|
32
|
+
const { advanceIndex } = require('../workspace/reducer');
|
|
33
33
|
const {
|
|
34
34
|
_test,
|
|
35
35
|
CHECKPOINT_CONTRACT,
|
|
@@ -80,6 +80,58 @@ test('a checkpoint read across two different HEADs is discarded', () => {
|
|
|
80
80
|
});
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
test('a checkpoint read across two different index or worktree states is discarded', () => {
|
|
84
|
+
const clean = {
|
|
85
|
+
baseOid: 'a'.repeat(40),
|
|
86
|
+
indexDiff: '',
|
|
87
|
+
worktreeDiff: '',
|
|
88
|
+
bytes: 0,
|
|
89
|
+
};
|
|
90
|
+
const staged = {
|
|
91
|
+
...clean,
|
|
92
|
+
indexDiff: 'staged bytes',
|
|
93
|
+
bytes: 12,
|
|
94
|
+
};
|
|
95
|
+
assert.deepEqual(gitTest.stableCheckpointCapture(clean, staged), {
|
|
96
|
+
baseOid: 'a'.repeat(40),
|
|
97
|
+
indexDiff: null,
|
|
98
|
+
worktreeDiff: null,
|
|
99
|
+
reason: 'observation-moved',
|
|
100
|
+
bytes: 12,
|
|
101
|
+
});
|
|
102
|
+
assert.deepEqual(gitTest.stableCheckpointCapture(staged, staged), staged);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('checkpoint capture requires two consecutive equal observations within its bound', async () => {
|
|
106
|
+
const clean = {
|
|
107
|
+
baseOid: 'a'.repeat(40),
|
|
108
|
+
indexDiff: '',
|
|
109
|
+
worktreeDiff: '',
|
|
110
|
+
bytes: 0,
|
|
111
|
+
};
|
|
112
|
+
const staged = {
|
|
113
|
+
...clean,
|
|
114
|
+
indexDiff: 'staged bytes',
|
|
115
|
+
bytes: 12,
|
|
116
|
+
};
|
|
117
|
+
const settling = [clean, staged, staged];
|
|
118
|
+
assert.deepEqual(
|
|
119
|
+
await gitTest.captureStableCheckpoint(() => Promise.resolve(settling.shift()), 3),
|
|
120
|
+
staged,
|
|
121
|
+
);
|
|
122
|
+
const alternating = [clean, staged, clean, staged];
|
|
123
|
+
assert.deepEqual(
|
|
124
|
+
await gitTest.captureStableCheckpoint(() => Promise.resolve(alternating.shift()), 4),
|
|
125
|
+
{
|
|
126
|
+
baseOid: 'a'.repeat(40),
|
|
127
|
+
indexDiff: null,
|
|
128
|
+
worktreeDiff: null,
|
|
129
|
+
reason: 'observation-moved',
|
|
130
|
+
bytes: 12,
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
83
135
|
async function run(cwd, ...args) {
|
|
84
136
|
await execFileAsync(
|
|
85
137
|
'git',
|
|
@@ -114,7 +166,7 @@ async function makeRepoPair() {
|
|
|
114
166
|
return { author, receiver };
|
|
115
167
|
}
|
|
116
168
|
|
|
117
|
-
test('a dirty worktree round-trips through
|
|
169
|
+
test('a dirty worktree round-trips through checkpoint trees, gitignore filtering included', async () => {
|
|
118
170
|
const { author, receiver } = await makeRepoPair();
|
|
119
171
|
|
|
120
172
|
// Every dirty shape at once: tracked edit, staged edit, deletion, new
|
|
@@ -129,13 +181,32 @@ test('a dirty worktree round-trips through capture and apply, gitignore filterin
|
|
|
129
181
|
fs.mkdirSync(path.join(author, 'node_modules', 'junk'), { recursive: true });
|
|
130
182
|
fs.writeFileSync(path.join(author, 'node_modules', 'junk', 'x.js'), 'never travels\n');
|
|
131
183
|
|
|
132
|
-
const captured = await
|
|
133
|
-
assert.ok(captured.
|
|
134
|
-
assert.ok(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
await
|
|
184
|
+
const captured = await captureCheckpoint(author);
|
|
185
|
+
assert.ok(captured.worktreeDiff.length > 0);
|
|
186
|
+
assert.ok(
|
|
187
|
+
!captured.worktreeDiff.includes('node_modules'),
|
|
188
|
+
'ignored files must never enter a checkpoint',
|
|
189
|
+
);
|
|
190
|
+
const baseTree = await merge.treeOf(receiver, captured.baseOid);
|
|
191
|
+
const indexTree = await merge.treeOfPatch(receiver, captured.baseOid, captured.indexDiff);
|
|
192
|
+
const worktreeTree = await merge.treeOfPatch(
|
|
193
|
+
receiver,
|
|
194
|
+
indexTree,
|
|
195
|
+
captured.worktreeDiff,
|
|
196
|
+
);
|
|
197
|
+
await merge.advance(receiver, baseTree, worktreeTree, { label: 'checkpoint-test-forward' });
|
|
198
|
+
const forwardIndexOperation = '1'.repeat(64);
|
|
199
|
+
await advanceIndex(
|
|
200
|
+
receiver,
|
|
201
|
+
await merge.indexTree(receiver),
|
|
202
|
+
indexTree,
|
|
203
|
+
forwardIndexOperation,
|
|
204
|
+
);
|
|
205
|
+
require('../workspace/transition').releaseGitMetadataReceipts(
|
|
206
|
+
receiver,
|
|
207
|
+
forwardIndexOperation,
|
|
208
|
+
'index',
|
|
209
|
+
);
|
|
139
210
|
|
|
140
211
|
assert.equal(
|
|
141
212
|
fs.readFileSync(path.join(receiver, 'README.md'), 'utf8'),
|
|
@@ -156,8 +227,20 @@ test('a dirty worktree round-trips through capture and apply, gitignore filterin
|
|
|
156
227
|
assert.equal(fs.existsSync(path.join(receiver, 'LICENSE')), false, 'deletions travel');
|
|
157
228
|
assert.equal(fs.existsSync(path.join(receiver, 'node_modules')), false);
|
|
158
229
|
|
|
159
|
-
//
|
|
160
|
-
await
|
|
230
|
+
// The same two primitives can advance back to the clean shared state.
|
|
231
|
+
await merge.advance(receiver, worktreeTree, baseTree, { label: 'checkpoint-test-reverse' });
|
|
232
|
+
const reverseIndexOperation = '2'.repeat(64);
|
|
233
|
+
await advanceIndex(
|
|
234
|
+
receiver,
|
|
235
|
+
await merge.indexTree(receiver),
|
|
236
|
+
baseTree,
|
|
237
|
+
reverseIndexOperation,
|
|
238
|
+
);
|
|
239
|
+
require('../workspace/transition').releaseGitMetadataReceipts(
|
|
240
|
+
receiver,
|
|
241
|
+
reverseIndexOperation,
|
|
242
|
+
'index',
|
|
243
|
+
);
|
|
161
244
|
const after = await captureWorktreeDiff(receiver);
|
|
162
245
|
assert.equal(after.diff, '');
|
|
163
246
|
});
|
|
@@ -171,6 +254,215 @@ test('capture is stable: two reads of an untouched tree produce the same revisio
|
|
|
171
254
|
assert.equal(revisionOf(first.baseOid, first.diff), revisionOf(second.baseOid, second.diff));
|
|
172
255
|
});
|
|
173
256
|
|
|
257
|
+
test('index ABA cannot produce a checkpoint that never physically existed', async () => {
|
|
258
|
+
const author = path.join(tempRoot, 'index-aba-author');
|
|
259
|
+
fs.mkdirSync(author);
|
|
260
|
+
await run(author, 'init');
|
|
261
|
+
fs.writeFileSync(path.join(author, 'file.txt'), 'floor\n');
|
|
262
|
+
await run(author, 'add', 'file.txt');
|
|
263
|
+
await run(author, 'commit', '-m', 'floor');
|
|
264
|
+
fs.writeFileSync(path.join(author, 'file.txt'), 'local\n');
|
|
265
|
+
|
|
266
|
+
const mover = spawn('/bin/sh', ['-c', `
|
|
267
|
+
index=0
|
|
268
|
+
while [ "$index" -lt 500 ]; do
|
|
269
|
+
git reset -q HEAD -- file.txt
|
|
270
|
+
git add file.txt
|
|
271
|
+
index=$((index + 1))
|
|
272
|
+
done
|
|
273
|
+
`], {
|
|
274
|
+
cwd: author,
|
|
275
|
+
env: { ...process.env, GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0' },
|
|
276
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
277
|
+
});
|
|
278
|
+
let moved = 0;
|
|
279
|
+
try {
|
|
280
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
281
|
+
const captured = await captureCheckpoint(author);
|
|
282
|
+
if (captured.reason === 'observation-moved') {
|
|
283
|
+
moved += 1;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
assert.ok(
|
|
287
|
+
captured.indexDiff.length > 0 || captured.worktreeDiff.length > 0,
|
|
288
|
+
'capture returned an impossible clean index + clean worktree mixture',
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
} finally {
|
|
292
|
+
mover.kill('SIGTERM');
|
|
293
|
+
await new Promise((resolve) => {
|
|
294
|
+
if (mover.exitCode !== null) resolve();
|
|
295
|
+
else mover.once('exit', resolve);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
assert.ok(moved > 0, 'the test did not overlap an index movement');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('directory membership ABA cannot hide an untracked file from capture', async () => {
|
|
302
|
+
const author = path.join(tempRoot, 'membership-aba-author');
|
|
303
|
+
fs.mkdirSync(author);
|
|
304
|
+
await run(author, 'init', '-b', 'main');
|
|
305
|
+
fs.writeFileSync(path.join(author, 'tracked.txt'), 'floor\n');
|
|
306
|
+
await run(author, 'add', 'tracked.txt');
|
|
307
|
+
await run(author, 'commit', '-m', 'floor');
|
|
308
|
+
|
|
309
|
+
const source = String.raw`
|
|
310
|
+
const childProcess = require('child_process');
|
|
311
|
+
const fs = require('fs');
|
|
312
|
+
const path = require('path');
|
|
313
|
+
const { promisify } = require('util');
|
|
314
|
+
const originalExecFile = childProcess.execFile;
|
|
315
|
+
const late = path.join(process.env.TEST_REPO, 'late.txt');
|
|
316
|
+
let otherCalls = 0;
|
|
317
|
+
const wrappedExecFile = function(file, args, options, callback) {
|
|
318
|
+
const isUntracked = file === 'git'
|
|
319
|
+
&& Array.isArray(args)
|
|
320
|
+
&& args.includes('ls-files')
|
|
321
|
+
&& args.includes('--others');
|
|
322
|
+
if (!isUntracked) return originalExecFile(file, args, options, callback);
|
|
323
|
+
otherCalls += 1;
|
|
324
|
+
fs.rmSync(late, { force: true });
|
|
325
|
+
return originalExecFile(file, args, options, (error, stdout, stderr) => {
|
|
326
|
+
fs.writeFileSync(late, 'late bytes\n');
|
|
327
|
+
callback(error, stdout, stderr);
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
wrappedExecFile[promisify.custom] = (file, args, options) => new Promise(
|
|
331
|
+
(resolve, reject) => wrappedExecFile(file, args, options, (error, stdout, stderr) => {
|
|
332
|
+
if (error) {
|
|
333
|
+
error.stdout = stdout;
|
|
334
|
+
error.stderr = stderr;
|
|
335
|
+
reject(error);
|
|
336
|
+
} else {
|
|
337
|
+
resolve({ stdout, stderr });
|
|
338
|
+
}
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
childProcess.execFile = wrappedExecFile;
|
|
342
|
+
const { captureCheckpoint } = require(process.env.GIT_MODULE);
|
|
343
|
+
captureCheckpoint(process.env.TEST_REPO).then((captured) => {
|
|
344
|
+
process.stdout.write(JSON.stringify({
|
|
345
|
+
captured,
|
|
346
|
+
otherCalls,
|
|
347
|
+
finalLateExists: fs.existsSync(late),
|
|
348
|
+
}));
|
|
349
|
+
}).catch((error) => {
|
|
350
|
+
process.stderr.write(error.stack || String(error));
|
|
351
|
+
process.exitCode = 1;
|
|
352
|
+
});
|
|
353
|
+
`;
|
|
354
|
+
const { stdout } = await execFileAsync(process.execPath, ['-e', source], {
|
|
355
|
+
env: {
|
|
356
|
+
...process.env,
|
|
357
|
+
TEST_REPO: author,
|
|
358
|
+
GIT_MODULE: require.resolve('../workspace/git'),
|
|
359
|
+
},
|
|
360
|
+
timeout: 30_000,
|
|
361
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
362
|
+
});
|
|
363
|
+
const result = JSON.parse(stdout);
|
|
364
|
+
assert.equal(result.finalLateExists, true);
|
|
365
|
+
assert.ok(result.otherCalls >= 4);
|
|
366
|
+
assert.equal(result.captured.reason, 'observation-moved');
|
|
367
|
+
assert.equal(result.captured.indexDiff, null);
|
|
368
|
+
assert.equal(result.captured.worktreeDiff, null);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('same-inode directory rename ABA cannot hide its untracked children', async () => {
|
|
372
|
+
const author = path.join(tempRoot, 'directory-rename-aba-author');
|
|
373
|
+
fs.mkdirSync(author);
|
|
374
|
+
await run(author, 'init', '-b', 'main');
|
|
375
|
+
fs.writeFileSync(path.join(author, 'tracked.txt'), 'floor\n');
|
|
376
|
+
await run(author, 'add', 'tracked.txt');
|
|
377
|
+
await run(author, 'commit', '-m', 'floor');
|
|
378
|
+
fs.mkdirSync(path.join(author, 'local'));
|
|
379
|
+
fs.writeFileSync(path.join(author, 'local', 'local.txt'), 'local bytes\n');
|
|
380
|
+
|
|
381
|
+
const source = String.raw`
|
|
382
|
+
const childProcess = require('child_process');
|
|
383
|
+
const fs = require('fs');
|
|
384
|
+
const path = require('path');
|
|
385
|
+
const { promisify } = require('util');
|
|
386
|
+
const originalExecFile = childProcess.execFile;
|
|
387
|
+
const held = path.join(process.env.TEST_REPO, 'local');
|
|
388
|
+
const outside = path.join(path.dirname(process.env.TEST_REPO), 'held-local');
|
|
389
|
+
const originalIno = fs.statSync(held).ino;
|
|
390
|
+
let intercepted = 0;
|
|
391
|
+
const wrappedExecFile = function(file, args, options, callback) {
|
|
392
|
+
const isUntracked = file === 'git'
|
|
393
|
+
&& Array.isArray(args)
|
|
394
|
+
&& args.includes('ls-files')
|
|
395
|
+
&& args.includes('--others')
|
|
396
|
+
&& !args.includes('--ignored');
|
|
397
|
+
if (!isUntracked) return originalExecFile(file, args, options, callback);
|
|
398
|
+
intercepted += 1;
|
|
399
|
+
fs.renameSync(held, outside);
|
|
400
|
+
return originalExecFile(file, args, options, (error, stdout, stderr) => {
|
|
401
|
+
fs.renameSync(outside, held);
|
|
402
|
+
callback(error, stdout, stderr);
|
|
403
|
+
});
|
|
404
|
+
};
|
|
405
|
+
wrappedExecFile[promisify.custom] = (file, args, options) => new Promise(
|
|
406
|
+
(resolve, reject) => wrappedExecFile(file, args, options, (error, stdout, stderr) => {
|
|
407
|
+
if (error) {
|
|
408
|
+
error.stdout = stdout;
|
|
409
|
+
error.stderr = stderr;
|
|
410
|
+
reject(error);
|
|
411
|
+
} else {
|
|
412
|
+
resolve({ stdout, stderr });
|
|
413
|
+
}
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
childProcess.execFile = wrappedExecFile;
|
|
417
|
+
const { captureCheckpoint } = require(process.env.GIT_MODULE);
|
|
418
|
+
captureCheckpoint(process.env.TEST_REPO).then((captured) => {
|
|
419
|
+
process.stdout.write(JSON.stringify({
|
|
420
|
+
captured,
|
|
421
|
+
intercepted,
|
|
422
|
+
finalExists: fs.existsSync(path.join(held, 'local.txt')),
|
|
423
|
+
sameInode: fs.statSync(held).ino === originalIno,
|
|
424
|
+
}));
|
|
425
|
+
}).catch((error) => {
|
|
426
|
+
process.stderr.write(error.stack || String(error));
|
|
427
|
+
process.exitCode = 1;
|
|
428
|
+
});
|
|
429
|
+
`;
|
|
430
|
+
const { stdout } = await execFileAsync(process.execPath, ['-e', source], {
|
|
431
|
+
env: {
|
|
432
|
+
...process.env,
|
|
433
|
+
TEST_REPO: author,
|
|
434
|
+
GIT_MODULE: require.resolve('../workspace/git'),
|
|
435
|
+
},
|
|
436
|
+
timeout: 30_000,
|
|
437
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
438
|
+
});
|
|
439
|
+
const result = JSON.parse(stdout);
|
|
440
|
+
assert.ok(result.intercepted >= 2);
|
|
441
|
+
assert.equal(result.finalExists, true);
|
|
442
|
+
assert.equal(result.sameInode, true);
|
|
443
|
+
assert.equal(result.captured.reason, 'observation-moved');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('an untracked nested repository is rejected instead of certified clean', async () => {
|
|
447
|
+
const author = path.join(tempRoot, 'nested-repository-author');
|
|
448
|
+
fs.mkdirSync(author);
|
|
449
|
+
await run(author, 'init', '-b', 'main');
|
|
450
|
+
fs.writeFileSync(path.join(author, 'tracked.txt'), 'floor\n');
|
|
451
|
+
await run(author, 'add', 'tracked.txt');
|
|
452
|
+
await run(author, 'commit', '-m', 'floor');
|
|
453
|
+
const nested = path.join(author, 'nested');
|
|
454
|
+
fs.mkdirSync(nested);
|
|
455
|
+
await run(nested, 'init', '-b', 'main');
|
|
456
|
+
fs.writeFileSync(path.join(nested, 'local.txt'), 'nested local bytes\n');
|
|
457
|
+
await run(nested, 'add', 'local.txt');
|
|
458
|
+
await run(nested, 'commit', '-m', 'nested');
|
|
459
|
+
|
|
460
|
+
const captured = await captureCheckpoint(author);
|
|
461
|
+
assert.equal(captured.reason, 'unsupported-nested-repository');
|
|
462
|
+
assert.equal(captured.indexDiff, null);
|
|
463
|
+
assert.equal(captured.worktreeDiff, null);
|
|
464
|
+
});
|
|
465
|
+
|
|
174
466
|
test('checkpoint identity ignores Git file-patch order', async () => {
|
|
175
467
|
const { author } = await makeRepoPair();
|
|
176
468
|
fs.appendFileSync(path.join(author, 'README.md'), 'tracked edit\n');
|
|
@@ -196,10 +488,11 @@ test('one checkpoint preserves the staged and unstaged layers separately', async
|
|
|
196
488
|
fs.appendFileSync(path.join(author, 'README.md'), 'unstaged\n');
|
|
197
489
|
|
|
198
490
|
const captured = await captureCheckpoint(author);
|
|
199
|
-
assert.
|
|
200
|
-
assert.ok(
|
|
201
|
-
assert.ok(captured.
|
|
202
|
-
assert.ok(captured.worktreeDiff.includes('
|
|
491
|
+
assert.equal(captured.reason, undefined, JSON.stringify(captured));
|
|
492
|
+
assert.ok(captured.indexDiff.includes('console.log(2)'), JSON.stringify(captured));
|
|
493
|
+
assert.ok(!captured.indexDiff.includes('console.log(3)'), JSON.stringify(captured));
|
|
494
|
+
assert.ok(captured.worktreeDiff.includes('console.log(3)'), JSON.stringify(captured));
|
|
495
|
+
assert.ok(captured.worktreeDiff.includes('unstaged'), JSON.stringify(captured));
|
|
203
496
|
|
|
204
497
|
const indexTree = await merge.treeOfPatch(author, captured.baseOid, captured.indexDiff);
|
|
205
498
|
const worktreeTree = await merge.treeOfPatch(author, indexTree, captured.worktreeDiff);
|
|
@@ -207,6 +500,23 @@ test('one checkpoint preserves the staged and unstaged layers separately', async
|
|
|
207
500
|
assert.equal(await runOutput(author, 'show', `${worktreeTree}:src/index.js`), 'console.log(3);\n');
|
|
208
501
|
});
|
|
209
502
|
|
|
503
|
+
test('same-size same-time saves cannot make Git mix staged identity with working bytes', async () => {
|
|
504
|
+
const { author } = await makeRepoPair();
|
|
505
|
+
await run(author, 'config', 'core.trustctime', 'false');
|
|
506
|
+
const file = path.join(author, 'src', 'index.js');
|
|
507
|
+
fs.writeFileSync(file, 'console.log(2);\n');
|
|
508
|
+
await run(author, 'add', 'src/index.js');
|
|
509
|
+
const stagedTime = fs.statSync(file);
|
|
510
|
+
fs.writeFileSync(file, 'console.log(3);\n');
|
|
511
|
+
fs.utimesSync(file, stagedTime.atime, stagedTime.mtime);
|
|
512
|
+
|
|
513
|
+
const captured = await captureCheckpoint(author);
|
|
514
|
+
assert.equal(captured.reason, undefined, JSON.stringify(captured));
|
|
515
|
+
assert.ok(captured.indexDiff.includes('console.log(2)'), JSON.stringify(captured));
|
|
516
|
+
assert.ok(!captured.indexDiff.includes('console.log(3)'), JSON.stringify(captured));
|
|
517
|
+
assert.ok(captured.worktreeDiff.includes('console.log(3)'), JSON.stringify(captured));
|
|
518
|
+
});
|
|
519
|
+
|
|
210
520
|
test('a clean tree captures as an empty patch, and oversized state is a named refusal', async () => {
|
|
211
521
|
const { author } = await makeRepoPair();
|
|
212
522
|
const clean = await captureWorktreeDiff(author);
|
|
@@ -223,43 +533,7 @@ test('checkpoint captures are local cache; their operations travel on the one wa
|
|
|
223
533
|
assert.equal(first, checkpointFile('m1', 'https://github.com/a/b'));
|
|
224
534
|
assert.notEqual(first, checkpointFile('m2', 'https://github.com/a/b'));
|
|
225
535
|
assert.notEqual(first, checkpointFile('m1', 'https://github.com/a/c'));
|
|
226
|
-
assert.match(first, /repo-checkpoints-
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
test('a materialized peer card is immediately a valid checkpoint base', () => {
|
|
230
|
-
const oid = 'a'.repeat(40);
|
|
231
|
-
assert.equal(_test.checkpointBaseReady({
|
|
232
|
-
head: { branch: 'main', oid },
|
|
233
|
-
}, {
|
|
234
|
-
appliedThrough: 12,
|
|
235
|
-
branch: 'main',
|
|
236
|
-
baseOid: oid,
|
|
237
|
-
indexDiff: '',
|
|
238
|
-
worktreeDiff: '',
|
|
239
|
-
}, {
|
|
240
|
-
receivedThrough: 12,
|
|
241
|
-
appliedThrough: 12,
|
|
242
|
-
pending: 0,
|
|
243
|
-
current: true,
|
|
244
|
-
}), true);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
test('checkpoint publication waits when the reducer is behind the rail', () => {
|
|
248
|
-
const oid = 'a'.repeat(40);
|
|
249
|
-
const card = { head: { branch: 'main', oid } };
|
|
250
|
-
const reduced = {
|
|
251
|
-
appliedThrough: 11,
|
|
252
|
-
branch: 'main',
|
|
253
|
-
baseOid: oid,
|
|
254
|
-
indexDiff: '',
|
|
255
|
-
worktreeDiff: '',
|
|
256
|
-
};
|
|
257
|
-
assert.equal(_test.checkpointBaseReady(card, reduced, {
|
|
258
|
-
receivedThrough: 12,
|
|
259
|
-
appliedThrough: 11,
|
|
260
|
-
pending: 0,
|
|
261
|
-
current: false,
|
|
262
|
-
}), false);
|
|
536
|
+
assert.match(first, /repo-checkpoints-v11/);
|
|
263
537
|
});
|
|
264
538
|
|
|
265
539
|
test('an uploading checkpoint payload is not published as a transition', () => {
|
|
@@ -267,7 +541,7 @@ test('an uploading checkpoint payload is not published as a transition', () => {
|
|
|
267
541
|
revision: 'a'.repeat(64),
|
|
268
542
|
payload: {
|
|
269
543
|
kind: 'pending',
|
|
270
|
-
contract: 'opaque-chunks@
|
|
544
|
+
contract: 'opaque-chunks@2',
|
|
271
545
|
checksum: 'b'.repeat(64),
|
|
272
546
|
},
|
|
273
547
|
};
|
|
@@ -277,7 +551,12 @@ test('an uploading checkpoint payload is not published as a transition', () => {
|
|
|
277
551
|
});
|
|
278
552
|
|
|
279
553
|
test('a small edit after a large file reuses the large file payload', () => {
|
|
280
|
-
const
|
|
554
|
+
const project = require('../workspace/wall').bindProject(
|
|
555
|
+
'018f0000-0000-7000-8000-000000000010',
|
|
556
|
+
'11111111-1111-4111-8111-111111111111',
|
|
557
|
+
path.join(tempRoot, 'granular-checkpoint'),
|
|
558
|
+
);
|
|
559
|
+
const repoKey = project.repoKey;
|
|
281
560
|
const baseOid = 'c'.repeat(40);
|
|
282
561
|
const largePatch = [
|
|
283
562
|
'diff --git a/blob.bin b/blob.bin',
|
|
@@ -329,139 +608,3 @@ test('a small edit after a large file reuses the large file payload', () => {
|
|
|
329
608
|
assert.equal(decoded.state, 'ready');
|
|
330
609
|
assert.equal(decoded.worktreeDiff, `${largePatch}${smallPatch}`);
|
|
331
610
|
});
|
|
332
|
-
|
|
333
|
-
test('a clean losing commit travels as parked metadata on a no-op checkpoint', () => {
|
|
334
|
-
const sharedValue = { revision: null, indexDiff: '', worktreeDiff: '' };
|
|
335
|
-
const parked = [{
|
|
336
|
-
oid: 'a'.repeat(40),
|
|
337
|
-
ref: `refs/amalgm/parked/${'a'.repeat(40)}`,
|
|
338
|
-
objects: { kind: 'bundle', checksum: 'b'.repeat(64) },
|
|
339
|
-
}];
|
|
340
|
-
const checkpoint = _test.checkpointForSharedState({
|
|
341
|
-
machine: 'machine-a',
|
|
342
|
-
repoKey: 'root:test',
|
|
343
|
-
branch: 'main',
|
|
344
|
-
baseOid: 'd'.repeat(40),
|
|
345
|
-
sharedValue,
|
|
346
|
-
parked,
|
|
347
|
-
capturedAt: '2026-07-24T00:00:00.000Z',
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
assert.equal(checkpoint.checkpoint, CHECKPOINT_CONTRACT);
|
|
351
|
-
assert.equal(checkpoint.baseOid, 'd'.repeat(40));
|
|
352
|
-
assert.equal(checkpoint.branch, 'main');
|
|
353
|
-
assert.deepEqual(checkpoint.from, sharedValue);
|
|
354
|
-
assert.deepEqual(checkpoint.to, sharedValue);
|
|
355
|
-
assert.deepEqual(checkpoint.parked, parked);
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
test('shared content clears the local pending checkpoint instead of publishing an acknowledgement', () => {
|
|
359
|
-
const sharedValue = { revision: 'shared', indexDiff: '', worktreeDiff: 'shared-patch' };
|
|
360
|
-
assert.equal(_test.checkpointForSharedState({
|
|
361
|
-
machine: 'machine-a',
|
|
362
|
-
repoKey: 'root:test',
|
|
363
|
-
branch: 'main',
|
|
364
|
-
baseOid: 'a'.repeat(40),
|
|
365
|
-
sharedValue,
|
|
366
|
-
parked: [],
|
|
367
|
-
}), null);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test('an acknowledged checkpoint remains a silent receipt until newer state replaces it', () => {
|
|
371
|
-
const oid = 'a'.repeat(40);
|
|
372
|
-
const checkpoint = {
|
|
373
|
-
baseOid: oid,
|
|
374
|
-
branch: 'main',
|
|
375
|
-
to: { revision: 'local-revision' },
|
|
376
|
-
};
|
|
377
|
-
const card = { head: { oid, branch: 'main' } };
|
|
378
|
-
|
|
379
|
-
assert.equal(
|
|
380
|
-
_test.checkpointWasAcknowledged(checkpoint, card, 'local-revision', 51),
|
|
381
|
-
true,
|
|
382
|
-
);
|
|
383
|
-
assert.equal(
|
|
384
|
-
_test.checkpointWasAcknowledged(checkpoint, card, 'newer-revision', 51),
|
|
385
|
-
false,
|
|
386
|
-
);
|
|
387
|
-
assert.equal(
|
|
388
|
-
_test.checkpointWasAcknowledged(checkpoint, card, 'local-revision', null),
|
|
389
|
-
false,
|
|
390
|
-
);
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
test('unpublished parked work uses a no-op checkpoint over the current shared value', () => {
|
|
394
|
-
const sharedValue = { revision: 'shared', indexDiff: '', worktreeDiff: 'shared-patch' };
|
|
395
|
-
const parked = [{ oid: 'b'.repeat(40), objects: { kind: 'bundle' } }];
|
|
396
|
-
const checkpoint = _test.checkpointForSharedState({
|
|
397
|
-
machine: 'machine-a',
|
|
398
|
-
repoKey: 'root:test',
|
|
399
|
-
branch: 'main',
|
|
400
|
-
baseOid: 'a'.repeat(40),
|
|
401
|
-
sharedValue,
|
|
402
|
-
parked,
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
assert.deepEqual(checkpoint.from, sharedValue);
|
|
406
|
-
assert.deepEqual(checkpoint.to, sharedValue);
|
|
407
|
-
assert.deepEqual(checkpoint.parked, parked);
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
test('only this machine keeps announcing its parked commits', () => {
|
|
411
|
-
const baseOid = 'a'.repeat(40);
|
|
412
|
-
const localOid = 'b'.repeat(40);
|
|
413
|
-
const peerOid = 'c'.repeat(40);
|
|
414
|
-
const pendingOid = 'd'.repeat(40);
|
|
415
|
-
const existing = {
|
|
416
|
-
branch: 'main',
|
|
417
|
-
baseOid,
|
|
418
|
-
parked: [{ oid: localOid, objects: { kind: 'pending' } }],
|
|
419
|
-
};
|
|
420
|
-
const reduced = {
|
|
421
|
-
parked: [
|
|
422
|
-
{ oid: localOid, version: 10, objects: { kind: 'pending' } },
|
|
423
|
-
{ oid: peerOid, version: 11, objects: { kind: 'bundle' } },
|
|
424
|
-
{ oid: pendingOid, objects: { kind: 'pending' } },
|
|
425
|
-
],
|
|
426
|
-
};
|
|
427
|
-
const card = { head: { branch: 'main', oid: baseOid } };
|
|
428
|
-
|
|
429
|
-
assert.deepEqual(
|
|
430
|
-
_test.localParkedEntries(existing, reduced, card).map((entry) => entry.oid),
|
|
431
|
-
[pendingOid],
|
|
432
|
-
);
|
|
433
|
-
assert.deepEqual(
|
|
434
|
-
_test.localParkedEntries(existing, reduced, {
|
|
435
|
-
head: { branch: 'main', oid: 'e'.repeat(40) },
|
|
436
|
-
}).map((entry) => entry.oid),
|
|
437
|
-
[pendingOid],
|
|
438
|
-
);
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
test('a server rail number does not turn the same parked checkpoint into new work', () => {
|
|
442
|
-
const value = { revision: null, indexDiff: '', worktreeDiff: '' };
|
|
443
|
-
const checkpoint = {
|
|
444
|
-
branch: 'main',
|
|
445
|
-
baseOid: 'a'.repeat(40),
|
|
446
|
-
from: value,
|
|
447
|
-
to: value,
|
|
448
|
-
parked: [{
|
|
449
|
-
oid: 'b'.repeat(40),
|
|
450
|
-
ref: `refs/amalgm/parked/${'b'.repeat(40)}`,
|
|
451
|
-
objects: { kind: 'bundle', checksum: 'c'.repeat(64) },
|
|
452
|
-
version: 12,
|
|
453
|
-
}],
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
assert.equal(sameTransition(checkpoint, {
|
|
457
|
-
...checkpoint,
|
|
458
|
-
parked: checkpoint.parked.map((entry) => ({ ...entry, version: 37 })),
|
|
459
|
-
}), true);
|
|
460
|
-
assert.equal(sameTransition(checkpoint, {
|
|
461
|
-
...checkpoint,
|
|
462
|
-
parked: checkpoint.parked.map((entry) => ({
|
|
463
|
-
...entry,
|
|
464
|
-
objects: { ...entry.objects, checksum: 'd'.repeat(64) },
|
|
465
|
-
})),
|
|
466
|
-
}), false);
|
|
467
|
-
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const test = require('node:test');
|
|
8
|
+
const durable = require('../workspace/durable');
|
|
9
|
+
|
|
10
|
+
test('durable state replacement leaves one complete value and no temporary file', () => {
|
|
11
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-durable-'));
|
|
12
|
+
const file = path.join(directory, 'state.json');
|
|
13
|
+
try {
|
|
14
|
+
durable.writeJson(file, { version: 1, value: 'old' });
|
|
15
|
+
durable.writeJson(file, { version: 2, value: 'new' });
|
|
16
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(file, 'utf8')), {
|
|
17
|
+
version: 2,
|
|
18
|
+
value: 'new',
|
|
19
|
+
});
|
|
20
|
+
assert.deepEqual(fs.readdirSync(directory), ['state.json']);
|
|
21
|
+
} finally {
|
|
22
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
});
|