diff --git a/test/integration/genesis.ts b/test/integration/genesis.ts index 4775048..7a0be91 100644 --- a/test/integration/genesis.ts +++ b/test/integration/genesis.ts @@ -67,6 +67,11 @@ export interface GenesisOptions { /** Where the shared library is built from — published before the base resolves it (ADR 0076). */ sdkSource: string; sdkRef?: string; + /** What of the base repo to build. Defaults to main; a feature branch under test overrides it. */ + toolsRef?: string; + /** The site the anchor is placed at on the private network. Must match how the operator/test + * places it afterwards, or the first re-place changes the overlay and recreates the control plane. */ + site?: string; /** What of the catalogue to build. A branch under test is the usual reason this is not main. */ catalogRef?: string; log?: (m: string) => void; @@ -149,6 +154,7 @@ export async function genesis(o: GenesisOptions): Promise { `--node ${node}`, `--registry ${registry}`, `--tools-source ${o.toolsSource}`, + `--tools-ref ${o.toolsRef ?? "main"}`, `--catalog-source ${o.catalogSource}`, `--catalog-ref ${o.catalogRef ?? "main"}`, `--sdk-source ${o.sdkSource}`, @@ -156,6 +162,7 @@ export async function genesis(o: GenesisOptions): Promise { // The choices, answered the unattended way. Both have one option today, so these are // redundant on purpose: the day a second filter exists, this bed keeps working instead of // refusing, and choosing becomes a thing it visibly does. + `--site ${o.site ?? "main"}`, `--private-network wireguard`, `--packet-filter nftables`, `--host ${HOST_PATH}`, diff --git a/test/integration/one-node-mesh.test.ts b/test/integration/one-node-mesh.test.ts index 07d962c..8619c08 100644 --- a/test/integration/one-node-mesh.test.ts +++ b/test/integration/one-node-mesh.test.ts @@ -217,7 +217,23 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi return out; } async function mesh(command: string, timeoutMs?: number): Promise { - return must(CONTROL, `docker exec mesh-control /mesh-control ${command}`, timeoutMs); + // The control plane is a live, mesh-MANAGED container: the mesh recreates it whenever its + // declaration changes — most visibly when the machine first gets its `.internal` name on the + // private network, which becomes the container's `--add-host` (containers are immutable, so a new + // spec is a new container). A `docker exec mesh-control` that lands in that brief recreate window + // fails with "container ... is not running". That is not the mesh being wrong — it is a command + // racing a legitimate restart — so it is retried until the control plane answers again. A real + // command failure (anything else) still throws at once. + const deadline = Date.now() + (timeoutMs ?? 120_000); + for (;;) { + const { out, ok } = await on(CONTROL, `docker exec mesh-control /mesh-control ${command}`, timeoutMs); + if (ok) return out; + if (/is not running|No such container/i.test(out) && Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 2_000)); + continue; + } + throw new Error(`${CONTROL}: docker exec mesh-control /mesh-control ${command}\n${out}`); + } } /** @@ -468,10 +484,14 @@ before(async () => { source, sourceRef, toolsSource: forgeUrl(BASE.repo), + toolsRef: refFor(BASE.repo), catalogSource: forgeUrl(MODULE.repo), catalogRef: refFor(MODULE.repo), sdkSource: forgeUrl("mesh-sdk"), sdkRef: refFor("mesh-sdk"), + // The same site N1 re-places the anchor at, so N1 is a no-op and the control plane is not + // recreated mid-test (its --add-host would otherwise change). + site: "hosting", log: (m) => console.log(m), }); } catch (err) {