diff --git a/test/integration/mesh-grant-end-to-end.test.ts b/test/integration/mesh-grant-end-to-end.test.ts index 496d1c2..f546c23 100644 --- a/test/integration/mesh-grant-end-to-end.test.ts +++ b/test/integration/mesh-grant-end-to-end.test.ts @@ -227,4 +227,16 @@ test("the mesh grants a consumer redis's cache, and the credential it delivers a // writing the contributions rather than the bed. const runtimeEnv = await must(`docker inspect mesh-redis --format '{{json .Config.Env}}'`); assert.doesNotMatch(runtimeEnv, /MESH_SEAL_KEY/, `a seal key was set after all — ADR 0048 is not what ran:\n${runtimeEnv}`); + + // A grant means exactly the consumer's own keys: under its name it reads and writes, outside it + // and on the server as a whole it is refused. Carried over from the large mesh bed's retired + // cache-grant test — without this a provisioner that granted everything would keep every bed green. + const asConsumer = (command: string) => + on(`docker exec redis redis-cli --user ${quote(as)} --pass ${quote(password)} --no-auth-warning ${command} 2>&1`); + assert.match((await asConsumer("SET cacheuser:proof yes")).out, /OK/, "the consumer cannot write under its own name"); + assert.match((await asConsumer("GET cacheuser:proof")).out, /yes/, "the consumer cannot read back what it wrote"); + assert.match((await asConsumer("SET other:proof no")).out, /NOPERM|no permissions/i, + "the consumer wrote outside its own keys, so the grant means more than it says"); + assert.match((await asConsumer("FLUSHALL")).out, /NOPERM|no permissions/i, + "the consumer flushed the whole server, so the grant means more than it says"); }); diff --git a/test/integration/mesh.test.ts b/test/integration/mesh.test.ts index 245fe70..34b56df 100644 --- a/test/integration/mesh.test.ts +++ b/test/integration/mesh.test.ts @@ -24,7 +24,7 @@ import { raise } from "../../src/lifecycle/raise.ts"; import type { HeldImage } from "../../src/pinning.ts"; import { destroy, exec } from "../../src/lifecycle/operate.ts"; import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, foundationBundle, onTheMachine, catalogueModule, deriveTheFilterOn } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, onTheMachine, catalogueModule, deriveTheFilterOn, catalogueIsPresent } from "./harness.ts"; import { incus } from "../../src/incus/client.ts"; import { machineName } from "../../src/lifecycle/names.ts"; import { ready, returnTo, keep, rememberStock, warmStock } from "../../src/warm.ts"; @@ -44,7 +44,8 @@ const skip = !capability.usable ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" : !bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" - : false; + // The anchor's filter and the resolvers are the catalogue's (ADR 0088, issue 074). + : catalogueIsPresent() || false; const SCENARIO = "two-nodes"; let instanceId = ""; @@ -202,6 +203,14 @@ function tokenFrom(said: string): string { /** The builder started by hand on the anchor, against the foundation broker's plain port on * loopback — the one that builds until a builder module can (see the retired test's note). */ async function startBuilder(): Promise { + // The binary is disk and survives a snapshot; a snapshot taken without it does not gain it on a + // return, so it is pushed whenever the machine has none. + if (!(await on("anchor", `test -x /usr/local/bin/mesh-builder`)).ok) { + 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", `pgrep -x mesh-builder >/dev/null || ` + `(MESH_BROKER_AMQP='amqp://guest:guest@127.0.0.1:5672/' MESH_REGISTRY=${registry} ` + @@ -269,13 +278,7 @@ before(async () => { // 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 startBuilder(); - } + if (builder) await startBuilder(); if (warming) { // Snapshotted only now, with everything up: a state worth returning to is the one after the // part nobody wants to repeat. @@ -1386,9 +1389,9 @@ test("every name under a machine resolves to that machine", { // because a wildcard pointing at nothing resolves and then hangs — where an unresolvable name // fails at once and says which name it was. // - // Both, and that is not tidiness: `mesh-resolver` requires name resolution, which requires the - // network, so unassigning the domain module alone leaves the machine on the network — pulled - // back by its own requirement. The mesh was right and this test was wrong the first time. + // Both: the resolver's data follows the private network, so the machine leaves the network as + // well as the resolver, and what is asserted is that the machine that stayed is answered for and + // the one that left is not. await mesh("unassign laptop dnsmasq"); await mesh("unassign laptop networking"); await mesh("push anchor"); @@ -1646,9 +1649,12 @@ test("a third-party workload is adopted, with the credential it already had", { // Running them needs their images stocked and two provisioners built, which is a separate and // larger job. This is the half that can be known now, and it is the half where a design fault // would live. -// Three tests lived here that read the mesh's example modules, which moved to the catalogue: -// "the real modules resolve together" (whole-mesh-novox installs the catalogue's modules together -// and proves the composed declaration), "the forge runs, on a database the mesh gave it" (the same -// bed, gitea on the mesh's postgres) and "a consumer's cache grant means exactly its own keys" -// (mesh-grant-end-to-end, against the catalogue's redis). Retired 2026-09-22 rather than rewritten -// into copies of those beds (novox/hq issue 074). +// Three tests lived here that read the mesh's example modules, which moved to the catalogue. +// Retired 2026-09-22 rather than rewritten into copies of the beds that stand where they stood +// (novox/hq issue 074): "the real modules resolve together" — whole-mesh-novox installs the +// catalogue's modules together and its gate is the composed declaration accepted and every core +// container running; "the forge runs, on a database the mesh gave it" — the same bed, which gates +// on gitea running but does not yet ask it to answer on its port with the credential it was given, +// a gap that bed should close; "a consumer's cache grant means exactly its own keys" — its tenancy +// assertions (a write outside the consumer's keys and a FLUSHALL are refused) moved into +// mesh-grant-end-to-end, against the catalogue's redis.