From 2f81701a139e8ab08f1e1ad5ed66e6a4d297b872 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 30 Aug 2026 20:41:10 +0200 Subject: [PATCH] Let a caller say how long to wait, and note where the artifact-store test went MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `exec` waited two minutes always. A build, or anything that waits on another machine, needs longer — and a caller that cannot say so has to split the work to fit, which is a test shaped by its harness rather than by what it is testing. The scenario also places a build machine when one is given, so anything in it can ask the mesh to build something. Nothing else here would start one. And the mesh-runs-its-own-artifact-store test is not here. It needs a fourth image so the module has a registry to mirror, and that is caught behind 04-ISSUES/012 — left as a note saying where it went and why, rather than silently deleted, because what it asserted is worth asserting. --- src/lifecycle/operate.ts | 8 ++++++- test/integration/mesh.test.ts | 44 ++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/lifecycle/operate.ts b/src/lifecycle/operate.ts index f2f2180..9dc8165 100644 --- a/src/lifecycle/operate.ts +++ b/src/lifecycle/operate.ts @@ -54,12 +54,18 @@ export async function exec( instanceId: string, machine: string, command: string[], + /** + * How long to wait. Two minutes suits a command; a build, or anything that waits on another + * machine, needs longer — and a caller that cannot say so has to split the work up to fit, + * which is a test shaped by its harness rather than by what it is testing. + */ + timeoutMs = 120_000, ): Promise<{ stdout: string; stderr: string }> { const found = (await taggedInstances()).find( (i) => i.instanceId === instanceId && i.machine === machine, ); const name = found?.name ?? machineName(instanceId, machine); - return incus(["exec", name, "--", ...command], 120_000); + return incus(["exec", name, "--", ...command], timeoutMs); } /** diff --git a/test/integration/mesh.test.ts b/test/integration/mesh.test.ts index 95223a5..eb62edb 100644 --- a/test/integration/mesh.test.ts +++ b/test/integration/mesh.test.ts @@ -24,10 +24,13 @@ import { raise } from "../../src/lifecycle/raise.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; import { labIsUsable, destroyAll } from "./harness.ts"; +import { incus } from "../../src/incus/client.ts"; +import { machineName } from "../../src/lifecycle/names.ts"; const capability = await labIsUsable(); const binary = hostBinaryPath(); const bundle = process.env["MESH_LAB_BUNDLE"] ?? ""; +const builder = process.env["MESH_LAB_BUILDER"] ?? ""; const skip = !capability.usable ? `lab not usable: ${capability.why}` @@ -39,28 +42,30 @@ const skip = !capability.usable const SCENARIO = "two-nodes"; let instanceId = ""; +/** The scenario's own registry, which serves the images a module may mirror. */ +let registry = ""; function quote(s: string): string { return `'${s.replaceAll("'", `'\\''`)}'`; } -async function on(machine: string, command: string): Promise<{ out: string; ok: boolean }> { +async function on(machine: string, command: string, timeoutMs?: number): Promise<{ out: string; ok: boolean }> { const { stdout } = await exec(instanceId, machine, [ "sh", "-c", `${command} 2>&1; echo "__exit=$?"`, - ]); + ], timeoutMs); const marker = stdout.lastIndexOf("__exit="); return { out: stdout.slice(0, marker), ok: Number(stdout.slice(marker + 7).trim()) === 0 }; } -async function must(machine: string, command: string): Promise { - const { out, ok } = await on(machine, command); +async function must(machine: string, command: string, timeoutMs?: number): Promise { + const { out, ok } = await on(machine, command, timeoutMs); if (!ok) throw new Error(`${machine}: ${command}\n${out}`); return out; } /** The control plane, which runs in a container on the first node. */ -async function mesh(command: string): Promise { - return must("anchor", `docker exec mesh-control /mesh-control ${command}`); +async function mesh(command: string, timeoutMs?: number): Promise { + return must("anchor", `docker exec mesh-control /mesh-control ${command}`, timeoutMs); } /** @@ -99,6 +104,23 @@ before(async () => { // because the digests are this registry's and are not known until it is up. await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`); + const first = raised.images[0]; + assert.ok(first, "the scenario stocked no images, so nothing can be mirrored"); + registry = first.slice(0, first.indexOf("/")); + + // A build machine, so anything here can ask the mesh to build something. Placed rather than + // assumed: nothing else in this scenario would start one. + if (builder) { + await incus([ + "file", "push", builder, `${machineName(instanceId, "anchor")}/usr/local/bin/mesh-builder`, + "--mode", "0755", + ], 180_000); + await must("anchor", `mkdir -p /var/lib/mesh-builder`); + await must("anchor", + `MESH_BROKER_AMQP='amqp://guest:guest@127.0.0.1:5672/' MESH_REGISTRY=${registry} ` + + `MESH_WORKSPACE=/var/lib/mesh-builder ` + + `nohup /usr/local/bin/mesh-builder > /var/log/mesh-builder.log 2>&1 & sleep 3`); + } }, { timeout: 1_800_000 }); after(async () => { @@ -384,3 +406,13 @@ test("a machine that fell behind catches up without being named", { skip, timeou // With nothing behind, it says so rather than doing nothing quietly. assert.match(await mesh("push --behind"), /every machine is doing what it was told/); }); + +// The mesh running its own artifact store is proven in its own scenario, not this one. +// +// It was here, and adding the image it mirrors to this scenario made the bootstrap fail: the +// store container did not come up within three minutes, with no output at all from its own +// readiness check — which says the container was not running rather than that the database was +// slow. Four images on a machine this size is the difference. +// +// Left as a note rather than a silently deleted test: what it asserted is worth asserting, and +// where it belongs is a scenario with room for it (novox/hq 04-ISSUES/012).