amalgm 0.1.249 → 0.1.250

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/cli.js CHANGED
@@ -1456,6 +1456,8 @@ async function register(positionals) {
1456
1456
  const targets = (positionals.length > 0 ? positionals : [process.cwd()])
1457
1457
  .map((target) => path.resolve(target));
1458
1458
 
1459
+ console.log(`Registering ${targets.length === 1 ? targets[0] : `${targets.length} locations`}…`);
1460
+
1459
1461
  // A resolved port is not a running runtime — the manifest supplies a
1460
1462
  // default whether or not anything listens — so both "no port for this
1461
1463
  // scope" and "nothing answered" mean the same thing to the user.
@@ -1487,6 +1489,8 @@ async function register(positionals) {
1487
1489
  const target = targets[index];
1488
1490
  console.log(`Registered ${rootPath}`);
1489
1491
  console.log(` ${record.type} ${record.uuid}`);
1492
+ console.log(' Watching: on');
1493
+ console.log(' Cloud: queued');
1490
1494
  if (rootPath !== target) console.log(` (${target} is already covered by this tree)`);
1491
1495
  }
1492
1496
  }
@@ -1504,6 +1508,7 @@ async function add(positionals) {
1504
1508
  throw new Error('amalgm add requires a cloud entity UUID');
1505
1509
  }
1506
1510
  const targetPath = path.resolve(positionals[1] || process.cwd());
1511
+ console.log(`Adding ${uuid} to ${targetPath}…`);
1507
1512
  const port = servicePorts().find(([name]) => name === 'amalgm-mcp')?.[1];
1508
1513
  let answer = null;
1509
1514
  if (port) {
@@ -1539,6 +1544,9 @@ async function add(positionals) {
1539
1544
  return;
1540
1545
  }
1541
1546
  let job = answer.payload;
1547
+ if (job?.state === 'waiting_for_content' || job?.state === 'materializing') {
1548
+ console.log(' Downloading cloud content…');
1549
+ }
1542
1550
  const deadline = Date.now() + 120_000;
1543
1551
  while (job?.state === 'waiting_for_content' || job?.state === 'materializing') {
1544
1552
  if (Date.now() > deadline) {
@@ -1555,6 +1563,8 @@ async function add(positionals) {
1555
1563
  return;
1556
1564
  }
1557
1565
  console.log(`Added ${job.entityUuid} to ${job.targetPath}`);
1566
+ console.log(' Watching: on');
1567
+ console.log(' Cloud: live');
1558
1568
  }
1559
1569
 
1560
1570
  /** POST JSON to a local runtime service, carrying the runtime token. */
@@ -50,7 +50,10 @@ function runtimeBranchForLabel(label, env = process.env) {
50
50
  }
51
51
 
52
52
  function npmTagForRuntimeLabel(label) {
53
- return label === 'preview' ? 'canary' : 'latest';
53
+ // Releases are promoted deliberately to `main`; `latest` is retained as
54
+ // the public compatibility channel and may therefore lag the runtime we
55
+ // boot. A main installation must update from the channel it actually runs.
56
+ return label === 'preview' ? 'canary' : 'main';
54
57
  }
55
58
 
56
59
  function sanitizeScopeSegment(value, fallback = 'local') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.249",
3
+ "version": "0.1.250",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -613,6 +613,45 @@ function createEntityCloud({ db, userId, deviceId, content = null }) {
613
613
  return materialization(db.prepare('SELECT * FROM entity_materializations WHERE job_id = ?').get(jobId));
614
614
  }
615
615
 
616
+ /** A completed add is a live binding, not a one-time download. A remote
617
+ * record beneath that binding moves the durable job back to its content
618
+ * gate. The service owns the later filesystem application; this module
619
+ * owns only the durable intent to do it. */
620
+ function refreshMaterializations({ resourceId, record, version }) {
621
+ const refreshed = [];
622
+ for (const job of db.prepare(`
623
+ SELECT * FROM entity_materializations
624
+ WHERE resource_id = ? AND state = 'complete'
625
+ ORDER BY created_at ASC
626
+ `).all(resourceId)) {
627
+ const tree = catalogTree({ resourceId, uuid: job.entity_uuid });
628
+ if (!tree.some((candidate) => candidate.uuid === record.uuid)) continue;
629
+ db.prepare(`
630
+ UPDATE entity_materializations
631
+ SET head_version = ?, state = 'waiting_for_content', error = NULL, updated_at = ?
632
+ WHERE job_id = ?
633
+ `).run(Number(version), now(), job.job_id);
634
+ refreshed.push(job.job_id);
635
+ }
636
+ return refreshed;
637
+ }
638
+
639
+ function hasUnsettledLocalMutation(uuid) {
640
+ const rows = db.prepare(`
641
+ SELECT operation_json FROM entity_cloud_mutations
642
+ WHERE resource_id = ? AND mutation_state <> 'synced'
643
+ ORDER BY sequence ASC
644
+ `).all(replica()?.resourceId || '');
645
+ return rows.some((row) => {
646
+ try {
647
+ return JSON.parse(row.operation_json)?.record?.uuid === uuid;
648
+ } catch {
649
+ // A corrupt local outbox must never authorize an overwrite.
650
+ return true;
651
+ }
652
+ });
653
+ }
654
+
616
655
  function claim({ limit = 16, leaseMs = 30_000 } = {}) {
617
656
  const current = replica();
618
657
  if (!current || !['active', BOOTSTRAP_RECONCILIATION_STATE].includes(current.state)) return [];
@@ -781,9 +820,13 @@ function createEntityCloud({ db, userId, deviceId, content = null }) {
781
820
  }
782
821
  // Remote mutations update the cloud-only catalog. They never create a
783
822
  // local watcher; `amalgm add` alone turns catalog identity into ground.
823
+ // A pre-existing local record is normally a conflict, except for a
824
+ // completed add binding with no unsent local edit. That binding will
825
+ // apply the authoritative cloud record through its own watcher-safe
826
+ // materializer below; it must not be stranded at its first snapshot.
784
827
  let local;
785
828
  try { local = registry.syncRecord(record.uuid); } catch { local = null; }
786
- if (local && !sameRecord(local, record)) {
829
+ if (local && !sameRecord(local, record) && hasUnsettledLocalMutation(record.uuid)) {
787
830
  throw new Error('remote entity mutation conflicts with this device; refresh the compacted cloud head before applying it locally');
788
831
  }
789
832
  upsertCatalogRecord({
@@ -793,7 +836,11 @@ function createEntityCloud({ db, userId, deviceId, content = null }) {
793
836
  record,
794
837
  });
795
838
  if (Number(version) > current.headVersion) saveReplica({ ...current, headVersion: Number(version) });
796
- return { duplicate: true, version: Number(version) };
839
+ return {
840
+ duplicate: true,
841
+ version: Number(version),
842
+ materializations: refreshMaterializations({ resourceId: current.resourceId, record, version }),
843
+ };
797
844
  }
798
845
 
799
846
  return {
@@ -816,6 +863,7 @@ function createEntityCloud({ db, userId, deviceId, content = null }) {
816
863
  completeMaterialization,
817
864
  failMaterialization,
818
865
  materializationStatus,
866
+ hasUnsettledLocalMutation,
819
867
  catalogTree,
820
868
  replica,
821
869
  };
@@ -744,6 +744,7 @@ function createRegistration(options) {
744
744
 
745
745
  /** The latest edge pass's refusals — visible, never silent. */
746
746
  reported: () => [...reported],
747
+ addressOf,
747
748
  repoState: (uuid) => repoStates.get(uuid),
748
749
  tree: treeAt,
749
750
  trees: () => new Map(treeRoots()
@@ -369,11 +369,167 @@ function createRegistrationService({
369
369
  }
370
370
  }
371
371
 
372
+ /** The local replica of `amalgm add` is Amalgm-managed ground. This is
373
+ * deliberately not a two-way directory merger: if local bytes have drifted
374
+ * from the last authoritative record, the import stops rather than choosing
375
+ * a winner behind the user's back. Otherwise a remote cloud head is
376
+ * projected into the existing binding, with journal suppression so the
377
+ * receiver never echoes its own download back to cloud. */
378
+ function applyLiveCloudTree(job) {
379
+ const tree = entityCloud.catalogTree({ resourceId: job.resourceId, uuid: job.entityUuid });
380
+ const root = tree[0];
381
+ const active = tree.filter((record) => record.status === 'active');
382
+ const unsupported = tree.find((record) => ['repo.git', 'link', 'reference'].includes(record.type));
383
+ if (unsupported) {
384
+ throw new Error(`live cloud add for ${unsupported.type} is not available yet; the binding was left unchanged`);
385
+ }
386
+ if (!['workspace', 'file.text', 'file.binary'].includes(root.type)) {
387
+ throw new Error(`cloud root ${root.uuid} is ${root.type}; this live materializer supports workspace and file trees only`);
388
+ }
389
+ for (const record of active) {
390
+ if (['file.text', 'file.binary'].includes(record.type)
391
+ && !entityContent.complete({ contentHash: record.payloadVersion })) return false;
392
+ }
393
+
394
+ const parent = fs.realpathSync(job.targetPath);
395
+ const destination = path.join(parent, root.name);
396
+ const boundRoot = boundary.current.tree(destination)?.root();
397
+ if (!boundRoot || boundRoot.uuid !== root.uuid) {
398
+ throw new Error(`cloud binding ${root.uuid} no longer owns ${destination}; refusing to overwrite local ground`);
399
+ }
400
+
401
+ const children = new Map();
402
+ for (const record of active) {
403
+ if (record.parentUUID === null) continue;
404
+ const group = children.get(record.parentUUID) || [];
405
+ group.push(record);
406
+ children.set(record.parentUUID, group);
407
+ }
408
+ for (const group of children.values()) group.sort((left, right) => left.name.localeCompare(right.name));
409
+
410
+ function localRecord(uuid) {
411
+ try { return boundary.current.registry.syncRecord(uuid); } catch { return null; }
412
+ }
413
+
414
+ function addressOf(uuid) {
415
+ try { return boundary.current.addressOf(uuid); } catch { return null; }
416
+ }
417
+
418
+ function fileHash(file) {
419
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
420
+ }
421
+
422
+ // Before changing logical identity, prove that every local byte we could
423
+ // overwrite is still the byte named by the old authoritative record.
424
+ // An unsent local edit or an out-of-band edit is a conflict, never an
425
+ // implicit cloud-wins overwrite.
426
+ for (const record of tree) {
427
+ const current = localRecord(record.uuid);
428
+ if (!current || current.version === record.version) continue;
429
+ if (entityCloud.hasUnsettledLocalMutation(record.uuid)) {
430
+ throw new Error(`local change for ${record.uuid} is still waiting for cloud; refusing to overwrite it`);
431
+ }
432
+ if (!['file.text', 'file.binary'].includes(current.type)) continue;
433
+ const currentPath = addressOf(record.uuid);
434
+ if (!currentPath || !fs.existsSync(currentPath)) {
435
+ throw new Error(`local file for ${record.uuid} is missing; refusing to overwrite an unverified binding`);
436
+ }
437
+ if (fileHash(currentPath) !== current.payloadVersion) {
438
+ throw new Error(`local file ${currentPath} changed outside Amalgm; refusing to overwrite it`);
439
+ }
440
+ }
441
+
442
+ function applyKnownRecords() {
443
+ // Catalog container versions are deliberately regenerated by the
444
+ // authority after child mutations. Applying an unchanged container
445
+ // before its later child would therefore compare an old derived version.
446
+ // Only import records whose stored logical fields actually changed; the
447
+ // local registry derives the same container version after the child lands.
448
+ const depth = new Map([[root.uuid, 0]]);
449
+ for (const record of tree) {
450
+ if (record.parentUUID !== null) depth.set(record.uuid, (depth.get(record.parentUUID) || 0) + 1);
451
+ }
452
+ for (const record of [...tree].sort((left, right) => (depth.get(left.uuid) || 0) - (depth.get(right.uuid) || 0))) {
453
+ const current = localRecord(record.uuid);
454
+ if (current && current.type === record.type && current.parentUUID === record.parentUUID
455
+ && current.name === record.name && current.status === record.status
456
+ && current.payloadVersion === record.payloadVersion) continue;
457
+ boundary.current.registry.applyCloudRecord(record);
458
+ }
459
+ }
460
+
461
+ function atomicWrite(file, bytes) {
462
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.amalgm-live-${crypto.randomBytes(10).toString('hex')}`);
463
+ try {
464
+ fs.writeFileSync(temporary, bytes, { mode: 0o600, flag: 'wx' });
465
+ fs.renameSync(temporary, file);
466
+ } finally {
467
+ fs.rmSync(temporary, { force: true });
468
+ }
469
+ }
470
+
471
+ function removeInactiveChild(parentRecord, name, target) {
472
+ const known = boundary.current.registry.children(parentRecord.uuid)
473
+ .find((candidate) => candidate.name === name);
474
+ if (!known || known.status === 'active') {
475
+ throw new Error(`local item ${target} is not owned by this cloud binding; refusing to remove it`);
476
+ }
477
+ if (['file.text', 'file.binary'].includes(known.type) && fs.existsSync(target)) {
478
+ const previous = localRecord(known.uuid);
479
+ if (previous?.payloadVersion && fileHash(target) !== previous.payloadVersion) {
480
+ throw new Error(`local file ${target} changed outside Amalgm; refusing to remove it`);
481
+ }
482
+ }
483
+ fs.rmSync(target, { recursive: true, force: true });
484
+ }
485
+
486
+ function project(record, target) {
487
+ if (record.type === 'workspace' || record.type === 'folder') {
488
+ if (fs.existsSync(target)) {
489
+ if (!fs.statSync(target).isDirectory()) {
490
+ throw new Error(`cloud container ${record.uuid} conflicts with local non-directory ${target}`);
491
+ }
492
+ } else {
493
+ fs.mkdirSync(target, { mode: 0o700 });
494
+ }
495
+ const wanted = new Set((children.get(record.uuid) || []).map((child) => child.name));
496
+ for (const child of children.get(record.uuid) || []) project(child, path.join(target, child.name));
497
+ for (const entry of fs.readdirSync(target)) {
498
+ if (!wanted.has(entry)) removeInactiveChild(record, entry, path.join(target, entry));
499
+ }
500
+ return;
501
+ }
502
+ if (!['file.text', 'file.binary'].includes(record.type)) {
503
+ throw new Error(`cloud entity ${record.uuid} is ${record.type}; this live materializer cannot write it`);
504
+ }
505
+ if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
506
+ throw new Error(`cloud file ${record.uuid} conflicts with local directory ${target}`);
507
+ }
508
+ const bytes = entityContent.bytes({ contentHash: record.payloadVersion });
509
+ if (fs.existsSync(target) && Buffer.compare(fs.readFileSync(target), bytes) === 0) return;
510
+ atomicWrite(target, bytes);
511
+ }
512
+
513
+ boundary.current.registry.withoutJournal(() => {
514
+ applyKnownRecords();
515
+ project(root, destination);
516
+ const observerRoot = observer.listRoots().find((entry) => entry.path === destination);
517
+ if (!observerRoot) throw new Error(`cloud binding ${root.uuid} has no local watcher root at ${destination}`);
518
+ observer.scanNow(observerRoot.rootId);
519
+ boundary.current.flushIfDirty();
520
+ });
521
+ return { destination, root };
522
+ }
523
+
372
524
  function advanceMaterializations() {
373
525
  for (const job of entityCloud.pendingMaterializations()) {
374
526
  if (job.state !== 'waiting_for_content') continue;
375
527
  try {
376
- const result = materializeCloudTree(job);
528
+ const root = entityCloud.catalogTree({ resourceId: job.resourceId, uuid: job.entityUuid })[0];
529
+ const destination = path.join(fs.realpathSync(job.targetPath), root.name);
530
+ const result = boundary.current.tree(destination)?.root()?.uuid === root.uuid
531
+ ? applyLiveCloudTree(job)
532
+ : materializeCloudTree(job);
377
533
  if (result) entityCloud.completeMaterialization({ jobId: job.jobId });
378
534
  } catch (error) {
379
535
  entityCloud.failMaterialization({ jobId: job.jobId, message: error.message });
@@ -402,6 +558,27 @@ function createRegistrationService({
402
558
  advanceMaterializations();
403
559
  return entityCloud.materializationStatus(job.jobId);
404
560
  },
561
+ /** The inbound realtime rail is part of the add binding. Routes call
562
+ * these composition methods so receiving an authoritative record or one
563
+ * of its immutable content parts immediately advances the local replica;
564
+ * users never run a separate sync command. */
565
+ ingestCloudMutation: ({ envelope, version, committedAt }) => {
566
+ const ingested = entityCloud.ingest({
567
+ registry: boundary.current.registry,
568
+ envelope,
569
+ version,
570
+ committedAt,
571
+ });
572
+ advanceMaterializations();
573
+ return ingested;
574
+ },
575
+ receiveCloudContent: ({ resourceId, contentHash, kind, partIndex, sha256, dataBase64 }) => {
576
+ const received = entityCloud.receiveDownload({
577
+ resourceId, contentHash, kind, partIndex, sha256, dataBase64,
578
+ });
579
+ advanceMaterializations();
580
+ return received;
581
+ },
405
582
  advanceMaterializations,
406
583
  bootstrapUserHome,
407
584
  exactUserHomeCloudRecords,
@@ -240,7 +240,7 @@ async function handleEntityRoutes(ctx) {
240
240
  const body = await ctx.readJsonBody();
241
241
  const service = registrationService();
242
242
  try {
243
- const received = service.entityCloud.receiveDownload({
243
+ const received = service.receiveCloudContent({
244
244
  resourceId: body?.resourceId,
245
245
  contentHash: body?.contentHash,
246
246
  kind: body?.kind,
@@ -248,7 +248,6 @@ async function handleEntityRoutes(ctx) {
248
248
  sha256: body?.sha256,
249
249
  dataBase64: body?.dataBase64,
250
250
  });
251
- service.advanceMaterializations();
252
251
  ctx.sendJson(200, { ...received });
253
252
  } catch (error) {
254
253
  ctx.sendJson(400, { error: error.message });
@@ -290,12 +289,12 @@ async function handleEntityRoutes(ctx) {
290
289
  const body = await ctx.readJsonBody();
291
290
  const service = registrationService();
292
291
  try {
293
- ctx.sendJson(200, service.entityCloud.ingest({
294
- registry: service.registration.registry,
292
+ const ingested = service.ingestCloudMutation({
295
293
  envelope: body?.envelope,
296
294
  version: body?.version,
297
295
  committedAt: body?.committedAt,
298
- }));
296
+ });
297
+ ctx.sendJson(200, ingested);
299
298
  } catch (error) {
300
299
  ctx.sendJson(409, { error: error.message });
301
300
  }
@@ -116,3 +116,108 @@ test('add materializes one compacted cloud head, preserves its UUIDs, and starts
116
116
  fs.rmSync(rootDir, { recursive: true, force: true });
117
117
  }
118
118
  });
119
+
120
+ test('an added ordinary workspace stays live: a later cloud file head replaces local bytes without producing an echo mutation', async () => {
121
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-add-live-materialization-'));
122
+ const sourceDir = path.join(rootDir, 'source');
123
+ const targetState = path.join(rootDir, 'target-state');
124
+ const targetParent = path.join(rootDir, 'checkout');
125
+ fs.mkdirSync(sourceDir, { recursive: true });
126
+ fs.mkdirSync(targetParent, { recursive: true });
127
+
128
+ const firstBytes = Buffer.from('first authoritative content\n');
129
+ const firstHash = sha256(firstBytes);
130
+ fs.writeFileSync(path.join(sourceDir, 'readme.md'), firstBytes);
131
+ const sourceDb = new Database(':memory:');
132
+ const sourceContent = createEntityContent({ dir: sourceDir });
133
+ const sourceCloud = createEntityCloud({
134
+ db: sourceDb, userId: 'user-1', deviceId: 'source-device', content: sourceContent,
135
+ });
136
+ const sourceRegistry = createRegistry({ db: sourceDb, ephemeral: true, emit: () => {}, journal: sourceCloud.journal });
137
+ const root = sourceRegistry.enrollRoot({ type: 'workspace', name: 'project' });
138
+ const file = sourceRegistry.create({
139
+ type: 'file.text', parentUUID: root.uuid, name: 'readme.md', payloadVersion: firstHash,
140
+ });
141
+ sourceContent.captureFile({ filePath: path.join(sourceDir, 'readme.md'), contentHash: firstHash });
142
+ const initialParts = uploadedParts(sourceContent, sourceCloud.identity().resourceId, firstHash);
143
+ const snapshot = snapshotFromRecords(sourceRegistry.syncRecords());
144
+ const snapshotBytes = Buffer.from(JSON.stringify(snapshot), 'utf8');
145
+
146
+ const target = createRegistrationService({
147
+ dir: targetState,
148
+ homeDir: path.join(rootDir, 'target-home'),
149
+ cloudUserId: 'user-1',
150
+ cloudDeviceId: 'target-device',
151
+ });
152
+ try {
153
+ target.entityCloud.install({
154
+ registry: target.registration.registry,
155
+ resourceId: sourceCloud.identity().resourceId,
156
+ authorityEpoch: 7,
157
+ headVersion: 41,
158
+ snapshotBase64: snapshotBytes.toString('base64'),
159
+ snapshotChecksum: sha256(snapshotBytes),
160
+ });
161
+ for (const part of [...initialParts.filter((part) => part.kind === 'manifest'), ...initialParts.filter((part) => part.kind === 'chunk')]) {
162
+ target.entityCloud.receiveDownload({
163
+ resourceId: sourceCloud.identity().resourceId,
164
+ contentHash: firstHash,
165
+ kind: part.kind,
166
+ partIndex: part.kind === 'manifest' ? 0 : part.partIndex,
167
+ sha256: part.sha256,
168
+ dataBase64: part.dataBase64,
169
+ });
170
+ }
171
+ assert.equal((await target.add({ uuid: root.uuid, targetPath: targetParent })).state, 'complete');
172
+
173
+ const secondBytes = Buffer.from('second authoritative content\n');
174
+ const secondHash = sha256(secondBytes);
175
+ fs.writeFileSync(path.join(sourceDir, 'readme.md'), secondBytes);
176
+ sourceContent.captureFile({ filePath: path.join(sourceDir, 'readme.md'), contentHash: secondHash });
177
+ const secondParts = uploadedParts(sourceContent, sourceCloud.identity().resourceId, secondHash);
178
+ const changed = sourceRegistry.attest(file.uuid, { type: 'file.text', payloadVersion: secondHash });
179
+ const remoteRecord = sourceRegistry.syncRecord(changed.uuid);
180
+
181
+ const ingested = target.ingestCloudMutation({
182
+ envelope: {
183
+ resourceId: sourceCloud.identity().resourceId,
184
+ mutationId: 'ent_remote_file_change',
185
+ contract: 'amalgm-entity-registry@1',
186
+ schemaVersion: 1,
187
+ operationKind: 'entity.upsert',
188
+ operation: { kind: 'entity.upsert', record: remoteRecord },
189
+ },
190
+ version: 42,
191
+ committedAt: '2026-08-05T00:00:00.000Z',
192
+ });
193
+ assert.equal(ingested.materializations.length, 1, 'the completed add became a live pending application');
194
+ assert.equal(fs.readFileSync(path.join(targetParent, 'project', 'readme.md'), 'utf8'), firstBytes.toString(),
195
+ 'the destination waits for verified immutable bytes');
196
+
197
+ for (const part of [...secondParts.filter((part) => part.kind === 'manifest'), ...secondParts.filter((part) => part.kind === 'chunk')]) {
198
+ target.receiveCloudContent({
199
+ resourceId: sourceCloud.identity().resourceId,
200
+ contentHash: secondHash,
201
+ kind: part.kind,
202
+ partIndex: part.kind === 'manifest' ? 0 : part.partIndex,
203
+ sha256: part.sha256,
204
+ dataBase64: part.dataBase64,
205
+ });
206
+ }
207
+ assert.deepEqual(fs.readFileSync(path.join(targetParent, 'project', 'readme.md')), secondBytes);
208
+ assert.equal(target.registration.registry.syncRecord(file.uuid).payloadVersion, secondHash);
209
+ assert.equal(target.observer.status().find((entry) => entry.path === fs.realpathSync(path.join(targetParent, 'project')))?.watching, true);
210
+
211
+ const targetDb = new Database(path.join(targetState, 'identity.db'), { readonly: true });
212
+ try {
213
+ assert.equal(targetDb.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 0,
214
+ 'the receiving watcher did not echo the imported cloud edit as a new local mutation');
215
+ } finally {
216
+ targetDb.close();
217
+ }
218
+ } finally {
219
+ target.close();
220
+ sourceDb.close();
221
+ fs.rmSync(rootDir, { recursive: true, force: true });
222
+ }
223
+ });