amalgm 0.1.252 → 0.1.254
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/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +3 -3
- package/runtime/scripts/amalgm-mcp/registration/index.js +21 -7
- package/runtime/scripts/amalgm-mcp/registration/service.js +62 -6
- package/runtime/scripts/amalgm-mcp/registration/tree.js +10 -1
- package/runtime/scripts/amalgm-mcp/registry/index.js +29 -2
- package/runtime/scripts/amalgm-mcp/tests/entity-cloud.test.js +5 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-materialization.test.js +89 -0
package/package.json
CHANGED
|
@@ -600,14 +600,14 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
|
|
|
600
600
|
if (typeof targetPath !== 'string' || !targetPath) throw new Error('materialization requires a destination path');
|
|
601
601
|
const tree = catalogTree({ resourceId: current.resourceId, uuid });
|
|
602
602
|
if (tree[0].status !== 'active') throw new Error(`cloud entity ${uuid} is not active`);
|
|
603
|
-
const unsupported = tree.find((record) =>
|
|
603
|
+
const unsupported = tree.find((record) => record.type === 'reference');
|
|
604
604
|
if (unsupported) {
|
|
605
|
-
throw new Error(`cloud entity ${unsupported.uuid} is
|
|
605
|
+
throw new Error(`cloud entity ${unsupported.uuid} is a managed reference; reference projection is not materialized by amalgm add yet`);
|
|
606
606
|
}
|
|
607
607
|
const prior = db.prepare(`
|
|
608
608
|
SELECT * FROM entity_materializations
|
|
609
609
|
WHERE resource_id = ? AND entity_uuid = ? AND target_path = ?
|
|
610
|
-
AND state IN ('waiting_for_content', 'materializing'
|
|
610
|
+
AND state IN ('waiting_for_content', 'materializing')
|
|
611
611
|
ORDER BY created_at DESC LIMIT 1
|
|
612
612
|
`).get(current.resourceId, tree[0].uuid, targetPath);
|
|
613
613
|
if (prior) return materialization(prior);
|
|
@@ -505,8 +505,12 @@ function createRegistration(options) {
|
|
|
505
505
|
}
|
|
506
506
|
const covering = treeRootCovering(ground);
|
|
507
507
|
if (covering) {
|
|
508
|
-
|
|
509
|
-
|
|
508
|
+
// This address is already registered ground. Its prior Register →
|
|
509
|
+
// Watch transition remains the proof of its identity; repeating a
|
|
510
|
+
// harmless "ensure the safe home exists" request must not rescan every
|
|
511
|
+
// edge and repository in every tree. Doorbells own later convergence.
|
|
512
|
+
// We still prove the one property this caller may rely on now: that
|
|
513
|
+
// the existing watcher is live (and retry its own failed birth once).
|
|
510
514
|
requireLiveWatchers(covering.path);
|
|
511
515
|
return { rootPath: covering.path, record: rootRecordOf(covering.rootId) };
|
|
512
516
|
}
|
|
@@ -546,11 +550,21 @@ function createRegistration(options) {
|
|
|
546
550
|
// Start's first census must run before a parent lens is allowed to
|
|
547
551
|
// conclude that a pre-registered nested repo is absent: unobserved is
|
|
548
552
|
// not absent. Only then do evidence lenses reconcile that census.
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
553
|
+
//
|
|
554
|
+
// An imported cloud tree is different only in authority, not in
|
|
555
|
+
// mechanics: its first census supplies local observer evidence but must
|
|
556
|
+
// never turn the authoritative snapshot back into a fresh outbound
|
|
557
|
+
// mutation. Keep the whole first Watch/Detect transition inside the
|
|
558
|
+
// existing journal boundary used for the seeded structural bind.
|
|
559
|
+
const startWatch = () => {
|
|
560
|
+
observer.start(root.rootId);
|
|
561
|
+
ensureTrees();
|
|
562
|
+
reconcileInitialRepoWorktrees(root.path);
|
|
563
|
+
flushIfDirty();
|
|
564
|
+
requireLiveWatchers(root.path);
|
|
565
|
+
};
|
|
566
|
+
if (suppressJournal) registry.withoutJournal(startWatch);
|
|
567
|
+
else startWatch();
|
|
554
568
|
return { rootPath: root.path, record: rootRecordOf(root.rootId) };
|
|
555
569
|
}
|
|
556
570
|
|
|
@@ -376,15 +376,18 @@ function createRegistrationService({
|
|
|
376
376
|
return false;
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
|
-
if (!['workspace', 'repo.git', 'file.text', 'file.binary'].includes(root.type)) {
|
|
380
|
-
throw new Error(`cloud root ${root.uuid} is ${root.type}; this materializer supports workspace, repository, and
|
|
379
|
+
if (!['workspace', 'repo.git', 'file.text', 'file.binary', 'link'].includes(root.type)) {
|
|
380
|
+
throw new Error(`cloud root ${root.uuid} is ${root.type}; this materializer supports workspace, repository, file, and link trees only`);
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
const parent = fs.realpathSync(job.targetPath);
|
|
384
384
|
const parentStat = fs.statSync(parent);
|
|
385
385
|
if (!parentStat.isDirectory()) throw new Error(`add destination is not a directory: ${parent}`);
|
|
386
386
|
const destination = path.join(parent, root.name);
|
|
387
|
-
|
|
387
|
+
const destinationExists = (() => {
|
|
388
|
+
try { fs.lstatSync(destination); return true; } catch { return false; }
|
|
389
|
+
})();
|
|
390
|
+
if (destinationExists) throw new Error(`refusing to add ${root.name}: destination already exists at ${destination}`);
|
|
388
391
|
const staging = path.join(parent, `.${root.name}.amalgm-materializing-${crypto.randomBytes(10).toString('hex')}`);
|
|
389
392
|
let promoted = false;
|
|
390
393
|
|
|
@@ -404,6 +407,10 @@ function createRegistrationService({
|
|
|
404
407
|
fs.writeFileSync(target, entityContent.bytes({ contentHash: record.payloadVersion }), { mode: 0o600, flag: 'wx' });
|
|
405
408
|
return;
|
|
406
409
|
}
|
|
410
|
+
if (record.type === 'link') {
|
|
411
|
+
fs.symlinkSync(record.payloadVersion, target);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
407
414
|
throw new Error(`cloud entity ${record.uuid} is ${record.type}; this materializer cannot write it yet`);
|
|
408
415
|
}
|
|
409
416
|
|
|
@@ -457,12 +464,12 @@ function createRegistrationService({
|
|
|
457
464
|
const tree = entityCloud.catalogTree({ resourceId: job.resourceId, uuid: job.entityUuid });
|
|
458
465
|
const root = tree[0];
|
|
459
466
|
const active = tree.filter((record) => record.status === 'active');
|
|
460
|
-
const unsupported = tree.find((record) =>
|
|
467
|
+
const unsupported = tree.find((record) => record.type === 'reference');
|
|
461
468
|
if (unsupported) {
|
|
462
469
|
throw new Error(`live cloud add for ${unsupported.type} is not available yet; the binding was left unchanged`);
|
|
463
470
|
}
|
|
464
|
-
if (!['workspace', 'repo.git', 'file.text', 'file.binary'].includes(root.type)) {
|
|
465
|
-
throw new Error(`cloud root ${root.uuid} is ${root.type}; this live materializer supports workspace, repository, and
|
|
471
|
+
if (!['workspace', 'repo.git', 'file.text', 'file.binary', 'link'].includes(root.type)) {
|
|
472
|
+
throw new Error(`cloud root ${root.uuid} is ${root.type}; this live materializer supports workspace, repository, file, and link trees only`);
|
|
466
473
|
}
|
|
467
474
|
for (const record of active) {
|
|
468
475
|
if (['file.text', 'file.binary'].includes(record.type)
|
|
@@ -512,9 +519,11 @@ function createRegistrationService({
|
|
|
512
519
|
// overwrite is still the byte named by the old authoritative record.
|
|
513
520
|
// An unsent local edit or an out-of-band edit is a conflict, never an
|
|
514
521
|
// implicit cloud-wins overwrite.
|
|
522
|
+
const locallyBound = new Set();
|
|
515
523
|
for (const record of tree) {
|
|
516
524
|
const current = localRecord(record.uuid);
|
|
517
525
|
if (!current || current.version === record.version) continue;
|
|
526
|
+
locallyBound.add(record.uuid);
|
|
518
527
|
if (entityCloud.hasUnsettledLocalMutation(record.uuid)) {
|
|
519
528
|
throw new Error(`local change for ${record.uuid} is still waiting for cloud; refusing to overwrite it`);
|
|
520
529
|
}
|
|
@@ -528,6 +537,15 @@ function createRegistrationService({
|
|
|
528
537
|
}
|
|
529
538
|
continue;
|
|
530
539
|
}
|
|
540
|
+
if (current.type === 'link') {
|
|
541
|
+
const currentPath = addressOf(record.uuid);
|
|
542
|
+
let stat = null;
|
|
543
|
+
try { stat = currentPath ? fs.lstatSync(currentPath) : null; } catch {}
|
|
544
|
+
if (!stat || !stat.isSymbolicLink() || fs.readlinkSync(currentPath) !== current.payloadVersion) {
|
|
545
|
+
throw new Error(`local link ${currentPath || record.uuid} changed outside Amalgm; refusing to overwrite it`);
|
|
546
|
+
}
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
531
549
|
if (!['file.text', 'file.binary'].includes(current.type)) continue;
|
|
532
550
|
const currentPath = addressOf(record.uuid);
|
|
533
551
|
if (!currentPath || !fs.existsSync(currentPath)) {
|
|
@@ -567,6 +585,16 @@ function createRegistrationService({
|
|
|
567
585
|
}
|
|
568
586
|
}
|
|
569
587
|
|
|
588
|
+
function atomicLink(file, target) {
|
|
589
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.amalgm-live-${crypto.randomBytes(10).toString('hex')}`);
|
|
590
|
+
try {
|
|
591
|
+
fs.symlinkSync(target, temporary);
|
|
592
|
+
fs.renameSync(temporary, file);
|
|
593
|
+
} finally {
|
|
594
|
+
fs.rmSync(temporary, { force: true });
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
570
598
|
function removeInactiveChild(parentRecord, name, target) {
|
|
571
599
|
const known = boundary.current.registry.children(parentRecord.uuid)
|
|
572
600
|
.find((candidate) => candidate.name === name);
|
|
@@ -579,6 +607,14 @@ function createRegistrationService({
|
|
|
579
607
|
throw new Error(`local file ${target} changed outside Amalgm; refusing to remove it`);
|
|
580
608
|
}
|
|
581
609
|
}
|
|
610
|
+
if (known.type === 'link') {
|
|
611
|
+
const previous = localRecord(known.uuid);
|
|
612
|
+
let stat = null;
|
|
613
|
+
try { stat = fs.lstatSync(target); } catch {}
|
|
614
|
+
if (!stat || !stat.isSymbolicLink() || fs.readlinkSync(target) !== previous?.payloadVersion) {
|
|
615
|
+
throw new Error(`local link ${target} changed outside Amalgm; refusing to remove it`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
582
618
|
fs.rmSync(target, { recursive: true, force: true });
|
|
583
619
|
}
|
|
584
620
|
|
|
@@ -602,6 +638,26 @@ function createRegistrationService({
|
|
|
602
638
|
applyRepo(target, repoState(record));
|
|
603
639
|
return;
|
|
604
640
|
}
|
|
641
|
+
if (record.type === 'link') {
|
|
642
|
+
let present = false;
|
|
643
|
+
try { fs.lstatSync(target); present = true; } catch {}
|
|
644
|
+
if (present) {
|
|
645
|
+
const stat = fs.lstatSync(target);
|
|
646
|
+
if (stat.isSymbolicLink() && fs.readlinkSync(target) === record.payloadVersion) return;
|
|
647
|
+
// This address was just proven to be the old authoritative UUID
|
|
648
|
+
// above. The registry now holds the new link head, so replace the
|
|
649
|
+
// literal projection atomically rather than treating its old target
|
|
650
|
+
// as unrelated ground. A brand-new cloud link retains the refusal:
|
|
651
|
+
// it may never overwrite an existing local address.
|
|
652
|
+
if (locallyBound.has(record.uuid)) {
|
|
653
|
+
atomicLink(target, record.payloadVersion);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
throw new Error(`cloud link ${record.uuid} conflicts with local ground ${target}`);
|
|
657
|
+
}
|
|
658
|
+
atomicLink(target, record.payloadVersion);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
605
661
|
if (!['file.text', 'file.binary'].includes(record.type)) {
|
|
606
662
|
throw new Error(`cloud entity ${record.uuid} is ${record.type}; this live materializer cannot write it`);
|
|
607
663
|
}
|
|
@@ -172,7 +172,16 @@ function registerDirectory(dirPath, parentUUID, {
|
|
|
172
172
|
const identity = seeded(entryRelativePath, {
|
|
173
173
|
type: 'link', parentUUID, name: entry.name, root: false,
|
|
174
174
|
});
|
|
175
|
-
registry.create({
|
|
175
|
+
registry.create({
|
|
176
|
+
type: identity.type,
|
|
177
|
+
parentUUID,
|
|
178
|
+
name: entry.name,
|
|
179
|
+
// A cloud materializer has already verified the literal link text.
|
|
180
|
+
// Preserve it through the structural bind; normal local registration
|
|
181
|
+
// still leaves this null until Watch observes the filesystem.
|
|
182
|
+
payloadVersion: identity.payloadVersion ?? null,
|
|
183
|
+
uuid: identity.uuid,
|
|
184
|
+
});
|
|
176
185
|
continue;
|
|
177
186
|
}
|
|
178
187
|
if (stat.isDirectory()) {
|
|
@@ -183,6 +183,18 @@ function createRegistry(options) {
|
|
|
183
183
|
let bellPending = false;
|
|
184
184
|
const deferred = []; // bells born inside an open transaction: they ring at commit, or a rollback silences them
|
|
185
185
|
let journalSuppression = 0;
|
|
186
|
+
// A container's head is derived from its active direct membership. A
|
|
187
|
+
// reconciliation may need that answer for many descendants, but it is
|
|
188
|
+
// unchanged until a child enters, leaves, moves, or is renamed. Cache the
|
|
189
|
+
// derivation here, at the registry boundary, and invalidate it with the
|
|
190
|
+
// membership transition itself.
|
|
191
|
+
const membershipHashes = new Map();
|
|
192
|
+
|
|
193
|
+
function invalidateMembership(...uuids) {
|
|
194
|
+
for (const uuid of uuids) {
|
|
195
|
+
if (uuid !== null && uuid !== undefined) membershipHashes.delete(uuid);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
186
198
|
|
|
187
199
|
function ring(event) {
|
|
188
200
|
try {
|
|
@@ -273,8 +285,12 @@ function createRegistry(options) {
|
|
|
273
285
|
|
|
274
286
|
/** Container payload: the sorted active direct membership (childUUID, name). */
|
|
275
287
|
function membershipHash(uuid) {
|
|
288
|
+
const cached = membershipHashes.get(uuid);
|
|
289
|
+
if (cached) return cached;
|
|
276
290
|
const pairs = store.activeChildrenOf(uuid).map((child) => [child.uuid, child.name]);
|
|
277
|
-
|
|
291
|
+
const hash = crypto.createHash('sha256').update(JSON.stringify(pairs)).digest('hex');
|
|
292
|
+
membershipHashes.set(uuid, hash);
|
|
293
|
+
return hash;
|
|
278
294
|
}
|
|
279
295
|
|
|
280
296
|
function payloadFor(row) {
|
|
@@ -361,6 +377,9 @@ function createRegistry(options) {
|
|
|
361
377
|
const previous = record(row);
|
|
362
378
|
store.update(row.uuid, { ...row, ...changes });
|
|
363
379
|
const nextRow = mustGet(row.uuid);
|
|
380
|
+
if (changes.status !== undefined && changes.status !== row.status) {
|
|
381
|
+
invalidateMembership(row.parentUuid);
|
|
382
|
+
}
|
|
364
383
|
const next = record(nextRow);
|
|
365
384
|
journalRecord(nextRow, row);
|
|
366
385
|
publish({ type: 'entity.committed', record: next, previous });
|
|
@@ -451,6 +470,9 @@ function createRegistry(options) {
|
|
|
451
470
|
const adoptedWorkspace = row.parentUuid === null && row.type === 'workspace';
|
|
452
471
|
return { uuid, parentUuid: parentUUID, name, ...(adoptedWorkspace ? { type: 'folder' } : {}) };
|
|
453
472
|
}));
|
|
473
|
+
for (const { uuid, parentUUID } of moves) {
|
|
474
|
+
invalidateMembership(rows.get(uuid).parentUuid, parentUUID);
|
|
475
|
+
}
|
|
454
476
|
return moves.map(({ uuid }, index) => {
|
|
455
477
|
const nextRow = mustGet(uuid);
|
|
456
478
|
const next = record(nextRow);
|
|
@@ -536,6 +558,7 @@ function createRegistry(options) {
|
|
|
536
558
|
status: 'active', payloadVersion,
|
|
537
559
|
};
|
|
538
560
|
store.insert(row);
|
|
561
|
+
invalidateMembership(parentUUID);
|
|
539
562
|
const next = record(row);
|
|
540
563
|
journalRecord(row, null);
|
|
541
564
|
publish({ type: 'entity.committed', record: next, previous: null });
|
|
@@ -639,17 +662,21 @@ function createRegistry(options) {
|
|
|
639
662
|
if (!previous && incoming.parentUuid !== null && !store.byUuid(incoming.parentUuid)) {
|
|
640
663
|
throw new Error(`cloud entity ${incoming.uuid} names missing parent ${incoming.parentUuid}`);
|
|
641
664
|
}
|
|
665
|
+
const previousRecord = previous ? record(previous) : null;
|
|
642
666
|
if (previous) {
|
|
643
667
|
store.update(incoming.uuid, incoming);
|
|
644
668
|
} else {
|
|
645
669
|
store.insert(incoming);
|
|
646
670
|
}
|
|
647
671
|
const current = mustGet(incoming.uuid);
|
|
672
|
+
if (!previous || previous.parentUuid !== current.parentUuid
|
|
673
|
+
|| previous.name !== current.name || previous.status !== current.status) {
|
|
674
|
+
invalidateMembership(previous?.parentUuid, current.parentUuid);
|
|
675
|
+
}
|
|
648
676
|
const synced = syncRecord(current);
|
|
649
677
|
if (synced.version !== incoming.expectedVersion) {
|
|
650
678
|
throw new Error(`cloud entity ${incoming.uuid} version does not match its logical record`);
|
|
651
679
|
}
|
|
652
|
-
const previousRecord = previous ? record(previous) : null;
|
|
653
680
|
if (!previous || !sameLogicalRecord(previous, current)) {
|
|
654
681
|
publish({ type: 'entity.committed', record: record(current), previous: previousRecord });
|
|
655
682
|
}
|
|
@@ -221,6 +221,11 @@ test('a compacted remote snapshot enters the cloud catalog, not locally watched
|
|
|
221
221
|
assert.deepEqual(targetRegistry.syncRecords(), [], 'a remote graph does not pretend to be watched local ground');
|
|
222
222
|
const job = targetCloud.startMaterialization({ uuid: root.uuid, targetPath: targetDir });
|
|
223
223
|
assert.equal(job.state, 'waiting_for_content');
|
|
224
|
+
targetCloud.completeMaterialization({ jobId: job.jobId });
|
|
225
|
+
const retry = targetCloud.startMaterialization({ uuid: root.uuid, targetPath: targetDir });
|
|
226
|
+
assert.notEqual(retry.jobId, job.jobId,
|
|
227
|
+
'a completed job is historical evidence, not permission to claim a destination still exists');
|
|
228
|
+
assert.equal(retry.state, 'waiting_for_content');
|
|
224
229
|
assert.deepEqual(targetCloud.catalogTree({ resourceId, uuid: root.uuid }).map((record) => record.uuid), [root.uuid, file.uuid]);
|
|
225
230
|
assert.deepEqual(targetCloud.claimDownload(), {
|
|
226
231
|
resourceId,
|
|
@@ -229,6 +229,95 @@ test('an added ordinary workspace stays live: a later cloud file head replaces l
|
|
|
229
229
|
}
|
|
230
230
|
});
|
|
231
231
|
|
|
232
|
+
test('add preserves a symlink UUID and later retargets the same literal link without an echo', async () => {
|
|
233
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-add-link-materialization-'));
|
|
234
|
+
const sourceDir = path.join(rootDir, 'source');
|
|
235
|
+
const targetState = path.join(rootDir, 'target-state');
|
|
236
|
+
const targetParent = path.join(rootDir, 'checkout');
|
|
237
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
238
|
+
fs.mkdirSync(targetParent, { recursive: true });
|
|
239
|
+
const firstBytes = Buffer.from('first target\n');
|
|
240
|
+
const secondBytes = Buffer.from('second target\n');
|
|
241
|
+
const firstHash = sha256(firstBytes);
|
|
242
|
+
const secondHash = sha256(secondBytes);
|
|
243
|
+
fs.writeFileSync(path.join(sourceDir, 'first.md'), firstBytes);
|
|
244
|
+
fs.writeFileSync(path.join(sourceDir, 'second.md'), secondBytes);
|
|
245
|
+
fs.symlinkSync('first.md', path.join(sourceDir, 'current.md'));
|
|
246
|
+
|
|
247
|
+
const sourceDb = new Database(':memory:');
|
|
248
|
+
const sourceContent = createEntityContent({ dir: sourceDir });
|
|
249
|
+
const sourceCloud = createEntityCloud({
|
|
250
|
+
db: sourceDb, userId: 'user-1', deviceId: 'source-device', content: sourceContent,
|
|
251
|
+
});
|
|
252
|
+
const sourceRegistry = createRegistry({ db: sourceDb, ephemeral: true, emit: () => {}, journal: sourceCloud.journal });
|
|
253
|
+
const root = sourceRegistry.enrollRoot({ type: 'workspace', name: 'project' });
|
|
254
|
+
sourceRegistry.create({ type: 'file.text', parentUUID: root.uuid, name: 'first.md', payloadVersion: firstHash });
|
|
255
|
+
sourceRegistry.create({ type: 'file.text', parentUUID: root.uuid, name: 'second.md', payloadVersion: secondHash });
|
|
256
|
+
const link = sourceRegistry.create({ type: 'link', parentUUID: root.uuid, name: 'current.md', payloadVersion: 'first.md' });
|
|
257
|
+
sourceContent.captureFile({ filePath: path.join(sourceDir, 'first.md'), contentHash: firstHash });
|
|
258
|
+
sourceContent.captureFile({ filePath: path.join(sourceDir, 'second.md'), contentHash: secondHash });
|
|
259
|
+
const initialParts = [
|
|
260
|
+
...uploadedParts(sourceContent, sourceCloud.identity().resourceId, firstHash),
|
|
261
|
+
...uploadedParts(sourceContent, sourceCloud.identity().resourceId, secondHash),
|
|
262
|
+
];
|
|
263
|
+
const snapshot = snapshotFromRecords(sourceRegistry.syncRecords());
|
|
264
|
+
const snapshotBytes = Buffer.from(JSON.stringify(snapshot), 'utf8');
|
|
265
|
+
const target = createRegistrationService({
|
|
266
|
+
dir: targetState, homeDir: path.join(rootDir, 'target-home'),
|
|
267
|
+
cloudUserId: 'user-1', cloudDeviceId: 'target-device',
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
target.entityCloud.install({
|
|
271
|
+
registry: target.registration.registry, resourceId: sourceCloud.identity().resourceId,
|
|
272
|
+
authorityEpoch: 7, headVersion: 41, snapshotBase64: snapshotBytes.toString('base64'),
|
|
273
|
+
snapshotChecksum: sha256(snapshotBytes),
|
|
274
|
+
});
|
|
275
|
+
for (const part of [...initialParts.filter((part) => part.kind === 'manifest'), ...initialParts.filter((part) => part.kind === 'chunk')]) {
|
|
276
|
+
target.receiveCloudContent({
|
|
277
|
+
resourceId: sourceCloud.identity().resourceId, contentHash: part.contentHash, kind: part.kind,
|
|
278
|
+
partIndex: part.kind === 'manifest' ? 0 : part.partIndex, sha256: part.sha256, dataBase64: part.dataBase64,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const job = await target.add({ uuid: root.uuid, targetPath: targetParent });
|
|
282
|
+
assert.equal(job.state, 'complete', job.error);
|
|
283
|
+
const destination = path.join(targetParent, 'project');
|
|
284
|
+
assert.equal(fs.readlinkSync(path.join(destination, 'current.md')), 'first.md');
|
|
285
|
+
assert.equal(target.registration.registry.syncRecord(link.uuid).uuid, link.uuid);
|
|
286
|
+
assert.equal(target.observer.status().find((entry) => entry.path === fs.realpathSync(destination))?.watching, true);
|
|
287
|
+
const afterAddDb = new Database(path.join(targetState, 'identity.db'), { readonly: true });
|
|
288
|
+
try {
|
|
289
|
+
assert.equal(afterAddDb.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 0,
|
|
290
|
+
'the first authoritative link binding does not create an outbound mutation');
|
|
291
|
+
} finally {
|
|
292
|
+
afterAddDb.close();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const changed = sourceRegistry.attest(link.uuid, { type: 'link', payloadVersion: 'second.md' });
|
|
296
|
+
const ingested = target.ingestCloudMutation({
|
|
297
|
+
envelope: {
|
|
298
|
+
resourceId: sourceCloud.identity().resourceId, mutationId: 'ent_remote_link_change',
|
|
299
|
+
contract: 'amalgm-entity-registry@1', schemaVersion: 1,
|
|
300
|
+
operationKind: 'entity.upsert', operation: { kind: 'entity.upsert', record: sourceRegistry.syncRecord(changed.uuid) },
|
|
301
|
+
},
|
|
302
|
+
version: 42, committedAt: '2026-08-05T00:00:00.000Z',
|
|
303
|
+
});
|
|
304
|
+
assert.equal(ingested.materializations.length, 1);
|
|
305
|
+
assert.equal(fs.readlinkSync(path.join(destination, 'current.md')), 'second.md');
|
|
306
|
+
assert.equal(target.registration.registry.syncRecord(link.uuid).payloadVersion, 'second.md');
|
|
307
|
+
const targetDb = new Database(path.join(targetState, 'identity.db'), { readonly: true });
|
|
308
|
+
try {
|
|
309
|
+
assert.equal(targetDb.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 0,
|
|
310
|
+
'the receiving link watcher did not echo the authoritative retarget upstream');
|
|
311
|
+
} finally {
|
|
312
|
+
targetDb.close();
|
|
313
|
+
}
|
|
314
|
+
} finally {
|
|
315
|
+
target.close();
|
|
316
|
+
sourceDb.close();
|
|
317
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
232
321
|
test('add recreates an actual Git repository from verified Repocard cargo, then arms its local watcher', async () => {
|
|
233
322
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-add-repo-materialization-'));
|
|
234
323
|
const sourceState = path.join(rootDir, 'source-state');
|