amalgm 0.1.249 → 0.1.251
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 +10 -0
- package/lib/runtime-identity.js +4 -1
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +173 -30
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +29 -0
- package/runtime/scripts/amalgm-mcp/registration/index.js +16 -3
- package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +8 -2
- package/runtime/scripts/amalgm-mcp/registration/repo-states.js +26 -4
- package/runtime/scripts/amalgm-mcp/registration/service.js +305 -6
- package/runtime/scripts/amalgm-mcp/registration/tree.js +29 -8
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +4 -5
- package/runtime/scripts/amalgm-mcp/tests/entity-cloud.test.js +50 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-materialization.test.js +242 -1
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +37 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const test = require('node:test');
|
|
4
4
|
const assert = require('node:assert/strict');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
6
7
|
const fs = require('fs');
|
|
7
8
|
const os = require('os');
|
|
8
9
|
const path = require('path');
|
|
@@ -12,6 +13,8 @@ const { createRegistry } = require('../registry');
|
|
|
12
13
|
const { createEntityCloud, snapshotFromRecords } = require('../registration/entity-cloud');
|
|
13
14
|
const { createEntityContent } = require('../registration/entity-content');
|
|
14
15
|
const { createRegistrationService } = require('../registration/service');
|
|
16
|
+
const { capture, verify } = require('../repocard');
|
|
17
|
+
const { transportBytes, localState } = require('../registration/repo-states');
|
|
15
18
|
|
|
16
19
|
function sha256(bytes) {
|
|
17
20
|
return crypto.createHash('sha256').update(bytes).digest('hex');
|
|
@@ -28,6 +31,10 @@ function uploadedParts(content, resourceId, contentHash) {
|
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
function git(repo, args) {
|
|
35
|
+
execFileSync('git', args, { cwd: repo, stdio: 'pipe' });
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
test('add materializes one compacted cloud head, preserves its UUIDs, and starts a local watcher without creating an outbound history replay', async () => {
|
|
32
39
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-add-materialization-'));
|
|
33
40
|
const sourceDir = path.join(rootDir, 'source');
|
|
@@ -93,7 +100,7 @@ test('add materializes one compacted cloud head, preserves its UUIDs, and starts
|
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
const job = await target.add({ uuid: root.uuid, targetPath: targetParent });
|
|
96
|
-
assert.equal(job.state, 'complete');
|
|
103
|
+
assert.equal(job.state, 'complete', job.error);
|
|
97
104
|
const destination = path.join(targetParent, 'project');
|
|
98
105
|
assert.deepEqual(fs.readFileSync(path.join(destination, 'readme.md')), bytes);
|
|
99
106
|
assert.equal(target.registration.registry.syncRecord(root.uuid).uuid, root.uuid);
|
|
@@ -116,3 +123,237 @@ test('add materializes one compacted cloud head, preserves its UUIDs, and starts
|
|
|
116
123
|
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
117
124
|
}
|
|
118
125
|
});
|
|
126
|
+
|
|
127
|
+
test('an added ordinary workspace stays live: a later cloud file head replaces local bytes without producing an echo mutation', async () => {
|
|
128
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-add-live-materialization-'));
|
|
129
|
+
const sourceDir = path.join(rootDir, 'source');
|
|
130
|
+
const targetState = path.join(rootDir, 'target-state');
|
|
131
|
+
const targetParent = path.join(rootDir, 'checkout');
|
|
132
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
133
|
+
fs.mkdirSync(targetParent, { recursive: true });
|
|
134
|
+
|
|
135
|
+
const firstBytes = Buffer.from('first authoritative content\n');
|
|
136
|
+
const firstHash = sha256(firstBytes);
|
|
137
|
+
fs.writeFileSync(path.join(sourceDir, 'readme.md'), firstBytes);
|
|
138
|
+
const sourceDb = new Database(':memory:');
|
|
139
|
+
const sourceContent = createEntityContent({ dir: sourceDir });
|
|
140
|
+
const sourceCloud = createEntityCloud({
|
|
141
|
+
db: sourceDb, userId: 'user-1', deviceId: 'source-device', content: sourceContent,
|
|
142
|
+
});
|
|
143
|
+
const sourceRegistry = createRegistry({ db: sourceDb, ephemeral: true, emit: () => {}, journal: sourceCloud.journal });
|
|
144
|
+
const root = sourceRegistry.enrollRoot({ type: 'workspace', name: 'project' });
|
|
145
|
+
const file = sourceRegistry.create({
|
|
146
|
+
type: 'file.text', parentUUID: root.uuid, name: 'readme.md', payloadVersion: firstHash,
|
|
147
|
+
});
|
|
148
|
+
sourceContent.captureFile({ filePath: path.join(sourceDir, 'readme.md'), contentHash: firstHash });
|
|
149
|
+
const initialParts = uploadedParts(sourceContent, sourceCloud.identity().resourceId, firstHash);
|
|
150
|
+
const snapshot = snapshotFromRecords(sourceRegistry.syncRecords());
|
|
151
|
+
const snapshotBytes = Buffer.from(JSON.stringify(snapshot), 'utf8');
|
|
152
|
+
|
|
153
|
+
const target = createRegistrationService({
|
|
154
|
+
dir: targetState,
|
|
155
|
+
homeDir: path.join(rootDir, 'target-home'),
|
|
156
|
+
cloudUserId: 'user-1',
|
|
157
|
+
cloudDeviceId: 'target-device',
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
target.entityCloud.install({
|
|
161
|
+
registry: target.registration.registry,
|
|
162
|
+
resourceId: sourceCloud.identity().resourceId,
|
|
163
|
+
authorityEpoch: 7,
|
|
164
|
+
headVersion: 41,
|
|
165
|
+
snapshotBase64: snapshotBytes.toString('base64'),
|
|
166
|
+
snapshotChecksum: sha256(snapshotBytes),
|
|
167
|
+
});
|
|
168
|
+
for (const part of [...initialParts.filter((part) => part.kind === 'manifest'), ...initialParts.filter((part) => part.kind === 'chunk')]) {
|
|
169
|
+
target.entityCloud.receiveDownload({
|
|
170
|
+
resourceId: sourceCloud.identity().resourceId,
|
|
171
|
+
contentHash: firstHash,
|
|
172
|
+
kind: part.kind,
|
|
173
|
+
partIndex: part.kind === 'manifest' ? 0 : part.partIndex,
|
|
174
|
+
sha256: part.sha256,
|
|
175
|
+
dataBase64: part.dataBase64,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
assert.equal((await target.add({ uuid: root.uuid, targetPath: targetParent })).state, 'complete');
|
|
179
|
+
|
|
180
|
+
const secondBytes = Buffer.from('second authoritative content\n');
|
|
181
|
+
const secondHash = sha256(secondBytes);
|
|
182
|
+
fs.writeFileSync(path.join(sourceDir, 'readme.md'), secondBytes);
|
|
183
|
+
sourceContent.captureFile({ filePath: path.join(sourceDir, 'readme.md'), contentHash: secondHash });
|
|
184
|
+
const secondParts = uploadedParts(sourceContent, sourceCloud.identity().resourceId, secondHash);
|
|
185
|
+
const changed = sourceRegistry.attest(file.uuid, { type: 'file.text', payloadVersion: secondHash });
|
|
186
|
+
const remoteRecord = sourceRegistry.syncRecord(changed.uuid);
|
|
187
|
+
|
|
188
|
+
const ingested = target.ingestCloudMutation({
|
|
189
|
+
envelope: {
|
|
190
|
+
resourceId: sourceCloud.identity().resourceId,
|
|
191
|
+
mutationId: 'ent_remote_file_change',
|
|
192
|
+
contract: 'amalgm-entity-registry@1',
|
|
193
|
+
schemaVersion: 1,
|
|
194
|
+
operationKind: 'entity.upsert',
|
|
195
|
+
operation: { kind: 'entity.upsert', record: remoteRecord },
|
|
196
|
+
},
|
|
197
|
+
version: 42,
|
|
198
|
+
committedAt: '2026-08-05T00:00:00.000Z',
|
|
199
|
+
});
|
|
200
|
+
assert.equal(ingested.materializations.length, 1, 'the completed add became a live pending application');
|
|
201
|
+
assert.equal(fs.readFileSync(path.join(targetParent, 'project', 'readme.md'), 'utf8'), firstBytes.toString(),
|
|
202
|
+
'the destination waits for verified immutable bytes');
|
|
203
|
+
|
|
204
|
+
for (const part of [...secondParts.filter((part) => part.kind === 'manifest'), ...secondParts.filter((part) => part.kind === 'chunk')]) {
|
|
205
|
+
target.receiveCloudContent({
|
|
206
|
+
resourceId: sourceCloud.identity().resourceId,
|
|
207
|
+
contentHash: secondHash,
|
|
208
|
+
kind: part.kind,
|
|
209
|
+
partIndex: part.kind === 'manifest' ? 0 : part.partIndex,
|
|
210
|
+
sha256: part.sha256,
|
|
211
|
+
dataBase64: part.dataBase64,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
assert.deepEqual(fs.readFileSync(path.join(targetParent, 'project', 'readme.md')), secondBytes);
|
|
215
|
+
assert.equal(target.registration.registry.syncRecord(file.uuid).payloadVersion, secondHash);
|
|
216
|
+
assert.equal(target.observer.status().find((entry) => entry.path === fs.realpathSync(path.join(targetParent, 'project')))?.watching, true);
|
|
217
|
+
|
|
218
|
+
const targetDb = new Database(path.join(targetState, 'identity.db'), { readonly: true });
|
|
219
|
+
try {
|
|
220
|
+
assert.equal(targetDb.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 0,
|
|
221
|
+
'the receiving watcher did not echo the imported cloud edit as a new local mutation');
|
|
222
|
+
} finally {
|
|
223
|
+
targetDb.close();
|
|
224
|
+
}
|
|
225
|
+
} finally {
|
|
226
|
+
target.close();
|
|
227
|
+
sourceDb.close();
|
|
228
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('add recreates an actual Git repository from verified Repocard cargo, then arms its local watcher', async () => {
|
|
233
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-add-repo-materialization-'));
|
|
234
|
+
const sourceState = path.join(rootDir, 'source-state');
|
|
235
|
+
const targetState = path.join(rootDir, 'target-state');
|
|
236
|
+
const sourceRepo = path.join(rootDir, 'source-repo');
|
|
237
|
+
const targetParent = path.join(rootDir, 'checkout');
|
|
238
|
+
fs.mkdirSync(sourceRepo, { recursive: true });
|
|
239
|
+
fs.mkdirSync(targetParent, { recursive: true });
|
|
240
|
+
git(sourceRepo, ['init', '--quiet']);
|
|
241
|
+
git(sourceRepo, ['config', 'user.email', 'test@example.com']);
|
|
242
|
+
git(sourceRepo, ['config', 'user.name', 'Amalgm Test']);
|
|
243
|
+
fs.writeFileSync(path.join(sourceRepo, 'tracked.md'), '# committed\n');
|
|
244
|
+
git(sourceRepo, ['add', 'tracked.md']);
|
|
245
|
+
git(sourceRepo, ['commit', '--quiet', '-m', 'initial']);
|
|
246
|
+
fs.writeFileSync(path.join(sourceRepo, 'tracked.md'), '# changed locally\n');
|
|
247
|
+
fs.writeFileSync(path.join(sourceRepo, 'untracked.txt'), 'not committed\n');
|
|
248
|
+
|
|
249
|
+
const source = createRegistrationService({
|
|
250
|
+
dir: sourceState, homeDir: path.join(rootDir, 'source-home'),
|
|
251
|
+
cloudUserId: 'user-1', cloudDeviceId: 'source-device',
|
|
252
|
+
});
|
|
253
|
+
const target = createRegistrationService({
|
|
254
|
+
dir: targetState, homeDir: path.join(rootDir, 'target-home'),
|
|
255
|
+
cloudUserId: 'user-1', cloudDeviceId: 'target-device',
|
|
256
|
+
});
|
|
257
|
+
try {
|
|
258
|
+
const bootstrap = source.entityCloud.bootstrap(source.registration.registry);
|
|
259
|
+
source.entityCloud.install({
|
|
260
|
+
registry: source.registration.registry, resourceId: bootstrap.resourceId,
|
|
261
|
+
authorityEpoch: 1, headVersion: 0, snapshotBase64: bootstrap.snapshotBase64,
|
|
262
|
+
snapshotChecksum: bootstrap.snapshotChecksum,
|
|
263
|
+
});
|
|
264
|
+
const registered = await source.register(sourceRepo);
|
|
265
|
+
const sourceDb = new Database(path.join(sourceState, 'identity.db'), { readonly: true });
|
|
266
|
+
const operations = sourceDb.prepare('SELECT operation_json FROM entity_cloud_mutations ORDER BY sequence').all()
|
|
267
|
+
.map((row) => JSON.parse(row.operation_json));
|
|
268
|
+
sourceDb.close();
|
|
269
|
+
const repoCloudRecord = operations.map((operation) => operation.record)
|
|
270
|
+
.find((record) => record?.uuid === registered.record.uuid);
|
|
271
|
+
assert.ok(repoCloudRecord?.transportVersion, 'the repo entity names immutable Repocard transport cargo');
|
|
272
|
+
const records = source.registration.registry.syncRecords().map((record) => (
|
|
273
|
+
record.uuid === registered.record.uuid ? repoCloudRecord : record
|
|
274
|
+
));
|
|
275
|
+
const snapshot = snapshotFromRecords(records);
|
|
276
|
+
const snapshotBytes = Buffer.from(JSON.stringify(snapshot), 'utf8');
|
|
277
|
+
target.entityCloud.install({
|
|
278
|
+
registry: target.registration.registry, resourceId: bootstrap.resourceId,
|
|
279
|
+
authorityEpoch: 1, headVersion: 1, snapshotBase64: snapshotBytes.toString('base64'),
|
|
280
|
+
snapshotChecksum: sha256(snapshotBytes),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const heads = new Set(records.flatMap((record) => {
|
|
284
|
+
if (['file.text', 'file.binary'].includes(record.type) && record.payloadVersion) return [record.payloadVersion];
|
|
285
|
+
if (record.type === 'repo.git' && record.transportVersion) return [record.transportVersion];
|
|
286
|
+
return [];
|
|
287
|
+
}));
|
|
288
|
+
for (const head of heads) {
|
|
289
|
+
const parts = uploadedParts(source.entityContent, bootstrap.resourceId, head);
|
|
290
|
+
for (const part of [...parts.filter((entry) => entry.kind === 'manifest'), ...parts.filter((entry) => entry.kind === 'chunk')]) {
|
|
291
|
+
target.receiveCloudContent({
|
|
292
|
+
resourceId: bootstrap.resourceId, contentHash: head, kind: part.kind,
|
|
293
|
+
partIndex: part.kind === 'manifest' ? 0 : part.partIndex,
|
|
294
|
+
sha256: part.sha256, dataBase64: part.dataBase64,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const job = await target.add({ uuid: registered.record.uuid, targetPath: targetParent });
|
|
300
|
+
assert.equal(job.state, 'complete', job.error);
|
|
301
|
+
const destination = path.join(targetParent, path.basename(sourceRepo));
|
|
302
|
+
const expected = source.registration.repoState(registered.record.uuid);
|
|
303
|
+
assert.equal(verify(destination, expected).ok, true, 'the downloaded Git state reproduces its Card + Checkpoint IDs');
|
|
304
|
+
assert.equal(target.registration.registry.syncRecord(registered.record.uuid).uuid, registered.record.uuid);
|
|
305
|
+
assert.equal(target.observer.status().find((entry) => entry.path === fs.realpathSync(destination))?.watching, true,
|
|
306
|
+
'the same successful add starts the destination repository watcher');
|
|
307
|
+
|
|
308
|
+
// A Git-only change exercises the ongoing Add binding without also
|
|
309
|
+
// changing child-file heads: the repo entity must remain a live source
|
|
310
|
+
// of truth after its first materialization, and the receiver must not
|
|
311
|
+
// echo its own applied state back into the cloud outbox.
|
|
312
|
+
git(sourceRepo, ['tag', 'amalgm-live-proof']);
|
|
313
|
+
const changedState = capture(sourceRepo, { withBundle: true });
|
|
314
|
+
const changedTransport = source.entityContent.captureBytes({ bytes: transportBytes(changedState) });
|
|
315
|
+
const changedLocalState = localState(changedState, changedTransport.contentHash);
|
|
316
|
+
let changedRecord;
|
|
317
|
+
source.registration.registry.transaction(() => {
|
|
318
|
+
source.registration.repoStates.put(registered.record.uuid, changedLocalState);
|
|
319
|
+
changedRecord = source.registration.registry.attest(registered.record.uuid, {
|
|
320
|
+
type: 'repo.git', payloadVersion: changedState.stateId,
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
const latest = new Database(path.join(sourceState, 'identity.db'), { readonly: true });
|
|
324
|
+
const remoteRecord = JSON.parse(latest.prepare(`
|
|
325
|
+
SELECT operation_json FROM entity_cloud_mutations
|
|
326
|
+
WHERE json_extract(operation_json, '$.record.uuid') = ?
|
|
327
|
+
ORDER BY sequence DESC LIMIT 1
|
|
328
|
+
`).get(registered.record.uuid).operation_json).record;
|
|
329
|
+
latest.close();
|
|
330
|
+
assert.equal(remoteRecord.transportVersion, changedTransport.contentHash);
|
|
331
|
+
const ingested = target.ingestCloudMutation({
|
|
332
|
+
envelope: {
|
|
333
|
+
resourceId: bootstrap.resourceId, mutationId: 'ent_remote_repo_change',
|
|
334
|
+
contract: 'amalgm-entity-registry@1', schemaVersion: 1,
|
|
335
|
+
operationKind: 'entity.upsert', operation: { kind: 'entity.upsert', record: remoteRecord },
|
|
336
|
+
},
|
|
337
|
+
version: 2, committedAt: '2026-08-05T00:00:00.000Z',
|
|
338
|
+
});
|
|
339
|
+
assert.equal(ingested.materializations.length, 1, 'the finished repository add becomes a live pending application');
|
|
340
|
+
const changedParts = uploadedParts(source.entityContent, bootstrap.resourceId, changedTransport.contentHash);
|
|
341
|
+
for (const part of [...changedParts.filter((entry) => entry.kind === 'manifest'), ...changedParts.filter((entry) => entry.kind === 'chunk')]) {
|
|
342
|
+
target.receiveCloudContent({
|
|
343
|
+
resourceId: bootstrap.resourceId, contentHash: changedTransport.contentHash,
|
|
344
|
+
kind: part.kind, partIndex: part.kind === 'manifest' ? 0 : part.partIndex,
|
|
345
|
+
sha256: part.sha256, dataBase64: part.dataBase64,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
assert.equal(verify(destination, changedLocalState).ok, true,
|
|
349
|
+
'a later cloud repo state re-materializes exactly on the existing checkout');
|
|
350
|
+
const targetDb = new Database(path.join(targetState, 'identity.db'), { readonly: true });
|
|
351
|
+
assert.equal(targetDb.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 0,
|
|
352
|
+
'the receiver never echoes its downloaded Git state upstream');
|
|
353
|
+
targetDb.close();
|
|
354
|
+
} finally {
|
|
355
|
+
source.close();
|
|
356
|
+
target.close();
|
|
357
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
358
|
+
}
|
|
359
|
+
});
|
|
@@ -303,6 +303,43 @@ test('a signed-in register call writes every entity delivery intent in identity.
|
|
|
303
303
|
}
|
|
304
304
|
});
|
|
305
305
|
|
|
306
|
+
test('a large signed-in register call publishes one complete cloud graph before Watch begins', async () => {
|
|
307
|
+
const service = createRegistrationService({
|
|
308
|
+
dir: path.join(tempRoot, 'state-cloud-register-large'),
|
|
309
|
+
homeDir: path.join(tempRoot, 'cloud-user-home-large'),
|
|
310
|
+
cloudUserId: 'user-cloud-register-large',
|
|
311
|
+
cloudDeviceId: 'computer-cloud-register-large',
|
|
312
|
+
});
|
|
313
|
+
const bootstrap = service.entityCloud.bootstrap(service.registration.registry);
|
|
314
|
+
service.entityCloud.install({
|
|
315
|
+
registry: service.registration.registry,
|
|
316
|
+
resourceId: bootstrap.resourceId,
|
|
317
|
+
authorityEpoch: 1,
|
|
318
|
+
headVersion: 0,
|
|
319
|
+
snapshotBase64: bootstrap.snapshotBase64,
|
|
320
|
+
snapshotChecksum: bootstrap.snapshotChecksum,
|
|
321
|
+
});
|
|
322
|
+
const ground = makeGround('cloud-register-large');
|
|
323
|
+
for (let index = 0; index < 500; index += 1) fs.mkdirSync(path.join(ground, `folder-${index}`));
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const registered = await service.register(ground);
|
|
327
|
+
const db = new Database(path.join(service.dir, 'identity.db'), { readonly: true });
|
|
328
|
+
const rows = db.prepare('SELECT operation_json, mutation_state FROM entity_cloud_mutations ORDER BY sequence').all();
|
|
329
|
+
db.close();
|
|
330
|
+
assert.equal(rows.length, 1, 'the cloud never sees a partially registered large tree');
|
|
331
|
+
const operation = JSON.parse(rows[0].operation_json);
|
|
332
|
+
assert.equal(operation.kind, 'entity.snapshot.replace');
|
|
333
|
+
assert.equal(operation.snapshot.records.length, 501, 'the root and every folder share one authoritative graph');
|
|
334
|
+
assert.equal(rows[0].mutation_state, 'pending');
|
|
335
|
+
assert.equal(service.observer.status().some((root) => (
|
|
336
|
+
root.path === registered.rootPath && root.watching && !root.degraded
|
|
337
|
+
)), true, 'the same registered root begins local watching');
|
|
338
|
+
} finally {
|
|
339
|
+
service.close();
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
306
343
|
test('register registers the named ground and every file beneath it', async () => {
|
|
307
344
|
const service = makeService('add');
|
|
308
345
|
const ground = makeGround('add');
|