catalogueModule() in the harness reads a module's manifest from the catalogue and rewrites only what the lab must: the build section goes, each artifact becomes the image the machine holds, images are pinned, and a bed may declare a host-port remap or a lab-local address. confluence, gitlab, openai-consumer, audit-logger, ollama, local-model-consumer, model-usage, mosquitto, anthropic-manager and anthropic-consumer now install the catalogue's manifest. A unit test refuses any inline copy naming a catalogue module unless the bed is declared with its reason; the declared list is the debt (novox/hq 04-ISSUES/073).
103 lines
6.8 KiB
TypeScript
103 lines
6.8 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
import { catalogueIsPresent, catalogueDir } from "./integration/harness.ts";
|
|
|
|
/**
|
|
* A bed installs a catalogue module by reading the catalogue, never by carrying a copy.
|
|
*
|
|
* The beds used to build the manifests they install inline, as literals taken from the catalogue
|
|
* when each bed was written. The copies did not move when the catalogue did: six modules were
|
|
* converted to file-delivered secrets and not one bed ran the converted shape, because every bed
|
|
* ran its own copy (novox/hq 04-ISSUES/073). "Proven in the lab" then meant "the copy was proven".
|
|
*
|
|
* So: a manifest literal in a bed that names a catalogue module is refused, unless the bed is
|
|
* listed below with the reason it still carries one. The list is the debt, and it only shrinks.
|
|
*
|
|
* What this reads: `module: "<name>"` and `"module": "<name>"` with a `version` close behind, in
|
|
* test/integration/*.test.ts, against the catalogue's directory names. A bed that hid the name
|
|
* behind a computed string would pass — this is a fence, not a proof, and the reviewer of a bed
|
|
* that builds a manifest inline is the proof.
|
|
*/
|
|
|
|
/**
|
|
* Beds that still carry an inline copy of a catalogue module's manifest, and why. Three reasons
|
|
* recur, and each names the work that removes the entry:
|
|
*
|
|
* BESIDE the catalogue's module CLAIMS the foundation's container (postgres claims mesh-store,
|
|
* lavinmq mesh-broker) and adopts it in place; the bed raises a second one beside the
|
|
* foundation's instead. Reading the catalogue changes what the bed raises — it would
|
|
* adopt — and the bed's assertions with it.
|
|
* WEARING the bed proves a mesh mechanism (a grant, a credential, a restart, a route) with a
|
|
* module cut down to the shape the mechanism needs — no upstream server, a secret in the
|
|
* environment, a requirement edge removed — and gives it a catalogue name. It is a mesh
|
|
* test wearing a catalogue module's name. It should carry a name of its own, or read the
|
|
* catalogue and meet the module's real requirements.
|
|
* DIFFERS a module bed whose copy differs from the catalogue in more than the lab may rewrite
|
|
* (an image, a port, an address). Reading the catalogue is the fix and needs a run.
|
|
*/
|
|
const STILL_CARRIED: Record<string, { modules: string[]; why: string }> = {
|
|
"assigned-catalogue-apps.test.ts": { modules: ["postgres", "mongodb", "unifi", "marrytts"],
|
|
why: "BESIDE (postgres); DIFFERS (unifi takes its credentials from the environment, mongodb and marrytts drop listens)" },
|
|
"assigned-catalogue-media.test.ts": { modules: ["sonarr", "radarr"],
|
|
why: "DIFFERS: both drop the route requirement the catalogue declares, and take their API keys from the environment" },
|
|
"assigned-catalogue-small.test.ts": { modules: ["postgres", "minio", "redis", "plex"],
|
|
why: "BESIDE (postgres); DIFFERS (minio's root password by env-file, redis minting its own secret instead of the vault's, plex without its server)" },
|
|
"assigned-model-usage.test.ts": { modules: ["postgres"], why: "BESIDE" },
|
|
"assigned-two-node-db.test.ts": { modules: ["redis", "baserow", "letta"],
|
|
why: "DIFFERS: redis mints its own secret, baserow drops its route requirement, letta drops its ports" },
|
|
"lavinmq-bed.test.ts": { modules: ["lavinmq", "amqp-ping"],
|
|
why: "BESIDE (lavinmq, with a bootstrap step and a data directory the catalogue has not got); DIFFERS (amqp-ping names its entrypoint)" },
|
|
"assigned-grafana.test.ts": { modules: ["grafana"], why: "WEARING: the sidecar alone, no Grafana, no route" },
|
|
"assigned-plex.test.ts": { modules: ["plex"], why: "WEARING: the sidecar alone, no Plex, the token in the environment" },
|
|
"assigned-redis.test.ts": { modules: ["redis"], why: "WEARING: its own secret, a lab seal key in the environment" },
|
|
"assigned-sonarr.test.ts": { modules: ["sonarr"], why: "WEARING: the sidecar alone against a forged config.xml" },
|
|
"mesh-grant-end-to-end.test.ts": { modules: ["redis"], why: "WEARING: a grant mechanism test" },
|
|
"minio-grant-end-to-end.test.ts": { modules: ["minio"], why: "WEARING: a grant mechanism test, the root password by env-file" },
|
|
"postgres-grant-end-to-end.test.ts": { modules: ["postgres"], why: "WEARING: a grant mechanism test, the superuser by env-file" },
|
|
"provider-on-backend-network.test.ts": { modules: ["redis"], why: "WEARING: a network mechanism test" },
|
|
"provider-uses-mesh-credential.test.ts": { modules: ["redis"], why: "WEARING: a credential mechanism test" },
|
|
"runtime-restart-on-config.test.ts": { modules: ["grafana"], why: "WEARING: a restart mechanism test" },
|
|
"route-forwarding.test.ts": { modules: ["route-proxy", "hello-web"],
|
|
why: "WEARING: route-proxy without its certificate authority, hello-web with the route shape ADR 0066 replaced" },
|
|
"mesh.test.ts": { modules: ["postgres", "builder", "umami"],
|
|
why: "WEARING: a postgres with no resources, a builder that builds itself, an umami that is another module of that name" },
|
|
};
|
|
|
|
const beds = resolve(import.meta.dirname, "integration");
|
|
|
|
test("a bed that installs a catalogue module reads the catalogue", (t) => {
|
|
const absent = catalogueIsPresent();
|
|
if (absent) {
|
|
// Said, not silent: a check that cannot see the catalogue has checked nothing.
|
|
t.skip(`cannot check — ${absent}`);
|
|
return;
|
|
}
|
|
const names = new Set(readdirSync(catalogueDir(), { withFileTypes: true })
|
|
.filter((d) => d.isDirectory() && existsSync(resolve(catalogueDir(), d.name, "module.json")))
|
|
.map((d) => d.name));
|
|
|
|
const offences: string[] = [];
|
|
for (const file of readdirSync(beds).filter((f) => f.endsWith(".test.ts")).sort()) {
|
|
const text = readFileSync(resolve(beds, file), "utf8");
|
|
const found = new Set<string>();
|
|
// A manifest literal: the module's name with its version close behind it. A `module:` key
|
|
// elsewhere (a table of what to register, a grant entry) has no version and is not one.
|
|
for (const m of text.matchAll(/(?:^|[\s{,])(?:"module"|module)\s*:\s*"([a-z0-9.-]+)"[^}]{0,160}?(?:"version"|version)\s*:/g)) {
|
|
if (names.has(m[1]!)) found.add(m[1]!);
|
|
}
|
|
const declared = STILL_CARRIED[file];
|
|
for (const name of [...found].sort()) {
|
|
if (declared?.modules.includes(name)) continue;
|
|
offences.push(`${file}: an inline manifest for the catalogue's '${name}'`);
|
|
}
|
|
for (const name of declared?.modules ?? []) {
|
|
if (!found.has(name)) offences.push(`${file}: declared as still carrying '${name}', and it does not — remove the declaration`);
|
|
}
|
|
}
|
|
assert.deepEqual(offences, [],
|
|
`a bed carries a copy of a catalogue manifest; read it with catalogueModule() from the harness:\n ${offences.join("\n ")}`);
|
|
});
|