@celilo/e2e 0.5.1 → 0.6.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.
Files changed (38) hide show
  1. package/bin/cele2e +14 -0
  2. package/bin/e2e-cache-images +57 -0
  3. package/bin/e2e-shell +36 -13
  4. package/bin/e2e-status +8 -2
  5. package/bin/e2e-up +5 -5
  6. package/completions/install-zsh-completion.sh +78 -0
  7. package/config/dns/iamtheinternet.org.zone +1 -1
  8. package/config/routing/docker-image-preload.service +14 -0
  9. package/config/routing/docker-image-preload.sh +24 -0
  10. package/config/routing/management-routes.sh +5 -2
  11. package/docker/Dockerfile.management +2 -0
  12. package/docker/Dockerfile.target-machine +20 -0
  13. package/docker/Dockerfile.target-machine-docker +11 -19
  14. package/docker/Dockerfile.target-technitium +97 -0
  15. package/netapps/authentik.netapp +0 -0
  16. package/netapps/caddy.netapp +0 -0
  17. package/netapps/greenwave.netapp +0 -0
  18. package/netapps/homebridge.netapp +0 -0
  19. package/netapps/iptables.netapp +0 -0
  20. package/netapps/knot-unbound-internal.netapp +0 -0
  21. package/netapps/namecheap.netapp +0 -0
  22. package/netapps/technitium.netapp +0 -0
  23. package/netapps/test-ansible-output.netapp +0 -0
  24. package/package.json +7 -1
  25. package/simulators/greenwave/state.ts +7 -12
  26. package/src/cli/build.ts +204 -0
  27. package/src/cli/command-registry.ts +99 -0
  28. package/src/cli/command-tree-parser.ts +52 -0
  29. package/src/cli/completion.ts +171 -0
  30. package/src/cli/index.ts +203 -0
  31. package/src/cli/scaffold.ts +109 -0
  32. package/src/container-manager.ts +16 -6
  33. package/src/docker-compose-generator.ts +12 -10
  34. package/src/network-builder.ts +21 -0
  35. package/src/parse-line.ts +136 -0
  36. package/src/runner.ts +81 -88
  37. package/src/types.ts +4 -0
  38. package/bin/cache-docker-images +0 -46
package/src/runner.ts CHANGED
@@ -20,6 +20,7 @@
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 { parseLine } from './parse-line';
23
24
 
24
25
  // ─── Config ──────────────────────────────────────────────────────────
25
26
 
@@ -33,6 +34,9 @@ const persistentFile = join(testDir, '.e2e-persistent.json');
33
34
  const rawArgs = process.argv.slice(2);
34
35
  const flagKeep = rawArgs.includes('--keep');
35
36
  const flagReuse = rawArgs.includes('--reuse');
37
+ const flagLive = rawArgs.includes('--live');
38
+ const flagPublished = rawArgs.includes('--published');
39
+ const flagVerbose = rawArgs.includes('--verbose');
36
40
  const patterns = rawArgs.filter((a) => !a.startsWith('--'));
37
41
 
38
42
  // ─── Colors ──────────────────────────────────────────────────────────
@@ -48,7 +52,7 @@ const reset = '\x1b[0m';
48
52
  const CLEAR_LINE = '\x1b[2K\r';
49
53
  const MOVE_UP = '\x1b[A';
50
54
 
51
- const interactive = process.stdout.isTTY && !process.argv.includes('--no-interactive');
55
+ const interactive = process.stdout.isTTY && !process.argv.includes('--no-interactive') && !flagVerbose;
52
56
 
53
57
  // ─── Timing ──────────────────────────────────────────────────────────
54
58
 
@@ -199,112 +203,66 @@ class ProgressDisplay {
199
203
  this.pending = null;
200
204
  this.subEventCount = 0;
201
205
  }
202
- }
203
-
204
- // ─── Line Parsing ────────────────────────────────────────────────────
205
206
 
206
- function parseLine(line: string, display: ProgressDisplay): void {
207
- // Protocol: [progress:start] doing | done
208
- const startMatch = line.match(/\[progress:start\] (.*?) \| (.*)/);
209
- if (startMatch) {
210
- display.startStep(startMatch[1], startMatch[2]);
211
- return;
212
- }
213
-
214
- // Protocol: [progress:done] optional message
215
- const doneMatch = line.match(/\[progress:done\](.*)/);
216
- if (doneMatch) {
217
- const msg = doneMatch[1].trim();
218
- display.doneStep(msg || undefined);
219
- return;
220
- }
221
-
222
- // Protocol: [progress:fail] message
223
- const failMatch = line.match(/\[progress:fail\] (.*)/);
224
- if (failMatch) {
225
- display.failStep(failMatch[1]);
226
- return;
227
- }
228
-
229
- // Legacy: [progress] message (treat as instant event)
230
- const legacyMatch = line.match(/\[progress\] (.*)/);
231
- if (legacyMatch) {
232
- display.instantEvent(legacyMatch[1]);
233
- return;
234
- }
235
-
236
- // Celilo CLI signals — shown as sub-events under the current step.
237
- // These must NOT finalize the test's progress step.
238
- const deployMatch = line.match(/Module '([^']+)' deployed successfully/);
239
- if (deployMatch) {
240
- display.subEvent(`${green}✔${reset} ${deployMatch[1]} deployed`);
241
- return;
242
- }
243
-
244
- const importMatch = line.match(/Successfully imported module: (\S+)/);
245
- if (importMatch) {
246
- display.subEvent(`${green}✔${reset} ${importMatch[1]} imported`);
247
- return;
248
- }
249
-
250
- if (line.includes('Deploying software')) {
251
- display.subEvent('running Ansible');
252
- return;
253
- }
254
- if (line.includes('Infrastructure selected')) {
255
- display.subEvent('infrastructure selected');
256
- return;
257
- }
258
- if (line.includes('Terraform skipped')) {
259
- display.subEvent('terraform skipped (existing machine)');
260
- return;
261
- }
262
- if (line.includes('terraform init') || line.includes('Terraform init')) {
263
- display.subEvent('running terraform init');
264
- return;
265
- }
266
- if (line.includes('terraform apply') || line.includes('Terraform apply')) {
267
- display.subEvent('running terraform apply');
268
- return;
269
- }
270
-
271
- const retryMatch = line.match(/RETRYING.*?(\d+) retries left/);
272
- if (retryMatch) {
273
- display.subEvent(`health check (${retryMatch[1]} retries left)`);
274
- return;
275
- }
276
-
277
- // Error lines
278
- if (line.includes('Error:') && !line.includes('bun test') && !line.includes('ipv4: Address already')) {
279
- const err = line.replace(/.*Error: /, '').slice(0, 70);
280
- display.subEvent(`${red}✗${reset} ${err}`);
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);
281
211
  }
282
212
  }
283
213
 
284
214
  // ─── Error Extraction ────────────────────────────────────────────────
285
215
 
216
+ // Strips ANSI escape sequences so pattern matching works on bun's coloured output
217
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
218
+ function stripAnsi(s: string): string {
219
+ return s.replace(ANSI_RE, '');
220
+ }
221
+
286
222
  function extractFailureMessage(lines: string[], exitCode: number): string {
287
- const errorIdx = lines.findIndex(
288
- (l) => l.startsWith('error:') && !l.includes('bun test'),
289
- );
223
+ const plain = lines.map(stripAnsi);
224
+
225
+ // bun's own test failure: "✗ test name [Xms]" — extract the failing test name
226
+ const bunFailIdx = plain.findIndex((l) => /^\s*✗\s+\S/.test(l) && !l.includes('failed ('));
227
+ if (bunFailIdx >= 0) {
228
+ const testName = plain[bunFailIdx].replace(/^\s*✗\s+/, '').replace(/\s*\[\d+ms\].*$/, '').trim();
229
+ // Grab the error detail on the next few lines (bun indents it under the test)
230
+ const detail = plain
231
+ .slice(bunFailIdx + 1, bunFailIdx + 6)
232
+ .map((l) => l.trim())
233
+ .filter((l) => l.length > 0 && !l.startsWith('at ') && !l.startsWith('│') && l !== '└─')
234
+ .slice(0, 2)
235
+ .join(' — ');
236
+ return detail ? `${testName}: ${detail}` : testName;
237
+ }
290
238
 
239
+ // "error: ..." lines (may be indented in bun output)
240
+ const errorIdx = plain.findIndex(
241
+ (l) => l.trimStart().startsWith('error:') && !l.includes('bun test'),
242
+ );
291
243
  if (errorIdx >= 0) {
292
- const context = lines
244
+ const context = plain
293
245
  .slice(errorIdx, errorIdx + 5)
294
246
  .map((l) => l.trim())
295
247
  .filter((l) => l.length > 0 && !l.startsWith('at '));
296
248
  return context.join('\n ');
297
249
  }
298
250
 
299
- const celiloError = lines.find(
251
+ const celiloError = plain.find(
300
252
  (l) => l.includes('CeliloCommandError') || (l.includes('celilo') && l.includes('failed')),
301
253
  );
302
254
  if (celiloError) return celiloError.slice(0, 120);
303
255
 
304
- const timeout = lines.find((l) => l.includes('Timeout') || l.includes('timed out'));
256
+ const timeout = plain.find((l) => l.includes('Timeout') || l.includes('timed out'));
305
257
  if (timeout) return timeout.slice(0, 120);
306
258
 
307
- return `exit code ${exitCode}`;
259
+ // Last resort: print the last few non-empty, non-progress lines from the output
260
+ const tail = plain
261
+ .filter((l) => l.length > 0 && !l.startsWith('[progress') && !l.startsWith('[e2e:') && !l.startsWith('[ansible:'))
262
+ .slice(-3)
263
+ .map((l) => l.trim())
264
+ .join(' | ');
265
+ return tail || `exit code ${exitCode}`;
308
266
  }
309
267
 
310
268
  // ─── Test Execution ──────────────────────────────────────────────────
@@ -314,6 +272,8 @@ interface TestResult {
314
272
  status: 'pass' | 'fail' | 'suspicious';
315
273
  duration: number;
316
274
  error?: string;
275
+ /** Last ~30 lines of raw test output, shown on failure so the user doesn't have to dig in logs */
276
+ rawTail?: string;
317
277
  hadDebugPause?: boolean;
318
278
  projectName?: string;
319
279
  }
@@ -333,9 +293,19 @@ async function runTest(
333
293
  let capturedProjectName: string | undefined;
334
294
 
335
295
  return new Promise<TestResult>((resolveTest) => {
336
- // Env vars let the test detect --keep / --reuse modes
296
+ // Env vars let the test detect --keep / --reuse / --live / --published modes
337
297
  const testEnv: NodeJS.ProcessEnv = { ...process.env };
338
298
  if (flagKeep) testEnv.CELILO_E2E_KEEP = '1';
299
+ if (flagLive) {
300
+ // Resolve infra root relative to the test directory (sibling repo convention)
301
+ const inferredRoot = resolve(testDir, '../../infra');
302
+ testEnv.CELILO_E2E_INFRA_ROOT = existsSync(join(inferredRoot, 'apps', 'celilo'))
303
+ ? inferredRoot
304
+ : resolve(PKG_DIR, '..', '..'); // fallback: @celilo/e2e is inside the infra monorepo
305
+ }
306
+ if (flagPublished) {
307
+ testEnv.CELILO_E2E_INFRA_ROOT = ''; // empty = force published mode
308
+ }
339
309
  if (flagReuse) {
340
310
  // Read the persistent project and pass it to the test
341
311
  try {
@@ -438,7 +408,7 @@ async function runTest(
438
408
  if (code === 0) {
439
409
  display.flush();
440
410
  } else {
441
- display.abandon();
411
+ display.abandonFailed();
442
412
  }
443
413
 
444
414
  writeFileSync(logFile, stdout + '\n---STDERR---\n' + stderr);
@@ -460,6 +430,20 @@ async function runTest(
460
430
  resolveTest({ name, status: 'pass', duration, hadDebugPause, projectName: capturedProjectName });
461
431
  }
462
432
  } else {
433
+ // Collect a raw tail: strip ANSI, drop internal protocol lines, keep last 30
434
+ const rawTail = (stdout + stderr)
435
+ .split('\n')
436
+ .map(stripAnsi)
437
+ .map((l) => l.trimEnd())
438
+ .filter((l) =>
439
+ l.length > 0 &&
440
+ !l.startsWith('[progress') &&
441
+ !l.startsWith('[e2e:') &&
442
+ !l.startsWith('[ansible:') &&
443
+ !l.startsWith('[debug:'),
444
+ )
445
+ .slice(-30)
446
+ .join('\n');
463
447
  resolveTest({
464
448
  name,
465
449
  status: 'fail',
@@ -467,6 +451,7 @@ async function runTest(
467
451
  hadDebugPause,
468
452
  projectName: capturedProjectName,
469
453
  error: extractFailureMessage(lines, code ?? 1),
454
+ rawTail,
470
455
  });
471
456
  }
472
457
  });
@@ -615,6 +600,14 @@ async function main() {
615
600
  if (result.error) {
616
601
  console.log(` ${red} └─ ${result.error}${reset}`);
617
602
  }
603
+ if (result.rawTail) {
604
+ console.log();
605
+ console.log(`${dim} ── test output ──────────────────────────────────────${reset}`);
606
+ for (const line of result.rawTail.split('\n')) {
607
+ console.log(` ${dim}${line}${reset}`);
608
+ }
609
+ console.log(`${dim} ─────────────────────────────────────────────────────${reset}`);
610
+ }
618
611
  failed++;
619
612
  }
620
613
  console.log();
package/src/types.ts CHANGED
@@ -4,6 +4,10 @@ export interface MachineSpec {
4
4
  name: string;
5
5
  ip: string;
6
6
  zone: Zone;
7
+ /** Use the Docker-capable image (normally only app zone; set for internal machines that need Docker) */
8
+ docker?: boolean;
9
+ /** Override the Dockerfile used for this machine (relative to the e2e package) */
10
+ dockerfile?: string;
7
11
  }
8
12
 
9
13
  export type TopologyPreset = 'default' | 'direct-internet';
@@ -1,46 +0,0 @@
1
- #!/usr/bin/env bash
2
- #
3
- # Pre-pull and save Docker images needed by e2e test modules.
4
- # Saves them as tarballs that get mounted into app-zone containers
5
- # for fast loading at boot time.
6
- #
7
- # Run this once (or after updating image versions) to populate the cache.
8
- # Images are stored in e2e/docker-image-cache/
9
-
10
- set -e
11
-
12
- CACHE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/docker-image-cache"
13
- mkdir -p "$CACHE_DIR"
14
-
15
- IMAGES=(
16
- "docker.io/library/postgres:15-alpine"
17
- "docker.io/library/redis:alpine"
18
- "ghcr.io/goauthentik/server:2024.2.2"
19
- )
20
-
21
- echo "Caching Docker images for e2e tests..."
22
- echo "Cache directory: $CACHE_DIR"
23
- echo ""
24
-
25
- for image in "${IMAGES[@]}"; do
26
- # Create a safe filename from the image name
27
- filename=$(echo "$image" | sed 's|[/:]|_|g').tar
28
-
29
- if [[ -f "$CACHE_DIR/$filename" ]]; then
30
- echo "✓ $image (already cached)"
31
- continue
32
- fi
33
-
34
- echo "▸ Pulling $image..."
35
- docker pull "$image"
36
-
37
- echo " Saving to $filename..."
38
- docker save "$image" > "$CACHE_DIR/$filename"
39
-
40
- size=$(du -h "$CACHE_DIR/$filename" | cut -f1)
41
- echo "✓ $image ($size)"
42
- done
43
-
44
- echo ""
45
- echo "Cache complete. $(ls "$CACHE_DIR"/*.tar 2>/dev/null | wc -l | tr -d ' ') images cached."
46
- echo "Total size: $(du -sh "$CACHE_DIR" | cut -f1)"