@celilo/e2e 0.2.1 → 0.4.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.
- package/bin/e2e-down +40 -15
- package/config/dns/iamtheinternet.org.zone +2 -2
- package/docker/Dockerfile.management +4 -1
- package/package.json +1 -1
- package/src/container-manager.ts +105 -8
- package/src/docker-compose-generator.ts +3 -3
- package/src/index.ts +3 -0
- package/src/network-builder.ts +37 -0
- package/src/runner.ts +50 -12
package/bin/e2e-down
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Tear down
|
|
2
|
+
# Tear down E2E test artifacts:
|
|
3
|
+
# - Persistent test networks (from `e2e-run --keep`)
|
|
4
|
+
# - Interactive sessions (from `e2e-up`)
|
|
5
|
+
# - Orphaned test runs
|
|
3
6
|
#
|
|
4
7
|
# Usage:
|
|
5
|
-
# ./bin/e2e-down #
|
|
6
|
-
# ./bin/e2e-down --keep #
|
|
7
|
-
# ./bin/e2e-down --all #
|
|
8
|
+
# ./bin/e2e-down # tear down everything e2e-related
|
|
9
|
+
# ./bin/e2e-down --keep # stop containers but keep volumes
|
|
10
|
+
# ./bin/e2e-down --all # also stop module containers that compete for resources
|
|
8
11
|
|
|
9
12
|
set -e
|
|
10
13
|
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
@@ -19,20 +22,42 @@ for arg in "$@"; do
|
|
|
19
22
|
esac
|
|
20
23
|
done
|
|
21
24
|
|
|
22
|
-
# 1.
|
|
25
|
+
# 1. Tear down any persistent test networks (look for marker files in common test dirs)
|
|
26
|
+
for dir in "$(pwd)" "$(pwd)/../../e2e" "$(pwd)/../../../e2e"; do
|
|
27
|
+
MARKER="$dir/.e2e-persistent.json"
|
|
28
|
+
if [ -f "$MARKER" ]; then
|
|
29
|
+
PROJECT=$(bun -e "console.log(JSON.parse(require('fs').readFileSync('$MARKER', 'utf-8')).projectName)" 2>/dev/null || echo "")
|
|
30
|
+
if [ -n "$PROJECT" ]; then
|
|
31
|
+
echo "Tearing down persistent network: $PROJECT"
|
|
32
|
+
docker compose -f docker-compose.test.yml -p "$PROJECT" down --volumes --remove-orphans 2>/dev/null || true
|
|
33
|
+
fi
|
|
34
|
+
rm -f "$MARKER"
|
|
35
|
+
fi
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
# Also check E2E_TEST_DIR if set
|
|
39
|
+
if [ -n "$E2E_TEST_DIR" ] && [ -f "$E2E_TEST_DIR/.e2e-persistent.json" ]; then
|
|
40
|
+
PROJECT=$(bun -e "console.log(JSON.parse(require('fs').readFileSync('$E2E_TEST_DIR/.e2e-persistent.json', 'utf-8')).projectName)" 2>/dev/null || echo "")
|
|
41
|
+
if [ -n "$PROJECT" ]; then
|
|
42
|
+
echo "Tearing down persistent network: $PROJECT"
|
|
43
|
+
docker compose -f docker-compose.test.yml -p "$PROJECT" down --volumes --remove-orphans 2>/dev/null || true
|
|
44
|
+
fi
|
|
45
|
+
rm -f "$E2E_TEST_DIR/.e2e-persistent.json"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# 2. Stop the interactive session (if running)
|
|
23
49
|
echo "Stopping interactive session..."
|
|
24
|
-
docker compose -p "
|
|
50
|
+
docker compose -p "celilo-e2e-interactive" down --remove-orphans 2>/dev/null || true
|
|
25
51
|
|
|
26
|
-
#
|
|
27
|
-
CONTAINERS=$(docker ps -a --filter "name=
|
|
52
|
+
# 3. Kill ALL containers matching celilo-e2e-* (orphaned test runs)
|
|
53
|
+
CONTAINERS=$(docker ps -a --filter "name=celilo-e2e" --format "{{.ID}}" 2>/dev/null)
|
|
28
54
|
if [ -n "$CONTAINERS" ]; then
|
|
29
55
|
COUNT=$(echo "$CONTAINERS" | wc -l | tr -d ' ')
|
|
30
56
|
echo "Removing $COUNT orphaned e2e containers..."
|
|
31
57
|
echo "$CONTAINERS" | xargs docker rm -f 2>/dev/null || true
|
|
32
58
|
fi
|
|
33
59
|
|
|
34
|
-
#
|
|
35
|
-
# These come from local docker-compose runs, not from e2e, but starve the VM.
|
|
60
|
+
# 4. Stop module containers that compete for resources (--all flag)
|
|
36
61
|
if [ "$STOP_ALL" = true ]; then
|
|
37
62
|
MODULE_PATTERNS="authentik- caddy- build-your-own-internet-"
|
|
38
63
|
for pattern in $MODULE_PATTERNS; do
|
|
@@ -45,23 +70,23 @@ if [ "$STOP_ALL" = true ]; then
|
|
|
45
70
|
done
|
|
46
71
|
fi
|
|
47
72
|
|
|
48
|
-
#
|
|
49
|
-
NETWORKS=$(docker network ls --filter "name=
|
|
73
|
+
# 5. Remove all celilo-e2e networks
|
|
74
|
+
NETWORKS=$(docker network ls --filter "name=celilo-e2e" --format "{{.ID}}" 2>/dev/null)
|
|
50
75
|
if [ -n "$NETWORKS" ]; then
|
|
51
76
|
echo "Removing e2e networks..."
|
|
52
77
|
echo "$NETWORKS" | xargs docker network rm 2>/dev/null || true
|
|
53
78
|
fi
|
|
54
79
|
|
|
55
|
-
#
|
|
80
|
+
# 6. Remove volumes (unless --keep)
|
|
56
81
|
if [ "$KEEP_VOLUMES" = false ]; then
|
|
57
|
-
VOLUMES=$(docker volume ls --filter "name=
|
|
82
|
+
VOLUMES=$(docker volume ls --filter "name=celilo-e2e" --format "{{.Name}}" 2>/dev/null)
|
|
58
83
|
if [ -n "$VOLUMES" ]; then
|
|
59
84
|
echo "Removing e2e volumes..."
|
|
60
85
|
echo "$VOLUMES" | xargs docker volume rm 2>/dev/null || true
|
|
61
86
|
fi
|
|
62
87
|
fi
|
|
63
88
|
|
|
64
|
-
#
|
|
89
|
+
# 7. Clean up generated compose files
|
|
65
90
|
rm -f docker-compose.test.yml
|
|
66
91
|
|
|
67
92
|
echo "E2E environment cleaned."
|
|
@@ -2,14 +2,14 @@ $ORIGIN iamtheinternet.org.
|
|
|
2
2
|
$TTL 300
|
|
3
3
|
|
|
4
4
|
@ IN SOA ns1.iamtheinternet.org. admin.iamtheinternet.org. (
|
|
5
|
-
|
|
5
|
+
2024010772 300 60 604800 300
|
|
6
6
|
)
|
|
7
7
|
|
|
8
8
|
@ IN NS ns1.iamtheinternet.org.
|
|
9
9
|
ns1 IN A 100.64.0.55
|
|
10
10
|
@ IN A 0.0.0.0
|
|
11
11
|
caddy IN A 100.100.0.100
|
|
12
|
-
www IN A
|
|
12
|
+
www IN A 100.100.0.100
|
|
13
13
|
deathstar IN A 100.100.0.100
|
|
14
14
|
test IN A 0.0.0.0
|
|
15
15
|
auth IN A 100.100.0.100
|
|
@@ -22,9 +22,12 @@ RUN update-ca-certificates
|
|
|
22
22
|
# Point apt at the simulated apt cache proxy (for runtime apt usage)
|
|
23
23
|
RUN echo 'Acquire::http::Proxy "http://100.64.0.2:3142";' > /etc/apt/apt.conf.d/00proxy
|
|
24
24
|
|
|
25
|
+
# Install celilo CLI from npm — provides the `celilo` command
|
|
26
|
+
RUN bun add -g @celilo/cli@0.1.4
|
|
27
|
+
|
|
25
28
|
COPY config/ssh/generate-keys.sh /config/ssh/generate-keys.sh
|
|
26
29
|
COPY config/routing/management-routes.sh /startup.sh
|
|
27
30
|
RUN chmod +x /startup.sh /config/ssh/generate-keys.sh
|
|
28
31
|
|
|
29
|
-
WORKDIR /
|
|
32
|
+
WORKDIR /root
|
|
30
33
|
CMD ["/startup.sh"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/e2e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|
package/src/container-manager.ts
CHANGED
|
@@ -13,7 +13,12 @@ const PACKAGE_ROOT = join(__dirname, '..');
|
|
|
13
13
|
* Find the Celilo project root by walking up from the package directory.
|
|
14
14
|
* Looks for apps/celilo/ as the marker.
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Try to find the Celilo project root by walking up from the package directory.
|
|
18
|
+
* Returns undefined if not found — the CLI is pre-installed in Docker so this
|
|
19
|
+
* is only needed for development (mounting local source).
|
|
20
|
+
*/
|
|
21
|
+
function findCeliloRoot(): string | undefined {
|
|
17
22
|
let dir = PACKAGE_ROOT;
|
|
18
23
|
for (let i = 0; i < 10; i++) {
|
|
19
24
|
if (existsSync(join(dir, 'apps', 'celilo'))) {
|
|
@@ -23,10 +28,7 @@ function findCeliloRoot(): string {
|
|
|
23
28
|
if (parent === dir) break;
|
|
24
29
|
dir = parent;
|
|
25
30
|
}
|
|
26
|
-
|
|
27
|
-
'Could not find Celilo project root (directory containing apps/celilo/).\n' +
|
|
28
|
-
'Set it explicitly: network().celilo("/path/to/infra").start()',
|
|
29
|
-
);
|
|
31
|
+
return undefined;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const COMPOSE_TIMEOUT = 120_000;
|
|
@@ -218,7 +220,7 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
218
220
|
|
|
219
221
|
// Build and start per-test containers only
|
|
220
222
|
console.log('[progress:start] building Docker images | images built');
|
|
221
|
-
run(`docker compose -f ${COMPOSE_FILE} -p ${projectName} build`, { cwd: composeDir, timeout:
|
|
223
|
+
run(`docker compose -f ${COMPOSE_FILE} -p ${projectName} build`, { cwd: composeDir, timeout: 600_000 });
|
|
222
224
|
|
|
223
225
|
console.log('[progress:start] starting containers | containers running');
|
|
224
226
|
run(`docker compose -f ${COMPOSE_FILE} -p ${projectName} up -d`, { cwd: composeDir, timeout: 120_000 });
|
|
@@ -296,7 +298,7 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
296
298
|
// Initialize celilo
|
|
297
299
|
console.log('[progress:start] initializing celilo | celilo initialized');
|
|
298
300
|
const initResult = dockerExec(projectName, composeDir, 'management',
|
|
299
|
-
`
|
|
301
|
+
`celilo system init --accept-defaults \
|
|
300
302
|
network.dmz.subnet=10.0.10.0/24 \
|
|
301
303
|
network.app.subnet=10.0.20.0/24 \
|
|
302
304
|
network.secure.subnet=10.0.30.0/24 \
|
|
@@ -308,6 +310,8 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
308
310
|
}
|
|
309
311
|
|
|
310
312
|
console.log('[progress:done] network ready');
|
|
313
|
+
// Emit project name so the runner can persist it if --keep is set
|
|
314
|
+
console.log(`[e2e:project] ${projectName}`);
|
|
311
315
|
|
|
312
316
|
const handle: NetworkHandle = {
|
|
313
317
|
projectName,
|
|
@@ -317,7 +321,7 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
317
321
|
const timeoutMs = typeof optsOrTimeout === 'number' ? optsOrTimeout : (optsOrTimeout?.timeoutMs ?? 120_000);
|
|
318
322
|
|
|
319
323
|
const result = await dockerExecAsync(projectName, composeDir, 'management',
|
|
320
|
-
`
|
|
324
|
+
`celilo ${cmd}`, timeoutMs);
|
|
321
325
|
|
|
322
326
|
if (check && result.exitCode !== 0) {
|
|
323
327
|
throw new CeliloCommandError(cmd, result);
|
|
@@ -360,6 +364,12 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
360
364
|
waitFor,
|
|
361
365
|
|
|
362
366
|
async stop(): Promise<void> {
|
|
367
|
+
// Respect --keep / --reuse: don't tear down the network
|
|
368
|
+
if (process.env.CELILO_E2E_KEEP === '1' || process.env.CELILO_E2E_REUSE === '1') {
|
|
369
|
+
console.log(`[progress:done] network kept alive (project: ${projectName})`);
|
|
370
|
+
activeProjects.delete(projectName); // don't re-teardown on exit
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
363
373
|
console.log('[progress:start] stopping network | network stopped');
|
|
364
374
|
try {
|
|
365
375
|
run(`docker compose -f ${COMPOSE_FILE} -p ${projectName} down --volumes --remove-orphans`,
|
|
@@ -373,3 +383,90 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
373
383
|
|
|
374
384
|
return handle;
|
|
375
385
|
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Reconnect to an existing test network (previously kept alive with --keep).
|
|
389
|
+
* Returns a NetworkHandle pointing to the existing Docker project. Skips
|
|
390
|
+
* all the setup (image build, container start, celilo init) — assumes the
|
|
391
|
+
* network is already running and healthy.
|
|
392
|
+
*
|
|
393
|
+
* If the project doesn't exist, throws with an actionable error.
|
|
394
|
+
*/
|
|
395
|
+
export function reconnectNetwork(projectName: string): NetworkHandle {
|
|
396
|
+
const composeDir = PACKAGE_ROOT;
|
|
397
|
+
|
|
398
|
+
// Verify the project actually exists
|
|
399
|
+
try {
|
|
400
|
+
const result = run(
|
|
401
|
+
`docker compose -f ${COMPOSE_FILE} -p ${projectName} ps -q`,
|
|
402
|
+
{ cwd: composeDir, timeout: 10_000 },
|
|
403
|
+
);
|
|
404
|
+
if (!result.trim()) {
|
|
405
|
+
throw new Error(`No containers found for project ${projectName}`);
|
|
406
|
+
}
|
|
407
|
+
} catch (err) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Cannot reconnect to network '${projectName}': ${err instanceof Error ? err.message : String(err)}\n` +
|
|
410
|
+
`Fix: Run without --reuse to create a fresh network, or check if containers were torn down.`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
activeProjects.add(projectName);
|
|
415
|
+
console.log(`[progress:done] reconnected to existing network (${projectName})`);
|
|
416
|
+
console.log(`[e2e:project] ${projectName}`);
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
projectName,
|
|
420
|
+
|
|
421
|
+
async celilo(cmd: string, optsOrTimeout?: number | { check?: boolean; timeoutMs?: number }): Promise<ExecResult> {
|
|
422
|
+
const check = typeof optsOrTimeout === 'object' ? (optsOrTimeout.check ?? true) : true;
|
|
423
|
+
const timeoutMs = typeof optsOrTimeout === 'number' ? optsOrTimeout : (optsOrTimeout?.timeoutMs ?? 120_000);
|
|
424
|
+
|
|
425
|
+
const result = await dockerExecAsync(projectName, composeDir, 'management',
|
|
426
|
+
`celilo ${cmd}`, timeoutMs);
|
|
427
|
+
|
|
428
|
+
if (check && result.exitCode !== 0) {
|
|
429
|
+
throw new CeliloCommandError(cmd, result);
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
exec(container: string, cmd: string, timeoutMs = 60_000): Promise<ExecResult> {
|
|
435
|
+
return Promise.resolve(dockerExec(projectName, composeDir, container, cmd, timeoutMs));
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
async dig(name: string): Promise<string> {
|
|
439
|
+
const result = dockerExec(projectName, composeDir, 'management',
|
|
440
|
+
`dig +short ${name}`);
|
|
441
|
+
return result.stdout.trim();
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
async debug(container = 'management'): Promise<void> {
|
|
445
|
+
console.log(`[debug:pause] ${projectName} ${container}`);
|
|
446
|
+
const signalFile = join(composeDir, `.debug-resume-${process.pid}`);
|
|
447
|
+
const start = Date.now();
|
|
448
|
+
const timeout = 86_400_000;
|
|
449
|
+
while (!existsSync(signalFile) && Date.now() - start < timeout) {
|
|
450
|
+
await new Promise(r => setTimeout(r, 500));
|
|
451
|
+
}
|
|
452
|
+
try { require('node:fs').unlinkSync(signalFile); } catch {}
|
|
453
|
+
console.log('[debug:resumed]');
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
waitFor,
|
|
457
|
+
|
|
458
|
+
async stop(): Promise<void> {
|
|
459
|
+
// Respect --keep / --reuse: don't tear down
|
|
460
|
+
if (process.env.CELILO_E2E_KEEP === '1' || process.env.CELILO_E2E_REUSE === '1') {
|
|
461
|
+
console.log(`[progress:done] network kept alive (project: ${projectName})`);
|
|
462
|
+
activeProjects.delete(projectName);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
run(`docker compose -f ${COMPOSE_FILE} -p ${projectName} down --volumes --remove-orphans`,
|
|
467
|
+
{ cwd: composeDir, timeout: 60_000 });
|
|
468
|
+
} catch {}
|
|
469
|
+
activeProjects.delete(projectName);
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
@@ -227,7 +227,7 @@ export function generateSharedInfraYaml(): string {
|
|
|
227
227
|
* References the shared infrastructure's isp-external network as external.
|
|
228
228
|
* Creates its own internal, dmz, app, secure networks.
|
|
229
229
|
*/
|
|
230
|
-
export function generateTestComposeYaml(config: NetworkConfig, celiloRoot
|
|
230
|
+
export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: string): string {
|
|
231
231
|
const networks: Record<string, unknown> = {
|
|
232
232
|
internal: networkDef('192.168.0.0/24', '192.168.0.250'),
|
|
233
233
|
dmz: networkDef('10.0.10.0/24', '10.0.10.250'),
|
|
@@ -259,7 +259,7 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot = '..'
|
|
|
259
259
|
networks: { internal: { ipv4_address: '192.168.0.100' } },
|
|
260
260
|
cap_add: ['NET_ADMIN'],
|
|
261
261
|
volumes: [
|
|
262
|
-
`${celiloRoot}:/celilo
|
|
262
|
+
...(celiloRoot ? [`${celiloRoot}:/celilo`] : []),
|
|
263
263
|
'ssh-keys:/ssh-keys',
|
|
264
264
|
...(config.managementVolumes ?? []),
|
|
265
265
|
],
|
|
@@ -371,7 +371,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
371
371
|
networks: { internal: { ipv4_address: '192.168.0.100' } },
|
|
372
372
|
cap_add: ['NET_ADMIN'],
|
|
373
373
|
volumes: [
|
|
374
|
-
`${celiloRoot}:/celilo
|
|
374
|
+
...(celiloRoot ? [`${celiloRoot}:/celilo`] : []),
|
|
375
375
|
'ssh-keys:/ssh-keys',
|
|
376
376
|
...(config.managementVolumes ?? []),
|
|
377
377
|
],
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
// Builder API
|
|
25
25
|
export { network, NetworkBuilder } from './network-builder';
|
|
26
26
|
|
|
27
|
+
// Reconnect to an existing network (for --reuse mode)
|
|
28
|
+
export { reconnectNetwork } from './container-manager';
|
|
29
|
+
|
|
27
30
|
// Progress reporting (emit signals the test runner displays)
|
|
28
31
|
export { progress } from './progress';
|
|
29
32
|
|
package/src/network-builder.ts
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
1
4
|
import { startNetwork } from './container-manager';
|
|
2
5
|
import type { NetworkConfig, NetworkHandle, TopologyPreset } from './types';
|
|
3
6
|
|
|
7
|
+
/**
|
|
8
|
+
* If a mounted path looks like a celilo module (has scripts/package.json),
|
|
9
|
+
* ensure its dependencies are installed on the host before Docker starts.
|
|
10
|
+
*/
|
|
11
|
+
async function ensureModuleDeps(hostPath: string): Promise<void> {
|
|
12
|
+
// Check common locations for a module's scripts dir
|
|
13
|
+
for (const scriptsDir of [
|
|
14
|
+
join(hostPath, 'scripts'),
|
|
15
|
+
join(hostPath, 'celilo', 'scripts'),
|
|
16
|
+
]) {
|
|
17
|
+
const pkgJson = join(scriptsDir, 'package.json');
|
|
18
|
+
const nodeModules = join(scriptsDir, 'node_modules');
|
|
19
|
+
if (existsSync(pkgJson) && !existsSync(nodeModules)) {
|
|
20
|
+
console.log(`[progress:start] installing deps in ${scriptsDir} | deps installed`);
|
|
21
|
+
try {
|
|
22
|
+
execSync('bun install', { cwd: scriptsDir, timeout: 30_000, stdio: 'pipe' });
|
|
23
|
+
} catch (err) {
|
|
24
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Failed to install module dependencies in ${scriptsDir}:\n${msg}\n\n` +
|
|
27
|
+
`Fix: cd ${scriptsDir} && bun install`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
4
34
|
export class NetworkBuilder {
|
|
5
35
|
private config: NetworkConfig = {
|
|
6
36
|
topology: 'default',
|
|
@@ -82,6 +112,13 @@ export class NetworkBuilder {
|
|
|
82
112
|
}
|
|
83
113
|
|
|
84
114
|
async start(): Promise<NetworkHandle> {
|
|
115
|
+
// Pre-flight: ensure mounted module directories have deps installed.
|
|
116
|
+
// Without this, `module import` tries to `bun install` inside Docker
|
|
117
|
+
// through the simulated internet, which is slow or fails.
|
|
118
|
+
for (const vol of this.config.managementVolumes) {
|
|
119
|
+
const [hostPath] = vol.split(':');
|
|
120
|
+
await ensureModuleDeps(hostPath);
|
|
121
|
+
}
|
|
85
122
|
return startNetwork(this.config);
|
|
86
123
|
}
|
|
87
124
|
}
|
package/src/runner.ts
CHANGED
|
@@ -27,7 +27,13 @@ const PKG_DIR = resolve(import.meta.dir, '..');
|
|
|
27
27
|
const testDir = process.env.E2E_TEST_DIR || process.cwd();
|
|
28
28
|
const testsPath = join(testDir, 'tests');
|
|
29
29
|
const timingFile = join(testDir, '.e2e-timing.json');
|
|
30
|
-
const
|
|
30
|
+
const persistentFile = join(testDir, '.e2e-persistent.json');
|
|
31
|
+
|
|
32
|
+
// Parse flags out of argv
|
|
33
|
+
const rawArgs = process.argv.slice(2);
|
|
34
|
+
const flagKeep = rawArgs.includes('--keep');
|
|
35
|
+
const flagReuse = rawArgs.includes('--reuse');
|
|
36
|
+
const patterns = rawArgs.filter((a) => !a.startsWith('--'));
|
|
31
37
|
|
|
32
38
|
// ─── Colors ──────────────────────────────────────────────────────────
|
|
33
39
|
|
|
@@ -309,6 +315,7 @@ interface TestResult {
|
|
|
309
315
|
duration: number;
|
|
310
316
|
error?: string;
|
|
311
317
|
hadDebugPause?: boolean;
|
|
318
|
+
projectName?: string;
|
|
312
319
|
}
|
|
313
320
|
|
|
314
321
|
async function runTest(
|
|
@@ -323,13 +330,29 @@ async function runTest(
|
|
|
323
330
|
|
|
324
331
|
display.reset(start, expectedDuration);
|
|
325
332
|
let hadDebugPause = false;
|
|
333
|
+
let capturedProjectName: string | undefined;
|
|
326
334
|
|
|
327
335
|
return new Promise<TestResult>((resolveTest) => {
|
|
336
|
+
// Env vars let the test detect --keep / --reuse modes
|
|
337
|
+
const testEnv: NodeJS.ProcessEnv = { ...process.env };
|
|
338
|
+
if (flagKeep) testEnv.CELILO_E2E_KEEP = '1';
|
|
339
|
+
if (flagReuse) {
|
|
340
|
+
// Read the persistent project and pass it to the test
|
|
341
|
+
try {
|
|
342
|
+
const data = JSON.parse(readFileSync(persistentFile, 'utf-8'));
|
|
343
|
+
testEnv.CELILO_E2E_REUSE = '1';
|
|
344
|
+
testEnv.CELILO_E2E_PROJECT = data.projectName;
|
|
345
|
+
} catch {
|
|
346
|
+
console.error(`${red}✗ --reuse: no persistent network found. Run with --keep first.${reset}`);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
328
351
|
// 1 hour timeout per test (debug sessions can be long)
|
|
329
352
|
const proc = spawn('bun', ['test', '--timeout', '3600000', file], {
|
|
330
353
|
cwd: PKG_DIR,
|
|
331
354
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
332
|
-
env:
|
|
355
|
+
env: testEnv,
|
|
333
356
|
});
|
|
334
357
|
|
|
335
358
|
let stdout = '';
|
|
@@ -393,6 +416,13 @@ async function runTest(
|
|
|
393
416
|
continue;
|
|
394
417
|
}
|
|
395
418
|
|
|
419
|
+
// Capture project name for persistent network tracking
|
|
420
|
+
const projectMatch = trimmed.match(/\[e2e:project\] (\S+)/);
|
|
421
|
+
if (projectMatch) {
|
|
422
|
+
capturedProjectName = projectMatch[1];
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
396
426
|
parseLine(trimmed, display);
|
|
397
427
|
}
|
|
398
428
|
}
|
|
@@ -423,10 +453,11 @@ async function runTest(
|
|
|
423
453
|
status: 'suspicious',
|
|
424
454
|
duration,
|
|
425
455
|
hadDebugPause,
|
|
456
|
+
projectName: capturedProjectName,
|
|
426
457
|
error: '0 assertions — test likely exited before reaching assertions',
|
|
427
458
|
});
|
|
428
459
|
} else {
|
|
429
|
-
resolveTest({ name, status: 'pass', duration, hadDebugPause });
|
|
460
|
+
resolveTest({ name, status: 'pass', duration, hadDebugPause, projectName: capturedProjectName });
|
|
430
461
|
}
|
|
431
462
|
} else {
|
|
432
463
|
resolveTest({
|
|
@@ -434,6 +465,7 @@ async function runTest(
|
|
|
434
465
|
status: 'fail',
|
|
435
466
|
duration,
|
|
436
467
|
hadDebugPause,
|
|
468
|
+
projectName: capturedProjectName,
|
|
437
469
|
error: extractFailureMessage(lines, code ?? 1),
|
|
438
470
|
});
|
|
439
471
|
}
|
|
@@ -505,15 +537,6 @@ async function main() {
|
|
|
505
537
|
console.log(`${bold}╚══════════════════════════════════════════════════════════════╝${reset}`);
|
|
506
538
|
console.log();
|
|
507
539
|
|
|
508
|
-
// Shared infra check
|
|
509
|
-
console.log(`${bold}Shared Infrastructure${reset}`);
|
|
510
|
-
if (isSharedInfraHealthy()) {
|
|
511
|
-
console.log(` ${green}✔ already running (DNS healthy)${reset}`);
|
|
512
|
-
} else {
|
|
513
|
-
console.log(` ${dim}Will start on first test...${reset}`);
|
|
514
|
-
}
|
|
515
|
-
console.log();
|
|
516
|
-
|
|
517
540
|
const suiteStart = Date.now();
|
|
518
541
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
519
542
|
const resultsDir = join(testDir, 'results', timestamp);
|
|
@@ -565,6 +588,21 @@ async function main() {
|
|
|
565
588
|
history[name] = result.duration;
|
|
566
589
|
}
|
|
567
590
|
|
|
591
|
+
// Write persistent network info if --keep was set and we captured a project
|
|
592
|
+
if (flagKeep && result.projectName) {
|
|
593
|
+
writeFileSync(
|
|
594
|
+
persistentFile,
|
|
595
|
+
JSON.stringify({
|
|
596
|
+
projectName: result.projectName,
|
|
597
|
+
testFile: file,
|
|
598
|
+
createdAt: new Date().toISOString(),
|
|
599
|
+
}, null, 2) + '\n',
|
|
600
|
+
);
|
|
601
|
+
console.log(`${dim} Persistent network: ${result.projectName}${reset}`);
|
|
602
|
+
console.log(`${dim} Reuse: ${basename(process.argv[1])} --reuse${reset}`);
|
|
603
|
+
console.log(`${dim} Tear down: e2e-down${reset}`);
|
|
604
|
+
}
|
|
605
|
+
|
|
568
606
|
if (result.status === 'pass') {
|
|
569
607
|
console.log(` ${green}✔ passed (${result.duration}s)${reset}`);
|
|
570
608
|
passed++;
|