Let a caller say how long to wait, and note where the artifact-store test

went

`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.
This commit is contained in:
2026-08-30 20:41:10 +02:00
parent c98ee7a82d
commit 2f81701a13
2 changed files with 45 additions and 7 deletions
+7 -1
View File
@@ -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);
}
/**
+38 -6
View File
@@ -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<string> {
const { out, ok } = await on(machine, command);
async function must(machine: string, command: string, timeoutMs?: number): Promise<string> {
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<string> {
return must("anchor", `docker exec mesh-control /mesh-control ${command}`);
async function mesh(command: string, timeoutMs?: number): Promise<string> {
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).