@celilo/e2e 0.19.1 → 0.19.3
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/e2e",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.3",
|
|
4
4
|
"description": "E2E test infrastructure for Celilo-deployed applications. Provides a simulated internet with DNS hierarchy, ACME server, firewalls, and target machines in Docker.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"README.md"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@celilo/capabilities": "^
|
|
40
|
+
"@celilo/capabilities": "^4.1.0",
|
|
41
41
|
"@celilo/cli-display": "^0.2.0",
|
|
42
42
|
"@celilo/event-bus": "^0.6.0",
|
|
43
43
|
"@celilo/terraform-fake": "^0.3.1",
|
|
@@ -275,6 +275,44 @@ describe('GET /api/v1/modules (search)', () => {
|
|
|
275
275
|
expect(body.modules.find((m) => m.name === 'caddy')?.icon).toBe('\u25cd');
|
|
276
276
|
});
|
|
277
277
|
|
|
278
|
+
test('409 when a second module declares a glyph the first holds, naming the holder', async () => {
|
|
279
|
+
// module-icons D8 — the registry refuses a duplicate glyph. The error must
|
|
280
|
+
// name the holding module: that is everything the author needs to pick a
|
|
281
|
+
// distinct glyph, and it is what the design decided a refusal carries
|
|
282
|
+
// instead of a server-suggested replacement.
|
|
283
|
+
expect((await publish('homebridge', '1.0.0+1', 'valid-token', 'Bridge', '\u22c8')).status).toBe(
|
|
284
|
+
200,
|
|
285
|
+
);
|
|
286
|
+
const res = await publish('caddy', '1.0.0+1', 'valid-token', 'Proxy', '\u22c8');
|
|
287
|
+
expect(res.status).toBe(409);
|
|
288
|
+
const body = (await res.json()) as { errors: Array<{ detail: string }> };
|
|
289
|
+
expect(body.errors[0]?.detail).toContain('homebridge');
|
|
290
|
+
// The refusal fired before any state was written: no payload, no index line.
|
|
291
|
+
expect((await fetch(`${baseUrl}/index/ca/dd/caddy`)).status).toBe(404);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('a module republishing its own glyph across versions is allowed', async () => {
|
|
295
|
+
// The holder is excluded from its own check — keeping your icon across
|
|
296
|
+
// versions is the normal case, not a collision.
|
|
297
|
+
expect((await publish('homebridge', '1.0.0+1', 'valid-token', 'Bridge', '\u22c8')).status).toBe(
|
|
298
|
+
200,
|
|
299
|
+
);
|
|
300
|
+
expect((await publish('homebridge', '1.0.1+1', 'valid-token', 'Bridge', '\u22c8')).status).toBe(
|
|
301
|
+
200,
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('a module whose latest version drops the icon frees the glyph', async () => {
|
|
306
|
+
// A module holds the glyph its LATEST non-yanked entry declares — the same
|
|
307
|
+
// selection the browse endpoints read. Once its newest version declares
|
|
308
|
+
// none, the glyph is free for the next publisher.
|
|
309
|
+
expect((await publish('homebridge', '1.0.0+1', 'valid-token', 'Bridge', '\u22c8')).status).toBe(
|
|
310
|
+
200,
|
|
311
|
+
);
|
|
312
|
+
expect((await publish('homebridge', '1.0.1+1', 'valid-token', 'Bridge')).status).toBe(200);
|
|
313
|
+
expect((await publish('caddy', '1.0.0+1', 'valid-token', 'Proxy', '\u22c8')).status).toBe(200);
|
|
314
|
+
});
|
|
315
|
+
|
|
278
316
|
test('total_downloads reflects actual download count, sort=downloads orders by it', async () => {
|
|
279
317
|
// Publish two modules, hit one's download endpoint several times.
|
|
280
318
|
// The search response should show those counts and sort=downloads
|
|
@@ -339,6 +339,23 @@ export function startServer(options: ServerOptions): ReturnType<typeof Bun.serve
|
|
|
339
339
|
return err(`Version ${name}@${vers} already exists — versions are immutable`, 409);
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
// The registry refuses a glyph another module already holds (module-icons
|
|
343
|
+
// D8, operator ruling 2026-09-01). The module's own earlier versions are
|
|
344
|
+
// excluded: republishing with the icon you already have is the normal
|
|
345
|
+
// case, not a collision. Before any mutation — a refusal must not leave a
|
|
346
|
+
// stored payload behind, the same ordering discipline as the check above.
|
|
347
|
+
if (icon) {
|
|
348
|
+
const holder = [...storage.latestIcons()].find(
|
|
349
|
+
([heldName, heldIcon]) => heldName !== name && heldIcon === icon,
|
|
350
|
+
);
|
|
351
|
+
if (holder) {
|
|
352
|
+
return err(
|
|
353
|
+
`icon '${icon}' is already used by module ${holder[0]} — declare a glyph no other module holds (openspec/changes/module-icons, D8)`,
|
|
354
|
+
409,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
342
359
|
const cksum = storage.storePackage(name, vers, fileData);
|
|
343
360
|
storage.appendIndex({
|
|
344
361
|
name,
|
|
@@ -189,6 +189,25 @@ export class RegistryStorage {
|
|
|
189
189
|
return versions.length > 0 ? { name, versions } : null;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/**
|
|
193
|
+
* The glyph each module holds, keyed by module name. A module holds the
|
|
194
|
+
* icon on its latest non-yanked entry — the same "latest" the browse
|
|
195
|
+
* endpoints read, so a module holds exactly the glyph a consumer would
|
|
196
|
+
* render for it. Modules whose latest entry declares no icon are absent.
|
|
197
|
+
*
|
|
198
|
+
* This is the set the publish-time duplicate-icon refusal compares against
|
|
199
|
+
* (openspec/changes/module-icons, D8): a new publish declaring a glyph in
|
|
200
|
+
* this map, under a different name, is rejected.
|
|
201
|
+
*/
|
|
202
|
+
latestIcons(): Map<string, string> {
|
|
203
|
+
const held = new Map<string, string>();
|
|
204
|
+
for (const { name, versions } of this.listModules()) {
|
|
205
|
+
const latest = versions.filter((v) => !v.yanked).at(-1) ?? versions.at(-1);
|
|
206
|
+
if (latest?.icon) held.set(name, latest.icon);
|
|
207
|
+
}
|
|
208
|
+
return held;
|
|
209
|
+
}
|
|
210
|
+
|
|
192
211
|
/**
|
|
193
212
|
* Per-module download counter. Stored as a single integer in
|
|
194
213
|
* `<dataDir>/downloads/<name>`. Read-modify-write, NOT atomic
|
package/src/container-manager.ts
CHANGED
|
@@ -614,20 +614,26 @@ function buildNetworkHandle(
|
|
|
614
614
|
);
|
|
615
615
|
// Give the box its fleet key BEFORE deploying, using the harness keypair.
|
|
616
616
|
//
|
|
617
|
-
// `ensureFleetKey` (
|
|
618
|
-
// `celilo-fleet`
|
|
619
|
-
//
|
|
620
|
-
// rig it is not survivable: every target
|
|
621
|
-
// baked into authorized_keys at boot,
|
|
622
|
-
// by nothing. celilo would hold a key
|
|
623
|
-
// `machine add` fails with "no matching
|
|
624
|
-
// — which reads as a missing fixture and
|
|
617
|
+
// `ensureFleetKey` (apps/celilo/src/services/fleet-key.ts, reached by the
|
|
618
|
+
// deploy through `celilo system ensure-fleet-key`) mints a new
|
|
619
|
+
// `celilo-fleet` key when none exists and records `ssh.public_key`. In a
|
|
620
|
+
// real fleet that is right. In the rig it is not survivable: every target
|
|
621
|
+
// container trusts the HARNESS key, baked into authorized_keys at boot,
|
|
622
|
+
// and a freshly minted key is trusted by nothing. celilo would hold a key
|
|
623
|
+
// no machine accepts, and the next `machine add` fails with "no matching
|
|
624
|
+
// private key was found in ~/.ssh/" — which reads as a missing fixture and
|
|
625
|
+
// is actually a rotated key.
|
|
625
626
|
//
|
|
626
627
|
// `ensureFleetKey` is idempotent and REUSES an existing key, so seeding it
|
|
627
628
|
// with the harness pair means the deploy runs its real code path and
|
|
628
629
|
// arrives at the key the fleet already trusts. This models an operator
|
|
629
630
|
// whose management box already has its fleet key, which is the ordinary
|
|
630
631
|
// case after the first deploy.
|
|
632
|
+
//
|
|
633
|
+
// The directory below must match `getFleetSshDir()`, which is
|
|
634
|
+
// `dirname(getDbPath())/.ssh`. The `db_path` set just above is what makes
|
|
635
|
+
// the two agree — the framework reads it from the env `on_install`
|
|
636
|
+
// exports before shelling out.
|
|
631
637
|
const stateSshDir = '/root/.local/share/celilo/.ssh';
|
|
632
638
|
dockerExec(
|
|
633
639
|
projectName,
|