Give the module the broker it asks for, rather than a module that asks for nothing
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
This commit is contained in:
@@ -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<string> {
|
||||
* 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<string> {
|
||||
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",
|
||||
]) {
|
||||
|
||||
Reference in New Issue
Block a user