From f04911a763e33c6ea17c407313572b6c057b13cd Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 14 Sep 2026 20:56:35 +0200 Subject: [PATCH] Give the module the broker it asks for, rather than a module that asks for nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mesh refused to place amqp-ping: nothing provides amqp. That refusal is right. The substrate raises a broker, but as a bundle resource — plumbing, not a module the mesh has a record of — so it offers nothing to anything, and a module wanting a broker wants one in the graph. lavinmq is that module and needs no building, its image being upstream, so this is a register and an assign. The alternative was to pick a module with no requires, which would have passed by testing less. Also: the control plane's image has no /tmp to copy a manifest into. Root does. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- test/integration/fresh-mesh.test.ts | 47 ++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/test/integration/fresh-mesh.test.ts b/test/integration/fresh-mesh.test.ts index b5b524b..70627ce 100644 --- a/test/integration/fresh-mesh.test.ts +++ b/test/integration/fresh-mesh.test.ts @@ -67,6 +67,16 @@ const REGISTRY = `${ANCHOR}:5000`; * under test (novox/hq SELF-UPGRADE-PLAN, rule 1). */ const BASE = { module: "mesh-tools", repo: "mesh-tools", path: "" }; +/** + * What `amqp-ping` requires, and what the substrate does not supply. + * + * The installer raises a broker, but as a bundle resource — plumbing, not a module the mesh has a + * record of, so it provides nothing to anything. A module asking for `amqp` is asking for a + * provider in the graph, and this is it. No build: its image is upstream. + */ +const PROVIDER = { module: "lavinmq" }; +/** Named once, because the step title is also how later steps say what they waited on. */ +const NEEDS = "the mesh runs a broker for that module to talk to"; const MODULE = { module: "amqp-ping", repo: "mesh-catalog", path: "modules/amqp-ping" }; const capability = await labIsUsable(); @@ -146,13 +156,17 @@ async function mesh(command: string, timeoutMs?: number): Promise { * Pushing the manifest to the machine and naming that path got `no such file or directory` from * inside mesh-control, which is correct and was briefly mistaken for a missing manifest. It is * copied the last step of the way with `docker cp`. + * + * **Into the root, not into /tmp.** The control plane's image is a minimal one and has no `/tmp` + * to copy into — `docker cp` says so in those words. `/` is the one directory every image has. */ async function registerModule(module: string, manifest: string): Promise { assert.ok(existsSync(manifest), `no manifest for ${module} at ${manifest}`); const onMachine = `/tmp/${module}.json`; + const inContainer = `/${module}.json`; await push(instanceId, CONTROL, manifest, onMachine); - await must(CONTROL, `docker cp ${onMachine} mesh-control:${onMachine}`); - return mesh(`module add ${onMachine}`); + await must(CONTROL, `docker cp ${onMachine} mesh-control:${inContainer}`); + return mesh(`module add ${inContainer}`); } function tokenFrom(said: string): string { const found = said.split("\n").map((l) => l.trim()).find((l) => l.length > 100 && !l.includes(" ")); @@ -297,12 +311,36 @@ before(async () => { return `${built}\n${builds}`; }); + // ---- 4b. WHAT THE MODULE NEEDS -------------------------------------------------------------- + // + // `amqp-ping` requires the `amqp` provision, and the mesh refused to place it: "nothing provides + // amqp, wanted by amqp-ping". That refusal is correct and is the reason this step exists rather + // than the reason to pick an easier module. **The substrate's broker is not a provider.** It is + // raised by the installer as part of the bundle, so it is a running container and not a module + // with something to offer — the mesh's own plumbing, not an entry in its graph. A module that + // wants a broker wants one the mesh knows about. + // + // `lavinmq` is that module, and it needs no building: its image is upstream, so this is a + // register and an assign, and it is a fair test of provisioning rather than of building. + await step(NEEDS, "the mesh builds a module standing on that base", async () => { + await registerModule(PROVIDER.module, resolve(catalogDir, PROVIDER.module, "module.json")); + await mesh(`assign ${CONTROL} ${PROVIDER.module}`); + await mesh(`push ${CONTROL}`, 600_000); + for (let i = 0; i < 60; i++) { + const ps = (await on(CONTROL, `docker ps --format '{{.Names}}\t{{.Status}}'`)).out; + const line = ps.split("\n").find((l) => l.includes(PROVIDER.module)); + if (line && /Up /.test(line)) return ps; + await new Promise((r) => setTimeout(r, 5_000)); + } + throw new Error(`${PROVIDER.module} never came up on ${CONTROL}:\n` + + (await on(CONTROL, `docker ps -a --format '{{.Names}}\t{{.Status}}'`)).out); + }); + // ---- 5. THE ANCHOR RUNS IT ------------------------------------------------------------------ // // The machine that built it. This is the case every earlier proof covered, and it is here as the // control for step 6: if this fails, step 6's failure says nothing about fetching. - await step("the anchor runs the module the mesh built", - "the mesh builds a module standing on that base", async () => { + await step("the anchor runs the module the mesh built", NEEDS, async () => { await mesh(`module issue ${MODULE.module} --node ${CONTROL}`).catch(() => {}); await mesh(`assign ${CONTROL} ${MODULE.module}`); await mesh(`push ${CONTROL}`, 600_000); @@ -362,6 +400,7 @@ for (const name of [ "three machines join it across the household gateway", "the mesh builds the shared base from source", "the mesh builds a module standing on that base", + NEEDS, "the anchor runs the module the mesh built", "a joined machine runs the module the mesh built", ]) {