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.
Files changed (86) hide show
  1. package/lib/shared-realtime-tunnel.js +22 -14
  2. package/package.json +1 -1
  3. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
  4. package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
  5. package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
  6. package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
  7. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
  8. package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
  9. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
  10. package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
  11. package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
  12. package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
  13. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
  14. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
  15. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
  16. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
  17. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
  18. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
  19. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
  20. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
  21. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
  22. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
  23. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
  24. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
  25. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
  26. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
  27. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
  28. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
  29. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +81 -3
  30. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +114 -8
  31. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
  32. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
  33. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
  34. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
  35. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
  36. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
  37. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
  38. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
  39. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
  40. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
  41. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
  42. package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
  43. package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
  44. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
  45. package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
  46. package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
  47. package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
  48. package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
  49. package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
  50. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
  51. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  52. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  53. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
  54. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
  55. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
  56. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
  57. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
  58. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
  59. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
  60. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
  61. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
  63. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
  64. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
  65. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
  66. package/runtime/scripts/amalgm-mcp/workspace/objects.js +119 -54
  67. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
  68. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
  69. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
  70. package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
  71. package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
  72. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
  73. package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
  74. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
  75. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
  76. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
  77. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
  78. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
  79. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
  80. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
  81. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
  82. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
  83. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
  84. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
  85. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
  86. package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
@@ -6,6 +6,7 @@ const fs = require('fs');
6
6
  const os = require('os');
7
7
  const path = require('path');
8
8
  const { execFile } = require('child_process');
9
+ const { EventEmitter } = require('events');
9
10
  const { promisify } = require('util');
10
11
 
11
12
  const execFileAsync = promisify(execFile);
@@ -23,6 +24,7 @@ const {
23
24
  repoCardServiceStats,
24
25
  startRepoCardService,
25
26
  stopRepoCardService,
27
+ _private,
26
28
  } = require('../workspace/cards');
27
29
 
28
30
  test.after(() => {
@@ -316,7 +318,7 @@ test('coverage shift and deletion inside a single pass still reads as missing',
316
318
  assert.equal(findCard(settled.cards, repo), undefined);
317
319
  });
318
320
 
319
- test('the live service hears a commit through the git-dir doorbell', async () => {
321
+ test('the live service hears a commit through the git-dir doorbell', async (t) => {
320
322
  const repo = await makeRepo('projects/belled');
321
323
  const projects = path.join(tempRoot, 'projects');
322
324
  upsertWorkspace({ path: projects, name: 'Projects', source: 'computer' });
@@ -324,6 +326,13 @@ test('the live service hears a commit through the git-dir doorbell', async () =>
324
326
  await refreshRepoCards();
325
327
  const before = findCard(listRepoCards(), repo);
326
328
  assert.equal(before.health, 'healthy');
329
+ // fs.watch reports resource exhaustion asynchronously on macOS.
330
+ await new Promise((resolve) => setTimeout(resolve, 50));
331
+ if (!_private.watcherComplete(before.replica)) {
332
+ stopRepoCardService();
333
+ t.skip('the host refused part or all of the required fs.watch set');
334
+ return;
335
+ }
327
336
 
328
337
  fs.writeFileSync(path.join(repo, 'ding.txt'), 'ding\n');
329
338
  await run(repo, 'add', '.');
@@ -341,7 +350,61 @@ test('the live service hears a commit through the git-dir doorbell', async () =>
341
350
  assert.ok(repoCardServiceStats().doorbellRings >= 1);
342
351
  });
343
352
 
344
- test('the live service hears nested branch creation and deletion', async () => {
353
+ test('a doorbell rebuild releases its old watch set before opening the replacement', async () => {
354
+ const repo = await makeRepo('projects/bounded-doorbell');
355
+ const projects = path.join(tempRoot, 'projects');
356
+ upsertWorkspace({ path: projects, name: 'Projects', source: 'computer' });
357
+ stopRepoCardService();
358
+ await refreshRepoCards();
359
+
360
+ const originalWatch = fs.watch;
361
+ let openHandles = 0;
362
+ let maximumOpenHandles = 0;
363
+ let handleLimit = Number.POSITIVE_INFINITY;
364
+ fs.watch = () => {
365
+ if (openHandles >= handleLimit) {
366
+ const error = new Error('simulated watch-handle ceiling');
367
+ error.code = 'EMFILE';
368
+ throw error;
369
+ }
370
+ openHandles += 1;
371
+ maximumOpenHandles = Math.max(maximumOpenHandles, openHandles);
372
+ const watcher = new EventEmitter();
373
+ let closed = false;
374
+ watcher.close = () => {
375
+ if (closed) return;
376
+ closed = true;
377
+ openHandles -= 1;
378
+ };
379
+ watcher.unref = () => watcher;
380
+ return watcher;
381
+ };
382
+
383
+ try {
384
+ startRepoCardService();
385
+ await refreshRepoCards();
386
+ const card = findCard(listRepoCards(), repo);
387
+ const initialHandles = _private.watcherCount(card.replica);
388
+ const initialTotal = openHandles;
389
+ assert.ok(initialHandles > 0);
390
+ assert.equal(_private.watcherComplete(card.replica), true);
391
+ assert.ok(initialTotal >= initialHandles);
392
+
393
+ handleLimit = initialTotal;
394
+ maximumOpenHandles = openHandles;
395
+ _private.rebuildDoorbell(card.replica, card);
396
+
397
+ assert.equal(_private.watcherComplete(card.replica), true);
398
+ assert.equal(openHandles, initialTotal);
399
+ assert.equal(maximumOpenHandles, initialTotal);
400
+ assert.equal(repoCardServiceStats().incompleteDoorbells, 0);
401
+ } finally {
402
+ stopRepoCardService();
403
+ fs.watch = originalWatch;
404
+ }
405
+ });
406
+
407
+ test('the targeted doorbell path hears nested branch creation and deletion', async () => {
345
408
  const repo = await makeRepo('projects/belled-refs');
346
409
  const projects = path.join(tempRoot, 'projects');
347
410
  upsertWorkspace({ path: projects, name: 'Projects', source: 'computer' });
@@ -351,6 +414,7 @@ test('the live service hears nested branch creation and deletion', async () => {
351
414
  const before = findCard(listRepoCards(), repo);
352
415
 
353
416
  await run(repo, 'branch', branch);
417
+ _private.ringDoorbell(before.replica);
354
418
  let deadline = Date.now() + 5_000;
355
419
  let created = findCard(listRepoCards(), repo);
356
420
  while (!created.refs.some((ref) => ref.name === branch) && Date.now() < deadline) {
@@ -360,6 +424,7 @@ test('the live service hears nested branch creation and deletion', async () => {
360
424
  assert.ok(created.refs.some((ref) => ref.name === branch));
361
425
 
362
426
  await run(repo, 'branch', '-D', branch);
427
+ _private.ringDoorbell(before.replica);
363
428
  deadline = Date.now() + 5_000;
364
429
  let deleted = findCard(listRepoCards(), repo);
365
430
  while (deleted.refs.some((ref) => ref.name === branch) && Date.now() < deadline) {
@@ -372,7 +437,7 @@ test('the live service hears nested branch creation and deletion', async () => {
372
437
  assert.equal(deleted.generation, before.generation + 2);
373
438
  });
374
439
 
375
- test('the live service hears an uncommitted nested edit without a polling pass', async () => {
440
+ test('the targeted doorbell path hears an uncommitted nested edit without a polling pass', async () => {
376
441
  const repo = await makeRepo('projects/live-dirty');
377
442
  const projects = path.join(tempRoot, 'projects');
378
443
  upsertWorkspace({ path: projects, name: 'Projects', source: 'computer' });
@@ -383,6 +448,8 @@ test('the live service hears an uncommitted nested edit without a polling pass',
383
448
  const nested = path.join(repo, 'src', 'nested');
384
449
  fs.mkdirSync(nested, { recursive: true });
385
450
  fs.writeFileSync(path.join(nested, 'live.txt'), 'live\n');
451
+ const beforeCard = findCard(listRepoCards(), repo);
452
+ _private.ringDoorbell(beforeCard.replica);
386
453
 
387
454
  const deadline = Date.now() + 5_000;
388
455
  let card = findCard(listRepoCards(), repo);
@@ -419,6 +486,7 @@ test('editing an already-dirty file rings the checkpoint directly', async () =>
419
486
  const card = findCard(listRepoCards(), repo);
420
487
  const file = path.join(repo, 'dirty.txt');
421
488
  fs.writeFileSync(file, 'first\n');
489
+ _private.ringDoorbell(card.replica);
422
490
 
423
491
  let deadline = Date.now() + 5_000;
424
492
  while (published.length === 0 && Date.now() < deadline) {
@@ -427,6 +495,7 @@ test('editing an already-dirty file rings the checkpoint directly', async () =>
427
495
  published.length = 0;
428
496
 
429
497
  fs.writeFileSync(file, 'second\n');
498
+ _private.ringDoorbell(card.replica);
430
499
  deadline = Date.now() + 5_000;
431
500
  while (published.length === 0 && Date.now() < deadline) {
432
501
  await new Promise((resolve) => setTimeout(resolve, 25));
@@ -13,6 +13,14 @@ const execFileAsync = promisify(execFile);
13
13
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-repo-cards-test-'));
14
14
  process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
15
15
  process.env.AMALGM_COMPUTER_ID = 'computer-test';
16
+ fs.mkdirSync(process.env.AMALGM_DIR, { recursive: true });
17
+ fs.writeFileSync(
18
+ path.join(process.env.AMALGM_DIR, 'computer.json'),
19
+ JSON.stringify({
20
+ computer_id: 'computer-test',
21
+ user_id: '11111111-1111-4111-8111-111111111111',
22
+ }),
23
+ );
16
24
 
17
25
  const { closeLocalDb, openLocalDb } = require('../state/db');
18
26
  const {
@@ -100,7 +108,7 @@ test('a clean repo cards with exact head, clean counts, and full identity', asyn
100
108
  await run(dir, 'remote', 'add', 'origin', 'git@github.com:amalgm/clean.git');
101
109
 
102
110
  const card = await buildRepoCard(dir);
103
- assert.equal(card.card, 'repo-card@2');
111
+ assert.equal(card.card, 'repo-card@3');
104
112
  assert.equal(card.machine, 'computer-test');
105
113
  assert.match(card.replica, /^rpl_[A-Za-z0-9_-]+$/);
106
114
  assert.equal(card.path, await fs.promises.realpath(dir));
@@ -406,12 +414,31 @@ test('unchanged commits reuse their card object source and one new oid is resolv
406
414
  await run(dir, 'branch', 'side');
407
415
  const local = await buildRepoCard(dir);
408
416
  await run(dir, 'remote', 'add', 'origin', 'https://github.com/amalgm/object-source-cache.git');
417
+ const { resourceId } = require('../workspace/wall').bindProject(
418
+ '018f0000-0000-7000-8000-00000000cace',
419
+ '11111111-1111-4111-8111-111111111111',
420
+ dir,
421
+ );
422
+ assert.equal(
423
+ require('../workspace/wall').projectForPath(dir)?.resourceId,
424
+ resourceId,
425
+ );
426
+ const ready = { kind: 'bundle', resourceId, checksum: 'ready' };
409
427
  const previous = {
410
428
  ...local,
411
429
  repo: { ...local.repo, remote: 'https://github.com/amalgm/object-source-cache' },
412
- objects: { kind: 'remote' },
413
- refs: local.refs.map((ref) => ({ ...ref, objects: { kind: 'remote' } })),
430
+ objects: ready,
431
+ refs: local.refs.map((ref) => ({ ...ref, objects: ready })),
414
432
  };
433
+ const classified = await classifyDirectory(dir);
434
+ const replica = require('../workspace/identity').replicaForGitDir(
435
+ await fs.promises.realpath(classified.gitDir),
436
+ );
437
+ assert.equal(previous.replica, replica.replicaId);
438
+ assert.equal(
439
+ cardTest.reusableObjectSource(local.head.oid, local.head.oid, ready, resourceId),
440
+ ready,
441
+ );
415
442
  const objects = require('../workspace/objects');
416
443
  const original = objects.objectSourceFor;
417
444
  let calls = 0;
@@ -422,8 +449,8 @@ test('unchanged commits reuse their card object source and one new oid is resolv
422
449
  try {
423
450
  const unchanged = await buildRepoCard(dir, previous);
424
451
  assert.equal(calls, 0);
425
- assert.equal(unchanged.objects.kind, 'remote');
426
- assert.ok(unchanged.refs.every((ref) => ref.objects.kind === 'remote'));
452
+ assert.equal(unchanged.objects.kind, 'bundle');
453
+ assert.ok(unchanged.refs.every((ref) => ref.objects.kind === 'bundle'));
427
454
 
428
455
  write(dir, 'next.txt', 'next\n');
429
456
  await run(dir, 'add', '.');
@@ -432,7 +459,7 @@ test('unchanged commits reuse their card object source and one new oid is resolv
432
459
  assert.equal(calls, 1, 'HEAD and its branch share one content-addressed lookup');
433
460
  assert.equal(changed.objects.kind, 'pending');
434
461
  assert.equal(changed.refs.find((ref) => ref.name === 'main').objects.kind, 'pending');
435
- assert.equal(changed.refs.find((ref) => ref.name === 'side').objects.kind, 'remote');
462
+ assert.equal(changed.refs.find((ref) => ref.name === 'side').objects.kind, 'bundle');
436
463
  } finally {
437
464
  objects.objectSourceFor = original;
438
465
  }
@@ -10,6 +10,8 @@ const { promisify } = require('util');
10
10
 
11
11
  const execFileAsync = promisify(execFile);
12
12
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-checkpoint-reducer-'));
13
+ process.env.AMALGM_DIR = path.join(root, '.amalgm');
14
+ process.env.AMALGM_WORKSPACES_DIR = path.join(process.env.AMALGM_DIR, 'workspaces');
13
15
  const merge = require('../workspace/merge');
14
16
 
15
17
  test.after(() => fs.rmSync(root, { recursive: true, force: true }));
@@ -118,3 +120,28 @@ test('an earlier inbound event advances underneath a pending local write', async
118
120
  assert.equal(fs.readFileSync(path.join(repo, 'same.txt'), 'utf8'), 'pending local\n');
119
121
  assert.equal(await treeText(repo, outcome.heldTree, 'same.txt'), 'inbound\n');
120
122
  });
123
+
124
+ test('an inbound file write cannot overwrite a local write that races its observation', async () => {
125
+ const { repo, oid, tree: clean } = await makeRepo('pending-local-race');
126
+ const inboundPatch = await patchFor(repo, { 'same.txt': 'inbound\n' });
127
+ const inbound = await merge.treeOfPatch(repo, oid, inboundPatch);
128
+ const transition = require('../workspace/transition');
129
+ const applyFiles = transition.applyFiles;
130
+ transition.applyFiles = async (...args) => {
131
+ fs.writeFileSync(path.join(repo, 'same.txt'), 'local writer won the race\n');
132
+ return applyFiles(...args);
133
+ };
134
+ try {
135
+ await assert.rejects(
136
+ merge.advance(repo, clean, inbound),
137
+ /local file changed before ordered apply/,
138
+ );
139
+ } finally {
140
+ transition.applyFiles = applyFiles;
141
+ }
142
+
143
+ assert.equal(
144
+ fs.readFileSync(path.join(repo, 'same.txt'), 'utf8'),
145
+ 'local writer won the race\n',
146
+ );
147
+ });