@celilo/e2e 0.7.17 → 0.9.0

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.
@@ -28,7 +28,33 @@ function run(cmd: string, opts?: { cwd?: string; timeout?: number }): string {
28
28
  }).trim();
29
29
  }
30
30
 
31
- let sharedInfraRunning = false;
31
+ /**
32
+ * Remove ALL celilo-e2e-* Docker resources by name prefix (#212). Used at
33
+ * start-of-run, where the run-lock guarantees no other session's stack is live.
34
+ * Graceful compose-down of the shared project first (clean network detach),
35
+ * then a force sweep that catches orphans from any prior invocation regardless
36
+ * of compose-project name or the path that created them.
37
+ */
38
+ function nukeE2eResources(e2eDir: string): void {
39
+ try {
40
+ run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} down --volumes --remove-orphans`,
41
+ { cwd: e2eDir, timeout: 30_000 });
42
+ } catch {}
43
+ try {
44
+ const ids = run('docker ps -aq --filter name=celilo-e2e', { timeout: 10_000 });
45
+ if (ids.trim()) run(`docker rm -f ${ids.replace(/\n/g, ' ')}`, { timeout: 60_000 });
46
+ } catch {}
47
+ try {
48
+ const nets = run('docker network ls --format "{{.Name}}"')
49
+ .split('\n')
50
+ .filter((n) => n.startsWith('celilo-e2e'));
51
+ for (const net of nets) {
52
+ try { run(`docker network rm ${net}`, { timeout: 5_000 }); } catch {}
53
+ }
54
+ } catch {}
55
+ try { run('docker network prune -f', { timeout: 10_000 }); } catch {}
56
+ try { run('docker volume prune -f', { timeout: 10_000 }); } catch {}
57
+ }
32
58
 
33
59
  /**
34
60
  * Check if the shared infrastructure is already running and healthy.
@@ -46,10 +72,8 @@ let sharedInfraRunning = false;
46
72
  * per-test stack failed with "network … declared as external,
47
73
  * but could not be found" when starting per-test services).
48
74
  *
49
- * Source of truth is Docker, never the module-local
50
- * `sharedInfraRunning` flag in isolation — each test file runs in a
51
- * fresh bun process so that flag is always `false` at startup. The
52
- * flag exists only as a within-process cache for redundant calls.
75
+ * Source of truth is always Docker — each test file runs in a fresh
76
+ * bun process, so there is no reliable in-process cache to lean on.
53
77
  */
54
78
  export function isSharedInfraRunning(): boolean {
55
79
  try {
@@ -58,20 +82,16 @@ export function isSharedInfraRunning(): boolean {
58
82
  { cwd: getE2eDir() },
59
83
  );
60
84
  if (containers.trim().length === 0) {
61
- sharedInfraRunning = false;
62
85
  return false;
63
86
  }
64
87
  const networks = run('docker network ls --format "{{.Name}}"');
65
88
  const networkLines = networks.split('\n');
66
89
  const haveAll = Object.values(SHARED_NETWORKS).every((n) => networkLines.includes(n));
67
90
  if (!haveAll) {
68
- sharedInfraRunning = false;
69
91
  return false;
70
92
  }
71
- sharedInfraRunning = true;
72
93
  return true;
73
94
  } catch {
74
- sharedInfraRunning = false;
75
95
  return false;
76
96
  }
77
97
  }
@@ -104,38 +124,14 @@ export async function ensureSharedInfra(): Promise<void> {
104
124
 
105
125
  const e2eDir = getE2eDir();
106
126
 
107
- // Clean up any stale shared infra resources
127
+ // Start-of-run cleanup. We hold the run-lock here (acquired in the runner /
128
+ // build path before any docker mutation), so NO other session's stack is
129
+ // live — it is safe to remove EVERY celilo-e2e-* resource by name prefix.
130
+ // This self-heals orphans left by a crashed / cross-path / cross-checkout
131
+ // prior run (e.g. a botched-mount container that wedges the next `up`),
132
+ // which the old compose-project-scoped `down` couldn't see (#212).
108
133
  console.log('[progress:start] cleaning up stale shared infrastructure | shared infra cleanup complete');
109
- try {
110
- run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} down --volumes --remove-orphans`,
111
- { cwd: e2eDir, timeout: 30_000 });
112
- } catch {}
113
-
114
- // Also clean up any per-test projects that reference our networks
115
- try {
116
- const networks = run('docker network ls --format "{{.Name}}"')
117
- .split('\n')
118
- .filter(n => n.startsWith('celilo-e2e-') && !n.startsWith(`${SHARED_PROJECT_NAME}_`));
119
- if (networks.length > 0) {
120
- const projects = [...new Set(networks.map(n => n.replace(/_[^_]+$/, '')))];
121
- for (const project of projects) {
122
- try { run(`docker compose -p ${project} down --volumes --remove-orphans`, { timeout: 30_000 }); } catch {}
123
- }
124
- }
125
- } catch {}
126
-
127
- try { run('docker network prune -f', { timeout: 10_000 }); } catch {}
128
- try { run('docker volume prune -f', { timeout: 10_000 }); } catch {}
129
-
130
- // Force-remove any leftover networks by name
131
- try {
132
- const staleNets = run('docker network ls --format "{{.Name}}"')
133
- .split('\n')
134
- .filter(n => n.startsWith('celilo-e2e'));
135
- for (const net of staleNets) {
136
- try { run(`docker network rm ${net}`, { timeout: 5_000 }); } catch {}
137
- }
138
- } catch {}
134
+ nukeE2eResources(e2eDir);
139
135
 
140
136
  // Refresh the bundled registry-server source so Dockerfile.registry's
141
137
  // COPY resolves whether we're in the monorepo (regenerated from the
@@ -188,7 +184,6 @@ export async function ensureSharedInfra(): Promise<void> {
188
184
  { cwd: e2eDir, timeout: 10_000 },
189
185
  );
190
186
  if (result.trim().length > 0) {
191
- sharedInfraRunning = true;
192
187
  console.log('[progress:done] shared infrastructure ready');
193
188
  return;
194
189
  }
@@ -237,21 +232,4 @@ export async function stopSharedInfra(): Promise<void> {
237
232
  } catch {
238
233
  // Best effort
239
234
  }
240
- sharedInfraRunning = false;
241
- }
242
-
243
- /**
244
- * Execute a command in a shared infrastructure container.
245
- */
246
- export function sharedExec(container: string, cmd: string, timeoutMs = 60_000): { stdout: string; exitCode: number } {
247
- try {
248
- const stdout = run(
249
- `docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} exec -T ${container} bash -c ${JSON.stringify(cmd)}`,
250
- { cwd: getE2eDir(), timeout: timeoutMs },
251
- );
252
- return { stdout, exitCode: 0 };
253
- } catch (err: unknown) {
254
- const e = err as { stdout?: string; status?: number };
255
- return { stdout: e.stdout?.toString() ?? '', exitCode: e.status ?? 1 };
256
- }
257
235
  }
package/src/types.ts CHANGED
@@ -193,11 +193,17 @@ export interface NetworkHandle {
193
193
  /** Resolve a DNS name from the management machine */
194
194
  dig(name: string): Promise<string>;
195
195
 
196
- /** Wait for a condition with timeout */
196
+ /**
197
+ * Wait for a condition with timeout. On timeout, `onTimeout` (if given) is
198
+ * invoked to collect live-state diagnostics that are appended to the error —
199
+ * so a readiness failure self-diagnoses instead of speculating
200
+ * (e2e-confidence #255).
201
+ */
197
202
  waitFor(
198
203
  check: () => Promise<boolean>,
199
204
  timeoutMs: number,
200
205
  label: string,
206
+ onTimeout?: () => string | Promise<string>,
201
207
  ): Promise<void>;
202
208
 
203
209
  /**
@@ -342,6 +348,15 @@ export interface ResponderValues {
342
348
  secretValues?: Record<string, string>;
343
349
  }
344
350
  >;
351
+ /**
352
+ * Aspect-consent decisions (ISS-0027 / #262). When a headless deploy emits
353
+ * `aspect.required.<module>.<role>`, the responder replies `{ consented }`
354
+ * so a module's base_module_aspect fan-out is approved without a TTY and
355
+ * without `--accept-aspects` at import. Lookup precedence: `<module>.<role>`,
356
+ * then `<module>`, then the `'*'` wildcard. Absent → skipped (never
357
+ * silently approved). Passed straight through to `celilo events respond`.
358
+ */
359
+ aspects?: Record<string, boolean>;
345
360
  }
346
361
 
347
362
  export const ZONE_GATEWAYS: Record<Zone, string> = {
package/AGENTS.md DELETED
@@ -1,117 +0,0 @@
1
- # AGENTS.md — `@celilo/e2e`
2
-
3
- `@celilo/e2e` is a **simulated internet in Docker** for end-to-end testing of
4
- [celilo](https://celilo.computer)-deployed apps. It stands up a DNS hierarchy
5
- (root → TLD → registrar → recursive resolver), an ACME server (Pebble, standing
6
- in for Let's Encrypt), an ISP router + customer firewall with real iptables NAT,
7
- zoned LAN networks, and systemd target machines — then runs celilo *inside* it as
8
- if it were deploying to a real home lab. This file orients an AI agent to write
9
- and run those tests. **Read the in-package docs below before searching the web** —
10
- they are the source of truth.
11
-
12
- This file ships **inside the npm package**: the `./`-prefixed docs in the doc map
13
- are installed alongside it (grep them offline). Repo-relative entries point at the
14
- source repository, which the tarball does not carry.
15
-
16
- ## Mental model (learn these five things)
17
-
18
- - **The sim is a faithful internet, not a convenience harness.** Public DNS only
19
- ever answers with the customer firewall's *external* IP; the firewall DNATs
20
- inbound to the right internal host. ACME challenges validate over the public
21
- path. A test that "passes" by short-circuiting any of this is testing a placebo
22
- — see **Inviolable rules** below.
23
- - **`network()` builder** — declare zones and the machines in them, `.start()` the
24
- topology, drive celilo with `.celilo(...)`, assert, then `.stop()`. This is the
25
- whole library surface (`import { network } from '@celilo/e2e'`).
26
- - **Zones** — `internal` (home LAN), `dmz`, `app`, `secure` are the customer-side
27
- networks; `isp-external` / `internet-external` model the public internet. Public
28
- IPs live only on the public networks; the firewall containers are the only place
29
- the two address spaces meet (via NAT/DNAT).
30
- - **Two consumption modes** — as a **library** (`network()` in a `bun test` file)
31
- or via the **`cele2e` CLI** (`cele2e run <test>`, `build-infra`, `list`). The
32
- shared infra (`celilo-e2e-shared`) is a *global* Docker compose project,
33
- independent of which checkout started it.
34
- - **`build-infra` gate** — anything baked into a Docker image (`docker/`,
35
- `config/`, `simulators/`, installed package tarballs) needs `cele2e build-infra`
36
- before it takes effect. Pure `src/*.ts` changes do not (bun runs TS directly).
37
-
38
- ## Writing a test (the happy path)
39
-
40
- ```ts
41
- import { test, expect } from 'bun:test';
42
- import { network } from '@celilo/e2e';
43
-
44
- test('caddy serves TLS in the dmz', async () => {
45
- const net = await network()
46
- .dmz({ caddy: '10.0.10.10' }) // machine name → zone-side IP
47
- .start();
48
- try {
49
- await net.celilo('module import caddy');
50
- await net.celilo('module deploy caddy --no-interactive');
51
- // assert against the deployed service via the public path, not the container IP
52
- } finally {
53
- await net.stop();
54
- }
55
- });
56
- ```
57
-
58
- - Prefer the **fixtures** in `./src/fixtures.ts` (`CADDY_DEPLOYMENT`,
59
- `FULL_STACK`, `TECHNITIUM_STACK`, …) over hand-rolling topology.
60
- - Drive **everything through the celilo CLI** or the module's documented surface —
61
- never `docker exec` to hand-edit a deployed container's config (that's the bug
62
- you're trying to catch).
63
- - Poll on the **observable condition** the deploy is meant to satisfy
64
- (`VantageProbe`, DNS resolution, an HTTP header) — never `setTimeout` to mask a
65
- race.
66
-
67
- ## CLI you'll use
68
-
69
- ```
70
- cele2e list # enumerate every test (module suites + top-level)
71
- cele2e build-infra # rebuild Docker images (required after image-baked changes)
72
- cele2e run <test> [--keep] # run one test; --keep leaves the network up for debugging
73
- cele2e run --all # full regression (every suite); --all-modules = modules only
74
- ```
75
-
76
- For Claude-driven / detached runs, fire via the repo wrapper
77
- `infra/scripts/cele2e-run.sh <test> [--keep|--build]` (fire-and-forget RUNID/LOG
78
- contract) rather than calling `cele2e run` from a long-lived watcher.
79
-
80
- ## Inviolable rules (breaking any turns the suite into a placebo)
81
-
82
- 1. **No RFC 1918 leaks past the public boundary.** Public DNS never serves an A
83
- record pointing at `10/8`, `172.16/12`, or `192.168/16` — only the firewall's
84
- external IP.
85
- 2. **Don't collapse the topology to fix a timing/connectivity bug.** Fix the
86
- *deployment ordering* (DNS record / DNAT rule / firewall opening in place
87
- before the dependent step), not the simulation.
88
- 3. **Public IPs only on the public networks.** Firewall containers bridge public
89
- and private — the only place those spaces meet, and only via NAT/DNAT.
90
- 4. **ACME challenges go through the public path** to the firewall's external IP.
91
- 5. **DDNS updates modify *public* DNS** with the firewall's external IP, never an
92
- internal target IP.
93
-
94
- Cheats (`/etc/hosts` overrides, `iptables -F`, `--insecure` in module code,
95
- hardcoded internal IPs, `|| true` on a celilo command, sleeping past a race) are
96
- fine for **bisecting** a failure but are never a **final solution** — confirm the
97
- hypothesis, then ship the real fix. Full list + rationale in `./README.md` and the
98
- repo `CLAUDE.md`.
99
-
100
- ## Doc map (read in this order)
101
-
102
- Shipped **inside this package** (offline, grep-friendly):
103
-
104
- - `./README.md` — network architecture, machines, routing, DNS, simulators,
105
- manual usage. Start here.
106
- - `./COVERAGE.md` — the array/object/map coverage audit (where N>1 paths are or
107
- aren't exercised). Consult before adding a multi-element feature test.
108
- - `./src/index.ts` — the public library exports (`network`, types, probes).
109
- - `./src/fixtures.ts` — ready-made topology presets to build tests on.
110
- - `./src/types.ts` — `NetworkHandle`, `Zone`, `MachineSpec`, `Vantage`, and the
111
- zone subnet/gateway constants.
112
-
113
- In the **source repo** (not carried in the tarball):
114
-
115
- - `infra/docs/RUNNING_CELE2E_TESTS.md` — the full operator guide; read before
116
- running the suite for real.
117
- - `https://celilo.computer/docs` — hosted docs (LAN; the repo docs lead).
package/COVERAGE.md DELETED
@@ -1,60 +0,0 @@
1
- # E2E Test Coverage Audit
2
-
3
- This document tracks how well the e2e suite exercises capability and module
4
- fields whose type is `array`, `object`, or `map` — anywhere the framework's
5
- public API admits "more than one of X."
6
-
7
- ## Why this exists
8
-
9
- A test that only ever uses N=1 will pass even when the multi-element path is
10
- broken. The bug fixed by `dns_registrar` 4.0.0 (see
11
- `apps/celilo/designs/DNS_REGISTRAR_MULTI_DOMAIN.md`) escaped the existing e2e
12
- suite for exactly this reason: the website-deploy-simple test put caddy, namecheap,
13
- and the website all on a single domain, and the multi-domain code path was
14
- never exercised.
15
-
16
- ## Audit checklist
17
-
18
- For every capability and every module manifest, identify each field whose
19
- type is `array`, `object`, or `map`. For each one, ask:
20
-
21
- 1. Does at least one e2e test exercise this field with **N > 1** distinct
22
- values?
23
- 2. Are those values **meaningfully different** (different domains, different
24
- zones, different ports — not just "two of the same kind of thing")?
25
- 3. Does the test assert behavior that would be **wrong if only the first
26
- element worked**?
27
-
28
- Anywhere all three answers aren't "yes," that's a gap to either close (write
29
- the test) or document why it's intentionally deferred.
30
-
31
- ## Coverage table
32
-
33
- | Field | N>1? | Meaningfully different? | Asserts non-first works? | Status |
34
- |---|---|---|---|---|
35
- | `namecheap.domains` | yes (`website-deploy-cross-domain`) | yes (different TLDs: `iamtheinternet.org` + `example.net`) | yes (verifies example.net DNS+TLS while iamtheinternet.org is canonical) | covered |
36
- | `namecheap.ddns_passwords` (per-domain map) | yes (same test) | yes (distinct passwords per domain) | yes (uses example.net's own password to update its A record) | covered |
37
- | `caddy.hostnames` | yes (`website-deploy-simple`) | partial (same domain in `website-deploy-simple.test.ts`); yes (cross-TLD in cross-domain test) | yes (cross-domain test serves a hostname that isn't `hostnames[0]`) | covered |
38
- | `iptables.exposed_services` | unclear — needs audit | — | — | **GAP** |
39
- | `public_web` registrations across modules | partial (`website-deploy-simple.test.ts` registers auth + website on the same domain) | needs cross-domain variant | partial | **GAP** |
40
- | `<every secret: object field>` | typically only one key | usually no | — | **AUDIT EACH** |
41
-
42
- ## Closing the gaps
43
-
44
- - **`iptables.exposed_services`**: write an e2e that exposes two services
45
- on the same machine via different ports and verifies both are reachable
46
- from outside.
47
- - **`public_web` cross-domain**: extend `website-deploy-simple.test.ts` (or a new
48
- test) to register routes on two different domains and verify each is
49
- served by the right backend.
50
- - **secret object fields**: as modules using these grow (e.g. third-party
51
- API integrations with multiple keys), add per-key tests.
52
-
53
- ## New-module checklist
54
-
55
- When authoring a module, list every field whose type permits multiple
56
- values. For each, write at least one e2e or integration test that uses
57
- **N ≥ 2** with meaningfully different values. Add a row to the table
58
- above (or document why you've deferred it).
59
-
60
- See `design/MODULE_DEVELOPMENT_GUIDE.md` for the full new-module checklist.
@@ -1,16 +0,0 @@
1
- $ORIGIN iamtheinternet.org.
2
- $TTL 300
3
-
4
- @ IN SOA ns1.iamtheinternet.org. admin.iamtheinternet.org. (
5
- 2024020247 300 60 604800 300
6
- )
7
-
8
- ; Public DNS — RFC 1918 addresses NEVER appear here. The whole point of
9
- ; the e2e simulation is to model real internet connectivity, where
10
- ; private IPs are unreachable from outside. All public-facing names
11
- ; resolve to the firewall's external IP (100.100.0.100), which DNATs
12
- ; inbound to the right internal host.
13
- @ IN NS ns1.iamtheinternet.org.
14
- ns1 IN A 100.64.0.55
15
- @ IN A 0.0.0.0
16
- www IN A 100.100.0.100