@celilo/e2e 0.10.4 → 0.11.1

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.
Files changed (68) hide show
  1. package/README.md +37 -28
  2. package/bin/e2e-status +1 -1
  3. package/bin/e2e-up +14 -14
  4. package/config/dhcp/dnsmasq.conf +3 -3
  5. package/config/resolver/unbound.conf +2 -2
  6. package/config/routing/fw-ext-routes.sh +4 -4
  7. package/config/routing/fw-isp-routes.sh +3 -3
  8. package/config/routing/fw-main-routes.sh +15 -4
  9. package/config/routing/management-routes.sh +11 -11
  10. package/config/routing/observer-setup.sh +3 -3
  11. package/config/routing/resolver-internal-routes.sh +5 -5
  12. package/config/routing/target-routes.sh +1 -1
  13. package/config/routing/target-setup.sh +18 -16
  14. package/config/socks/startup.sh +3 -3
  15. package/docker/Dockerfile.target-machine +15 -1
  16. package/docker/Dockerfile.target-machine-docker +6 -0
  17. package/docker/Dockerfile.target-technitium +6 -0
  18. package/npm-registry-server/src/packument.test.ts +2 -5
  19. package/npm-registry-server/src/packument.ts +3 -3
  20. package/npm-registry-server/src/server.ts +1 -1
  21. package/package.json +3 -3
  22. package/registry-server/src/bootstrap.ts +1 -1
  23. package/registry-server/src/introspection.test.ts +8 -12
  24. package/registry-server/src/server.test.ts +2 -2
  25. package/registry-server/src/server.ts +1 -1
  26. package/registry-server/src/storage.test.ts +8 -4
  27. package/registry-server/src/storage.ts +1 -1
  28. package/registry-server/src/validation.ts +2 -4
  29. package/scripts/pack-celilo-packages.ts +1 -4
  30. package/scripts/stage-apt-repo.ts +1 -1
  31. package/scripts/stage-libsignal.ts +1 -3
  32. package/simulators/greenwave/server.ts +4 -2
  33. package/simulators/greenwave/state.ts +24 -17
  34. package/simulators/isitup/server.ts +1 -5
  35. package/simulators/namecheap-ddns/server.ts +2 -4
  36. package/simulators/namecheap-ddns/zone-updater.test.ts +3 -1
  37. package/simulators/namecheap-ddns/zone-updater.ts +6 -10
  38. package/simulators/signal-cli/server.ts +3 -1
  39. package/src/address-plan.test.ts +95 -0
  40. package/src/browser.ts +3 -3
  41. package/src/bus-events.ts +2 -2
  42. package/src/cli/command-tree-parser.ts +8 -2
  43. package/src/cli/completion.ts +5 -8
  44. package/src/cli/index.ts +9 -2
  45. package/src/cli/module-discovery.ts +3 -1
  46. package/src/cli/scaffold.ts +2 -2
  47. package/src/cli-version-contract.ts +5 -6
  48. package/src/container-manager.ts +25 -8
  49. package/src/docker-compose-generator.ts +67 -39
  50. package/src/extract-failure.ts +92 -0
  51. package/src/fixtures.ts +10 -17
  52. package/src/index.ts +2 -2
  53. package/src/network-builder.ts +14 -9
  54. package/src/observer.test.ts +50 -19
  55. package/src/observer.ts +15 -8
  56. package/src/registry-bundle.ts +1 -3
  57. package/src/run-lock.test.ts +2 -2
  58. package/src/run-lock.ts +19 -5
  59. package/src/runner.ts +9 -67
  60. package/src/shared-infra.ts +50 -22
  61. package/src/socks-proxy.ts +16 -19
  62. package/src/types.ts +67 -17
  63. package/src/vantage-helpers.ts +6 -2
  64. package/src/vantage.test.ts +94 -40
  65. package/src/vantage.ts +24 -17
  66. package/src/wait-for-run.ts +1 -1
  67. package/src/zone-classifier.test.ts +32 -21
  68. package/src/zone-classifier.ts +7 -6
@@ -1,10 +1,6 @@
1
1
  import { afterEach, describe, expect, test } from 'bun:test';
2
2
  import { ADMIN_SCOPE } from './auth';
3
- import {
4
- IntrospectionVerifier,
5
- claimToScope,
6
- introspectionConfigFromEnv,
7
- } from './introspection';
3
+ import { IntrospectionVerifier, claimToScope, introspectionConfigFromEnv } from './introspection';
8
4
 
9
5
  const CONFIG = {
10
6
  endpoint: 'https://idp.test/application/o/introspect/',
@@ -88,9 +84,7 @@ describe('IntrospectionVerifier.scopeOf', () => {
88
84
  const verifier = new IntrospectionVerifier(CONFIG);
89
85
 
90
86
  test('active admin-group token → ADMIN_SCOPE', async () => {
91
- stubFetch(() =>
92
- introspectResponse({ active: true, sub: 'alice', groups: ['celilo-admins'] }),
93
- );
87
+ stubFetch(() => introspectResponse({ active: true, sub: 'alice', groups: ['celilo-admins'] }));
94
88
  expect(await verifier.scopeOf('Bearer idp-token')).toBe(ADMIN_SCOPE);
95
89
  });
96
90
 
@@ -106,9 +100,7 @@ describe('IntrospectionVerifier.scopeOf', () => {
106
100
 
107
101
  test('expired exp → null even if active', async () => {
108
102
  const pastSec = Math.floor(Date.now() / 1000) - 60;
109
- stubFetch(() =>
110
- introspectResponse({ active: true, groups: ['celilo-admins'], exp: pastSec }),
111
- );
103
+ stubFetch(() => introspectResponse({ active: true, groups: ['celilo-admins'], exp: pastSec }));
112
104
  expect(await verifier.scopeOf('idp-token')).toBeNull();
113
105
  });
114
106
 
@@ -238,7 +230,11 @@ describe('IntrospectionVerifier.identify (ce-1ch)', () => {
238
230
  });
239
231
 
240
232
  test('admin group is configurable, not a literal', async () => {
241
- const custom = new IntrospectionVerifier({ ...CONFIG, adminGroup: 'ops', publisherGroup: 'devs' });
233
+ const custom = new IntrospectionVerifier({
234
+ ...CONFIG,
235
+ adminGroup: 'ops',
236
+ publisherGroup: 'devs',
237
+ });
242
238
  stubFetch(() => introspectResponse({ active: true, sub: 'dan', groups: ['ops'] }));
243
239
  const id = await custom.identify('Bearer t');
244
240
  expect(id?.isAdmin).toBe(true);
@@ -714,7 +714,7 @@ describe('module-owner authorization (ce-1ch)', () => {
714
714
  const res = await fetch(`${url}/api/v1/modules/owners/caddy`, {
715
715
  headers: { Authorization: 'Bearer opaque-admin' },
716
716
  });
717
- expect((await res.json() as { owner: { ownerSub: string } }).owner.ownerSub).toBe('ops');
717
+ expect(((await res.json()) as { owner: { ownerSub: string } }).owner.ownerSub).toBe('ops');
718
718
  });
719
719
 
720
720
  test('admin reassign lets a new owner publish the name', async () => {
@@ -741,7 +741,7 @@ describe('module-owner authorization (ce-1ch)', () => {
741
741
  headers: { Authorization: 'Bearer opaque-admin' },
742
742
  });
743
743
  expect(ok.status).toBe(200);
744
- expect((await ok.json() as { owners: unknown[] }).owners).toEqual([]);
744
+ expect(((await ok.json()) as { owners: unknown[] }).owners).toEqual([]);
745
745
  });
746
746
 
747
747
  test('owner reassign requires admin (a plain publisher cannot)', async () => {
@@ -21,7 +21,6 @@
21
21
 
22
22
  import { join } from 'node:path';
23
23
  import { ADMIN_SCOPE, TokenAuth } from './auth';
24
- import { IntrospectionVerifier } from './introspection';
25
24
  import {
26
25
  type BootstrapEntry,
27
26
  bootstrapIndexEntry,
@@ -29,6 +28,7 @@ import {
29
28
  scanBootstrapDir,
30
29
  scanUploadsDir,
31
30
  } from './bootstrap';
31
+ import { IntrospectionVerifier } from './introspection';
32
32
  import { landingResponse } from './landing';
33
33
  import { ModuleOwnerStore, fileModuleOwnerPersistence } from './module-owner-store';
34
34
  import { type RateLimiter, clientIp, createRateLimiter } from './rate-limit';
@@ -37,9 +37,7 @@ describe('RegistryStorage.indexPath', () => {
37
37
  });
38
38
 
39
39
  test('long name uses first 4 chars for directories', () => {
40
- expect(storage.indexPath('homebridge')).toBe(
41
- join(dataDir, 'index', 'ho', 'me', 'homebridge'),
42
- );
40
+ expect(storage.indexPath('homebridge')).toBe(join(dataDir, 'index', 'ho', 'me', 'homebridge'));
43
41
  });
44
42
 
45
43
  test('real module names produce expected paths', () => {
@@ -128,7 +126,13 @@ describe('RegistryStorage listModules / getModule', () => {
128
126
 
129
127
  test('listModules returns stored modules with their index entries', () => {
130
128
  storage.storePackage('homebridge', '1.0.0+1', Buffer.from('a'));
131
- storage.appendIndex({ name: 'homebridge', vers: '1.0.0+1', deps: [], cksum: 'x', yanked: false });
129
+ storage.appendIndex({
130
+ name: 'homebridge',
131
+ vers: '1.0.0+1',
132
+ deps: [],
133
+ cksum: 'x',
134
+ yanked: false,
135
+ });
132
136
  const list = storage.listModules();
133
137
  expect(list).toHaveLength(1);
134
138
  expect(list[0].name).toBe('homebridge');
@@ -7,8 +7,8 @@
7
7
  * downloads/{name} — per-module download counter (single integer)
8
8
  */
9
9
 
10
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
11
10
  import { createHash } from 'node:crypto';
11
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
12
12
  import { dirname, join } from 'node:path';
13
13
 
14
14
  export interface IndexEntry {
@@ -44,16 +44,14 @@ export function validateNameAndVersion(
44
44
  return {
45
45
  ok: false,
46
46
  field: 'name',
47
- message:
48
- 'Invalid module name — must be kebab-case lowercase, 1-128 chars (e.g. "my-module")',
47
+ message: 'Invalid module name — must be kebab-case lowercase, 1-128 chars (e.g. "my-module")',
49
48
  };
50
49
  }
51
50
  if (!isValidVersion(vers)) {
52
51
  return {
53
52
  ok: false,
54
53
  field: 'vers',
55
- message:
56
- 'Invalid version — must match "<major>.<minor>.<patch>+<pkgrev>" (e.g. "1.4.2+1")',
54
+ message: 'Invalid version — must match "<major>.<minor>.<patch>+<pkgrev>" (e.g. "1.4.2+1")',
57
55
  };
58
56
  }
59
57
  return { ok: true };
@@ -127,10 +127,7 @@ export function rewriteWorkspacePins(
127
127
  const version = versions.get(name);
128
128
  if (!version) {
129
129
  throw new Error(
130
- `Cannot rewrite ${bucket} "${name}": "${spec}" — ${name} is not a known ` +
131
- `@celilo/* workspace package. Add it to the workspace, or fix the dep name. ` +
132
- `(This is the loud failure the old hardcoded pack list lacked — ` +
133
- `see apps/celilo/designs/WORKSPACE_PACKAGE_GRAPH.md.)`,
130
+ `Cannot rewrite ${bucket} "${name}": "${spec}" — ${name} is not a known @celilo/* workspace package. Add it to the workspace, or fix the dep name. (This is the loud failure the old hardcoded pack list lacked — see apps/celilo/designs/WORKSPACE_PACKAGE_GRAPH.md.)`,
134
131
  );
135
132
  }
136
133
  const range = spec.slice('workspace:'.length);
@@ -24,7 +24,7 @@
24
24
  */
25
25
 
26
26
  import { spawnSync } from 'node:child_process';
27
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
27
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs';
28
28
  import { join } from 'node:path';
29
29
 
30
30
  const green = '\x1b[32m';
@@ -102,9 +102,7 @@ export function stageLibsignal(repoRoot: string, pkgDir: string): boolean {
102
102
  console.log(`${yellow}skip${reset} ${dim}(build failed)${reset}`);
103
103
  return false;
104
104
  }
105
- console.log(
106
- `${green}✔${reset} ${dim}${Math.round((Date.now() - t0) / 1000)}s${reset}`,
107
- );
105
+ console.log(`${green}✔${reset} ${dim}${Math.round((Date.now() - t0) / 1000)}s${reset}`);
108
106
  }
109
107
 
110
108
  process.stdout.write(` ${'libsignal-jni (stage)'.padEnd(28)} `);
@@ -68,7 +68,7 @@ const tlsOptions = (() => {
68
68
 
69
69
  // Bind to internal interface only (like a real router's management UI)
70
70
  // The external interface should only handle port-forwarded traffic via iptables
71
- const BIND_HOST = process.env.ROUTER_BIND_HOST || '192.168.0.1';
71
+ const BIND_HOST = process.env.ROUTER_BIND_HOST || '10.226.1.1';
72
72
 
73
73
  const server = Bun.serve({
74
74
  port: PORT,
@@ -161,4 +161,6 @@ const server = Bun.serve({
161
161
  },
162
162
  });
163
163
 
164
- console.log(`[greenwave] C4000XG simulator listening on ${BIND_HOST}:${server.port} (${tlsOptions ? 'HTTPS' : 'HTTP'})`);
164
+ console.log(
165
+ `[greenwave] C4000XG simulator listening on ${BIND_HOST}:${server.port} (${tlsOptions ? 'HTTPS' : 'HTTP'})`,
166
+ );
@@ -17,11 +17,11 @@ let nextIndex = 1;
17
17
  const dhcpPool: Record<string, string> = {
18
18
  Alias: 'cpe-Pool-1',
19
19
  Enable: 'true',
20
- MinAddress: '192.168.0.2',
21
- MaxAddress: '192.168.0.150',
20
+ MinAddress: '10.226.1.2',
21
+ MaxAddress: '10.226.1.150',
22
22
  SubnetMask: '255.255.255.0',
23
- IPRouters: '192.168.0.1',
24
- DNSServers: '192.168.0.1',
23
+ IPRouters: '10.226.1.1',
24
+ DNSServers: '10.226.1.1',
25
25
  DomainName: '',
26
26
  LeaseTime: '3600',
27
27
  Status: 'Enabled',
@@ -117,11 +117,16 @@ export function setDhcpPool(params: Record<string, string>): CgiObject[] {
117
117
  rewriteDnsmasqConfig();
118
118
  }
119
119
 
120
- return [{ ObjName: 'Device.DHCPv4.Server.Pool.1.', Param: [{ ParamName: 'Success', ParamValue: 'Success' }] }];
120
+ return [
121
+ {
122
+ ObjName: 'Device.DHCPv4.Server.Pool.1.',
123
+ Param: [{ ParamName: 'Success', ParamValue: 'Success' }],
124
+ },
125
+ ];
121
126
  }
122
127
 
123
128
  function rewriteDnsmasqConfig(): void {
124
- const dnsServers = dhcpPool.DNSServers || '192.168.0.1';
129
+ const dnsServers = dhcpPool.DNSServers || '10.226.1.1';
125
130
  const domainName = dhcpPool.DomainName || '';
126
131
 
127
132
  const lines = [
@@ -129,8 +134,8 @@ function rewriteDnsmasqConfig(): void {
129
134
  'port=0',
130
135
  'interface=eth0',
131
136
  'bind-interfaces',
132
- 'dhcp-range=192.168.0.200,192.168.0.220,255.255.255.0,60s',
133
- 'dhcp-option=option:router,192.168.0.1',
137
+ 'dhcp-range=10.226.1.200,10.226.1.220,255.255.255.0,60s',
138
+ 'dhcp-option=option:router,10.226.1.1',
134
139
  `dhcp-option=option:dns-server,${dnsServers}`,
135
140
  'dhcp-lease-max=20',
136
141
  'log-dhcp',
@@ -144,10 +149,13 @@ function rewriteDnsmasqConfig(): void {
144
149
  writeFileSync('/etc/dnsmasq.d/e2e.conf', `${lines.join('\n')}\n`);
145
150
  // dnsmasq requires full restart to re-read config files (SIGHUP only reloads /etc/hosts)
146
151
  try {
147
- execSync('pkill -9 dnsmasq; sleep 0.5; dnsmasq --conf-dir=/etc/dnsmasq.d --log-facility=/dev/stderr', {
148
- timeout: 5000,
149
- shell: '/bin/bash',
150
- });
152
+ execSync(
153
+ 'pkill -9 dnsmasq; sleep 0.5; dnsmasq --conf-dir=/etc/dnsmasq.d --log-facility=/dev/stderr',
154
+ {
155
+ timeout: 5000,
156
+ shell: '/bin/bash',
157
+ },
158
+ );
151
159
  } catch {}
152
160
  console.log(`[greenwave] dnsmasq restarted: DNS=${dnsServers}, domain=${domainName}`);
153
161
  } catch (err) {
@@ -168,10 +176,9 @@ function applyIptablesRule(mapping: PortMapping): void {
168
176
  { timeout: 5000 },
169
177
  );
170
178
  // Allow forwarded traffic
171
- execSync(
172
- `iptables -A FORWARD -p ${p} -d ${intIp} --dport ${intPort} -j ACCEPT`,
173
- { timeout: 5000 },
174
- );
179
+ execSync(`iptables -A FORWARD -p ${p} -d ${intIp} --dport ${intPort} -j ACCEPT`, {
180
+ timeout: 5000,
181
+ });
175
182
  }
176
183
 
177
184
  try {
@@ -180,7 +187,7 @@ function applyIptablesRule(mapping: PortMapping): void {
180
187
  addRules('udp');
181
188
  }
182
189
  } catch (err) {
183
- console.error(`[greenwave] Failed to apply iptables rule:`, err);
190
+ console.error('[greenwave] Failed to apply iptables rule:', err);
184
191
  }
185
192
  }
186
193
 
@@ -41,11 +41,7 @@ async function probe(targetUrl: string): Promise<ProbeResponse> {
41
41
  };
42
42
  }
43
43
 
44
- const port = parsed.port
45
- ? Number(parsed.port)
46
- : parsed.protocol === 'https:'
47
- ? 443
48
- : 80;
44
+ const port = parsed.port ? Number(parsed.port) : parsed.protocol === 'https:' ? 443 : 80;
49
45
 
50
46
  const start = Date.now();
51
47
  try {
@@ -8,8 +8,8 @@
8
8
  * Returns XML matching the real Namecheap response format.
9
9
  */
10
10
 
11
- import { updateZone } from './zone-updater';
12
11
  import type { DdnsRecord } from './types';
12
+ import { updateZone } from './zone-updater';
13
13
 
14
14
  /**
15
15
  * Minimal structural type for the bits of Bun's server we use — avoids
@@ -56,9 +56,7 @@ function expectedPasswordFor(domain: string): string {
56
56
  const records: Map<string, DdnsRecord> = new Map();
57
57
 
58
58
  function xmlResponse(errCount: number, ip: string, errors: string[]): Response {
59
- const errorElements = errors
60
- .map((e, i) => `<Err${i + 1}>${e}</Err${i + 1}>`)
61
- .join('\n ');
59
+ const errorElements = errors.map((e, i) => `<Err${i + 1}>${e}</Err${i + 1}>`).join('\n ');
62
60
 
63
61
  const xml = `<?xml version="1.0" encoding="utf-16"?>
64
62
  <interface-response>
@@ -23,7 +23,9 @@ describe('namecheap-ddns protected records (e2e-confidence #257)', () => {
23
23
  // The guard runs before any readFileSync, so this throws the protection
24
24
  // error (not an ENOENT) even with no /config present — proving a DDNS apex
25
25
  // clobber of the website-sim can never reach the zone file.
26
- expect(() => updateZone('celilo.computer', '@', '100.64.0.1')).toThrow(/reserved e2e infrastructure/);
26
+ expect(() => updateZone('celilo.computer', '@', '100.64.0.1')).toThrow(
27
+ /reserved e2e infrastructure/,
28
+ );
27
29
  expect(() => updateZone('celilo.computer', 'www', '100.64.0.1')).toThrow(
28
30
  /reserved e2e infrastructure/,
29
31
  );
@@ -33,21 +33,17 @@ export function isProtectedRecord(domain: string, host: string): boolean {
33
33
  export function updateZone(domain: string, host: string, ip: string): void {
34
34
  if (isProtectedRecord(domain, host)) {
35
35
  throw new Error(
36
- `Refusing DDNS update to "${host}.${domain}": it is reserved e2e infrastructure ` +
37
- `(the website-sim), not a DDNS-managed host (e2e-confidence #257).`,
36
+ `Refusing DDNS update to "${host}.${domain}": it is reserved e2e infrastructure (the website-sim), not a DDNS-managed host (e2e-confidence #257).`,
38
37
  );
39
38
  }
40
39
  const zoneFile = `${ZONE_DIR}/${domain}.zone`;
41
40
  let content = readFileSync(zoneFile, 'utf-8');
42
41
 
43
42
  // Increment SOA serial (YYYYMMDDnn format — just increment the number)
44
- content = content.replace(
45
- /(\d{10})(\s+\d+\s+\d+\s+\d+\s+\d+\s*\))/,
46
- (_match, serial, rest) => {
47
- const newSerial = String(Number(serial) + 1);
48
- return `${newSerial}${rest}`;
49
- },
50
- );
43
+ content = content.replace(/(\d{10})(\s+\d+\s+\d+\s+\d+\s+\d+\s*\))/, (_match, serial, rest) => {
44
+ const newSerial = String(Number(serial) + 1);
45
+ return `${newSerial}${rest}`;
46
+ });
51
47
 
52
48
  // Check if host already has an A record. Match ANY existing value (\S+),
53
49
  // not just dotted-decimal: a prior update may have written a malformed
@@ -61,7 +57,7 @@ export function updateZone(domain: string, host: string, ip: string): void {
61
57
  content = content.replace(recordRegex, `${host} IN A ${ip}`);
62
58
  } else {
63
59
  // Append new record
64
- content = content.trimEnd() + `\n${host} IN A ${ip}\n`;
60
+ content = `${content.trimEnd()}\n${host} IN A ${ip}\n`;
65
61
  }
66
62
 
67
63
  writeFileSync(zoneFile, content);
@@ -280,7 +280,9 @@ Bun.serve({
280
280
 
281
281
  // Verified endpoints on the real daemon.
282
282
  if (url.pathname === '/api/v1/check') {
283
- return req.method === 'GET' ? new Response(null, { status: 200 }) : new Response(null, { status: 405 });
283
+ return req.method === 'GET'
284
+ ? new Response(null, { status: 200 })
285
+ : new Response(null, { status: 405 });
284
286
  }
285
287
  if (url.pathname === '/api/v1/events') {
286
288
  return new Response('', { status: 200, headers: { 'content-type': 'text/event-stream' } });
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The recurrence gate for #539.
3
+ *
4
+ * The sim used to number its zones exactly like a real celilo fleet, so a
5
+ * leaked stack on celilo's own forgejo-builder (which lives at 10.0.10.12 in
6
+ * the REAL dmz) put a second interface on the builder's own /24 and blackholed
7
+ * every containerized CI job's route to the forge. These tests make the two
8
+ * properties that prevent it non-negotiable:
9
+ *
10
+ * 1. every simulated private zone lives inside SIM_PRIVATE_SUPERNET, and
11
+ * 2. the retired fleet-colliding prefixes appear NOWHERE in the suite.
12
+ *
13
+ * (2) is the one that matters in practice: the plan itself is one table, but
14
+ * shell scripts, Knot zones and dnsmasq configs can't import TypeScript and so
15
+ * hard-code the values. This is what catches "I changed it in one place".
16
+ */
17
+
18
+ import { describe, expect, test } from 'bun:test';
19
+ import { execSync } from 'node:child_process';
20
+ import { SIM_PRIVATE_SUPERNET, ZONE_GATEWAYS, ZONE_SUBNETS, type Zone } from './types';
21
+ import { inCidr } from './zone-classifier';
22
+
23
+ const REPO_ROOT = execSync('git rev-parse --show-toplevel', {
24
+ cwd: import.meta.dir,
25
+ encoding: 'utf-8',
26
+ }).trim();
27
+
28
+ /**
29
+ * Prefixes the sim must never use again: the fleet's zone /24s and the
30
+ * operator's LAN. Prefix match — `10.0.10.` catches an address, a CIDR and a
31
+ * prose mention alike, which is what we want.
32
+ */
33
+ const RETIRED_PREFIXES = ['10.0.10.', '10.0.20.', '10.0.30.', '10.0.120.', '192.168.'];
34
+
35
+ /**
36
+ * Match the plain form AND the regex-escaped form (`192\.168\.0\.10`). Two
37
+ * escaped literals inside `toThrow(/…/)` survived the #539 sweep precisely
38
+ * because a naive search for the plain text does not see them.
39
+ */
40
+ function toPattern(prefix: string): string {
41
+ return prefix.replace(/\./g, '\\\\?\\.');
42
+ }
43
+
44
+ /** Everything that can put an address on a simulated network or assert one. */
45
+ const SCANNED_PATHS = ['packages/e2e', 'e2e/tests', 'modules/*/e2e'];
46
+
47
+ /**
48
+ * CHANGELOG is history — it describes what the addresses WERE. This test file
49
+ * necessarily names the retired prefixes to ban them.
50
+ */
51
+ const EXCLUDED = [/^packages\/e2e\/CHANGELOG\.md$/, /address-plan\.test\.ts$/];
52
+
53
+ function grepRetired(): string[] {
54
+ const pattern = RETIRED_PREFIXES.map(toPattern).join('|');
55
+ let out = '';
56
+ try {
57
+ out = execSync(`git grep -nE '${pattern}' -- ${SCANNED_PATHS.join(' ')}`, {
58
+ cwd: REPO_ROOT,
59
+ encoding: 'utf-8',
60
+ maxBuffer: 8 * 1024 * 1024,
61
+ });
62
+ } catch {
63
+ // git grep exits 1 when there are no matches — that is the passing case.
64
+ return [];
65
+ }
66
+ return out
67
+ .split('\n')
68
+ .filter(Boolean)
69
+ .filter((line) => !EXCLUDED.some((re) => re.test(line.split(':')[0])));
70
+ }
71
+
72
+ describe('simulated address plan (#539)', () => {
73
+ test('every zone lives inside the sim supernet', () => {
74
+ for (const zone of Object.keys(ZONE_SUBNETS) as Zone[]) {
75
+ const base = ZONE_SUBNETS[zone].split('/')[0];
76
+ expect(inCidr(base, SIM_PRIVATE_SUPERNET)).toBe(true);
77
+ expect(inCidr(ZONE_GATEWAYS[zone], SIM_PRIVATE_SUPERNET)).toBe(true);
78
+ expect(inCidr(ZONE_GATEWAYS[zone], ZONE_SUBNETS[zone])).toBe(true);
79
+ }
80
+ });
81
+
82
+ test('zones do not overlap each other', () => {
83
+ const subnets = Object.values(ZONE_SUBNETS);
84
+ expect(new Set(subnets).size).toBe(subnets.length);
85
+ });
86
+
87
+ test('the fleet-colliding prefixes appear nowhere in the suite', () => {
88
+ const hits = grepRetired();
89
+ expect(
90
+ hits,
91
+ `The e2e suite must not use a real fleet's addresses (#539). Offending lines:\n${hits.join('\n')}\n\n` +
92
+ `Use zoneIp()/ZONE_SUBNETS from packages/e2e/src/types.ts, or an address inside ${SIM_PRIVATE_SUPERNET}.`,
93
+ ).toEqual([]);
94
+ });
95
+ });
package/src/browser.ts CHANGED
@@ -14,10 +14,10 @@ export async function startBrowser(
14
14
  ): Promise<BrowserHandle> {
15
15
  if (!options.chromium) {
16
16
  throw new Error(
17
- "net.browser() requires a `chromium` launcher in options.\n\n" +
18
- "Pass it from your own playwright install:\n" +
17
+ 'net.browser() requires a `chromium` launcher in options.\n\n' +
18
+ 'Pass it from your own playwright install:\n' +
19
19
  " import { chromium } from 'playwright-core';\n" +
20
- " const browser = await net.browser({ vantage, chromium });",
20
+ ' const browser = await net.browser({ vantage, chromium });',
21
21
  );
22
22
  }
23
23
  const proxy = await startSocksProxy(projectName, options);
package/src/bus-events.ts CHANGED
@@ -18,11 +18,11 @@
18
18
  * without the bus, just without observability hooks.
19
19
  */
20
20
 
21
- import { defineEvents, openBus, type Bus } from '@celilo/event-bus';
21
+ import { existsSync, mkdirSync } from 'node:fs';
22
22
  import { homedir } from 'node:os';
23
23
  import { join } from 'node:path';
24
- import { existsSync, mkdirSync } from 'node:fs';
25
24
  import { dirname } from 'node:path';
25
+ import { type Bus, defineEvents, openBus } from '@celilo/event-bus';
26
26
  import { z } from 'zod';
27
27
 
28
28
  const NO_SCHEMAS = defineEvents({});
@@ -21,8 +21,14 @@ function parseCommandsFromHelp(text: string): string[] {
21
21
  let inCommandsSection = false;
22
22
 
23
23
  for (const line of text.split('\n')) {
24
- if (/^\s*Commands?:\s*$/i.test(line)) { inCommandsSection = true; continue; }
25
- if (inCommandsSection && /^\s*(Options|Examples|Usage):/i.test(line)) { inCommandsSection = false; continue; }
24
+ if (/^\s*Commands?:\s*$/i.test(line)) {
25
+ inCommandsSection = true;
26
+ continue;
27
+ }
28
+ if (inCommandsSection && /^\s*(Options|Examples|Usage):/i.test(line)) {
29
+ inCommandsSection = false;
30
+ continue;
31
+ }
26
32
  if (!inCommandsSection) continue;
27
33
 
28
34
  // Match " command-name" at 2-3 spaces indent, followed by space/bracket/end
@@ -3,13 +3,7 @@
3
3
  * All completions are static (no DB lookups needed).
4
4
  */
5
5
 
6
- import {
7
- BUILD_INFRA_FLAGS,
8
- COMMANDS,
9
- DOWN_FLAGS,
10
- RUN_FLAGS,
11
- UP_PRESETS,
12
- } from './command-registry';
6
+ import { BUILD_INFRA_FLAGS, COMMANDS, DOWN_FLAGS, RUN_FLAGS, UP_PRESETS } from './command-registry';
13
7
  import { findAllTestNames } from './module-discovery';
14
8
 
15
9
  export function getCompletions(words: string[], current: number): string[] {
@@ -58,7 +52,10 @@ export function getCompletions(words: string[], current: number): string[] {
58
52
 
59
53
  if (command === 'list' && currentIndex >= 1) {
60
54
  const used = new Set(args.slice(1));
61
- return filterSuggestions(['--paths'].filter((f) => !used.has(f)), args[currentIndex] || '');
55
+ return filterSuggestions(
56
+ ['--paths'].filter((f) => !used.has(f)),
57
+ args[currentIndex] || '',
58
+ );
62
59
  }
63
60
 
64
61
  if (command === 'completion' && currentIndex === 1) {
package/src/cli/index.ts CHANGED
@@ -333,6 +333,7 @@ switch (command) {
333
333
  }
334
334
  console.log(`\n${tests.length} tests. Run all: cele2e run --all`);
335
335
  process.exit(0);
336
+ break;
336
337
  }
337
338
 
338
339
  case 'up': {
@@ -350,15 +351,18 @@ switch (command) {
350
351
  throw err;
351
352
  }
352
353
  runScript('e2e-up', args);
354
+ break;
353
355
  }
354
356
 
355
357
  case 'down':
356
358
  // Tearing the network down frees the (possibly kept) lock.
357
359
  clearLock();
358
360
  runScript('e2e-down', args, { E2E_TEST_DIR: stateDir });
361
+ break;
359
362
 
360
363
  case 'shell':
361
364
  runScript('e2e-shell', args);
365
+ break;
362
366
 
363
367
  case 'release': {
364
368
  // Free a lock left by `--keep`/`up` (a `kept` stack) so the next run can
@@ -366,6 +370,7 @@ switch (command) {
366
370
  const had = clearLock();
367
371
  console.log(had ? 'Released the e2e run-lock.' : 'No e2e run-lock held.');
368
372
  process.exit(0);
373
+ break;
369
374
  }
370
375
 
371
376
  case 'status': {
@@ -394,18 +399,20 @@ switch (command) {
394
399
  console.log(` Project: ${data.projectName}`);
395
400
  console.log(` Test: ${data.testFile}`);
396
401
  console.log(` Created: ${data.createdAt}`);
397
- console.log(` Reuse: cele2e run --reuse`);
398
- console.log(` Tear down: cele2e down`);
402
+ console.log(' Reuse: cele2e run --reuse');
403
+ console.log(' Tear down: cele2e down');
399
404
  console.log('');
400
405
  } catch {
401
406
  /* ignore parse errors */
402
407
  }
403
408
  }
404
409
  runScript('e2e-status', args, projectName ? { CELILO_E2E_PROJECT: projectName } : undefined);
410
+ break;
405
411
  }
406
412
 
407
413
  case 'load':
408
414
  runScript('e2e-load', args);
415
+ break;
409
416
 
410
417
  case 'build-infra': {
411
418
  const save = args.includes('--save');
@@ -18,7 +18,9 @@ const MAX_DEPTH = 2;
18
18
 
19
19
  function readManifestTestsDir(manifestPath: string): string | undefined {
20
20
  try {
21
- const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as { e2e?: { tests_dir?: string } };
21
+ const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as {
22
+ e2e?: { tests_dir?: string };
23
+ };
22
24
  return parsed?.e2e?.tests_dir;
23
25
  } catch {
24
26
  return undefined;
@@ -60,8 +60,8 @@ describe('${name}', () => {
60
60
  }
61
61
 
62
62
  net = await network({ projectName: PROJECT_NAME })
63
- .dmz({ 'machine-1': '10.0.10.10' })
64
- // .app({ 'app-1': '10.0.20.10' })
63
+ .dmz({ 'machine-1': '10.226.10.10' })
64
+ // .app({ 'app-1': '10.226.20.10' })
65
65
  .start();
66
66
  }, 120_000);
67
67
 
@@ -17,7 +17,10 @@ export const MIN_CLI_VERSION = '0.9.0';
17
17
  * it doesn't, or `null` when OK. No I/O, so it's unit-testable without a
18
18
  * running management container.
19
19
  */
20
- export function checkCliVersion(versionStdout: string, minVersion = MIN_CLI_VERSION): string | null {
20
+ export function checkCliVersion(
21
+ versionStdout: string,
22
+ minVersion = MIN_CLI_VERSION,
23
+ ): string | null {
21
24
  // Output is `celilo <version>` (apps/celilo displayVersion). Grab the token.
22
25
  const version = versionStdout.match(/celilo\s+(\S+)/)?.[1];
23
26
  if (!version) {
@@ -27,11 +30,7 @@ export function checkCliVersion(versionStdout: string, minVersion = MIN_CLI_VERS
27
30
  );
28
31
  }
29
32
  if (Bun.semver.order(version, minVersion) < 0) {
30
- return (
31
- `Management image has celilo ${version}, harness requires >=${minVersion}. ` +
32
- `Rebuild the management image against a newer @celilo/cli ` +
33
- `(e.g. \`cele2e build-infra\`, or \`--published\` after publishing).`
34
- );
33
+ return `Management image has celilo ${version}, harness requires >=${minVersion}. Rebuild the management image against a newer @celilo/cli (e.g. \`cele2e build-infra\`, or \`--published\` after publishing).`;
35
34
  }
36
35
  return null;
37
36
  }