import { execSync } from "node:child_process"; import { request } from "node:https"; import { Socket } from "node:net"; /** * Brings up the device topology and waits until it is reachable. * * - docker mode (default): starts the two `meshtasticd` sim nodes via compose. * - hardware mode (E2E_DEVICE_MODE=hardware): skips compose; expects the env * endpoints to point at real devices. */ const MODE = process.env.E2E_DEVICE_MODE ?? "docker"; const COMPOSE_FILE = "e2e/device/docker-compose.yml"; const NODE_A_URL = process.env.E2E_NODE_A_URL ?? "https://127.0.0.1:9443"; const PEER_HOST = process.env.E2E_PEER_HOST ?? "127.0.0.1"; const PEER_PORT = Number(process.env.E2E_PEER_PORT ?? 14404); const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); /** Poll the device HTTPS phone API until it answers (cert is self-signed). */ async function waitForHttps(url: string, timeoutMs: number): Promise { const deadline = Date.now() + timeoutMs; let lastErr = "connection refused"; while (Date.now() < deadline) { const ok = await new Promise((resolve) => { const req = request( url, { method: "GET", rejectUnauthorized: false, timeout: 4000 }, (res) => { res.resume(); resolve((res.statusCode ?? 0) > 0); }, ); req.on("error", (e) => { lastErr = e.message; resolve(false); }); req.on("timeout", () => { req.destroy(); resolve(false); }); req.end(); }); if (ok) return; await sleep(1000); } throw new Error( `device webserver not ready at ${url} within ${timeoutMs}ms (last: ${lastErr})`, ); } /** Poll a TCP port until it accepts a connection. */ async function waitForTcp( host: string, port: number, timeoutMs: number, ): Promise { const deadline = Date.now() + timeoutMs; let lastErr = "connection refused"; while (Date.now() < deadline) { const ok = await new Promise((resolve) => { const sock = new Socket(); sock.setTimeout(3000); sock.once("connect", () => { sock.destroy(); resolve(true); }); sock.once("error", (e) => { lastErr = e.message; sock.destroy(); resolve(false); }); sock.once("timeout", () => { sock.destroy(); resolve(false); }); sock.connect(port, host); }); if (ok) return; await sleep(1000); } throw new Error( `peer node TCP not ready at ${host}:${port} within ${timeoutMs}ms (last: ${lastErr})`, ); } export default async function globalSetup(): Promise { if (MODE === "docker") { console.log("[e2e] bringing up meshtasticd two-node mesh ..."); execSync(`docker compose -f ${COMPOSE_FILE} up -d`, { stdio: "inherit" }); } else { console.log( `[e2e] hardware mode: device=${NODE_A_URL} peer=${PEER_HOST}:${PEER_PORT}`, ); } console.log(`[e2e] waiting for device webserver ${NODE_A_URL} ...`); await waitForHttps(`${NODE_A_URL}/api/v1/fromradio?all=true`, 120_000); console.log(`[e2e] waiting for peer node ${PEER_HOST}:${PEER_PORT} ...`); await waitForTcp(PEER_HOST, PEER_PORT, 60_000); console.log("[e2e] topology ready."); }