diff --git a/src/integrations.mjs b/src/integrations.mjs index 3929211..bf9ebdf 100644 --- a/src/integrations.mjs +++ b/src/integrations.mjs @@ -106,7 +106,10 @@ export function printSkillTargets() { const dot = isInstalled(ENGINES[key].bin) ? DOT.installed : DOT.missing; console.log(` ${dot} ${bone(key.padEnd(9))} ${ash("skills supported")}`); } - for (const key of ["codex", "opencode", "aider"]) { + // The engines with no skills primitive are whatever ENGINES has left over. + // Derived, not hardcoded: a hardcoded list silently drops any engine added + // later, so the matrix stops showing every engine moshcode supports. + for (const key of Object.keys(ENGINES).filter((k) => !SKILL_ENGINES.includes(k))) { console.log(` ${DOT.missing} ${bone(key.padEnd(9))} ${ash("no skills primitive")}`); } } diff --git a/test/support-matrix.test.mjs b/test/support-matrix.test.mjs new file mode 100644 index 0000000..bc3e933 --- /dev/null +++ b/test/support-matrix.test.mjs @@ -0,0 +1,76 @@ +// `/mcp list` and `/skill list` print a support matrix. Both must account for +// EVERY engine in ENGINES: a missing row is worse than a "not supported" row, +// because the user cannot tell whether the engine is unsupported or whether +// they typed the name wrong. +import assert from "node:assert/strict"; +import test from "node:test"; + +import { ENGINES } from "../src/engines.mjs"; +import { MCP_ENGINES } from "../src/mcp.mjs"; +import { SKILL_ENGINES } from "../src/skills.mjs"; +import { printMcpTargets, printSkillTargets } from "../src/integrations.mjs"; + +/** Run a printer and return its output with the ANSI colours stripped. */ +function capture(fn) { + const lines = []; + const original = console.log; + console.log = (...args) => lines.push(args.join(" ")); + try { + fn(); + } finally { + console.log = original; + } + return lines.join("\n").replace(/\x1b\[[0-9;]*m/g, ""); +} + +/** The engine rows only: the header line names no engine. */ +function rowKeys(out) { + return out + .split("\n") + .slice(1) + .map((l) => l.trim().replace(/^[●○]\s*/, "").split(/\s+/)[0]) + .filter(Boolean); +} + +test("/skill list lists every engine exactly once", () => { + const keys = rowKeys(capture(printSkillTargets)); + assert.deepEqual([...keys].sort(), Object.keys(ENGINES).sort()); + assert.equal(new Set(keys).size, keys.length); +}); + +test("/skill list marks privacycode as having no skills primitive", () => { + // privacycode is an opencode derivative, so it has no skills primitive — but + // it must still appear. It was absent from the matrix entirely. + const out = capture(printSkillTargets); + assert.match(out, /privacycode\s+no skills primitive/); +}); + +test("/skill list splits the rows by SKILL_ENGINES", () => { + const out = capture(printSkillTargets); + for (const key of Object.keys(ENGINES)) { + const supported = SKILL_ENGINES.includes(key); + assert.match( + out, + new RegExp(`${key}\\s+${supported ? "skills supported" : "no skills primitive"}`), + `${key} row should say ${supported ? "supported" : "no skills primitive"}`, + ); + } +}); + +test("/mcp list lists every engine exactly once", () => { + const keys = rowKeys(capture(printMcpTargets)); + assert.deepEqual([...keys].sort(), Object.keys(ENGINES).sort()); + assert.equal(new Set(keys).size, keys.length); +}); + +test("/mcp list splits the rows by MCP_ENGINES", () => { + const out = capture(printMcpTargets); + for (const key of Object.keys(ENGINES)) { + const supported = MCP_ENGINES.includes(key); + assert.match( + out, + new RegExp(`${key}\\s+${supported ? "mcp add supported" : "no MCP support"}`), + `${key} row should say ${supported ? "supported" : "no MCP support"}`, + ); + } +});