// WI-07 verification: diff the dashboard's old inline per-module maps // (READY_PATTERNS / moduleAppearance / moduleBrowseableRoots / // guessPortsForModule — kept in rcc.js as fallbacks) against the values // the manifests now serve via GET /api/modules (as loaded by // tools/module-meta-dump, which uses the controller's own LoadDir). // // ready_pattern + appearance were MOVED (must be value-identical → any // mismatch exits 1). ports + browseable_roots were already declared in // module.yaml; drift there is reported informationally (manifest wins). // // Run from repo root: node controller/.test/wi07-diff-maps.mjs import fs from "node:fs"; import path from "node:path"; import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); const rcc = fs.readFileSync(path.join(root, "controller/cmd/controller/static/rcc.js"), "utf8"); // Extract a brace-balanced object literal following `marker`. function extractObject(marker, from = 0) { const i = rcc.indexOf(marker, from); if (i < 0) throw new Error("marker not found: " + marker); const start = rcc.indexOf("{", i); let depth = 0, j = start; for (; j < rcc.length; j++) { const c = rcc[j]; if (c === "{") depth++; else if (c === "}") { depth--; if (depth === 0) break; } } return eval("(" + rcc.slice(start, j + 1) + ")"); } const READY = extractObject("const READY_PATTERNS = "); const APPEAR = extractObject("const moduleAppearance = "); const ROOTS = extractObject("const moduleBrowseableRoots = "); // ports map lives inside guessPortsForModule() const fnIdx = rcc.indexOf("function guessPortsForModule(id)"); if (fnIdx < 0) throw new Error("guessPortsForModule not found"); const PORTS = extractObject("const m = {", fnIdx); // Manifest side. const goExe = process.env.GO_EXE || "C:\\Program Files\\Go\\bin\\go.exe"; const manifest = JSON.parse(execFileSync(goExe, ["run", "./tools/module-meta-dump"], { cwd: root, encoding: "utf8" })); // Same compile rule the dashboard uses for manifest ready_pattern strings. function compileReady(s) { if (!s) return null; const m = /^\/([\s\S]+)\/([a-z]*)$/.exec(s); return m ? new RegExp(m[1], m[2]) : new RegExp(s); } const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b); let hardFail = 0; const ids = [...new Set([...Object.keys(manifest), ...Object.keys(READY), ...Object.keys(APPEAR), ...Object.keys(ROOTS), ...Object.keys(PORTS)])].sort(); for (const id of ids) { const man = manifest[id]; const lines = []; // ready_pattern — moved: must be identical where both sides have it. const oldRe = READY[id]; const newRe = man && compileReady(man.ready_pattern); if (oldRe && !man) lines.push(" ready: JS-only (no manifest dir) — fallback covers it"); else if (oldRe && !newRe) { lines.push(" ready: MISSING in manifest"); hardFail++; } else if (oldRe && newRe && (oldRe.source !== newRe.source || oldRe.flags !== newRe.flags)) { lines.push(` ready: MISMATCH\n old ${oldRe}\n new ${newRe}`); hardFail++; } // appearance — moved: compare emoji/grad/art/steam. const oa = APPEAR[id], na = man && man.appearance; if (oa && !man) lines.push(" appearance: JS-only (no manifest dir) — fallback covers it"); else if (oa && !na) { lines.push(" appearance: MISSING in manifest"); hardFail++; } else if (oa && na) { for (const k of ["emoji", "grad", "art", "steam"]) { if ((oa[k] || "") !== (na[k] || "")) { lines.push(` appearance.${k}: MISMATCH old=${JSON.stringify(oa[k] || "")} new=${JSON.stringify(na[k] || "")}`); hardFail++; } } } // browseable_roots — pre-existing in manifests; report drift. const orr = ROOTS[id], nrr = man && man.browseable_roots; if (orr && man && !eq(orr, nrr)) lines.push(` roots drift (manifest wins):\n old ${JSON.stringify(orr)}\n new ${JSON.stringify(nrr)}`); if (orr && !man) lines.push(" roots: JS-only (no manifest dir) — fallback covers it"); // ports — pre-existing in manifests; compare ignoring the additive env field. const op = PORTS[id]; const np = man && man.ports && man.ports.map(p => { const o = { name: p.name, proto: p.proto, default: p.default }; if (p.required) o.required = true; if (p.internal) o.internal = true; return o; }); const opNorm = op && op.map(p => { const o = { name: p.name, proto: p.proto, default: p.default }; if (p.required) o.required = true; if (p.internal) o.internal = true; return o; }); if (op && man && !eq(opNorm, np)) lines.push(` ports drift (manifest wins):\n old ${JSON.stringify(opNorm)}\n new ${JSON.stringify(np)}`); if (op && !man) lines.push(" ports: JS-only (no manifest dir) — fallback covers it"); if (lines.length) console.log(id + "\n" + lines.join("\n")); else console.log(id + "\n OK (identical)"); } console.log(hardFail ? `\nFAIL: ${hardFail} moved-field mismatch(es)` : "\nPASS: all moved fields (ready_pattern, appearance) value-identical"); process.exit(hardFail ? 1 : 0);