amalgm 0.1.246 → 0.1.247
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/README.md +5 -3
- package/lib/cli.js +92 -4
- package/lib/shared-realtime-tunnel.js +82 -19
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/automations/deliveries.js +276 -0
- package/runtime/scripts/amalgm-mcp/automations/rest.js +51 -0
- package/runtime/scripts/amalgm-mcp/automations/scheduler.js +11 -67
- package/runtime/scripts/amalgm-mcp/automations/store.js +67 -20
- package/runtime/scripts/amalgm-mcp/events/ingress.js +69 -215
- package/runtime/scripts/amalgm-mcp/events/processor.js +189 -0
- package/runtime/scripts/amalgm-mcp/lib/email-embeds.js +41 -0
- package/runtime/scripts/amalgm-mcp/lib/email-md.js +107 -1
- package/runtime/scripts/amalgm-mcp/lib/generated/email-render.json +10 -2
- package/runtime/scripts/amalgm-mcp/observer/README.md +8 -6
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +262 -22
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +86 -0
- package/runtime/scripts/amalgm-mcp/registration/index.js +4 -1
- package/runtime/scripts/amalgm-mcp/registration/service.js +102 -0
- package/runtime/scripts/amalgm-mcp/registration/tree.js +19 -5
- package/runtime/scripts/amalgm-mcp/registry/index.js +16 -1
- package/runtime/scripts/amalgm-mcp/registry/store.js +58 -7
- package/runtime/scripts/amalgm-mcp/server/routes/automations.js +8 -0
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +56 -0
- package/runtime/scripts/amalgm-mcp/server/routes/workspace.js +14 -0
- package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +2 -2
- package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/citations.html +5 -5
- package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/links.html +3 -3
- package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/notification-shell.html +1 -1
- package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/share-invite.html +13 -0
- package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/text.html +1 -1
- package/runtime/scripts/amalgm-mcp/tests/email-render.test.js +17 -1
- package/runtime/scripts/amalgm-mcp/tests/entity-cloud.test.js +189 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-content.test.js +126 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-materialization.test.js +118 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +126 -11
- package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +20 -6
- package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +8 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +8 -1
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +630 -102
- package/runtime/scripts/amalgm-mcp/tests/registration.test.js +45 -10
- package/runtime/scripts/amalgm-mcp/tests/workspace-materialize.test.js +54 -0
- package/runtime/scripts/amalgm-mcp/workspace/materialize.js +292 -0
- package/runtime/scripts/chat-core/contract.js +21 -3
- package/runtime/scripts/chat-core/engine.js +8 -0
- package/runtime/scripts/chat-core/server.js +11 -1
- package/runtime/scripts/chat-core/tests/workspace-preflight.test.js +116 -0
- package/runtime/scripts/chat-core/workspace-preflight.js +89 -0
- package/runtime/scripts/local-gateway.js +0 -1
|
@@ -90,23 +90,64 @@ CREATE TABLE IF NOT EXISTS entity_cloud_mutations (
|
|
|
90
90
|
);
|
|
91
91
|
CREATE INDEX IF NOT EXISTS entity_cloud_mutations_pending
|
|
92
92
|
ON entity_cloud_mutations (resource_id, mutation_state, sequence);
|
|
93
|
+
CREATE TABLE IF NOT EXISTS entity_cloud_catalog_state (
|
|
94
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
95
|
+
resource_id TEXT NOT NULL,
|
|
96
|
+
authority_epoch INTEGER NOT NULL CHECK (authority_epoch >= 1),
|
|
97
|
+
snapshot_version INTEGER NOT NULL CHECK (snapshot_version >= 0),
|
|
98
|
+
updated_at TEXT NOT NULL
|
|
99
|
+
);
|
|
100
|
+
CREATE TABLE IF NOT EXISTS entity_cloud_catalog (
|
|
101
|
+
resource_id TEXT NOT NULL,
|
|
102
|
+
entity_uuid TEXT NOT NULL,
|
|
103
|
+
record_json TEXT NOT NULL,
|
|
104
|
+
snapshot_epoch INTEGER NOT NULL CHECK (snapshot_epoch >= 1),
|
|
105
|
+
snapshot_version INTEGER NOT NULL CHECK (snapshot_version >= 0),
|
|
106
|
+
updated_at TEXT NOT NULL,
|
|
107
|
+
PRIMARY KEY (resource_id, entity_uuid)
|
|
108
|
+
);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS entity_cloud_catalog_snapshot
|
|
110
|
+
ON entity_cloud_catalog (resource_id, snapshot_epoch, snapshot_version);
|
|
111
|
+
CREATE TABLE IF NOT EXISTS entity_materializations (
|
|
112
|
+
job_id TEXT PRIMARY KEY,
|
|
113
|
+
resource_id TEXT NOT NULL,
|
|
114
|
+
entity_uuid TEXT NOT NULL,
|
|
115
|
+
target_path TEXT NOT NULL,
|
|
116
|
+
authority_epoch INTEGER NOT NULL CHECK (authority_epoch >= 1),
|
|
117
|
+
head_version INTEGER NOT NULL CHECK (head_version >= 0),
|
|
118
|
+
state TEXT NOT NULL CHECK (state IN ('waiting_for_content', 'materializing', 'complete', 'failed')),
|
|
119
|
+
error TEXT,
|
|
120
|
+
created_at TEXT NOT NULL,
|
|
121
|
+
updated_at TEXT NOT NULL
|
|
122
|
+
);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS entity_materializations_pending
|
|
124
|
+
ON entity_materializations (resource_id, state, created_at);
|
|
93
125
|
`;
|
|
94
126
|
|
|
95
127
|
const CORE_TABLES = ['registry_meta', 'entities', 'channel_bindings', 'contested_ground'];
|
|
96
128
|
const REPO_STATE_TABLES = [...CORE_TABLES, 'repo_states'];
|
|
97
|
-
const
|
|
129
|
+
const CLOUD_TABLES = ['entity_cloud_replica', 'entity_cloud_mutations'];
|
|
130
|
+
const MATERIALIZATION_TABLES = ['entity_cloud_catalog_state', 'entity_cloud_catalog', 'entity_materializations'];
|
|
131
|
+
const TABLES = [...REPO_STATE_TABLES, ...CLOUD_TABLES, ...MATERIALIZATION_TABLES];
|
|
98
132
|
|
|
99
133
|
// The schema generation this code speaks, stamped as PRAGMA user_version at
|
|
100
134
|
// creation — completeness is judged against a declared shape, never
|
|
101
135
|
// guessed. Generation 3 adds the current Card + Checkpoint document for
|
|
102
136
|
// each repo.git entity. Generation 4 adds the entity-cloud replica and its
|
|
103
137
|
// durable outbox to that SAME identity database: a local entity mutation and
|
|
104
|
-
// the cloud delivery intent must commit together.
|
|
138
|
+
// the cloud delivery intent must commit together. Generation 5 adds the
|
|
139
|
+
// cloud-only catalog and durable materialization jobs: remote graph facts may
|
|
140
|
+
// be known locally before any observer is bound, so they cannot masquerade
|
|
141
|
+
// as local registered ground. Generation 6 records the one snapshot version
|
|
142
|
+
// that populated that catalog; a later mutation tail cannot pose as a fresh
|
|
143
|
+
// active head for `amalgm add`. Generations 2 and 3 are
|
|
105
144
|
// additive predecessors: migrations only add tables beside existing identity
|
|
106
145
|
// records, never rewrite UUIDs or evidence bindings. The per-tree era never
|
|
107
146
|
// stamped a version and reads 0 — an older generation, refused distinctly
|
|
108
147
|
// from loss until an explicit migration exists.
|
|
109
|
-
const GENERATION =
|
|
148
|
+
const GENERATION = 6;
|
|
149
|
+
const MATERIALIZATION_GENERATION = 5;
|
|
150
|
+
const CLOUD_GENERATION = 4;
|
|
110
151
|
const REPO_STATE_GENERATION = 3;
|
|
111
152
|
const PREVIOUS_GENERATION = 2;
|
|
112
153
|
|
|
@@ -132,15 +173,25 @@ function inspect(db) {
|
|
|
132
173
|
|
|
133
174
|
function isMigratableGenerationShape(db, inspection) {
|
|
134
175
|
const generation = db.pragma('user_version', { simple: true });
|
|
176
|
+
if (generation === MATERIALIZATION_GENERATION) {
|
|
177
|
+
return inspection.missing.length === 1
|
|
178
|
+
&& inspection.missing[0] === 'entity_cloud_catalog_state'
|
|
179
|
+
&& TABLES.filter((name) => name !== 'entity_cloud_catalog_state').every((name) => !inspection.missing.includes(name));
|
|
180
|
+
}
|
|
181
|
+
if (generation === CLOUD_GENERATION) {
|
|
182
|
+
return inspection.missing.length === MATERIALIZATION_TABLES.length
|
|
183
|
+
&& MATERIALIZATION_TABLES.every((name) => inspection.missing.includes(name))
|
|
184
|
+
&& [...REPO_STATE_TABLES, ...CLOUD_TABLES].every((name) => !inspection.missing.includes(name));
|
|
185
|
+
}
|
|
135
186
|
if (generation === PREVIOUS_GENERATION) {
|
|
136
|
-
return inspection.missing.length ===
|
|
137
|
-
&& ['repo_states',
|
|
187
|
+
return inspection.missing.length === ['repo_states', ...CLOUD_TABLES, ...MATERIALIZATION_TABLES].length
|
|
188
|
+
&& ['repo_states', ...CLOUD_TABLES, ...MATERIALIZATION_TABLES]
|
|
138
189
|
.every((name) => inspection.missing.includes(name))
|
|
139
190
|
&& CORE_TABLES.every((name) => !inspection.missing.includes(name));
|
|
140
191
|
}
|
|
141
192
|
return generation === REPO_STATE_GENERATION
|
|
142
|
-
&& inspection.missing.length ===
|
|
143
|
-
&& [
|
|
193
|
+
&& inspection.missing.length === [...CLOUD_TABLES, ...MATERIALIZATION_TABLES].length
|
|
194
|
+
&& [...CLOUD_TABLES, ...MATERIALIZATION_TABLES]
|
|
144
195
|
.every((name) => inspection.missing.includes(name))
|
|
145
196
|
&& REPO_STATE_TABLES.every((name) => !inspection.missing.includes(name));
|
|
146
197
|
}
|
|
@@ -31,6 +31,14 @@ async function handleAutomationRoutes(ctx) {
|
|
|
31
31
|
await automationsRest.handleRuns(await ctx.readJsonBody(), ctx.sendJson);
|
|
32
32
|
return true;
|
|
33
33
|
}
|
|
34
|
+
if (ctx.pathname === '/automations/deliveries' && ctx.method === 'POST') {
|
|
35
|
+
await automationsRest.handleDeliveries(await ctx.readJsonBody(), ctx.sendJson);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (ctx.pathname === '/automations/deliveries/retry' && ctx.method === 'POST') {
|
|
39
|
+
await automationsRest.handleDeliveryRetry(await ctx.readJsonBody(), ctx.sendJson);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
34
42
|
if (ctx.pathname === '/automations/validate' && ctx.method === 'POST') {
|
|
35
43
|
await automationsRest.handleValidate(await ctx.readJsonBody(), ctx.sendJson);
|
|
36
44
|
return true;
|
|
@@ -211,6 +211,37 @@ async function handleEntityRoutes(ctx) {
|
|
|
211
211
|
return true;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
// Cloud-to-local content is claimed by the existing realtime tunnel. A
|
|
215
|
+
// materialization job never opens R2 itself; it asks the authenticated
|
|
216
|
+
// gateway for the exact immutable manifest/chunk named by its pinned
|
|
217
|
+
// snapshot, then retries safely until its local content cache verifies it.
|
|
218
|
+
if (ctx.pathname === '/entities/cloud/content/downloads/claim' && ctx.method === 'POST') {
|
|
219
|
+
const service = registrationService();
|
|
220
|
+
service.advanceMaterializations();
|
|
221
|
+
ctx.sendJson(200, { download: service.entityCloud.claimDownload() });
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (ctx.pathname === '/entities/cloud/content/downloads/receive' && ctx.method === 'POST') {
|
|
226
|
+
const body = await ctx.readJsonBody();
|
|
227
|
+
const service = registrationService();
|
|
228
|
+
try {
|
|
229
|
+
const received = service.entityCloud.receiveDownload({
|
|
230
|
+
resourceId: body?.resourceId,
|
|
231
|
+
contentHash: body?.contentHash,
|
|
232
|
+
kind: body?.kind,
|
|
233
|
+
partIndex: body?.partIndex,
|
|
234
|
+
sha256: body?.sha256,
|
|
235
|
+
dataBase64: body?.dataBase64,
|
|
236
|
+
});
|
|
237
|
+
service.advanceMaterializations();
|
|
238
|
+
ctx.sendJson(200, { ...received });
|
|
239
|
+
} catch (error) {
|
|
240
|
+
ctx.sendJson(400, { error: error.message });
|
|
241
|
+
}
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
214
245
|
if (ctx.pathname === '/entities/cloud/mutations/ack' && ctx.method === 'POST') {
|
|
215
246
|
const body = await ctx.readJsonBody();
|
|
216
247
|
try {
|
|
@@ -257,6 +288,31 @@ async function handleEntityRoutes(ctx) {
|
|
|
257
288
|
return true;
|
|
258
289
|
}
|
|
259
290
|
|
|
291
|
+
if (ctx.pathname === '/entities/add' && ctx.method === 'POST') {
|
|
292
|
+
const body = await ctx.readJsonBody();
|
|
293
|
+
const service = registrationService();
|
|
294
|
+
try {
|
|
295
|
+
const job = await service.add({ uuid: body?.uuid, targetPath: body?.path });
|
|
296
|
+
ctx.sendJson(job.state === 'complete' ? 200 : 202, job);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
ctx.sendJson(409, { error: error.message });
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const materializationMatch = /^\/entities\/add\/(mat_[A-Za-z0-9_-]+)$/.exec(ctx.pathname);
|
|
304
|
+
if (materializationMatch && ctx.method === 'GET') {
|
|
305
|
+
const service = registrationService();
|
|
306
|
+
service.advanceMaterializations();
|
|
307
|
+
const job = service.entityCloud.materializationStatus(materializationMatch[1]);
|
|
308
|
+
if (!job) {
|
|
309
|
+
ctx.sendJson(404, { error: 'cloud materialization does not exist' });
|
|
310
|
+
} else {
|
|
311
|
+
ctx.sendJson(200, job);
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
260
316
|
if (ctx.pathname !== '/entities/register' || ctx.method !== 'POST') return false;
|
|
261
317
|
|
|
262
318
|
const body = await ctx.readJsonBody();
|
|
@@ -87,6 +87,20 @@ async function handleWorkspaceRoutes(ctx) {
|
|
|
87
87
|
await workspaceRest.handleCloudWorkspaceAttachment(await ctx.readJsonBody(), ctx.sendJson);
|
|
88
88
|
return true;
|
|
89
89
|
}
|
|
90
|
+
if (ctx.pathname === '/workspace/materialize' && ctx.method === 'POST') {
|
|
91
|
+
try {
|
|
92
|
+
const result = await require('../../workspace/materialize').materializeWorkspace(
|
|
93
|
+
await ctx.readJsonBody(),
|
|
94
|
+
);
|
|
95
|
+
ctx.sendJson(200, result);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
ctx.sendJson(Number(error?.statusCode) || 500, {
|
|
98
|
+
error: error instanceof Error ? error.message : 'Workspace materialization failed',
|
|
99
|
+
code: error?.code || 'workspace_materialization_failed',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
90
104
|
if (ctx.pathname === '/workspace/access' && ctx.method === 'GET') {
|
|
91
105
|
workspaceRest.handleAccessRead(ctx.getQuery(), ctx.sendJson);
|
|
92
106
|
return true;
|
|
@@ -231,7 +231,7 @@ test('a membership that cannot be read completely is unsettled — never quietly
|
|
|
231
231
|
});
|
|
232
232
|
|
|
233
233
|
test('STITCH: production detection and the adapter speak one truth — no parallel detector', () => {
|
|
234
|
-
// The observer's content decision (
|
|
234
|
+
// The observer's content decision (safeContent) now reads through the
|
|
235
235
|
// adapter socket. Prove agreement on real ground: the hash the
|
|
236
236
|
// production observer records for a file is byte-identical to the
|
|
237
237
|
// truth the adapter answers for the same ground.
|
|
@@ -42,7 +42,7 @@ async function live(lay, run) {
|
|
|
42
42
|
try {
|
|
43
43
|
const dir = makeDir('live');
|
|
44
44
|
lay(dir);
|
|
45
|
-
const { observer, events } = makeObserver({ settleMs: 25 });
|
|
45
|
+
const { observer, events } = makeObserver({ settleMs: 25, prove: fake.prove });
|
|
46
46
|
const root = observer.enrollRoot(dir);
|
|
47
47
|
observer.start();
|
|
48
48
|
try {
|
|
@@ -174,7 +174,7 @@ test('entrance — the single-file root\'s parent doorbell: the parent hears wha
|
|
|
174
174
|
const holder = makeDir('single');
|
|
175
175
|
const file = path.join(holder, 'solo.md');
|
|
176
176
|
fs.writeFileSync(file, 'v1');
|
|
177
|
-
const { observer, events } = makeObserver({ settleMs: 25 });
|
|
177
|
+
const { observer, events } = makeObserver({ settleMs: 25, prove: fake.prove });
|
|
178
178
|
const root = observer.enrollRoot(file);
|
|
179
179
|
assert.equal(root.kind, 'file', 'the smallest root: one channel');
|
|
180
180
|
observer.start();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<p style="margin:0;line-height:1.75;color:#ffffff">Inline file citation: <span title="Project notes" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align
|
|
2
|
-
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Every icon kind: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align
|
|
3
|
-
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Special filenames: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align
|
|
4
|
-
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Folder: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align
|
|
5
|
-
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Unknown extension falls back: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align
|
|
1
|
+
<p style="margin:0;line-height:1.75;color:#ffffff">Inline file citation: <span title="Project notes" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-path-markdown-a8a29e" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>notes.md</span> mid-sentence.</p>
|
|
2
|
+
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Every icon kind: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-react" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>app.tsx</span> letter-react, <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-letter-ts-3178c6-ffffff" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>main.ts</span> letter, <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-path-markdown-a8a29e" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>readme.md</span> path, <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-braces-facc15" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>data.json</span> lucide, <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-filetext-f87171" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>report.pdf</span> lucide-color.</p></div>
|
|
3
|
+
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Special filenames: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-package-facc15" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>package.json</span> and <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-lock-facc15" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>.env.local</span> and <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-package-38bdf8" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>Dockerfile</span>.</p></div>
|
|
4
|
+
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Folder: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-folder-d6d3d1" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>src</span>, skill: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-box-d6d3d1" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>pdf-tools</span>, agent: <span title="Research agent" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-atsign-ffffff" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>@researcher</span>.</p></div>
|
|
5
|
+
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Unknown extension falls back: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-file-a8a29e" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>blob.xyz</span>.</p></div>
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
<p style="margin:0;line-height:1.75;color:#ffffff">Web citation: read <a href="https://www.anthropic.com/news" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align
|
|
2
|
-
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Bare-ish label: <a href="https://example.com/" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align
|
|
3
|
-
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">A link with <strong style="color:#ffffff;font-weight:600">bold</strong> around it: <strong style="color:#ffffff;font-weight:600">see <a href="https://docs.example.dev/guide" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align
|
|
1
|
+
<p style="margin:0;line-height:1.75;color:#ffffff">Web citation: read <a href="https://www.anthropic.com/news" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="https://www.google.com/s2/favicons?domain=www.anthropic.com&sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>Anthropic</a> for details.</p>
|
|
2
|
+
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Bare-ish label: <a href="https://example.com/" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="https://www.google.com/s2/favicons?domain=example.com&sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>example.com</a> and a deep link <a href="https://amalgm.ai/tab?kind=tasks&id=run-1" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="https://www.google.com/s2/favicons?domain=amalgm.ai&sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>View run</a>.</p></div>
|
|
3
|
+
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">A link with <strong style="color:#ffffff;font-weight:600">bold</strong> around it: <strong style="color:#ffffff;font-weight:600">see <a href="https://docs.example.dev/guide" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="https://www.google.com/s2/favicons?domain=docs.example.dev&sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>docs</a></strong> now.</p></div>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<body style="margin:0;padding:0;background:#000000;font-family:"SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont, system-ui, sans-serif">
|
|
4
4
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#000000"><tr><td align="center" style="padding:0">
|
|
5
5
|
<table role="presentation" width="520" cellpadding="0" cellspacing="0" style="max-width:520px;width:100%"><tr><td style="padding:40px 28px 48px">
|
|
6
|
-
<div style="font-size:14px;color:#ffffff"><p style="margin:0;line-height:1.75;color:#ffffff">Run finished. See <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align
|
|
6
|
+
<div style="font-size:14px;color:#ffffff"><p style="margin:0;line-height:1.75;color:#ffffff">Run finished. See <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-path-markdown-a8a29e" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>notes.md</span>.</p></div>
|
|
7
7
|
<p style="margin:24px 0 0"><a href="https://amalgm.ai/tab?kind=tasks&id=run-1" style="color:#60a5fa;text-decoration:none;font-size:14px">View details →</a></p>
|
|
8
8
|
<p style="margin:48px 0 0;font-size:12px;color:#ffffff">amalgm.ai</p>
|
|
9
9
|
</td></tr></table>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
|
3
|
+
<body style="margin:0;padding:0;background:#000000;font-family:'SF Pro Text', 'SF Pro Display', -apple-system, BlinkMacSystemFont, system-ui, sans-serif">
|
|
4
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#000000"><tr><td align="center" style="padding:0">
|
|
5
|
+
<table role="presentation" width="520" cellpadding="0" cellspacing="0" style="max-width:520px;width:100%;background:#000000;border:1px solid #292524;border-radius:8px;border-collapse:separate;overflow:hidden"><tr><td style="padding:40px 28px 48px">
|
|
6
|
+
<div style="font-size:14px;color:#ffffff"><h2 style="margin:0;font-size:16px;font-weight:400;color:#ffffff;line-height:1.35"><strong style="font-weight:650">Aayush Trivedi</strong> shared a file with you</h2>
|
|
7
|
+
<div style="margin:15px 0 0"><a href="https://amalgm.ai/s/s1i_abcdefghijklmnopqrstuvwxyzABCDEFGH123456789" target="_blank" style="text-decoration:none;display:block;background:#000000;border:1px solid #1c1917;border-radius:12px;padding:0;margin:2px 0"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse"><tr><td style="padding:8px 0 8px 12px;vertical-align:middle;line-height:0;width:14px"><img src="cid:amalgm-icon-lucide-filetext-f87171" alt="" width="14" height="14" style="display:inline-block;width:14px;height:14px;border:0"></td><td style="padding:8px 12px 8px 10px;vertical-align:middle"><span style="font-size:14px;line-height:18px;color:#ffffff;font-weight:650;white-space:nowrap">Q3 launch brief.pdf</span></td><td align="right" style="padding:8px 12px 8px 0;vertical-align:middle;line-height:0;width:14px"><img src="cid:amalgm-icon-lucide-externallink-ffffff" alt="" width="14" height="14" style="display:inline-block;width:14px;height:14px;border:0"></td></tr></table></a></div>
|
|
8
|
+
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">You can <strong style="color:#ffffff;font-weight:600">edit</strong> this file.</p></div>
|
|
9
|
+
<div style="margin:15px 0 0"><blockquote style="margin:0;line-height:1.75;font-style:italic;padding:0 0 0 16px;border-left:3px solid #292524;color:#d6d3d1">sam@acme.com — sign in with this address to accept.</blockquote></div></div>
|
|
10
|
+
<p style="margin:48px 0 0;font-size:12px;color:#ffffff">amalgm.ai</p>
|
|
11
|
+
</td></tr></table>
|
|
12
|
+
</td></tr></table>
|
|
13
|
+
</body></html>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div style="margin:15px 0 0"><h2 style="margin:0;font-size:16px;font-weight:650;color:#ffffff;line-height:1.35">Heading two</h2></div>
|
|
3
3
|
<div style="margin:15px 0 0"><h3 style="margin:0;font-size:15px;font-weight:650;color:#ffffff;line-height:1.35">Heading three</h3></div>
|
|
4
4
|
<div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Body text with <strong style="color:#ffffff;font-weight:600">bold</strong>, <em>italic</em>, <strong style="color:#ffffff;font-weight:600"><em>bold italic</em></strong>, and <code style="background:#0a0a0a;border:1px solid #1c1917;padding:1.875px 7.5px;border-radius:5.625px;font-size:12px;line-height:1.2;font-family:'JetBrains Mono', 'SFMono-Regular', Menlo, monospace;color:#f5f5f4">inline code</code>.</p></div>
|
|
5
|
-
<div style="margin:15px 0 0"><ul style="margin:0;padding-left:24px;color:#ffffff"><li style="margin:4px 0;padding:3.75px 0;line-height:1.5;color:#ffffff">bullet one</li><li style="margin:4px 0;padding:3.75px 0;line-height:1.5;color:#ffffff">bullet two with <a href="https://example.com/a" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align
|
|
5
|
+
<div style="margin:15px 0 0"><ul style="margin:0;padding-left:24px;color:#ffffff"><li style="margin:4px 0;padding:3.75px 0;line-height:1.5;color:#ffffff">bullet one</li><li style="margin:4px 0;padding:3.75px 0;line-height:1.5;color:#ffffff">bullet two with <a href="https://example.com/a" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="https://www.google.com/s2/favicons?domain=example.com&sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>a link</a></li></ul></div>
|
|
6
6
|
<div style="margin:15px 0 0"><ol style="margin:0;padding-left:24px;color:#ffffff"><li style="margin:4px 0;padding:3.75px 0;line-height:1.5;color:#ffffff">ordered one</li><li style="margin:4px 0;padding:3.75px 0;line-height:1.5;color:#ffffff">ordered two</li></ol></div>
|
|
7
7
|
<div style="margin:15px 0 0"><blockquote style="margin:0;line-height:1.75;font-style:italic;padding:0 0 0 16px;border-left:3px solid #292524;color:#d6d3d1">A quote line continues here.</blockquote></div>
|
|
8
8
|
<div style="margin:15px 0 0"><div style="border:1px solid #292524;border-radius:12px;overflow:hidden;margin:0"><table style="width:100%;border-collapse:collapse"><thead><tr><th style="padding:5.625px 11.25px;text-align:left;vertical-align:top;font-size:14px;line-height:1.5;border-right:1px solid #292524;border-bottom:1px solid #292524;background:#0c0b0a;font-weight:500;color:#ffffff">Metric</th><th style="padding:5.625px 11.25px;text-align:left;vertical-align:top;font-size:14px;line-height:1.5;border-bottom:1px solid #292524;background:#0c0b0a;font-weight:500;color:#ffffff">Value</th></tr></thead><tbody><tr><td style="padding:5.625px 11.25px;text-align:left;vertical-align:top;font-size:14px;line-height:1.5;border-right:1px solid #292524;border-bottom:1px solid #292524;color:#ffffff">Sessions</td><td style="padding:5.625px 11.25px;text-align:left;vertical-align:top;font-size:14px;line-height:1.5;border-bottom:1px solid #292524;color:#ffffff">42</td></tr><tr><td style="padding:5.625px 11.25px;text-align:left;vertical-align:top;font-size:14px;line-height:1.5;border-right:1px solid #292524;color:#ffffff">Errors</td><td style="padding:5.625px 11.25px;text-align:left;vertical-align:top;font-size:14px;line-height:1.5;color:#ffffff">0</td></tr></tbody></table></div></div>
|
|
@@ -17,7 +17,11 @@ const fs = require('node:fs');
|
|
|
17
17
|
const path = require('node:path');
|
|
18
18
|
const test = require('node:test');
|
|
19
19
|
|
|
20
|
-
const {
|
|
20
|
+
const {
|
|
21
|
+
markdownToEmailHtml,
|
|
22
|
+
formatNotificationEmail,
|
|
23
|
+
formatShareInviteEmail,
|
|
24
|
+
} = require('../lib/email-md');
|
|
21
25
|
const { prepareEmailIconAttachments } = require('../lib/email-icons');
|
|
22
26
|
|
|
23
27
|
const FIXTURE_DIR = path.join(__dirname, 'email-render-fixtures');
|
|
@@ -56,6 +60,18 @@ test('email render golden: notification shell', () => {
|
|
|
56
60
|
assertGolden('notification-shell.html', `${html}\n`);
|
|
57
61
|
});
|
|
58
62
|
|
|
63
|
+
test('email render golden: share invite shell', () => {
|
|
64
|
+
const html = formatShareInviteEmail({
|
|
65
|
+
sharerName: 'Aayush Trivedi',
|
|
66
|
+
recipientEmail: 'sam@acme.com',
|
|
67
|
+
entityName: 'Q3 launch brief.pdf',
|
|
68
|
+
entityType: 'file',
|
|
69
|
+
inviteUrl: 'https://amalgm.ai/s/s1i_abcdefghijklmnopqrstuvwxyzABCDEFGH123456789',
|
|
70
|
+
access: 'edit',
|
|
71
|
+
});
|
|
72
|
+
assertGolden('share-invite.html', `${html}\n`);
|
|
73
|
+
});
|
|
74
|
+
|
|
59
75
|
test('every cid icon referenced by goldens has a PNG asset', () => {
|
|
60
76
|
const htmls = fs
|
|
61
77
|
.readdirSync(GOLDEN_DIR)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const Database = require('better-sqlite3');
|
|
7
|
+
const { createRegistry } = require('../registry');
|
|
8
|
+
const { createEntityCloud, parseSnapshot, snapshotFromRecords } = require('../registration/entity-cloud');
|
|
9
|
+
const { createEntityContent } = require('../registration/entity-content');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
test('the entity cloud snapshot establishes identity, then each later entity commit has one durable ordered outbox row', () => {
|
|
15
|
+
const db = new Database(':memory:');
|
|
16
|
+
const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1' });
|
|
17
|
+
const registry = createRegistry({
|
|
18
|
+
db,
|
|
19
|
+
ephemeral: true,
|
|
20
|
+
emit: () => {},
|
|
21
|
+
journal: cloud.journal,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const root = registry.enrollRoot({ type: 'workspace', name: 'ground' });
|
|
25
|
+
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 0,
|
|
26
|
+
'initial bootstrap identity is carried by the first immutable snapshot, not an unscoped tail');
|
|
27
|
+
|
|
28
|
+
const bootstrap = cloud.bootstrap(registry);
|
|
29
|
+
const snapshot = parseSnapshot(Buffer.from(bootstrap.snapshotBase64, 'base64'));
|
|
30
|
+
assert.deepEqual(snapshot.records.map((record) => record.uuid), [root.uuid]);
|
|
31
|
+
cloud.install({
|
|
32
|
+
registry,
|
|
33
|
+
resourceId: bootstrap.resourceId,
|
|
34
|
+
authorityEpoch: 1,
|
|
35
|
+
headVersion: 0,
|
|
36
|
+
snapshotBase64: bootstrap.snapshotBase64,
|
|
37
|
+
snapshotChecksum: bootstrap.snapshotChecksum,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const first = registry.create({ type: 'file.text', parentUUID: root.uuid, name: 'one.md', payloadVersion: 'head-one' });
|
|
41
|
+
const second = registry.create({ type: 'file.text', parentUUID: root.uuid, name: 'two.md', payloadVersion: 'head-two' });
|
|
42
|
+
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 2);
|
|
43
|
+
|
|
44
|
+
const firstClaim = cloud.claim();
|
|
45
|
+
assert.equal(firstClaim.length, 1, 'one resource emits one operation at a time so base versions cannot race');
|
|
46
|
+
assert.equal(firstClaim[0].baseVersion, 0);
|
|
47
|
+
assert.equal(firstClaim[0].operation.record.uuid, first.uuid);
|
|
48
|
+
cloud.acknowledge({
|
|
49
|
+
resourceId: firstClaim[0].resourceId,
|
|
50
|
+
mutationId: firstClaim[0].mutationId,
|
|
51
|
+
version: 1,
|
|
52
|
+
committedAt: '2026-08-04T00:00:00.000Z',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const secondClaim = cloud.claim();
|
|
56
|
+
assert.equal(secondClaim.length, 1);
|
|
57
|
+
assert.equal(secondClaim[0].baseVersion, 1, 'the next head starts from the acknowledged cloud version');
|
|
58
|
+
assert.equal(secondClaim[0].operation.record.uuid, second.uuid);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('a signed-in registration is refused until the cloud entity resource is active', () => {
|
|
62
|
+
const db = new Database(':memory:');
|
|
63
|
+
const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1' });
|
|
64
|
+
const registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
|
|
65
|
+
assert.throws(() => cloud.assertRegistrationReady(), /entity cloud registry is not ready/);
|
|
66
|
+
registry.enrollRoot({ type: 'workspace', name: 'bootstrap-only' });
|
|
67
|
+
const bootstrap = cloud.bootstrap(registry);
|
|
68
|
+
cloud.install({
|
|
69
|
+
registry,
|
|
70
|
+
resourceId: bootstrap.resourceId,
|
|
71
|
+
authorityEpoch: 1,
|
|
72
|
+
headVersion: 0,
|
|
73
|
+
snapshotBase64: bootstrap.snapshotBase64,
|
|
74
|
+
snapshotChecksum: bootstrap.snapshotChecksum,
|
|
75
|
+
});
|
|
76
|
+
assert.doesNotThrow(() => cloud.assertRegistrationReady());
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a compacted remote snapshot enters the cloud catalog, not locally watched identity, and add claims its immutable content', () => {
|
|
80
|
+
const sourceDb = new Database(':memory:');
|
|
81
|
+
const sourceCloud = createEntityCloud({ db: sourceDb, userId: 'user-1', deviceId: 'source-device' });
|
|
82
|
+
const sourceRegistry = createRegistry({ db: sourceDb, ephemeral: true, emit: () => {}, journal: sourceCloud.journal });
|
|
83
|
+
const root = sourceRegistry.enrollRoot({ type: 'workspace', name: 'project' });
|
|
84
|
+
const contentHash = 'b'.repeat(64);
|
|
85
|
+
const file = sourceRegistry.create({
|
|
86
|
+
type: 'file.text', parentUUID: root.uuid, name: 'readme.md', payloadVersion: contentHash,
|
|
87
|
+
});
|
|
88
|
+
const snapshot = snapshotFromRecords(sourceRegistry.syncRecords());
|
|
89
|
+
const bytes = Buffer.from(JSON.stringify(snapshot), 'utf8');
|
|
90
|
+
|
|
91
|
+
const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-entity-catalog-'));
|
|
92
|
+
const targetDb = new Database(':memory:');
|
|
93
|
+
const targetContent = createEntityContent({ dir: targetDir });
|
|
94
|
+
const targetCloud = createEntityCloud({
|
|
95
|
+
db: targetDb, userId: 'user-1', deviceId: 'target-device', content: targetContent,
|
|
96
|
+
});
|
|
97
|
+
const targetRegistry = createRegistry({ db: targetDb, ephemeral: true, emit: () => {}, journal: targetCloud.journal });
|
|
98
|
+
const resourceId = sourceCloud.identity().resourceId;
|
|
99
|
+
targetCloud.install({
|
|
100
|
+
registry: targetRegistry,
|
|
101
|
+
resourceId,
|
|
102
|
+
authorityEpoch: 3,
|
|
103
|
+
headVersion: 12,
|
|
104
|
+
snapshotBase64: bytes.toString('base64'),
|
|
105
|
+
snapshotChecksum: require('crypto').createHash('sha256').update(bytes).digest('hex'),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
assert.deepEqual(targetRegistry.syncRecords(), [], 'a remote graph does not pretend to be watched local ground');
|
|
109
|
+
const job = targetCloud.startMaterialization({ uuid: root.uuid, targetPath: targetDir });
|
|
110
|
+
assert.equal(job.state, 'waiting_for_content');
|
|
111
|
+
assert.deepEqual(targetCloud.catalogTree({ resourceId, uuid: root.uuid }).map((record) => record.uuid), [root.uuid, file.uuid]);
|
|
112
|
+
assert.deepEqual(targetCloud.claimDownload(), {
|
|
113
|
+
resourceId,
|
|
114
|
+
contentHash,
|
|
115
|
+
kind: 'manifest',
|
|
116
|
+
partIndex: 0,
|
|
117
|
+
sha256: null,
|
|
118
|
+
});
|
|
119
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('add accepts only a catalog installed by the active compacted snapshot, never a snapshot plus a mutation tail', () => {
|
|
123
|
+
const sourceDb = new Database(':memory:');
|
|
124
|
+
const sourceCloud = createEntityCloud({ db: sourceDb, userId: 'user-1', deviceId: 'source-device' });
|
|
125
|
+
const sourceRegistry = createRegistry({ db: sourceDb, ephemeral: true, emit: () => {}, journal: sourceCloud.journal });
|
|
126
|
+
const root = sourceRegistry.enrollRoot({ type: 'workspace', name: 'project' });
|
|
127
|
+
const file = sourceRegistry.create({
|
|
128
|
+
type: 'file.text', parentUUID: root.uuid, name: 'readme.md', payloadVersion: 'b'.repeat(64),
|
|
129
|
+
});
|
|
130
|
+
const initialSnapshot = snapshotFromRecords(sourceRegistry.syncRecords());
|
|
131
|
+
const initialBytes = Buffer.from(JSON.stringify(initialSnapshot), 'utf8');
|
|
132
|
+
|
|
133
|
+
const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-active-head-'));
|
|
134
|
+
const targetDb = new Database(':memory:');
|
|
135
|
+
const targetCloud = createEntityCloud({ db: targetDb, userId: 'user-1', deviceId: 'target-device' });
|
|
136
|
+
const targetRegistry = createRegistry({ db: targetDb, ephemeral: true, emit: () => {}, journal: targetCloud.journal });
|
|
137
|
+
const resourceId = sourceCloud.identity().resourceId;
|
|
138
|
+
try {
|
|
139
|
+
targetCloud.install({
|
|
140
|
+
registry: targetRegistry,
|
|
141
|
+
resourceId,
|
|
142
|
+
authorityEpoch: 3,
|
|
143
|
+
headVersion: 12,
|
|
144
|
+
snapshotBase64: initialBytes.toString('base64'),
|
|
145
|
+
snapshotChecksum: require('crypto').createHash('sha256').update(initialBytes).digest('hex'),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const cloudFile = initialSnapshot.records.find((record) => record.uuid === file.uuid);
|
|
149
|
+
const changed = { ...cloudFile, name: 'renamed.md', version: 'c'.repeat(64) };
|
|
150
|
+
targetCloud.ingest({
|
|
151
|
+
registry: targetRegistry,
|
|
152
|
+
envelope: {
|
|
153
|
+
resourceId,
|
|
154
|
+
mutationId: 'ent_remote_change',
|
|
155
|
+
contract: 'amalgm-entity-registry@1',
|
|
156
|
+
schemaVersion: 1,
|
|
157
|
+
operationKind: 'entity.upsert',
|
|
158
|
+
operation: { kind: 'entity.upsert', record: changed },
|
|
159
|
+
},
|
|
160
|
+
version: 13,
|
|
161
|
+
committedAt: '2026-08-04T00:00:00.000Z',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
assert.throws(
|
|
165
|
+
() => targetCloud.startMaterialization({ uuid: root.uuid, targetPath: targetDir }),
|
|
166
|
+
/catalog is not pinned to the active cloud head/,
|
|
167
|
+
'the mutation updated live catalog facts, but not the snapshot barrier',
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const cloudRoot = initialSnapshot.records.find((record) => record.uuid === root.uuid);
|
|
171
|
+
const compactedBytes = Buffer.from(JSON.stringify(snapshotFromRecords([cloudRoot, changed])), 'utf8');
|
|
172
|
+
targetCloud.install({
|
|
173
|
+
registry: targetRegistry,
|
|
174
|
+
resourceId,
|
|
175
|
+
authorityEpoch: 3,
|
|
176
|
+
headVersion: 13,
|
|
177
|
+
snapshotBase64: compactedBytes.toString('base64'),
|
|
178
|
+
snapshotChecksum: require('crypto').createHash('sha256').update(compactedBytes).digest('hex'),
|
|
179
|
+
});
|
|
180
|
+
assert.equal(
|
|
181
|
+
targetCloud.startMaterialization({ uuid: root.uuid, targetPath: targetDir }).headVersion,
|
|
182
|
+
13,
|
|
183
|
+
'the fresh compacted snapshot—not a history replay—makes add available again',
|
|
184
|
+
);
|
|
185
|
+
} finally {
|
|
186
|
+
sourceDb.close();
|
|
187
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
});
|