@celilo/e2e 0.9.2 → 0.9.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.
Files changed (33) hide show
  1. package/config/proxy/squid.conf +7 -2
  2. package/config/resolver/unbound.conf +13 -3
  3. package/docker/Dockerfile.minio +1 -1
  4. package/docker/Dockerfile.target-machine +1 -1
  5. package/docker/Dockerfile.target-machine-docker +1 -1
  6. package/docker/Dockerfile.target-technitium +1 -1
  7. package/package.json +3 -3
  8. package/scripts/pack-celilo-packages.ts +79 -51
  9. package/src/cli/build.ts +28 -9
  10. package/src/cli/command-registry.ts +34 -6
  11. package/src/cli/index.ts +53 -21
  12. package/src/container-manager.ts +1 -1
  13. package/src/runner.ts +128 -57
  14. package/config/dns/example.net.zone +0 -16
  15. package/config/dns/iamtheinternet.org.zone +0 -16
  16. package/registry-server/package.json +0 -17
  17. package/registry-server/src/auth.test.ts +0 -76
  18. package/registry-server/src/auth.ts +0 -105
  19. package/registry-server/src/bootstrap-packaging.test.ts +0 -71
  20. package/registry-server/src/bootstrap.ts +0 -246
  21. package/registry-server/src/index.ts +0 -16
  22. package/registry-server/src/landing.ts +0 -90
  23. package/registry-server/src/rate-limit.test.ts +0 -62
  24. package/registry-server/src/rate-limit.ts +0 -83
  25. package/registry-server/src/scoped-token-store.test.ts +0 -93
  26. package/registry-server/src/scoped-token-store.ts +0 -110
  27. package/registry-server/src/server.test.ts +0 -509
  28. package/registry-server/src/server.ts +0 -572
  29. package/registry-server/src/storage.test.ts +0 -148
  30. package/registry-server/src/storage.ts +0 -150
  31. package/registry-server/src/validation.test.ts +0 -86
  32. package/registry-server/src/validation.ts +0 -60
  33. package/registry-server/tsconfig.json +0 -15
@@ -30,8 +30,13 @@ sslcrtd_program /usr/lib/squid/security_file_certgen -s /var/spool/squid/ssl_db
30
30
  access_log stdio:/var/log/squid/access.log
31
31
  cache_log /var/log/squid/cache.log
32
32
 
33
- # Resolve via real public DNS (reachable from real-internet bridge)
34
- dns_nameservers 8.8.8.8 1.1.1.1
33
+ # Resolve via Docker's embedded resolver, which relays to the host's DNS.
34
+ # Same reason as the resolver's forward-zone (config/resolver/unbound.conf):
35
+ # hardcoding public resolvers breaks the harness on any network that blocks
36
+ # direct UDP/53 egress, and squid is what every simulated container's HTTP(S)
37
+ # egress transits — so a squid that can't resolve fails every apt fetch and
38
+ # every `bun add` with "connection refused".
39
+ dns_nameservers 127.0.0.11
35
40
 
36
41
  # Timeouts
37
42
  connect_timeout 30 seconds
@@ -50,8 +50,18 @@ stub-zone:
50
50
  name: "park-your-domain.com."
51
51
  stub-addr: 100.64.0.55
52
52
 
53
- # Everything else → real public DNS (reachable via real-internet bridge)
53
+ # Everything else → the real internet, via Docker's embedded resolver.
54
+ #
55
+ # 127.0.0.11 is Docker's per-container DNS; it relays to whatever the HOST is
56
+ # configured to use. Hardcoding 8.8.8.8/1.1.1.1 here made the whole harness
57
+ # unrunnable on any network that blocks direct UDP/53 to public resolvers —
58
+ # corporate networks and some ISPs force their own resolver, and the symptom is
59
+ # deeply unhelpful: unbound times out, squid's `dns_nameservers` times out with
60
+ # it, and `bun add` reports "ConnectionRefused downloading package manifest
61
+ # drizzle-orm" from inside the bake. Going through the host's resolver works
62
+ # everywhere the host itself has working DNS, which is the actual requirement.
63
+ #
64
+ # `do-not-query-localhost: no` above is what permits this.
54
65
  forward-zone:
55
66
  name: "."
56
- forward-addr: 8.8.8.8
57
- forward-addr: 1.1.1.1
67
+ forward-addr: 127.0.0.11
@@ -4,7 +4,7 @@
4
4
  # s3.ts) talks plain S3 with forcePathStyle + a custom endpoint, so it drives
5
5
  # MinIO unchanged. This sim is the destination for `celilo backup --storage`
6
6
  # (push) and the source for `celilo backup pull` (download) in the migration
7
- # restore e2e (P5, v2/CELILO_MGMT_MIGRATION.md).
7
+ # restore e2e (P5, openspec/specs/management-server-backup/spec.md).
8
8
  #
9
9
  # Placement: internet-external (100.64.0.0/24) like the other public services —
10
10
  # the management box reaches it OUTBOUND over its internet egress, exactly the
@@ -34,7 +34,7 @@ RUN update-ca-certificates
34
34
  # AND /etc/environment so Ansible's SSH-invoked npm — a session that does NOT
35
35
  # inherit Docker ENV — also picks it up via pam_env. Without this, a module's
36
36
  # `npm install` against the npm-registry-sim fails SELF_SIGNED_CERT_IN_CHAIN.
37
- # (v2/EVENT_DRIVEN_HOOK_SUBSCRIPTIONS.md L6.)
37
+ # (openspec/specs/event-driven-hook-subscriptions/spec.md L6.)
38
38
  ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
39
39
  RUN echo 'NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt' >> /etc/environment
40
40
 
@@ -38,7 +38,7 @@ RUN update-ca-certificates
38
38
  # Node/npm ignore the OS trust store; point them at the bundle that now includes
39
39
  # the e2e CAs (mirrors Dockerfile.management), in both the image env and
40
40
  # /etc/environment so Ansible's SSH-invoked npm picks it up via pam_env.
41
- # (v2/EVENT_DRIVEN_HOOK_SUBSCRIPTIONS.md L6.)
41
+ # (openspec/specs/event-driven-hook-subscriptions/spec.md L6.)
42
42
  ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
43
43
  RUN echo 'NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt' >> /etc/environment
44
44
 
@@ -36,7 +36,7 @@ RUN update-ca-certificates
36
36
  # Node/npm ignore the OS trust store; point them at the bundle that now includes
37
37
  # the e2e CAs (mirrors Dockerfile.management), in both the image env and
38
38
  # /etc/environment so Ansible's SSH-invoked npm picks it up via pam_env.
39
- # (v2/EVENT_DRIVEN_HOOK_SUBSCRIPTIONS.md L6.)
39
+ # (openspec/specs/event-driven-hook-subscriptions/spec.md L6.)
40
40
  ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
41
41
  RUN echo 'NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt' >> /etc/environment
42
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/e2e",
3
- "version": "0.9.2",
3
+ "version": "0.9.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,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.8",
40
+ "@celilo/cli-display": "workspace:^",
41
+ "@celilo/event-bus": "workspace:^",
42
42
  "yaml": "^2.8.0",
43
43
  "zod": "^3.24.1"
44
44
  },
@@ -6,21 +6,21 @@
6
6
  * install.sh runs:
7
7
  * bun add -g @celilo/cli @celilo/event-bus @celilo/e2e
8
8
  *
9
- * Their transitive @celilo/* dependency closure is:
10
- * @celilo/cli → @celilo/capabilities, @celilo/cli-display, @celilo/event-bus
11
- * @celilo/event-bus → (none)
12
- * @celilo/e2e → @celilo/cli-display, @celilo/event-bus
13
- * @celilo/capabilities → (none)
14
- * @celilo/cli-display → (none)
9
+ * Which packages must land in the simulator is the transitive runtime closure
10
+ * of those roots — DERIVED from the live workspace by `installClosure()`, never
11
+ * hand-listed here. A hand-maintained list is exactly what turned this red for
12
+ * days when `@celilo/core` was added as a cli dep and forgotten
13
+ * (apps/celilo/designs/WORKSPACE_PACKAGE_GRAPH.md).
15
14
  *
16
- * So 5 workspace packages need to land in the simulator. We rewrite each
17
- * package's `workspace:^` deps to concrete `^<workspace-version>` ranges
18
- * OURSELVES before `bun pm pack` — we do NOT rely on bun's lock-based
19
- * expansion, which goes stale after a cross-caret version bump and pins
20
- * dependents to an unresolvable old range (ISS-0103: a `0.3→0.4`
15
+ * We rewrite each packed package's `workspace:^` deps to concrete
16
+ * `^<workspace-version>` ranges OURSELVES before `bun pm pack` — we do NOT rely
17
+ * on bun's lock-based expansion, which goes stale after a cross-caret version
18
+ * bump and pins dependents to an unresolvable old range (ISS-0103: a `0.3→0.4`
21
19
  * capabilities bump left the lock at 0.3, so packed cli pinned
22
- * `^0.3.0-alpha.0`, excluding the 0.4.x the sim served). package.json is
23
- * restored after packing so `workspace:^` stays in source.
20
+ * `^0.3.0-alpha.0`, excluding the 0.4.x the sim served). The rewrite map is the
21
+ * FULL workspace version map (every @celilo/* package), so any `workspace:` dep
22
+ * that can't be resolved is a genuine error and throws (see rewriteWorkspacePins).
23
+ * package.json is restored after packing so `workspace:^` stays in source.
24
24
  *
25
25
  * Output directory is typically `packages/e2e/.npm-registry-cache/` —
26
26
  * staged into the npm-registry-sim Docker image at build time.
@@ -30,11 +30,53 @@
30
30
  */
31
31
 
32
32
  import { execSync } from 'node:child_process';
33
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
34
- import { join } from 'node:path';
33
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
34
+ import { join, resolve } from 'node:path';
35
+ import {
36
+ installClosure,
37
+ readWorkspace,
38
+ workspaceVersionMap,
39
+ } from '../../../scripts/workspace-graph';
35
40
  import { ensureRegistryServerBundle } from '../src/registry-bundle';
36
41
 
37
- const REPO_ROOT = join(import.meta.dir, '..', '..', '..');
42
+ /**
43
+ * Resolve the celilo monorepo root by walking up from `startDir` (the caller's
44
+ * cwd by default) looking for `apps/celilo/package.json`.
45
+ *
46
+ * This replaces a hardcoded `join(import.meta.dir, '..', '..', '..')`. That
47
+ * derivation assumed the script always lives at `<repo>/packages/e2e/scripts/`,
48
+ * which holds in a checkout and nowhere else: the script ships inside the
49
+ * published `@celilo/e2e` tarball, where `../../..` resolves to the consumer's
50
+ * `node_modules/` and every path built from it points at
51
+ * `node_modules/apps/celilo/...` → ENOENT (ce-dae). `build.ts` already spawns
52
+ * this script with `cwd: <repoRoot>`, so the caller is the single source of
53
+ * truth for where the workspace is — take it from there, don't re-derive it.
54
+ * Walking up (rather than requiring cwd to BE the root exactly) also lets it run
55
+ * from a subdir of the checkout, e.g. `bun test` from `packages/e2e`.
56
+ */
57
+ export function resolveRepoRoot(startDir: string = process.cwd()): string {
58
+ let dir = startDir;
59
+ for (let i = 0; i < 10; i++) {
60
+ if (existsSync(join(dir, 'apps', 'celilo', 'package.json'))) return dir;
61
+ const parent = resolve(dir, '..');
62
+ if (parent === dir) break;
63
+ dir = parent;
64
+ }
65
+ throw new Error(
66
+ `pack-celilo-packages: no celilo checkout found from '${startDir}' (looked for apps/celilo/package.json walking up). This is a monorepo-internal dev script — run it from a checkout via 'cele2e build-infra'.`,
67
+ );
68
+ }
69
+
70
+ // Lazily memoized so importing this module (e.g. the rewriteWorkspacePins unit
71
+ // test) never triggers resolution — only the packing path, which runs inside a
72
+ // checkout, resolves the root.
73
+ let _repoRoot: string | undefined;
74
+ function repoRoot(): string {
75
+ if (_repoRoot === undefined) {
76
+ _repoRoot = resolveRepoRoot();
77
+ }
78
+ return _repoRoot;
79
+ }
38
80
 
39
81
  interface PackageJson {
40
82
  name?: string;
@@ -83,7 +125,14 @@ export function rewriteWorkspacePins(
83
125
  for (const [name, spec] of Object.entries(deps)) {
84
126
  if (!spec.startsWith('workspace:')) continue;
85
127
  const version = versions.get(name);
86
- if (!version) continue;
128
+ if (!version) {
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.)`,
134
+ );
135
+ }
87
136
  const range = spec.slice('workspace:'.length);
88
137
  let newSpec: string;
89
138
  if (range === '*' || range === '') newSpec = '*';
@@ -98,27 +147,12 @@ export function rewriteWorkspacePins(
98
147
  return rewrites;
99
148
  }
100
149
 
101
- /**
102
- * Workspace packages that install.sh's transitive @celilo/* closure depends
103
- * on. Hardcoded by name → relative path so a typo in package.json or a future
104
- * rename gets caught at build time rather than at test time. If a new
105
- * @celilo/* workspace package becomes a runtime dep of cli/event-bus/e2e,
106
- * add it here.
107
- */
108
- const PACKAGES_TO_PACK = [
109
- { name: '@celilo/cli', path: 'apps/celilo' },
110
- { name: '@celilo/capabilities', path: 'packages/capabilities' },
111
- { name: '@celilo/cli-display', path: 'packages/cli-display' },
112
- { name: '@celilo/e2e', path: 'packages/e2e' },
113
- { name: '@celilo/event-bus', path: 'packages/event-bus' },
114
- ] as const;
115
-
116
150
  interface CliArgs {
117
151
  destination: string;
118
152
  }
119
153
 
120
154
  function parseArgs(): CliArgs {
121
- const defaultDest = join(REPO_ROOT, 'packages', 'e2e', '.npm-registry-cache');
155
+ const defaultDest = join(repoRoot(), 'packages', 'e2e', '.npm-registry-cache');
122
156
  let destination = defaultDest;
123
157
  for (const arg of process.argv.slice(2)) {
124
158
  if (arg.startsWith('--dest=')) {
@@ -132,7 +166,7 @@ function parseArgs(): CliArgs {
132
166
  }
133
167
 
134
168
  function pack(pkgPath: string, destination: string, versions: Map<string, string>): string {
135
- const fullPath = join(REPO_ROOT, pkgPath);
169
+ const fullPath = join(repoRoot(), pkgPath);
136
170
  const pkgJsonPath = join(fullPath, 'package.json');
137
171
  if (!existsSync(pkgJsonPath)) {
138
172
  throw new Error(`No package.json in ${fullPath}`);
@@ -173,13 +207,6 @@ function pack(pkgPath: string, destination: string, versions: Map<string, string
173
207
  }
174
208
  }
175
209
 
176
- function workspaceVersion(pkgPath: string): string {
177
- const json = JSON.parse(readFileSync(join(REPO_ROOT, pkgPath, 'package.json'), 'utf-8')) as {
178
- version: string;
179
- };
180
- return json.version;
181
- }
182
-
183
210
  function main(): void {
184
211
  const { destination } = parseArgs();
185
212
 
@@ -195,22 +222,23 @@ function main(): void {
195
222
  // "published without its registry-server bundle"). Regenerate it from the
196
223
  // sibling packages/registry-server before packing, exactly as buildDockerImages
197
224
  // does in-monorepo. (This is why npm-consumer-smoke was red — ce-3rs.)
198
- ensureRegistryServerBundle(join(REPO_ROOT, 'packages', 'e2e'));
225
+ ensureRegistryServerBundle(join(repoRoot(), 'packages', 'e2e'));
199
226
 
200
227
  console.log(`Packing @celilo/* packages → ${destination}`);
201
228
  console.log('');
202
229
 
203
- // The name→version map used to rewrite each package's workspace:^ deps to
204
- // concrete ranges that resolve against the co-packed versions (ISS-0103).
205
- const versions = new Map<string, string>(
206
- PACKAGES_TO_PACK.map(({ name, path }) => [name, workspaceVersion(path)]),
207
- );
230
+ // Derive the package set + version map from the live workspace (never a
231
+ // hardcoded list). The rewrite map is the FULL workspace (every @celilo/*
232
+ // package) so any workspace: dep resolves or throws; the pack set is the
233
+ // runtime install closure of install.sh's roots.
234
+ const workspace = readWorkspace(repoRoot());
235
+ const versions = workspaceVersionMap(workspace);
236
+ const toPack = installClosure(workspace);
208
237
 
209
238
  const results: Array<{ name: string; version: string; tarball: string }> = [];
210
- for (const { name, path } of PACKAGES_TO_PACK) {
211
- const version = workspaceVersion(path);
212
- console.log(`▸ ${name}@${version} (${path})`);
213
- const tarball = pack(path, destination, versions);
239
+ for (const { name, dir, version } of toPack) {
240
+ console.log(`▸ ${name}@${version} (${dir})`);
241
+ const tarball = pack(dir, destination, versions);
214
242
  results.push({ name, version, tarball });
215
243
  console.log(` → ${tarball}`);
216
244
  console.log('');
package/src/cli/build.ts CHANGED
@@ -25,11 +25,22 @@ import { gunzipSync } from 'node:zlib';
25
25
  import { stageAptRepo } from '../../scripts/stage-apt-repo';
26
26
  import { ensureRegistryServerBundle } from '../registry-bundle';
27
27
 
28
- /** Walk up from pkgDir to find the monorepo root (contains a `modules/` dir). */
29
- function findMonorepoRoot(pkgDir: string): string | null {
28
+ /**
29
+ * Walk up from pkgDir to find the celilo monorepo root.
30
+ *
31
+ * The marker is `apps/celilo/package.json` — a file that only a real celilo
32
+ * checkout has — NOT the presence of a `modules/` directory. A bare `modules/`
33
+ * is too weak a signal: a stray empty `node_modules/modules` (left by a botched
34
+ * `bun add -g`) made this return the global node_modules as "the monorepo root",
35
+ * which skipped `stageFromPublic()` and crashed the npm-consumer path in the
36
+ * workspace packer (ce-dbc). Matches the marker `container-manager.ts`'s
37
+ * findCeliloRoot already uses. Returns null in any consumer install, so callers
38
+ * correctly fall through to the published-source path.
39
+ */
40
+ export function findMonorepoRoot(pkgDir: string): string | null {
30
41
  let dir = pkgDir;
31
42
  for (let i = 0; i < 10; i++) {
32
- if (existsSync(join(dir, 'modules'))) return dir;
43
+ if (existsSync(join(dir, 'apps', 'celilo', 'package.json'))) return dir;
33
44
  const parent = resolve(dir, '..');
34
45
  if (parent === dir) break;
35
46
  dir = parent;
@@ -171,16 +182,24 @@ async function stageSimulatorInputs(pkgDir: string): Promise<void> {
171
182
  /**
172
183
  * The published @celilo/* packages the npm-registry-sim must serve so
173
184
  * install.sh's `bun add -g @celilo/cli @celilo/event-bus @celilo/e2e`
174
- * resolves its full transitive closure against the sim. Mirrors
175
- * PACKAGES_TO_PACK in scripts/pack-celilo-packages.ts (the monorepo
176
- * equivalent that packs from workspace source instead of fetching).
185
+ * resolves its full transitive closure against the sim.
186
+ *
187
+ * This is the ONE place the install closure is a literal rather than derived:
188
+ * build.ts runs consumer-side (inside the published @celilo/e2e tarball, with
189
+ * NO monorepo present), so it cannot read the workspace package.json set at
190
+ * runtime the way the monorepo packer does. `installClosure(readWorkspace())`
191
+ * is the source of truth; this literal must match it, and
192
+ * published-packages-in-sync.test.ts fails the instant they drift (that test is
193
+ * why forgetting a package here can't recur — see WORKSPACE_PACKAGE_GRAPH.md).
194
+ * Keep this sorted the same as installClosure()'s topo output for easy diffing.
177
195
  */
178
- const PUBLISHED_CELILO_PACKAGES = [
179
- '@celilo/cli',
196
+ export const PUBLISHED_CELILO_PACKAGES = [
180
197
  '@celilo/capabilities',
181
198
  '@celilo/cli-display',
182
- '@celilo/e2e',
199
+ '@celilo/core',
183
200
  '@celilo/event-bus',
201
+ '@celilo/e2e',
202
+ '@celilo/cli',
184
203
  ] as const;
185
204
 
186
205
  /**
@@ -25,16 +25,31 @@ export const COMMANDS: CommandDef[] = [
25
25
  name: 'run',
26
26
  description: 'Run e2e tests — pass a module path or pattern, or --all / --all-modules',
27
27
  flags: [
28
- { name: '--all', description: 'Run the full regression: every module suite + top-level e2e/tests/' },
29
- { name: '--complete', description: 'Alias of --all: run every test, including ci-unsafe (quarantined) ones' },
30
- { name: '--ci-safe', description: 'Full regression EXCEPT tests marked cele2e-ci-unsafe (used by the nightly)' },
28
+ {
29
+ name: '--all',
30
+ description:
31
+ 'Full regression: every module suite + top-level e2e/tests/ (quarantined cele2e-ci-unsafe tests skipped)',
32
+ },
33
+ {
34
+ name: '--complete',
35
+ description:
36
+ 'Run EVERYTHING including quarantined (cele2e-ci-unsafe) tests — the only mode that opts into them',
37
+ },
38
+ {
39
+ name: '--ci-safe',
40
+ description:
41
+ 'Alias for the default full regression (quarantined tests skipped); kept for the nightly',
42
+ },
31
43
  { name: '--all-modules', description: 'Run e2e tests for all modules with e2e/ directories' },
32
44
  { name: '--keep', description: 'Keep network running after tests' },
33
45
  { name: '--reuse', description: 'Reuse existing network if running' },
34
46
  { name: '--live', description: 'Use live (non-simulated) internet' },
35
47
  { name: '--published', description: 'Use published .netapp packages' },
36
48
  { name: '--notify', description: 'Desktop notification when the run finishes' },
37
- { name: '--shuffle', description: 'Randomize test order to surface order-dependent bugs (seed is logged)' },
49
+ {
50
+ name: '--shuffle',
51
+ description: 'Randomize test order to surface order-dependent bugs (seed is logged)',
52
+ },
38
53
  { name: '--seed=', description: 'Replay a specific --shuffle order (e.g. --seed=12345)' },
39
54
  ],
40
55
  },
@@ -65,7 +80,10 @@ export const COMMANDS: CommandDef[] = [
65
80
  description: 'Rebuild Docker images and package all modules as .netapps',
66
81
  flags: [
67
82
  { name: '--save', description: 'Save Docker images to tarball after build' },
68
- { name: '--skip-modules', description: 'Skip .netapp packaging (only rebuild Docker images)' },
83
+ {
84
+ name: '--skip-modules',
85
+ description: 'Skip .netapp packaging (only rebuild Docker images)',
86
+ },
69
87
  ],
70
88
  },
71
89
  {
@@ -109,7 +127,17 @@ export const COMMANDS: CommandDef[] = [
109
127
 
110
128
  export const UP_PRESETS = ['--caddy', '--full-stack', '--infrastructure'];
111
129
 
112
- export const RUN_FLAGS = ['--all', '--complete', '--ci-safe', '--all-modules', '--keep', '--reuse', '--live', '--published', '--notify'];
130
+ export const RUN_FLAGS = [
131
+ '--all',
132
+ '--complete',
133
+ '--ci-safe',
134
+ '--all-modules',
135
+ '--keep',
136
+ '--reuse',
137
+ '--live',
138
+ '--published',
139
+ '--notify',
140
+ ];
113
141
 
114
142
  export const DOWN_FLAGS = ['--keep', '--all'];
115
143
 
package/src/cli/index.ts CHANGED
@@ -19,19 +19,27 @@ import { spawnSync } from 'node:child_process';
19
19
  import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
20
20
  import { basename, dirname, join, resolve } from 'node:path';
21
21
  import { parse as parseYaml } from 'yaml';
22
+ import {
23
+ E2eBusyError,
24
+ acquireRunLock,
25
+ clearLock,
26
+ formatBusy,
27
+ lockStatus,
28
+ markKept,
29
+ } from '../run-lock';
22
30
  import { runBuild } from './build';
23
- import { runScaffold } from './scaffold';
24
31
  import { generateBashCompletion, generateZshCompletion, getCompletions } from './completion';
25
32
  import { findAllModules, findModulesWithMatchingTest } from './module-discovery';
26
- import { acquireRunLock, clearLock, E2eBusyError, formatBusy, lockStatus, markKept } from '../run-lock';
33
+ import { runScaffold } from './scaffold';
27
34
 
28
35
  const PKG_DIR = resolve(import.meta.dir, '../..');
29
36
  const BIN_DIR = join(PKG_DIR, 'bin');
30
37
 
31
-
32
38
  function readManifestTestsDir(manifestPath: string): string | undefined {
33
39
  try {
34
- const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as { e2e?: { tests_dir?: string } };
40
+ const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as {
41
+ e2e?: { tests_dir?: string };
42
+ };
35
43
  return parsed?.e2e?.tests_dir;
36
44
  } catch {
37
45
  return undefined;
@@ -90,14 +98,26 @@ function discoverSuiteTests(suiteRoot: string): { name: string; file: string; gr
90
98
  ? resolve(moduleDir, readManifestTestsDir(join(moduleDir, 'manifest.yml')) ?? 'e2e')
91
99
  : tp;
92
100
  if (!existsSync(testsDir)) continue;
93
- for (const f of readdirSync(testsDir).filter((f) => f.endsWith('.test.ts')).sort()) {
94
- out.push({ name: f.replace(/\.test\.ts$/, ''), file: join(testsDir, f), group: basename(moduleDir) });
101
+ for (const f of readdirSync(testsDir)
102
+ .filter((f) => f.endsWith('.test.ts'))
103
+ .sort()) {
104
+ out.push({
105
+ name: f.replace(/\.test\.ts$/, ''),
106
+ file: join(testsDir, f),
107
+ group: basename(moduleDir),
108
+ });
95
109
  }
96
110
  }
97
111
  const topLevel = join(suiteRoot, 'e2e', 'tests');
98
112
  if (existsSync(topLevel)) {
99
- for (const f of readdirSync(topLevel).filter((f) => f.endsWith('.test.ts')).sort()) {
100
- out.push({ name: f.replace(/\.test\.ts$/, ''), file: join(topLevel, f), group: '(top-level)' });
113
+ for (const f of readdirSync(topLevel)
114
+ .filter((f) => f.endsWith('.test.ts'))
115
+ .sort()) {
116
+ out.push({
117
+ name: f.replace(/\.test\.ts$/, ''),
118
+ file: join(topLevel, f),
119
+ group: '(top-level)',
120
+ });
101
121
  }
102
122
  }
103
123
  return out;
@@ -200,9 +220,10 @@ switch (command) {
200
220
  args.includes('--complete') ||
201
221
  args.includes('--ci-safe')
202
222
  ) {
203
- // --complete and --ci-safe both run the full discovery (every module +
204
- // the repo's top-level e2e/tests/). --ci-safe additionally has the runner
205
- // skip tests marked `cele2e-ci-unsafe`. --all-modules stays modules-only.
223
+ // --all / --complete / --ci-safe all run the full discovery (every module +
224
+ // the repo's top-level e2e/tests/). Quarantined (`cele2e-ci-unsafe`) tests
225
+ // are skipped by default; only --complete opts back into them (the runner
226
+ // sees the flag — it's kept below). --all-modules stays modules-only.
206
227
  const wantTopLevel =
207
228
  args.includes('--all') || args.includes('--complete') || args.includes('--ci-safe');
208
229
  const suiteRoot = resolveSuiteRoot();
@@ -222,9 +243,9 @@ switch (command) {
222
243
  }
223
244
  runScript(
224
245
  'e2e-run',
225
- // Strip the discovery aliases; KEEP --ci-safe so the runner applies the
226
- // `cele2e-ci-unsafe` quarantine exclusion.
227
- args.filter((a) => a !== '--all' && a !== '--all-modules' && a !== '--complete'),
246
+ // Strip the discovery-only aliases; KEEP --complete (the runner uses it to
247
+ // opt back into quarantined tests) and --ci-safe (harmless/no-op now).
248
+ args.filter((a) => a !== '--all' && a !== '--all-modules'),
228
249
  env,
229
250
  );
230
251
  }
@@ -236,10 +257,14 @@ switch (command) {
236
257
 
237
258
  if (modulePath) {
238
259
  const absModulePath = resolve(modulePath);
239
- runScript('e2e-run', args.filter((a) => a !== modulePath), {
240
- E2E_TEST_DIR: stateDir,
241
- E2E_MODULE_DIR: absModulePath,
242
- });
260
+ runScript(
261
+ 'e2e-run',
262
+ args.filter((a) => a !== modulePath),
263
+ {
264
+ E2E_TEST_DIR: stateDir,
265
+ E2E_MODULE_DIR: absModulePath,
266
+ },
267
+ );
243
268
  }
244
269
 
245
270
  // If cwd is not a module, walk sibling modules so the user can invoke
@@ -261,7 +286,9 @@ switch (command) {
261
286
  for (const m of matches) {
262
287
  console.error(` ${basename(m.moduleDir)}: ${m.matchedTests.join(', ')}`);
263
288
  }
264
- console.error('\nDisambiguate by running from inside the module directory or passing its path.');
289
+ console.error(
290
+ '\nDisambiguate by running from inside the module directory or passing its path.',
291
+ );
265
292
  process.exit(1);
266
293
  }
267
294
  } else if (positionals.length === 0) {
@@ -364,7 +391,9 @@ switch (command) {
364
391
  console.log(` Reuse: cele2e run --reuse`);
365
392
  console.log(` Tear down: cele2e down`);
366
393
  console.log('');
367
- } catch { /* ignore parse errors */ }
394
+ } catch {
395
+ /* ignore parse errors */
396
+ }
368
397
  }
369
398
  runScript('e2e-status', args, projectName ? { CELILO_E2E_PROJECT: projectName } : undefined);
370
399
  }
@@ -380,7 +409,10 @@ switch (command) {
380
409
  // build-infra mutates the shared Docker images — serialize against runs.
381
410
  // Released via the lock's exit handler (runBuild may process.exit on failure).
382
411
  try {
383
- acquireRunLock({ test: 'build-infra', runId: process.env.CELE2E_RUN_ID ?? crypto.randomUUID() });
412
+ acquireRunLock({
413
+ test: 'build-infra',
414
+ runId: process.env.CELE2E_RUN_ID ?? crypto.randomUUID(),
415
+ });
384
416
  } catch (err) {
385
417
  if (err instanceof E2eBusyError) {
386
418
  console.error(err.message);
@@ -922,7 +922,7 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
922
922
  if (config.managementVariant !== 'vanilla') {
923
923
  assertCliVersion(projectName, composeDir);
924
924
  console.log('[progress:start] initializing celilo | celilo initialized');
925
- // Honest harness (v2/NETWORK_CONFIG_TO_FIREWALL.md): seed ONLY the
925
+ // Honest harness (openspec/specs/progressive-zone-disclosure/spec.md): seed ONLY the
926
926
  // `internal` zone — the network the management box is genuinely on —
927
927
  // plus DNS. dmz/app/secure are NOT pre-seeded; they come into being
928
928
  // when a test deploys the firewall (net.deployFirewall()), exactly as