@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
@@ -23,12 +23,13 @@ export interface FlagDef {
23
23
  export const COMMANDS: CommandDef[] = [
24
24
  {
25
25
  name: 'run',
26
- description: 'Run e2e tests (pattern filters by test file name)',
26
+ description: 'Run e2e tests — pass a module path or pattern, or --all-modules',
27
27
  flags: [
28
28
  { name: '--keep', description: 'Keep network running after tests' },
29
29
  { name: '--reuse', description: 'Reuse existing network if running' },
30
30
  { name: '--live', description: 'Use live (non-simulated) internet' },
31
31
  { name: '--published', description: 'Use published .netapp packages' },
32
+ { name: '--all-modules', description: 'Run e2e tests for all modules with e2e/ directories' },
32
33
  ],
33
34
  },
34
35
  {
@@ -92,7 +93,7 @@ export const COMMANDS: CommandDef[] = [
92
93
 
93
94
  export const UP_PRESETS = ['--caddy', '--full-stack', '--infrastructure'];
94
95
 
95
- export const RUN_FLAGS = ['--keep', '--reuse', '--live', '--published'];
96
+ export const RUN_FLAGS = ['--keep', '--reuse', '--live', '--published', '--all-modules'];
96
97
 
97
98
  export const DOWN_FLAGS = ['--keep', '--all'];
98
99
 
@@ -10,6 +10,7 @@ import {
10
10
  RUN_FLAGS,
11
11
  UP_PRESETS,
12
12
  } from './command-registry';
13
+ import { findAllTestNames } from './module-discovery';
13
14
 
14
15
  export function getCompletions(words: string[], current: number): string[] {
15
16
  const args = words[0] === 'cele2e' ? words.slice(1) : words;
@@ -26,10 +27,13 @@ export function getCompletions(words: string[], current: number): string[] {
26
27
 
27
28
  if (command === 'run' && currentIndex >= 1) {
28
29
  const used = new Set(args.slice(1));
29
- return filterSuggestions(
30
- RUN_FLAGS.filter((f) => !used.has(f)),
31
- args[currentIndex] || '',
32
- );
30
+ const flags = RUN_FLAGS.filter((f) => !used.has(f));
31
+ const currentArg = args[currentIndex] || '';
32
+ if (currentArg.startsWith('--')) {
33
+ return filterSuggestions(flags, currentArg);
34
+ }
35
+ const testNames = findAllTestNames(process.cwd());
36
+ return filterSuggestions([...testNames, ...flags], currentArg);
33
37
  }
34
38
 
35
39
  if (command === 'up' && currentIndex === 1) {
@@ -87,6 +91,13 @@ export function generateZshCompletion(): string {
87
91
  _cele2e() {
88
92
  local curcontext="$curcontext" state line
89
93
  typeset -A opt_args
94
+ # Capture the original command line — _arguments mutates words/CURRENT
95
+ # as it consumes arguments, but our dynamic completer needs the originals.
96
+ # zsh dynamic scoping makes these visible to nested functions.
97
+ local -a _cele2e_orig_words
98
+ local _cele2e_orig_current
99
+ _cele2e_orig_words=( "\${words[@]}" )
100
+ _cele2e_orig_current=$CURRENT
90
101
 
91
102
  _arguments -C \\
92
103
  '1: :_cele2e_commands' \\
@@ -101,7 +112,8 @@ _cele2e() {
101
112
  '--reuse[Reuse existing network if running]' \\
102
113
  '--live[Use live (non-simulated) internet]' \\
103
114
  '--published[Use published .netapp packages]' \\
104
- '*:test pattern:'
115
+ '--all-modules[Run e2e tests for all modules with e2e/ directories]' \\
116
+ '*::test name or module path:_cele2e_run_args'
105
117
  ;;
106
118
  up)
107
119
  _arguments '1: :_cele2e_up_presets'
@@ -166,6 +178,18 @@ _cele2e_completion_shells() {
166
178
  _describe 'shell' shells
167
179
  }
168
180
 
181
+ # Dynamic completion for 'cele2e run' positional args — calls the CLI to
182
+ # discover test names from modules under the current directory. Uses the
183
+ # original (un-stripped) words array captured by _cele2e.
184
+ _cele2e_run_args() {
185
+ local -a completions
186
+ local IFS=$'\\n'
187
+ completions=( \${(f)"$(cele2e --get-completions "\${_cele2e_orig_words[@]}" "$((_cele2e_orig_current - 1))" 2>/dev/null)"} )
188
+ if (( \${#completions[@]} > 0 )); then
189
+ _describe 'test' completions
190
+ fi
191
+ }
192
+
169
193
  _cele2e "$@"
170
194
  `;
171
195
  }
package/src/cli/index.ts CHANGED
@@ -17,21 +17,42 @@
17
17
 
18
18
  import { spawnSync } from 'node:child_process';
19
19
  import { existsSync, readFileSync, rmSync } from 'node:fs';
20
- import { join, resolve } from 'node:path';
20
+ import { basename, dirname, join, resolve } from 'node:path';
21
+ import { parse as parseYaml } from 'yaml';
21
22
  import { runBuild } from './build';
22
23
  import { runScaffold } from './scaffold';
23
24
  import { generateBashCompletion, generateZshCompletion, getCompletions } from './completion';
25
+ import { findAllModules, findModulesWithMatchingTest } from './module-discovery';
24
26
 
25
27
  const PKG_DIR = resolve(import.meta.dir, '../..');
26
28
  const BIN_DIR = join(PKG_DIR, 'bin');
27
29
 
28
- function detectTestDir(): string {
29
- if (process.env.E2E_TEST_DIR) return process.env.E2E_TEST_DIR;
30
+
31
+ function readManifestTestsDir(manifestPath: string): string | undefined {
32
+ try {
33
+ const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as { e2e?: { tests_dir?: string } };
34
+ return parsed?.e2e?.tests_dir;
35
+ } catch {
36
+ return undefined;
37
+ }
38
+ }
39
+
40
+ function detectTestDir(): { stateDir: string; testsPath: string } {
41
+ if (process.env.E2E_TEST_DIR) {
42
+ const stateDir = process.env.E2E_TEST_DIR;
43
+ return { stateDir, testsPath: join(stateDir, 'tests') };
44
+ }
30
45
  const cwd = process.cwd();
31
- // Check e2e/tests/ first — more specific than a generic tests/ directory
32
- if (existsSync(join(cwd, 'e2e', 'tests'))) return join(cwd, 'e2e');
33
- if (existsSync(join(cwd, 'tests'))) return cwd;
34
- return cwd;
46
+ const manifestPath = join(cwd, 'manifest.yml');
47
+ if (existsSync(manifestPath)) {
48
+ const testsDir = readManifestTestsDir(manifestPath);
49
+ if (testsDir) {
50
+ const testsPath = resolve(cwd, testsDir);
51
+ return { stateDir: dirname(testsPath), testsPath };
52
+ }
53
+ }
54
+ if (existsSync(join(cwd, 'tests'))) return { stateDir: cwd, testsPath: join(cwd, 'tests') };
55
+ return { stateDir: cwd, testsPath: join(cwd, 'tests') };
35
56
  }
36
57
 
37
58
  function runScript(script: string, args: string[], env?: Record<string, string>): never {
@@ -82,8 +103,9 @@ Options for \`build-infra\`:
82
103
  --skip-modules Skip .netapp packaging (only rebuild Docker images)
83
104
 
84
105
  Examples:
85
- cele2e run # run all tests
106
+ cele2e run # in a module: run its tests; outside: run all modules' tests
86
107
  cele2e run deploy # run tests matching "deploy"
108
+ cele2e run caddy-direct-internet # from modules/, finds the matching module automatically
87
109
  cele2e run --reuse deploy # reuse existing network
88
110
  cele2e up --full-stack # interactive full-stack environment
89
111
  cele2e shell caddy # debug running caddy container
@@ -95,7 +117,7 @@ Examples:
95
117
  }
96
118
 
97
119
  const [, , command, ...args] = process.argv;
98
- const testDir = detectTestDir();
120
+ const { stateDir, testsPath: detectedTestsPath } = detectTestDir();
99
121
 
100
122
  // Handle --get-completions before routing (used by shell completion scripts)
101
123
  if (command === '--get-completions') {
@@ -107,20 +129,68 @@ if (command === '--get-completions') {
107
129
  }
108
130
 
109
131
  switch (command) {
110
- case 'run':
111
- runScript('e2e-run', args, { E2E_TEST_DIR: testDir });
132
+ case 'run': {
133
+ // Check if any non-flag arg is a module path (has manifest.yml)
134
+ const modulePath = args
135
+ .filter((a) => !a.startsWith('--'))
136
+ .find((a) => existsSync(join(resolve(a), 'manifest.yml')));
137
+
138
+ if (modulePath) {
139
+ const absModulePath = resolve(modulePath);
140
+ runScript('e2e-run', args.filter((a) => a !== modulePath), {
141
+ E2E_TEST_DIR: stateDir,
142
+ E2E_MODULE_DIR: absModulePath,
143
+ });
144
+ }
145
+
146
+ // If cwd is not a module, walk sibling modules so the user can invoke
147
+ // `cele2e run` from infra/modules/ (or the repo root) without cd-ing in.
148
+ const cwdHasManifest = existsSync(join(process.cwd(), 'manifest.yml'));
149
+ const positionals = args.filter((a) => !a.startsWith('--'));
150
+
151
+ if (!cwdHasManifest) {
152
+ if (positionals.length === 1) {
153
+ const matches = findModulesWithMatchingTest(process.cwd(), positionals[0]);
154
+ if (matches.length === 1) {
155
+ runScript('e2e-run', args, {
156
+ E2E_TEST_DIR: stateDir,
157
+ E2E_MODULE_DIR: matches[0].moduleDir,
158
+ });
159
+ }
160
+ if (matches.length > 1) {
161
+ console.error(`Multiple modules contain tests matching "${positionals[0]}":`);
162
+ for (const m of matches) {
163
+ console.error(` ${basename(m.moduleDir)}: ${m.matchedTests.join(', ')}`);
164
+ }
165
+ console.error('\nDisambiguate by running from inside the module directory or passing its path.');
166
+ process.exit(1);
167
+ }
168
+ } else if (positionals.length === 0) {
169
+ const allModules = findAllModules(process.cwd());
170
+ if (allModules.length > 0) {
171
+ runScript('e2e-run', args, {
172
+ E2E_TEST_DIR: stateDir,
173
+ E2E_MODULE_DIRS: allModules.join('\n'),
174
+ });
175
+ }
176
+ }
177
+ }
178
+
179
+ runScript('e2e-run', args, { E2E_TEST_DIR: stateDir, E2E_TESTS_PATH: detectedTestsPath });
180
+ break;
181
+ }
112
182
 
113
183
  case 'up':
114
184
  runScript('e2e-up', args);
115
185
 
116
186
  case 'down':
117
- runScript('e2e-down', args, { E2E_TEST_DIR: testDir });
187
+ runScript('e2e-down', args, { E2E_TEST_DIR: stateDir });
118
188
 
119
189
  case 'shell':
120
190
  runScript('e2e-shell', args);
121
191
 
122
192
  case 'status': {
123
- const persistentFile = join(testDir, '.e2e-persistent.json');
193
+ const persistentFile = join(stateDir, '.e2e-persistent.json');
124
194
  let projectName: string | undefined;
125
195
  if (existsSync(persistentFile)) {
126
196
  try {
@@ -150,7 +220,7 @@ switch (command) {
150
220
  }
151
221
 
152
222
  case 'clear-timing': {
153
- const timingFile = join(testDir, '.e2e-timing.json');
223
+ const timingFile = join(stateDir, '.e2e-timing.json');
154
224
  if (existsSync(timingFile)) {
155
225
  rmSync(timingFile);
156
226
  console.log(`Cleared timing data: ${timingFile}`);
@@ -166,7 +236,7 @@ switch (command) {
166
236
  console.error('Error: scaffold requires a test name\n cele2e scaffold <name>');
167
237
  process.exit(1);
168
238
  }
169
- runScaffold({ testDir, name });
239
+ runScaffold({ testsPath: detectedTestsPath, name });
170
240
  break;
171
241
  }
172
242
 
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Walks the cwd tree to find Celilo modules and their e2e tests.
3
+ *
4
+ * Used so that `cele2e run <test-name>` works from `infra/modules/` (or the
5
+ * repo root) without needing to cd into the module first, and so tab
6
+ * completion can suggest test names discovered across all modules.
7
+ *
8
+ * Scans direct subdirs and one level deeper — covers `modules/` (where each
9
+ * subdir is a module) and `infra/` (where `modules/<name>/` is one level
10
+ * deeper).
11
+ */
12
+
13
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
14
+ import { join, resolve } from 'node:path';
15
+ import { parse as parseYaml } from 'yaml';
16
+
17
+ const MAX_DEPTH = 2;
18
+
19
+ function readManifestTestsDir(manifestPath: string): string | undefined {
20
+ try {
21
+ const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as { e2e?: { tests_dir?: string } };
22
+ return parsed?.e2e?.tests_dir;
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ }
27
+
28
+ function listTestNames(testsPath: string): string[] {
29
+ if (!existsSync(testsPath)) return [];
30
+ try {
31
+ return readdirSync(testsPath)
32
+ .filter((f) => f.endsWith('.test.ts'))
33
+ .map((f) => f.replace(/\.test\.ts$/, ''));
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ function isModule(dir: string): { manifestPath: string; testsPath: string } | null {
40
+ const manifestPath = join(dir, 'manifest.yml');
41
+ if (!existsSync(manifestPath)) return null;
42
+ // Match the runner's resolution: prefer e2e.tests_dir from the manifest,
43
+ // fall back to a conventional e2e/ directory at the module root.
44
+ const testsDir = readManifestTestsDir(manifestPath) ?? 'e2e';
45
+ return { manifestPath, testsPath: resolve(dir, testsDir) };
46
+ }
47
+
48
+ function walkModules(cwd: string, visit: (moduleDir: string, testsPath: string) => void): void {
49
+ function scan(dir: string, depth: number): void {
50
+ if (depth > MAX_DEPTH) return;
51
+ let entries: string[];
52
+ try {
53
+ entries = readdirSync(dir);
54
+ } catch {
55
+ return;
56
+ }
57
+ for (const entry of entries) {
58
+ if (entry.startsWith('.') || entry === 'node_modules') continue;
59
+ const subdir = join(dir, entry);
60
+ try {
61
+ if (!statSync(subdir).isDirectory()) continue;
62
+ } catch {
63
+ continue;
64
+ }
65
+ const mod = isModule(subdir);
66
+ if (mod) {
67
+ visit(subdir, mod.testsPath);
68
+ } else if (depth < MAX_DEPTH) {
69
+ scan(subdir, depth + 1);
70
+ }
71
+ }
72
+ }
73
+ scan(cwd, 1);
74
+ }
75
+
76
+ export interface ModuleTestMatch {
77
+ moduleDir: string;
78
+ matchedTests: string[];
79
+ }
80
+
81
+ /**
82
+ * Find modules under `cwd` whose e2e dir contains a test name including `pattern`.
83
+ * Used by `cele2e run <pattern>` when invoked outside any module dir.
84
+ */
85
+ export function findModulesWithMatchingTest(cwd: string, pattern: string): ModuleTestMatch[] {
86
+ const results: ModuleTestMatch[] = [];
87
+ walkModules(cwd, (moduleDir, testsPath) => {
88
+ const matched = listTestNames(testsPath).filter((t) => t.includes(pattern));
89
+ if (matched.length > 0) results.push({ moduleDir, matchedTests: matched });
90
+ });
91
+ return results;
92
+ }
93
+
94
+ /**
95
+ * Collect all test names reachable from `cwd`. Includes tests in cwd itself
96
+ * (when cwd is a module) and tests in any module discovered under cwd.
97
+ */
98
+ export function findAllTestNames(cwd: string): string[] {
99
+ const names = new Set<string>();
100
+ const cwdMod = isModule(cwd);
101
+ if (cwdMod) {
102
+ for (const n of listTestNames(cwdMod.testsPath)) names.add(n);
103
+ }
104
+ walkModules(cwd, (_moduleDir, testsPath) => {
105
+ for (const n of listTestNames(testsPath)) names.add(n);
106
+ });
107
+ return Array.from(names).sort();
108
+ }
109
+
110
+ /**
111
+ * Return the absolute paths of all modules discoverable under `cwd` that have
112
+ * an e2e tests directory containing at least one `*.test.ts` file.
113
+ */
114
+ export function findAllModules(cwd: string): string[] {
115
+ const modules: string[] = [];
116
+ walkModules(cwd, (moduleDir, testsPath) => {
117
+ if (listTestNames(testsPath).length > 0) modules.push(moduleDir);
118
+ });
119
+ return modules.sort();
120
+ }
@@ -10,7 +10,7 @@ const dim = '\x1b[2m';
10
10
  const reset = '\x1b[0m';
11
11
 
12
12
  interface ScaffoldOptions {
13
- testDir: string;
13
+ testsPath: string;
14
14
  name: string;
15
15
  }
16
16
 
@@ -92,12 +92,11 @@ describe('${name}', () => {
92
92
  }
93
93
 
94
94
  export function runScaffold(options: ScaffoldOptions): void {
95
- const { testDir, name } = options;
95
+ const { testsPath, name } = options;
96
96
 
97
- const testsDir = join(testDir, 'tests');
98
- mkdirSync(testsDir, { recursive: true });
97
+ mkdirSync(testsPath, { recursive: true });
99
98
 
100
- const outPath = join(testsDir, `${name}.test.ts`);
99
+ const outPath = join(testsPath, `${name}.test.ts`);
101
100
  if (existsSync(outPath)) {
102
101
  console.error(`Error: ${outPath} already exists`);
103
102
  process.exit(1);