@celilo/e2e 0.3.0 → 0.5.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-build +46 -3
- package/bin/e2e-down +40 -15
- package/config/dns/iamtheinternet.org.zone +1 -1
- package/docker/Dockerfile.management +11 -1
- package/docker/Dockerfile.target-machine +3 -0
- package/docker/Dockerfile.target-machine-docker +3 -0
- package/netapps/.gitkeep +0 -0
- package/netapps/authentik.netapp +0 -0
- package/netapps/caddy.netapp +0 -0
- package/netapps/greenwave.netapp +0 -0
- package/netapps/iptables.netapp +0 -0
- package/netapps/namecheap.netapp +0 -0
- package/package.json +2 -1
- package/src/container-manager.ts +158 -3
- package/src/index.ts +3 -0
- package/src/runner.ts +50 -3
package/bin/e2e-build
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
#
|
|
3
|
-
# Pre-build all E2E Docker images.
|
|
3
|
+
# Pre-build all E2E Docker images and package standard modules as .netapps.
|
|
4
4
|
#
|
|
5
5
|
# Run once before the test suite. Subsequent test runs use the cached
|
|
6
6
|
# images and skip the build phase entirely (~5s vs ~3min per test).
|
|
7
7
|
#
|
|
8
8
|
# Usage:
|
|
9
|
-
# ./e2e/bin/e2e-build # build all images
|
|
9
|
+
# ./e2e/bin/e2e-build # build netapps + all images
|
|
10
10
|
# ./e2e/bin/e2e-build --save # build + save to tarball for colima restarts
|
|
11
11
|
#
|
|
12
12
|
|
|
@@ -14,6 +14,11 @@ set -e
|
|
|
14
14
|
|
|
15
15
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
16
|
E2E_DIR="$(dirname "$SCRIPT_DIR")"
|
|
17
|
+
PKG_DIR="$E2E_DIR"
|
|
18
|
+
REPO_ROOT="$(cd "$E2E_DIR/../.." && pwd)"
|
|
19
|
+
MODULES_DIR="$REPO_ROOT/modules"
|
|
20
|
+
NETAPPS_DIR="$E2E_DIR/netapps"
|
|
21
|
+
|
|
17
22
|
cd "$E2E_DIR"
|
|
18
23
|
|
|
19
24
|
GREEN='\033[0;32m'
|
|
@@ -21,6 +26,43 @@ DIM='\033[2m'
|
|
|
21
26
|
BOLD='\033[1m'
|
|
22
27
|
NC='\033[0m'
|
|
23
28
|
|
|
29
|
+
# ── Step 1: Package standard modules as .netapp files ─────────────────────────
|
|
30
|
+
|
|
31
|
+
STANDARD_MODULES=(namecheap greenwave iptables caddy authentik)
|
|
32
|
+
|
|
33
|
+
echo -e "${BOLD}Packaging standard modules...${NC}"
|
|
34
|
+
echo ""
|
|
35
|
+
|
|
36
|
+
mkdir -p "$NETAPPS_DIR"
|
|
37
|
+
|
|
38
|
+
for MODULE in "${STANDARD_MODULES[@]}"; do
|
|
39
|
+
SRC="$MODULES_DIR/$MODULE"
|
|
40
|
+
OUT="$NETAPPS_DIR/$MODULE.netapp"
|
|
41
|
+
|
|
42
|
+
if [ ! -d "$SRC" ]; then
|
|
43
|
+
echo -e " ${DIM}skip${NC} $MODULE ${DIM}(not found at $SRC)${NC}"
|
|
44
|
+
continue
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
printf " %-20s " "$MODULE"
|
|
48
|
+
BUILD_START=$(date +%s)
|
|
49
|
+
|
|
50
|
+
# Use the wrapper script if available, else bun directly
|
|
51
|
+
if [ -x "$REPO_ROOT/celilo" ]; then
|
|
52
|
+
"$REPO_ROOT/celilo" package "$SRC" --output "$OUT" > /dev/null 2>&1
|
|
53
|
+
else
|
|
54
|
+
bun run "$REPO_ROOT/apps/celilo/src/cli/index.ts" package "$SRC" --output "$OUT" > /dev/null 2>&1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
BUILD_END=$(date +%s)
|
|
58
|
+
SIZE=$(du -h "$OUT" 2>/dev/null | cut -f1)
|
|
59
|
+
echo -e "${GREEN}✔${NC} ${DIM}$((BUILD_END - BUILD_START))s ${SIZE}${NC}"
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
echo ""
|
|
63
|
+
|
|
64
|
+
# ── Step 2: Build Docker images ───────────────────────────────────────────────
|
|
65
|
+
|
|
24
66
|
echo -e "${BOLD}Building E2E Docker images...${NC}"
|
|
25
67
|
echo ""
|
|
26
68
|
|
|
@@ -48,7 +90,8 @@ TOTAL_TIME=$((END - START))
|
|
|
48
90
|
echo ""
|
|
49
91
|
echo -e "${GREEN}All $TOTAL images built in ${TOTAL_TIME}s${NC}"
|
|
50
92
|
|
|
51
|
-
# Optionally save to tarball
|
|
93
|
+
# ── Step 3: Optionally save images to tarball ─────────────────────────────────
|
|
94
|
+
|
|
52
95
|
if [ "$1" = "--save" ]; then
|
|
53
96
|
echo ""
|
|
54
97
|
echo -e "${BOLD}Saving images to tarball...${NC}"
|
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."
|
|
@@ -22,9 +22,19 @@ 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
|
|
25
|
+
# Install celilo CLI from npm — provides the fallback `celilo` command
|
|
26
26
|
RUN bun add -g @celilo/cli@0.1.4
|
|
27
27
|
|
|
28
|
+
# Shim: prefer /celilo source when mounted (dev loop), fall back to npm install.
|
|
29
|
+
# This means `celilo ...` runs the monorepo's source code during e2e tests
|
|
30
|
+
# instead of the pinned npm version — critical for iterating on fixes.
|
|
31
|
+
RUN printf '#!/bin/bash\nif [ -d /celilo/apps/celilo ]; then\n exec bun run /celilo/apps/celilo/src/cli/index.ts "$@"\nfi\nexec /root/.bun/bin/celilo "$@"\n' > /usr/local/bin/celilo \
|
|
32
|
+
&& chmod +x /usr/local/bin/celilo
|
|
33
|
+
|
|
34
|
+
# Standard modules pre-packaged as .netapp files — built by e2e-build.
|
|
35
|
+
# Tests import from /netapps/<module>.netapp instead of the monorepo mount.
|
|
36
|
+
COPY netapps/ /netapps/
|
|
37
|
+
|
|
28
38
|
COPY config/ssh/generate-keys.sh /config/ssh/generate-keys.sh
|
|
29
39
|
COPY config/routing/management-routes.sh /startup.sh
|
|
30
40
|
RUN chmod +x /startup.sh /config/ssh/generate-keys.sh
|
package/netapps/.gitkeep
ADDED
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/e2e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"config/",
|
|
22
22
|
"simulators/",
|
|
23
23
|
"bin/",
|
|
24
|
+
"netapps/",
|
|
24
25
|
"README.md"
|
|
25
26
|
],
|
|
26
27
|
"dependencies": {
|
package/src/container-manager.ts
CHANGED
|
@@ -158,6 +158,66 @@ async function waitFor(
|
|
|
158
158
|
throw new Error(`Timeout waiting for ${label} after ${timeoutMs}ms`);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Run `docker compose build` with per-service progress signals.
|
|
163
|
+
* Uses --progress=plain so output is parseable on non-TTY.
|
|
164
|
+
* Emits one [progress:start] per service being built so the runner
|
|
165
|
+
* shows "building <service>" as each image is compiled.
|
|
166
|
+
*/
|
|
167
|
+
async function streamingBuild(
|
|
168
|
+
projectName: string,
|
|
169
|
+
composeDir: string,
|
|
170
|
+
composeFile: string,
|
|
171
|
+
): Promise<void> {
|
|
172
|
+
return new Promise<void>((resolve, reject) => {
|
|
173
|
+
const proc = spawn(
|
|
174
|
+
'docker',
|
|
175
|
+
['compose', '-f', composeFile, '-p', projectName, 'build', '--progress=plain'],
|
|
176
|
+
{ cwd: composeDir },
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const seenServices = new Set<string>();
|
|
180
|
+
let stderr = '';
|
|
181
|
+
|
|
182
|
+
function handleLine(line: string) {
|
|
183
|
+
// --progress=plain lines look like: #5 [management 2/4] RUN apt-get ...
|
|
184
|
+
const svcMatch = line.match(/\[([a-z][a-z0-9-]*)\s+\d+\/\d+\]/);
|
|
185
|
+
if (svcMatch) {
|
|
186
|
+
const svc = svcMatch[1];
|
|
187
|
+
if (!seenServices.has(svc)) {
|
|
188
|
+
seenServices.add(svc);
|
|
189
|
+
// Each new service auto-finalizes the previous [progress:start] in the runner.
|
|
190
|
+
console.log(`[progress:start] building image: ${svc} | ${svc} image ready`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let buf = '';
|
|
196
|
+
function onData(chunk: Buffer) {
|
|
197
|
+
buf += chunk.toString();
|
|
198
|
+
const lines = buf.split('\n');
|
|
199
|
+
buf = lines.pop() ?? '';
|
|
200
|
+
for (const line of lines) handleLine(line.trim());
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
proc.stdout.on('data', onData);
|
|
204
|
+
proc.stderr.on('data', (chunk: Buffer) => {
|
|
205
|
+
stderr += chunk.toString();
|
|
206
|
+
onData(chunk);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
proc.on('close', (code) => {
|
|
210
|
+
if (buf.trim()) handleLine(buf.trim());
|
|
211
|
+
if (code === 0) {
|
|
212
|
+
console.log('[progress:done] images built');
|
|
213
|
+
resolve();
|
|
214
|
+
} else {
|
|
215
|
+
reject(new Error(`docker compose build failed (exit ${code}):\n${stderr.slice(-500)}`));
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
161
221
|
export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle> {
|
|
162
222
|
// Pre-flight: fail if live containers are running that will compete for resources.
|
|
163
223
|
try {
|
|
@@ -218,9 +278,9 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
218
278
|
const yaml = generateTestComposeYaml(config, celiloRoot);
|
|
219
279
|
writeFileSync(join(composeDir, COMPOSE_FILE), yaml);
|
|
220
280
|
|
|
221
|
-
// Build and start per-test containers only
|
|
222
|
-
|
|
223
|
-
|
|
281
|
+
// Build and start per-test containers only — stream output so the runner
|
|
282
|
+
// can show which service is currently being built (can take minutes on cold cache).
|
|
283
|
+
await streamingBuild(projectName, composeDir, COMPOSE_FILE);
|
|
224
284
|
|
|
225
285
|
console.log('[progress:start] starting containers | containers running');
|
|
226
286
|
run(`docker compose -f ${COMPOSE_FILE} -p ${projectName} up -d`, { cwd: composeDir, timeout: 120_000 });
|
|
@@ -310,6 +370,8 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
310
370
|
}
|
|
311
371
|
|
|
312
372
|
console.log('[progress:done] network ready');
|
|
373
|
+
// Emit project name so the runner can persist it if --keep is set
|
|
374
|
+
console.log(`[e2e:project] ${projectName}`);
|
|
313
375
|
|
|
314
376
|
const handle: NetworkHandle = {
|
|
315
377
|
projectName,
|
|
@@ -362,6 +424,12 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
362
424
|
waitFor,
|
|
363
425
|
|
|
364
426
|
async stop(): Promise<void> {
|
|
427
|
+
// Respect --keep / --reuse: don't tear down the network
|
|
428
|
+
if (process.env.CELILO_E2E_KEEP === '1' || process.env.CELILO_E2E_REUSE === '1') {
|
|
429
|
+
console.log(`[progress:done] network kept alive (project: ${projectName})`);
|
|
430
|
+
activeProjects.delete(projectName); // don't re-teardown on exit
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
365
433
|
console.log('[progress:start] stopping network | network stopped');
|
|
366
434
|
try {
|
|
367
435
|
run(`docker compose -f ${COMPOSE_FILE} -p ${projectName} down --volumes --remove-orphans`,
|
|
@@ -375,3 +443,90 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
375
443
|
|
|
376
444
|
return handle;
|
|
377
445
|
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Reconnect to an existing test network (previously kept alive with --keep).
|
|
449
|
+
* Returns a NetworkHandle pointing to the existing Docker project. Skips
|
|
450
|
+
* all the setup (image build, container start, celilo init) — assumes the
|
|
451
|
+
* network is already running and healthy.
|
|
452
|
+
*
|
|
453
|
+
* If the project doesn't exist, throws with an actionable error.
|
|
454
|
+
*/
|
|
455
|
+
export function reconnectNetwork(projectName: string): NetworkHandle {
|
|
456
|
+
const composeDir = PACKAGE_ROOT;
|
|
457
|
+
|
|
458
|
+
// Verify the project actually exists
|
|
459
|
+
try {
|
|
460
|
+
const result = run(
|
|
461
|
+
`docker compose -f ${COMPOSE_FILE} -p ${projectName} ps -q`,
|
|
462
|
+
{ cwd: composeDir, timeout: 10_000 },
|
|
463
|
+
);
|
|
464
|
+
if (!result.trim()) {
|
|
465
|
+
throw new Error(`No containers found for project ${projectName}`);
|
|
466
|
+
}
|
|
467
|
+
} catch (err) {
|
|
468
|
+
throw new Error(
|
|
469
|
+
`Cannot reconnect to network '${projectName}': ${err instanceof Error ? err.message : String(err)}\n` +
|
|
470
|
+
`Fix: Run without --reuse to create a fresh network, or check if containers were torn down.`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
activeProjects.add(projectName);
|
|
475
|
+
console.log(`[progress:done] reconnected to existing network (${projectName})`);
|
|
476
|
+
console.log(`[e2e:project] ${projectName}`);
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
projectName,
|
|
480
|
+
|
|
481
|
+
async celilo(cmd: string, optsOrTimeout?: number | { check?: boolean; timeoutMs?: number }): Promise<ExecResult> {
|
|
482
|
+
const check = typeof optsOrTimeout === 'object' ? (optsOrTimeout.check ?? true) : true;
|
|
483
|
+
const timeoutMs = typeof optsOrTimeout === 'number' ? optsOrTimeout : (optsOrTimeout?.timeoutMs ?? 120_000);
|
|
484
|
+
|
|
485
|
+
const result = await dockerExecAsync(projectName, composeDir, 'management',
|
|
486
|
+
`celilo ${cmd}`, timeoutMs);
|
|
487
|
+
|
|
488
|
+
if (check && result.exitCode !== 0) {
|
|
489
|
+
throw new CeliloCommandError(cmd, result);
|
|
490
|
+
}
|
|
491
|
+
return result;
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
exec(container: string, cmd: string, timeoutMs = 60_000): Promise<ExecResult> {
|
|
495
|
+
return Promise.resolve(dockerExec(projectName, composeDir, container, cmd, timeoutMs));
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
async dig(name: string): Promise<string> {
|
|
499
|
+
const result = dockerExec(projectName, composeDir, 'management',
|
|
500
|
+
`dig +short ${name}`);
|
|
501
|
+
return result.stdout.trim();
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
async debug(container = 'management'): Promise<void> {
|
|
505
|
+
console.log(`[debug:pause] ${projectName} ${container}`);
|
|
506
|
+
const signalFile = join(composeDir, `.debug-resume-${process.pid}`);
|
|
507
|
+
const start = Date.now();
|
|
508
|
+
const timeout = 86_400_000;
|
|
509
|
+
while (!existsSync(signalFile) && Date.now() - start < timeout) {
|
|
510
|
+
await new Promise(r => setTimeout(r, 500));
|
|
511
|
+
}
|
|
512
|
+
try { require('node:fs').unlinkSync(signalFile); } catch {}
|
|
513
|
+
console.log('[debug:resumed]');
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
waitFor,
|
|
517
|
+
|
|
518
|
+
async stop(): Promise<void> {
|
|
519
|
+
// Respect --keep / --reuse: don't tear down
|
|
520
|
+
if (process.env.CELILO_E2E_KEEP === '1' || process.env.CELILO_E2E_REUSE === '1') {
|
|
521
|
+
console.log(`[progress:done] network kept alive (project: ${projectName})`);
|
|
522
|
+
activeProjects.delete(projectName);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
run(`docker compose -f ${COMPOSE_FILE} -p ${projectName} down --volumes --remove-orphans`,
|
|
527
|
+
{ cwd: composeDir, timeout: 60_000 });
|
|
528
|
+
} catch {}
|
|
529
|
+
activeProjects.delete(projectName);
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
}
|
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/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
|
}
|
|
@@ -556,6 +588,21 @@ async function main() {
|
|
|
556
588
|
history[name] = result.duration;
|
|
557
589
|
}
|
|
558
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
|
+
|
|
559
606
|
if (result.status === 'pass') {
|
|
560
607
|
console.log(` ${green}✔ passed (${result.duration}s)${reset}`);
|
|
561
608
|
passed++;
|