@celilo/e2e 0.6.0 → 0.6.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 (43) hide show
  1. package/README.md +2 -1
  2. package/config/dns/celilo.computer.zone +16 -0
  3. package/config/dns/iamtheinternet.org.zone +6 -4
  4. package/config/dns/knot-namecheap.conf +8 -0
  5. package/config/dns/knot-tld.conf +4 -0
  6. package/config/dns/lunacycle.net.zone +16 -0
  7. package/config/dns/net.zone +11 -0
  8. package/config/dns/org.zone +5 -0
  9. package/config/dns/root.zone +2 -0
  10. package/config/dns/templates/iamtheinternet.org.zone +16 -0
  11. package/config/dns/templates/lunacycle.net.zone +16 -0
  12. package/config/resolver/unbound.conf +10 -0
  13. package/config/routing/management-routes.sh +19 -0
  14. package/config/routing/resolver-internal-routes.sh +59 -0
  15. package/docker/Dockerfile.internal-resolver +16 -0
  16. package/docker/Dockerfile.isitup +37 -0
  17. package/docker/Dockerfile.registry +34 -0
  18. package/netapps/authentik.netapp +0 -0
  19. package/netapps/caddy.netapp +0 -0
  20. package/netapps/greenwave.netapp +0 -0
  21. package/netapps/homebridge.netapp +0 -0
  22. package/netapps/iptables.netapp +0 -0
  23. package/netapps/knot-unbound-internal.netapp +0 -0
  24. package/netapps/namecheap.netapp +0 -0
  25. package/netapps/technitium.netapp +0 -0
  26. package/netapps/test-ansible-output.netapp +0 -0
  27. package/package.json +2 -1
  28. package/simulators/isitup/Caddyfile +11 -0
  29. package/simulators/isitup/entrypoint.sh +47 -0
  30. package/simulators/isitup/server.ts +112 -0
  31. package/simulators/namecheap-ddns/server.ts +31 -2
  32. package/src/cli/build.ts +17 -3
  33. package/src/cli/command-registry.ts +3 -2
  34. package/src/cli/completion.ts +29 -5
  35. package/src/cli/index.ts +85 -15
  36. package/src/cli/module-discovery.ts +120 -0
  37. package/src/cli/scaffold.ts +4 -5
  38. package/src/container-manager.ts +358 -71
  39. package/src/docker-compose-generator.ts +119 -17
  40. package/src/parse-line.ts +14 -3
  41. package/src/runner.ts +122 -165
  42. package/src/simulator-ips.ts +42 -0
  43. package/src/types.ts +24 -0
package/src/runner.ts CHANGED
@@ -20,13 +20,37 @@
20
20
  import { spawn, spawnSync, execSync } from 'node:child_process';
21
21
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
22
22
  import { basename, join, resolve } from 'node:path';
23
+ import { parse as parseYaml } from 'yaml';
24
+ import { ProgressDisplay } from '@celilo/cli-display';
23
25
  import { parseLine } from './parse-line';
24
26
 
25
27
  // ─── Config ──────────────────────────────────────────────────────────
26
28
 
27
29
  const PKG_DIR = resolve(import.meta.dir, '..');
28
30
  const testDir = process.env.E2E_TEST_DIR || process.cwd();
29
- const testsPath = join(testDir, 'tests');
31
+ /** Set by `cele2e run <module-path>` to run tests from that module's e2e/ dir */
32
+ const moduleDir = process.env.E2E_MODULE_DIR;
33
+ /** Set by `cele2e run` (no args) when invoked outside a module — runs every discovered module's tests. */
34
+ const moduleDirs = process.env.E2E_MODULE_DIRS
35
+ ? process.env.E2E_MODULE_DIRS.split('\n').filter(Boolean)
36
+ : moduleDir
37
+ ? [moduleDir]
38
+ : [];
39
+
40
+ function resolveModuleTestsDir(dir: string): string {
41
+ const manifestPath = join(dir, 'manifest.yml');
42
+ if (existsSync(manifestPath)) {
43
+ try {
44
+ const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as { e2e?: { tests_dir?: string } };
45
+ if (parsed?.e2e?.tests_dir) return resolve(dir, parsed.e2e.tests_dir);
46
+ } catch {}
47
+ }
48
+ return join(dir, 'e2e');
49
+ }
50
+
51
+ const testsPath = moduleDir
52
+ ? resolveModuleTestsDir(moduleDir)
53
+ : (process.env.E2E_TESTS_PATH ?? join(testDir, 'tests'));
30
54
  const timingFile = join(testDir, '.e2e-timing.json');
31
55
  const persistentFile = join(testDir, '.e2e-persistent.json');
32
56
 
@@ -48,10 +72,6 @@ const red = '\x1b[31m';
48
72
  const cyan = '\x1b[36m';
49
73
  const reset = '\x1b[0m';
50
74
 
51
- // Terminal control for interactive mode
52
- const CLEAR_LINE = '\x1b[2K\r';
53
- const MOVE_UP = '\x1b[A';
54
-
55
75
  const interactive = process.stdout.isTTY && !process.argv.includes('--no-interactive') && !flagVerbose;
56
76
 
57
77
  // ─── Timing ──────────────────────────────────────────────────────────
@@ -75,142 +95,6 @@ function formatDuration(secs: number): string {
75
95
  return `${secs}s`;
76
96
  }
77
97
 
78
- /** Format countdown: positive = remaining, negative = over estimate */
79
- function formatCountdown(elapsedSecs: number, expectedSecs: number | undefined): string {
80
- if (!expectedSecs) return `${dim}${String(elapsedSecs).padStart(4)}s${reset}`;
81
- const remaining = expectedSecs - elapsedSecs;
82
- if (remaining >= 0) {
83
- return `${dim}${String(remaining).padStart(4)}s${reset}`;
84
- }
85
- return `${red}${String(remaining).padStart(4)}s${reset}`;
86
- }
87
-
88
- // ─── Progress Display ────────────────────────────────────────────────
89
-
90
- interface PendingStep {
91
- doingMsg: string;
92
- doneMsg: string;
93
- startTime: number;
94
- }
95
-
96
- class ProgressDisplay {
97
- private pending: PendingStep | null = null;
98
- private testStart = 0;
99
- private expectedDuration: number | undefined;
100
- /** Number of sub-event lines printed below the current "… doing" line */
101
- private subEventCount = 0;
102
-
103
- reset(testStart: number, expectedDuration: number | undefined) {
104
- this.pending = null;
105
- this.testStart = testStart;
106
- this.expectedDuration = expectedDuration;
107
- this.subEventCount = 0;
108
- }
109
-
110
- private time(): string {
111
- const elapsed = Math.floor((Date.now() - this.testStart) / 1000);
112
- return formatCountdown(elapsed, this.expectedDuration);
113
- }
114
-
115
- /**
116
- * In interactive mode: erase the "… doing" line and all sub-event
117
- * lines below it, then print the "✔ done" line in their place.
118
- * In non-interactive mode: just append the done line.
119
- */
120
- private finalizePending() {
121
- if (!this.pending) return;
122
- if (interactive) {
123
- // Erase sub-event lines + the pending "…" line
124
- const linesToErase = this.subEventCount + 1;
125
- for (let i = 0; i < linesToErase; i++) {
126
- process.stdout.write(`${MOVE_UP}${CLEAR_LINE}`);
127
- }
128
- }
129
- process.stdout.write(` ${this.time()} ${green}✔${reset} ${this.pending.doneMsg}\n`);
130
- this.pending = null;
131
- this.subEventCount = 0;
132
- }
133
-
134
- private finalizeFailed(message: string) {
135
- if (interactive && this.pending) {
136
- const linesToErase = this.subEventCount + 1;
137
- for (let i = 0; i < linesToErase; i++) {
138
- process.stdout.write(`${MOVE_UP}${CLEAR_LINE}`);
139
- }
140
- }
141
- process.stdout.write(` ${this.time()} ${red}✗${reset} ${message}\n`);
142
- this.pending = null;
143
- this.subEventCount = 0;
144
- }
145
-
146
- startStep(doingMsg: string, doneMsg: string) {
147
- this.finalizePending();
148
- this.pending = { doingMsg, doneMsg, startTime: Date.now() };
149
- this.subEventCount = 0;
150
- process.stdout.write(` ${this.time()} ${dim}…${reset} ${doingMsg}\n`);
151
- }
152
-
153
- doneStep(overrideMessage?: string) {
154
- if (this.pending && overrideMessage) {
155
- this.pending.doneMsg = overrideMessage;
156
- }
157
- this.finalizePending();
158
- }
159
-
160
- failStep(message: string) {
161
- this.finalizeFailed(message);
162
- }
163
-
164
- instantEvent(message: string) {
165
- this.finalizePending();
166
- process.stdout.write(` ${this.time()} ${message}\n`);
167
- }
168
-
169
- /**
170
- * Show a sub-event under the current "… doing" step.
171
- * Interactive: replaces the "… doing" line with the sub-event,
172
- * then reprints "… doing" below it — so the pending line is always last.
173
- * Non-interactive: just appends indented.
174
- */
175
- subEvent(message: string) {
176
- if (interactive && this.pending) {
177
- // Replace the "… doing" line with this sub-event (permanent)
178
- process.stdout.write(`${MOVE_UP}${CLEAR_LINE}`);
179
- process.stdout.write(` ${this.time()} ${message}\n`);
180
- // Reprint the "… doing" line as the new bottom line
181
- process.stdout.write(` ${this.time()} ${dim}…${reset} ${this.pending.doingMsg}\n`);
182
- this.subEventCount++;
183
- } else {
184
- process.stdout.write(` ${this.time()} ${message}\n`);
185
- if (this.pending) this.subEventCount++;
186
- }
187
- }
188
-
189
- /** Finalize pending on success */
190
- flush() {
191
- this.finalizePending();
192
- }
193
-
194
- /** Abandon pending step on failure (erase the … line in interactive mode) */
195
- abandon() {
196
- if (!this.pending) return;
197
- if (interactive) {
198
- const linesToErase = this.subEventCount + 1;
199
- for (let i = 0; i < linesToErase; i++) {
200
- process.stdout.write(`${MOVE_UP}${CLEAR_LINE}`);
201
- }
202
- }
203
- this.pending = null;
204
- this.subEventCount = 0;
205
- }
206
-
207
- /** Like abandon() but marks the step as failed so the user can see where it broke */
208
- abandonFailed() {
209
- if (!this.pending) return;
210
- this.finalizeFailed(this.pending.doingMsg);
211
- }
212
- }
213
-
214
98
  // ─── Error Extraction ────────────────────────────────────────────────
215
99
 
216
100
  // Strips ANSI escape sequences so pattern matching works on bun's coloured output
@@ -334,9 +218,9 @@ async function runTest(
334
218
 
335
219
  const signalFile = join(PKG_DIR, `.debug-resume-${proc.pid}`);
336
220
  const border = '═'.repeat(56);
337
- process.stdout.write(`\n${bold}\x1b[33m╔${border}╗${reset}\n`);
338
- process.stdout.write(`${bold}\x1b[33m║ DEBUG: test paused ║${reset}\n`);
339
- process.stdout.write(`${bold}\x1b[33m╠${border}╣${reset}\n`);
221
+ process.stdout.write(`\n${bold}\x1b[33m╔${border}${reset}\n`);
222
+ process.stdout.write(`${bold}\x1b[33m║ DEBUG: test paused${reset}\n`);
223
+ process.stdout.write(`${bold}\x1b[33m╠${border}${reset}\n`);
340
224
  process.stdout.write(`${bold}\x1b[33m║${reset} Project: ${bold}${project}${reset}\n`);
341
225
  process.stdout.write(`${bold}\x1b[33m║${reset} Container: ${bold}${container}${reset}\n`);
342
226
  process.stdout.write(`${bold}\x1b[33m║${reset}\n`);
@@ -347,7 +231,7 @@ async function runTest(
347
231
 
348
232
  if (interactive) {
349
233
  process.stdout.write(`${bold}\x1b[33m║${reset} ${dim}Or get a shell: type 'exit' to resume the test.${reset}\n`);
350
- process.stdout.write(`${bold}\x1b[33m╚${border}╝${reset}\n\n`);
234
+ process.stdout.write(`${bold}\x1b[33m╚${border}${reset}\n\n`);
351
235
 
352
236
  spawnSync('docker', [
353
237
  'compose', '-f', 'docker-compose.test.yml', '-p', project,
@@ -358,7 +242,7 @@ async function runTest(
358
242
  });
359
243
  } else {
360
244
  process.stdout.write(`${bold}\x1b[33m║${reset} ${dim}Resume: touch ${signalFile}${reset}\n`);
361
- process.stdout.write(`${bold}\x1b[33m╚${border}╝${reset}\n\n`);
245
+ process.stdout.write(`${bold}\x1b[33m╚${border}${reset}\n\n`);
362
246
 
363
247
  // Non-interactive: just wait for the signal file (no shell)
364
248
  // The caller creates the file when they're done investigating.
@@ -393,6 +277,11 @@ async function runTest(
393
277
  continue;
394
278
  }
395
279
 
280
+ // Detect bun test failure so pending progress steps show ✗ instead of ✔
281
+ if (/^\s*\(fail\)\s/.test(trimmed) || /^\s*✗\s+.+\[/.test(trimmed)) {
282
+ display.markTestFailing();
283
+ }
284
+
396
285
  parseLine(trimmed, display);
397
286
  }
398
287
  }
@@ -484,18 +373,63 @@ function stopSharedInfra(): void {
484
373
  try { execSync('docker network prune -f', { stdio: 'pipe', timeout: 10_000 }); } catch {}
485
374
  }
486
375
 
487
- // ─── Main ────────────────────────────────────────────────────────────
376
+ // ─── Docker Check ────────────────────────────────────────────────────
488
377
 
489
- async function main() {
490
- if (!existsSync(testsPath)) {
491
- console.error(`${red}No tests/ directory found at ${testsPath}${reset}`);
378
+ function assertDockerAvailable(): void {
379
+ try {
380
+ execSync('docker info', { stdio: 'pipe', timeout: 5_000 });
381
+ } catch {
382
+ console.error(`\n${red}✗ Docker is not running.${reset}`);
383
+ console.error(` Start it with: ${bold}colima start${reset}`);
384
+ console.error(` Then retry: ${bold}cele2e run${reset}\n`);
492
385
  process.exit(1);
493
386
  }
387
+ }
388
+
389
+ // ─── Main ────────────────────────────────────────────────────────────
494
390
 
495
- let testFiles = readdirSync(testsPath)
496
- .filter((f) => f.endsWith('.test.ts'))
497
- .sort()
498
- .map((f) => resolve(testsPath, f));
391
+ async function main() {
392
+ assertDockerAvailable();
393
+
394
+ let testFiles: string[] = [];
395
+
396
+ if (moduleDirs.length > 0) {
397
+ // One or more modules — collect tests from each module's e2e dir.
398
+ for (const dir of moduleDirs) {
399
+ if (!existsSync(join(dir, 'manifest.yml'))) {
400
+ console.error(`${red}Not a celilo module: no manifest.yml found at ${dir}${reset}`);
401
+ process.exit(1);
402
+ }
403
+ const tp = resolveModuleTestsDir(dir);
404
+ if (!existsSync(tp)) {
405
+ console.error(`${red}No tests directory found at ${tp}${reset}`);
406
+ console.error(`${red}Set e2e.tests_dir in ${join(dir, 'manifest.yml')} to point to your test files.${reset}`);
407
+ process.exit(1);
408
+ }
409
+ // Auto-install e2e/ dependencies if package.json exists but node_modules is missing
410
+ const e2ePkg = join(tp, 'package.json');
411
+ const e2eNodeModules = join(tp, 'node_modules');
412
+ if (existsSync(e2ePkg) && !existsSync(e2eNodeModules)) {
413
+ process.stdout.write(`${dim}Installing e2e dependencies for ${basename(dir)}...${reset}\n`);
414
+ execSync('bun install', { cwd: tp, stdio: 'inherit' });
415
+ process.stdout.write('\n');
416
+ }
417
+ const files = readdirSync(tp)
418
+ .filter((f) => f.endsWith('.test.ts'))
419
+ .sort()
420
+ .map((f) => resolve(tp, f));
421
+ testFiles.push(...files);
422
+ }
423
+ } else {
424
+ if (!existsSync(testsPath)) {
425
+ console.error(`${red}No tests/ directory found at ${testsPath}${reset}`);
426
+ process.exit(1);
427
+ }
428
+ testFiles = readdirSync(testsPath)
429
+ .filter((f) => f.endsWith('.test.ts'))
430
+ .sort()
431
+ .map((f) => resolve(testsPath, f));
432
+ }
499
433
 
500
434
  if (patterns.length > 0) {
501
435
  testFiles = testFiles.filter((f) => patterns.some((p) => basename(f, '.test.ts').includes(p)));
@@ -506,20 +440,40 @@ async function main() {
506
440
  process.exit(1);
507
441
  }
508
442
 
443
+ // --keep keeps a single test network alive after the run; only one network
444
+ // can live in `.e2e-persistent.json` at a time, so multiple tests would
445
+ // either fight for that slot or run with no functional --keep at all. Bail
446
+ // early with a clear message instead of silently doing the wrong thing.
447
+ if (flagKeep && testFiles.length > 1) {
448
+ console.error(
449
+ `${red}--keep matched ${testFiles.length} tests; --keep only works with a single test.${reset}`,
450
+ );
451
+ console.error(
452
+ `${dim}Narrow the pattern (e.g. \`cele2e run ${basename(testFiles[0], '.test.ts')} --keep\`) or drop --keep.${reset}`,
453
+ );
454
+ process.exit(1);
455
+ }
456
+
509
457
  const history = loadTiming();
510
458
  const testNames = testFiles.map((f) => basename(f, '.test.ts'));
511
459
  const hasHistory = testNames.some((n) => history[n]);
512
460
  const suiteExpected = testNames.reduce((sum, n) => sum + (history[n] || 0), 0);
513
461
 
462
+ const moduleLabel =
463
+ moduleDirs.length === 1
464
+ ? ` — ${basename(moduleDirs[0])}`
465
+ : moduleDirs.length > 1
466
+ ? ` — ${moduleDirs.length} modules`
467
+ : '';
514
468
  console.log();
515
- console.log(`${bold}╔══════════════════════════════════════════════════════════════╗${reset}`);
516
- console.log(`${bold}║ Celilo E2E Tests ║${reset}`);
469
+ console.log(`${bold}╔══════════════════════════════════════════════════════════════${reset}`);
470
+ console.log(`${bold}║ Celilo E2E Tests${moduleLabel}${reset}`);
517
471
  if (hasHistory) {
518
- console.log(`${bold}║ ${testFiles.length} test(s) — expected ~${formatDuration(suiteExpected).padEnd(30)}║${reset}`);
472
+ console.log(`${bold}║ ${testFiles.length} test(s) — expected ~${formatDuration(suiteExpected)}${reset}`);
519
473
  } else {
520
- console.log(`${bold}║ ${testFiles.length} test(s) — first run, no timing history ║${reset}`);
474
+ console.log(`${bold}║ ${testFiles.length} test(s) — first run, no timing history${reset}`);
521
475
  }
522
- console.log(`${bold}╚══════════════════════════════════════════════════════════════╝${reset}`);
476
+ console.log(`${bold}╚══════════════════════════════════════════════════════════════${reset}`);
523
477
  console.log();
524
478
 
525
479
  const suiteStart = Date.now();
@@ -527,7 +481,10 @@ async function main() {
527
481
  const resultsDir = join(testDir, 'results', timestamp);
528
482
  mkdirSync(resultsDir, { recursive: true });
529
483
 
530
- const display = new ProgressDisplay();
484
+ const display = new ProgressDisplay({
485
+ mode: 'render',
486
+ out: { write: process.stdout.write.bind(process.stdout), isTTY: interactive },
487
+ });
531
488
  let passed = 0;
532
489
  let failed = 0;
533
490
  const results: TestResult[] = [];
@@ -585,7 +542,7 @@ async function main() {
585
542
  );
586
543
  console.log(`${dim} Persistent network: ${result.projectName}${reset}`);
587
544
  console.log(`${dim} Reuse: ${basename(process.argv[1])} --reuse${reset}`);
588
- console.log(`${dim} Tear down: e2e-down${reset}`);
545
+ console.log(`${dim} Tear down: cele2e down${reset}`);
589
546
  }
590
547
 
591
548
  if (result.status === 'pass') {
@@ -628,11 +585,11 @@ async function main() {
628
585
  const suiteDuration = Math.floor((Date.now() - suiteStart) / 1000);
629
586
 
630
587
  console.log();
631
- console.log(`${bold}╔══════════════════════════════════════════════════════════════╗${reset}`);
588
+ console.log(`${bold}╔══════════════════════════════════════════════════════════════${reset}`);
632
589
  console.log(
633
- `${bold}║ Results: ${green}${passed} passed${reset}${bold}, ${red}${failed} failed${reset}${bold} — ${formatDuration(suiteDuration)} total${' '.repeat(20)}║${reset}`,
590
+ `${bold}║ Results: ${green}${passed} passed${reset}${bold}, ${red}${failed} failed${reset}${bold} — ${formatDuration(suiteDuration)} total${reset}`,
634
591
  );
635
- console.log(`${bold}╠══════════════════════════════════════════════════════════════╣${reset}`);
592
+ console.log(`${bold}╠══════════════════════════════════════════════════════════════${reset}`);
636
593
 
637
594
  for (const r of results) {
638
595
  const icon = r.status === 'pass' ? '✓' : '✗';
@@ -643,7 +600,7 @@ async function main() {
643
600
  }
644
601
  }
645
602
 
646
- console.log(`${bold}╚══════════════════════════════════════════════════════════════╝${reset}`);
603
+ console.log(`${bold}╚══════════════════════════════════════════════════════════════${reset}`);
647
604
  console.log();
648
605
  console.log(`${dim}Full logs: ${resultsDir}${reset}`);
649
606
 
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Single source of truth for the IPs of e2e simulator containers on
3
+ * the `internet-external` Docker network.
4
+ *
5
+ * Renumbering one of these used to mean tracking down the value in
6
+ * eight-ish places (compose files, generator, zone files, resolver
7
+ * configs, routing scripts). This module is the canonical record for
8
+ * code; non-code files (Knot zones, shell scripts) still hard-code the
9
+ * values, but `scripts/check-simulator-ips.sh` flags any drift in CI.
10
+ *
11
+ * To renumber a simulator:
12
+ * 1. Update the constant here.
13
+ * 2. Run `bash packages/e2e/scripts/check-simulator-ips.sh` — it
14
+ * lists every non-code file that still has the old value.
15
+ * 3. Edit those files. Re-run the check until it passes.
16
+ * 4. `cele2e build-infra` to rebake the images.
17
+ */
18
+
19
+ export const SIMULATOR_IPS = {
20
+ /** Authoritative root DNS (`.` zone). */
21
+ ROOT_DNS: '100.64.0.53',
22
+ /** Authoritative TLD DNS for `com.`, `org.`, `net.`. */
23
+ TLD_DNS: '100.64.0.54',
24
+ /** Authoritative DNS for the customer domains; also serves the DDNS update endpoint at port 8080. */
25
+ NAMECHEAP_DNS: '100.64.0.55',
26
+ /** Internal e2e module registry (replaces production celilo.computer in tests). */
27
+ REGISTRY: '100.64.0.56',
28
+ /** External-website prober simulator (mimics isitup.org). */
29
+ ISITUP: '100.64.0.57',
30
+ /** Pebble ACME server (replaces production Let's Encrypt). */
31
+ PEBBLE: '100.64.0.100',
32
+ } as const;
33
+
34
+ export type SimulatorIpName = keyof typeof SIMULATOR_IPS;
35
+
36
+ /**
37
+ * Iterable list of `[name, ip]` pairs — useful for the sanity check
38
+ * script and any future consumer that wants to enumerate the set.
39
+ */
40
+ export const SIMULATOR_IP_ENTRIES: ReadonlyArray<readonly [SimulatorIpName, string]> = (
41
+ Object.entries(SIMULATOR_IPS) as Array<[SimulatorIpName, string]>
42
+ ).map(([k, v]) => [k, v] as const);
package/src/types.ts CHANGED
@@ -31,6 +31,12 @@ export interface NetworkConfig {
31
31
  * If not set, auto-detected by walking up from the package directory.
32
32
  */
33
33
  celiloRoot?: string;
34
+ /**
35
+ * Skip DNS zone scrubbing before the test starts. Useful for --keep
36
+ * debugging where you want to inspect the exact DNS state left by a
37
+ * previous test. Default: false (scrub runs).
38
+ */
39
+ skipDnsScrub?: boolean;
34
40
  }
35
41
 
36
42
  export interface ExecResult {
@@ -81,6 +87,24 @@ export interface NetworkHandle {
81
87
  label: string,
82
88
  ): Promise<void>;
83
89
 
90
+ /**
91
+ * Set the Caddy ACME CA to the Pebble endpoint so TLS certificates are
92
+ * issued by the simulated Let's Encrypt rather than the real one.
93
+ *
94
+ * Call this after importing the caddy module and before deploying it.
95
+ * Every e2e test that involves TLS must call this — it is not automatic
96
+ * so the call site is self-documenting.
97
+ */
98
+ configureAcme(): Promise<void>;
99
+
100
+ /**
101
+ * Package a local module and upload it to the e2e registry so it can be
102
+ * imported via `celilo module import public-registry <name>`.
103
+ *
104
+ * @param localPath - Absolute or relative path to the module source directory
105
+ */
106
+ publishModule(localPath: string): Promise<void>;
107
+
84
108
  /** Tear down the entire network */
85
109
  stop(): Promise<void>;
86
110