@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.1",
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": "^3.3.0",
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
@@ -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` (modules/celilo-mgmt/scripts/discovery.ts) mints a new
618
- // `celilo-fleet` key under the state dir when none exists, and the deploy
619
- // then sets `ssh.public_key` to it. In a real fleet that is right. In the
620
- // rig it is not survivable: every target container trusts the HARNESS key,
621
- // baked into authorized_keys at boot, and a freshly minted key is trusted
622
- // by nothing. celilo would hold a key no machine accepts, and the next
623
- // `machine add` fails with "no matching private key was found in ~/.ssh/"
624
- // — which reads as a missing fixture and is actually a rotated key.
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,