@celilo/e2e 0.11.0 → 0.11.2

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 (46) hide show
  1. package/npm-registry-server/src/packument.test.ts +2 -5
  2. package/npm-registry-server/src/packument.ts +3 -3
  3. package/npm-registry-server/src/server.ts +1 -1
  4. package/package.json +3 -3
  5. package/registry-server/src/bootstrap.ts +1 -1
  6. package/registry-server/src/introspection.test.ts +8 -12
  7. package/registry-server/src/server.test.ts +2 -2
  8. package/registry-server/src/server.ts +1 -1
  9. package/registry-server/src/storage.test.ts +8 -4
  10. package/registry-server/src/storage.ts +1 -1
  11. package/registry-server/src/validation.ts +2 -4
  12. package/scripts/consumer-paths-changed.ts +83 -0
  13. package/scripts/pack-celilo-packages.ts +1 -4
  14. package/scripts/stage-apt-repo.ts +1 -1
  15. package/scripts/stage-libsignal.ts +1 -3
  16. package/simulators/greenwave/server.ts +3 -1
  17. package/simulators/greenwave/state.ts +17 -10
  18. package/simulators/isitup/server.ts +1 -5
  19. package/simulators/namecheap-ddns/server.ts +2 -4
  20. package/simulators/namecheap-ddns/zone-updater.test.ts +3 -1
  21. package/simulators/namecheap-ddns/zone-updater.ts +6 -10
  22. package/simulators/signal-cli/server.ts +3 -1
  23. package/src/browser.ts +3 -3
  24. package/src/bus-events.ts +2 -2
  25. package/src/cli/command-tree-parser.ts +8 -2
  26. package/src/cli/completion.ts +5 -8
  27. package/src/cli/index.ts +9 -2
  28. package/src/cli/module-discovery.ts +3 -1
  29. package/src/cli-version-contract.ts +5 -6
  30. package/src/extract-failure.ts +92 -0
  31. package/src/fixtures.ts +4 -11
  32. package/src/network-builder.ts +12 -7
  33. package/src/observer.test.ts +42 -11
  34. package/src/observer.ts +12 -5
  35. package/src/registry-bundle.ts +1 -3
  36. package/src/run-lock.test.ts +2 -2
  37. package/src/run-lock.ts +19 -5
  38. package/src/runner.ts +9 -67
  39. package/src/shared-infra.ts +50 -22
  40. package/src/socks-proxy.ts +14 -17
  41. package/src/vantage-helpers.ts +5 -1
  42. package/src/vantage.test.ts +78 -24
  43. package/src/vantage.ts +24 -17
  44. package/src/wait-for-run.ts +1 -1
  45. package/src/zone-classifier.test.ts +7 -1
  46. package/src/zone-classifier.ts +2 -2
@@ -1,8 +1,8 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
1
2
  import { execSync } from 'node:child_process';
2
3
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
4
  import { tmpdir } from 'node:os';
4
5
  import { join } from 'node:path';
5
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
6
6
 
7
7
  import {
8
8
  buildPackument,
@@ -28,10 +28,7 @@ function packFixture(name: string, version: string): string {
28
28
  const slug = `${name.replace(/[@/]/g, '_')}-${version}`;
29
29
  const pkgDir = join(tmp, 'pkg', slug);
30
30
  mkdirSync(pkgDir, { recursive: true });
31
- writeFileSync(
32
- join(pkgDir, 'package.json'),
33
- JSON.stringify({ name, version, main: 'index.js' }),
34
- );
31
+ writeFileSync(join(pkgDir, 'package.json'), JSON.stringify({ name, version, main: 'index.js' }));
35
32
  writeFileSync(join(pkgDir, 'index.js'), `module.exports = '${name}@${version}';\n`);
36
33
 
37
34
  const stagingDir = join(tmp, 'staging', slug);
@@ -52,7 +52,7 @@ function* iterTarEntries(tar: Buffer): Generator<{ name: string; data: Buffer }>
52
52
 
53
53
  const rawName = header.subarray(0, 100).toString('utf-8').replace(/\0.*$/, '');
54
54
  const sizeStr = header.subarray(124, 136).toString('utf-8').replace(/\0.*$/, '').trim();
55
- const size = parseInt(sizeStr, 8) || 0;
55
+ const size = Number.parseInt(sizeStr, 8) || 0;
56
56
  const dataStart = offset + 512;
57
57
  const dataEnd = dataStart + size;
58
58
 
@@ -109,8 +109,8 @@ export function loadTarballsFromDir(dir: string): TarballMeta[] {
109
109
  */
110
110
  export function highestVersion(versions: string[]): string {
111
111
  return [...versions].sort((a, b) => {
112
- const pa = a.split('.').map((s) => parseInt(s, 10) || 0);
113
- const pb = b.split('.').map((s) => parseInt(s, 10) || 0);
112
+ const pa = a.split('.').map((s) => Number.parseInt(s, 10) || 0);
113
+ const pb = b.split('.').map((s) => Number.parseInt(s, 10) || 0);
114
114
  for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
115
115
  const da = pa[i] ?? 0;
116
116
  const db = pb[i] ?? 0;
@@ -16,7 +16,7 @@
16
16
  * different staging directory in tests.
17
17
  */
18
18
 
19
- import { indexTarballs, loadTarballsFromDir, type TarballMeta } from './packument';
19
+ import { type TarballMeta, indexTarballs, loadTarballsFromDir } from './packument';
20
20
  import { buildPackument } from './packument';
21
21
 
22
22
  interface ServerOptions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/e2e",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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,8 +37,8 @@
37
37
  "README.md"
38
38
  ],
39
39
  "dependencies": {
40
- "@celilo/cli-display": "^0.1.9",
41
- "@celilo/event-bus": "^0.1.10",
40
+ "@celilo/cli-display": "^0.1.10",
41
+ "@celilo/event-bus": "^0.2.0",
42
42
  "yaml": "^2.8.0",
43
43
  "zod": "^3.24.1"
44
44
  },
@@ -15,9 +15,9 @@
15
15
  * inert — storage.ts is the only code that touches disk.
16
16
  */
17
17
 
18
+ import { createHash } from 'node:crypto';
18
19
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
19
20
  import { basename, dirname, join, relative } from 'node:path';
20
- import { createHash } from 'node:crypto';
21
21
  import type { IndexEntry } from './storage';
22
22
 
23
23
  export interface BootstrapEntry {
@@ -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 };
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Does this push touch the npm-consumer-smoke consumer path?
4
+ *
5
+ * This list used to live in `npm-consumer-smoke.yml`'s `on.push.paths`, where
6
+ * it decided whether the WORKFLOW TRIGGERED at all. That made a required status
7
+ * check that often posted no status: a push touching none of these paths
8
+ * created no run, so the `npm-consumer-smoke*` context branch protection
9
+ * requires never reported, and the PR was blocked on a check that was never
10
+ * going to run (celilo#603).
11
+ *
12
+ * The workflow now always triggers and always reports; this decides whether it
13
+ * does the expensive Docker work. Same paths, same intent — the filter moved
14
+ * from "should CI speak" to "should CI do the work".
15
+ *
16
+ * DEFAULT TO RUNNING. Every uncertainty here resolves to `true`. Skipping when
17
+ * we should have run is a silent false green on a release-path gate; running
18
+ * when we needn't costs a few minutes of builder time.
19
+ */
20
+
21
+ /** Kept verbatim from the trigger's former `paths:` list. */
22
+ export const CONSUMER_PATHS = [
23
+ 'packages/e2e/**',
24
+ 'packages/registry-server/**',
25
+ // The publish driver (moved from the dead apps/celilo/scripts/publish.ts).
26
+ 'apps/celilo/src/cli/commands/publish/**',
27
+ 'scripts/publish.ts',
28
+ // (The derived release graph lives at packages/e2e/scripts/workspace-graph.ts,
29
+ // already covered by packages/e2e/** above.)
30
+ // Every workspace package.json — the actual input to the package graph.
31
+ // Narrowed to workspace members (not **/package.json) so a devDependency bump
32
+ // in an unrelated dir doesn't fire this heavy Docker job; the cheap sync gate
33
+ // in ci.yml catches graph drift on every PR regardless.
34
+ 'apps/*/package.json',
35
+ 'packages/*/package.json',
36
+ 'package.json',
37
+ ];
38
+
39
+ interface PushCommit {
40
+ added?: string[];
41
+ modified?: string[];
42
+ removed?: string[];
43
+ }
44
+
45
+ export interface PushPayload {
46
+ commits?: PushCommit[];
47
+ total_commits?: number;
48
+ }
49
+
50
+ export function touchesConsumerPath(files: string[]): boolean {
51
+ return files.some((file) => CONSUMER_PATHS.some((glob) => new Bun.Glob(glob).match(file)));
52
+ }
53
+
54
+ export function pushIsRelevant(payload: PushPayload): boolean {
55
+ const commits = payload.commits ?? [];
56
+
57
+ // No commit list to reason from — a payload shape we don't understand is not
58
+ // evidence that nothing changed.
59
+ if (commits.length === 0) return true;
60
+
61
+ // Forgejo caps `commits[]` on a large push while `total_commits` reports the
62
+ // real number. The unlisted commits are exactly the ones we cannot inspect.
63
+ if ((payload.total_commits ?? commits.length) > commits.length) return true;
64
+
65
+ // A commit carrying none of the three arrays told us nothing about its
66
+ // files. That is not the same as a commit that changed nothing — no such
67
+ // commit exists — so it is an unknown, and unknowns run.
68
+ if (commits.some((c) => !c.added && !c.modified && !c.removed)) return true;
69
+
70
+ return touchesConsumerPath(
71
+ commits.flatMap((c) => [...(c.added ?? []), ...(c.modified ?? []), ...(c.removed ?? [])]),
72
+ );
73
+ }
74
+
75
+ if (import.meta.main) {
76
+ const eventPath = process.env.GITHUB_EVENT_PATH;
77
+ // A manual dispatch (or any non-push event) means someone asked for it.
78
+ const relevant =
79
+ process.env.GITHUB_EVENT_NAME !== 'push' || !eventPath
80
+ ? true
81
+ : pushIsRelevant(await Bun.file(eventPath).json());
82
+ console.log(relevant ? 'true' : 'false');
83
+ }
@@ -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)} `);
@@ -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
+ );
@@ -117,7 +117,12 @@ 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 {
@@ -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' } });
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;