// docs-guard-exempt: docs/adr/58-...md is a comment citation; it is not read. // allow-test-rule: source-text-is-the-product // Reads .md/.json/.yml product files whose deployed text IS what the // runtime loads — testing text content tests the deployed contract. /** * Installer Module — Sections 6–8 + 12. * * Covers: installRuntimeArtifacts parameterised layout loop, * uninstallRuntimeArtifacts all runtimes, Contract 6 counter-test * (unknown runtime rejected), and legacy migration tests. * * Consolidates (original sources from #3758): * install-uninstall-layout-loop.test.cjs * * Closes #3758 */ 'use strict'; process.env.MSD_TEST_MODE = '1'; const { test, describe, before, after } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const crypto = require('node:crypto'); const os = require('node:os'); const espree = require('espree'); const fc = require('./helpers/fast-check-setup.cjs'); const { splitLines, joinLines } = require('../msd-core/bin/lib/text-lines.cjs'); const { createTempDir, cleanup, writePackageSourceMarkerFixture } = require('./helpers.cjs'); const { runNode } = require('./helpers/process-seam.cjs'); const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); // Bound the writer subprocess so a regression that hangs the writer // (or the dispatcher) cannot deadlock CI (PR #3003 CR feedback). // 30s is generous for what should complete in <1s; if it trips, // surface that as a clear test failure rather than CI hanging. const WRITER_SUBPROCESS_TIMEOUT_MS = 30_000; // Bounds a real `msd-tools.cjs capability install` subprocess run against a // fully-installed tree; digits coincide with the shared // HOOK_FANOUT_TIMEOUT_MS/SEAM_DEFAULT_TIMEOUT_MS constants but this is // neither a hook fan-out nor an omitted-default seam call, so kept as its // own constant. const CAPABILITY_INSTALL_TIMEOUT_MS = 60000; const { INSTALL_SCRIPT, MANIFEST_NAME, installerEnv, stripAnsi, runMinimalInstall, walk, } = require('./helpers/install-shared.cjs'); const { installRuntimeArtifacts, installOpencodeFamilySkills, uninstallRuntimeArtifacts, _resolveUserArtifactStagingRoot, } = require('../msd-core/bin/lib/install-engine.cjs'); const { stageUserArtifacts, } = require('../msd-core/bin/lib/user-artifact-staging.cjs'); const { parseRuntimeInput, allRuntimes, } = require('../bin/install.js'); const { resolveRuntimeArtifactLayout, } = require('../msd-core/bin/lib/runtime-artifact-layout.cjs'); const { applySurface } = require('../msd-core/bin/lib/surface.cjs'); const { escapeRegex } = require('../msd-core/bin/lib/pattern.cjs'); const { loadSkillsManifest, resolveProfile, CAPABILITY_SKILL_MARKER, } = require('../msd-core/bin/lib/install-profiles.cjs'); const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'msd'); const MANIFEST = loadSkillsManifest(REAL_COMMANDS_DIR); const RESOLVED_CORE = resolveProfile({ modes: ['core'], manifest: MANIFEST }); function loadFreshInstallerWithInstallPlanStub(stub) { return loadFreshInstallerWithPlanStubs({ installStub: stub }); } function loadFreshInstallerWithPlanStubs({ installStub, uninstallStub }) { const installPath = require.resolve('../bin/install.js'); const planPath = require.resolve('../msd-core/bin/lib/runtime-artifact-install-plan.cjs'); const planModule = require(planPath); const originalInstall = planModule.createRuntimeArtifactInstallPlan; const originalUninstall = planModule.createRuntimeArtifactUninstallPlan; if (installStub) planModule.createRuntimeArtifactInstallPlan = installStub; if (uninstallStub) planModule.createRuntimeArtifactUninstallPlan = uninstallStub; delete require.cache[installPath]; const installer = require('../bin/install.js'); return { installer, restore() { planModule.createRuntimeArtifactInstallPlan = originalInstall; planModule.createRuntimeArtifactUninstallPlan = originalUninstall; delete require.cache[installPath]; }, }; } // ─── Section 6: installRuntimeArtifacts — parameterised layout loop ────────── describe('installRuntimeArtifacts — consumes Runtime Artifact Install Plan Module', () => { test('executes returned copy items and cleanup obligations', (t) => { const configDir = createTempDir('msd-install-plan-adapter-'); const sourceDir = createTempDir('msd-install-plan-source-'); const cleanupDir = createTempDir('msd-install-plan-cleanup-'); t.after(() => { cleanup(configDir); cleanup(sourceDir); cleanup(cleanupDir); }); fs.writeFileSync(path.join(sourceDir, 'proof.md'), '# proof\n'); fs.writeFileSync(path.join(cleanupDir, 'temp.md'), '# cleanup\n'); let planArgs; const { restore } = loadFreshInstallerWithInstallPlanStub((args) => { planArgs = args; return { ok: true, plan: { cleanupDirs: [cleanupDir], items: [ { kind: 'agents', sourceDir, destDir: path.join(configDir, 'agents') }, ], }, }; }); t.after(restore); // Claude has a flat agents kind; the returned copy item lands proof.md under agents/. // `installer` above only reloads bin/install.js (needed for OTHER stubs in // this file); installRuntimeArtifacts itself always lived in install-engine.cjs // (bin/install.js's #2876-retired re-export was an alias to the same // singleton), so calling the direct import exercises the identical, // already-monkeypatched module instance. installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE); assert.strictEqual(planArgs.layout.runtime, 'claude'); assert.strictEqual(planArgs.layout.configDir, configDir); assert.strictEqual(planArgs.layout.scope, 'global'); assert.strictEqual(planArgs.resolvedProfile, RESOLVED_CORE); assert.strictEqual(planArgs.resolveAttribution('claude'), undefined); assert.ok(fs.existsSync(path.join(configDir, 'agents', 'proof.md'))); assert.ok(!fs.existsSync(cleanupDir), 'returned cleanup dir must be removed after copy'); }); test('cleans returned obligations when planning fails', (t) => { const configDir = createTempDir('msd-install-plan-fail-'); const cleanupDir = createTempDir('msd-install-plan-fail-cleanup-'); t.after(() => { cleanup(configDir); cleanup(cleanupDir); }); fs.writeFileSync(path.join(cleanupDir, 'temp.md'), '# cleanup\n'); const { restore } = loadFreshInstallerWithInstallPlanStub(() => ({ ok: false, kind: 'rewrite_failed', failedKind: 'commands', message: 'planned failure', cleanupDirs: [cleanupDir], })); t.after(restore); assert.throws( () => installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE), /planned failure/, ); assert.ok(!fs.existsSync(cleanupDir), 'failure cleanup dir must be removed'); }); }); describe('installRuntimeArtifacts — durable Runtime Surface corpus (#4132)', () => { test('#4132: provisioning refuses a msd-core ancestor symlink that escapes configDir', (t) => { const root = createTempDir('msd-corpus-parent-symlink-'); const configDir = path.join(root, 'config'); const outside = path.join(root, 'outside'); fs.mkdirSync(configDir); fs.mkdirSync(outside); t.after(() => cleanup(root)); try { fs.symlinkSync(outside, path.join(configDir, 'msd-core'), process.platform === 'win32' ? 'junction' : 'dir'); } catch (error) { t.skip(`directory symlink creation unavailable: ${error.code || error.message}`); return; } assert.throws( () => installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE), /symlink|refusing/i, ); assert.equal(fs.existsSync(path.join(outside, 'commands')), false, 'commands must not escape configDir'); assert.equal(fs.existsSync(path.join(outside, 'agents')), false, 'agents must not escape configDir'); }); test('#4132: provisioning does not follow a compatibility-marker symlink outside configDir', (t) => { const root = createTempDir('msd-corpus-marker-symlink-'); const configDir = path.join(root, '.claude'); const outsideMarker = path.join(root, 'outside-marker'); const outsideCheckout = path.join(root, 'outside-checkout'); fs.mkdirSync(configDir); fs.cpSync(path.join(__dirname, '..', 'commands'), path.join(outsideCheckout, 'commands'), { recursive: true }); fs.cpSync(path.join(__dirname, '..', 'agents'), path.join(outsideCheckout, 'agents'), { recursive: true }); fs.appendFileSync(path.join(outsideCheckout, 'commands', 'msd', 'help.md'), '\nMARKER_SYMLINK_SOURCE\n'); const outsideMarkerBytes = path.join(outsideCheckout, 'commands', 'msd') + '\n'; fs.writeFileSync(outsideMarker, outsideMarkerBytes); t.after(() => cleanup(root)); try { fs.symlinkSync(outsideMarker, path.join(configDir, '.msd-source'), 'file'); } catch (error) { t.skip(`file symlink creation unavailable: ${error.code || error.message}`); return; } runMinimalInstall({ runtime: 'claude', scope: 'global', root }); assert.equal( fs.readFileSync(outsideMarker, 'utf8'), outsideMarkerBytes, 'the compatibility marker write must remain confined to configDir', ); assert.doesNotMatch( fs.readFileSync(path.join(configDir, 'skills', 'msd-help', 'SKILL.md'), 'utf8'), /MARKER_SYMLINK_SOURCE/, 'the installer must not read a source provider through a symlinked marker file', ); }); test('global Codex owns the canonical commands and agents corpus', (t) => { const configDir = createTempDir('msd-codex-surface-corpus-'); t.after(() => cleanup(configDir)); sandboxHome(t, configDir); installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE); assert.deepStrictEqual( snapshot2362Dir(path.join(configDir, 'msd-core', 'commands', 'msd')), snapshot2362Dir(path.join(__dirname, '..', 'commands', 'msd')), 'installed commands corpus must match the package source tree', ); assert.deepStrictEqual( snapshot2362Dir(path.join(configDir, 'msd-core', 'agents')), snapshot2362Dir(path.join(__dirname, '..', 'agents')), 'installed agents corpus must match the package source tree', ); }); test('agents-only and empty layouts derive corpus needs from layout kinds', (t) => { const vscodeDir = createTempDir('msd-vscode-surface-corpus-'); t.after(() => { cleanup(vscodeDir); }); installRuntimeArtifacts('vscode', vscodeDir, 'global', RESOLVED_CORE); assert.ok(!fs.existsSync(path.join(vscodeDir, 'msd-core', 'commands'))); assert.ok(!fs.existsSync(path.join(vscodeDir, 'msd-core', 'agents'))); }); test('local Claude and Codex installs do not gain the global corpus', (t) => { const claudeDir = createTempDir('msd-claude-local-no-corpus-'); const codexDir = createTempDir('msd-codex-local-no-corpus-'); t.after(() => { cleanup(claudeDir); cleanup(codexDir); }); installRuntimeArtifacts('claude', claudeDir, 'local', RESOLVED_CORE); installRuntimeArtifacts('codex', codexDir, 'local', RESOLVED_CORE); for (const configDir of [claudeDir, codexDir]) { assert.ok(!fs.existsSync(path.join(configDir, 'msd-core', 'commands'))); assert.ok(!fs.existsSync(path.join(configDir, 'msd-core', 'agents'))); } }); test('refresh restores canonical bytes and removes only manifest-proven stale corpus files', (t) => { const configDir = createTempDir('msd-corpus-refresh-'); t.after(() => cleanup(configDir)); sandboxHome(t, configDir); installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE); const commandsDir = path.join(configDir, 'msd-core', 'commands', 'msd'); const canonicalName = fs.readdirSync(commandsDir).find((name) => name.endsWith('.md')); assert.ok(canonicalName); fs.writeFileSync(path.join(commandsDir, canonicalName), '# corrupted\n'); fs.writeFileSync(path.join(commandsDir, 'stale-owned.md'), '# stale\n'); fs.writeFileSync(path.join(commandsDir, 'unknown-neighbour.md'), '# preserve\n'); fs.writeFileSync(path.join(configDir, 'msd-file-manifest.json'), JSON.stringify({ files: { 'msd-core/commands/msd/stale-owned.md': '0'.repeat(64) }, })); installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE); assert.strictEqual( fs.readFileSync(path.join(commandsDir, canonicalName), 'utf8'), fs.readFileSync(path.join(__dirname, '..', 'commands', 'msd', canonicalName), 'utf8'), ); assert.ok(!fs.existsSync(path.join(commandsDir, 'stale-owned.md'))); assert.strictEqual(fs.readFileSync(path.join(commandsDir, 'unknown-neighbour.md'), 'utf8'), '# preserve\n'); }); test('a partial corpus copy failure is rejected until a later install repairs it', (t) => { const configDir = createTempDir('msd-corpus-copy-failure-'); t.after(() => cleanup(configDir)); sandboxHome(t, configDir); const agentsDestination = path.join(configDir, 'msd-core', 'agents'); const originalCpSync = fs.cpSync; t.mock.method(fs, 'cpSync', (source, destination, options) => { if (destination === agentsDestination) { fs.mkdirSync(destination, { recursive: true }); fs.writeFileSync(path.join(destination, 'msd-planner.md'), '# partial agent\n'); throw new Error('injected corpus copy failure'); } return originalCpSync(source, destination, options); }); assert.throws( () => installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE), /injected corpus copy failure/, ); const incompleteLayout = resolveRuntimeArtifactLayout('codex', configDir, 'global'); assert.throws( () => incompleteLayout.kinds.find((kind) => kind.kind === 'skills').stage(RESOLVED_CORE), /install or upgrade msd-core/, ); t.mock.restoreAll(); installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE); assert.deepStrictEqual( snapshot2362Dir(agentsDestination), snapshot2362Dir(path.join(__dirname, '..', 'agents')), 'a later install must heal the partial agents corpus', ); }); test('an unreadable prior manifest preserves unproven neighbours while refreshing canonical bytes', (t) => { const configDir = createTempDir('msd-corpus-manifest-read-failure-'); t.after(() => cleanup(configDir)); sandboxHome(t, configDir); const commandsDir = path.join(configDir, 'msd-core', 'commands', 'msd'); fs.mkdirSync(commandsDir, { recursive: true }); const canonicalName = fs.readdirSync(path.join(__dirname, '..', 'commands', 'msd')) .find((name) => name.endsWith('.md')); assert.ok(canonicalName); fs.writeFileSync(path.join(commandsDir, canonicalName), '# corrupted\n'); fs.writeFileSync(path.join(commandsDir, 'unproven-neighbour.md'), '# preserve\n'); const manifestPath = path.join(configDir, 'msd-file-manifest.json'); fs.writeFileSync(manifestPath, JSON.stringify({ files: { 'msd-core/commands/msd/unproven-neighbour.md': '0'.repeat(64), } })); const originalReadFileSync = fs.readFileSync; t.mock.method(fs, 'readFileSync', (filePath, ...args) => { if (filePath === manifestPath) throw new Error('injected manifest read failure'); return originalReadFileSync(filePath, ...args); }); installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE); assert.equal( fs.readFileSync(path.join(commandsDir, canonicalName), 'utf8'), fs.readFileSync(path.join(__dirname, '..', 'commands', 'msd', canonicalName), 'utf8'), ); assert.equal( fs.readFileSync(path.join(commandsDir, 'unproven-neighbour.md'), 'utf8'), '# preserve\n', 'an unreadable manifest provides no ownership proof for deletion', ); }); test('a stale corpus removal failure leaves a resolver-rejected corpus', (t) => { const configDir = createTempDir('msd-corpus-remove-failure-'); t.after(() => cleanup(configDir)); sandboxHome(t, configDir); const commandsDir = path.join(configDir, 'msd-core', 'commands', 'msd'); const agentsDir = path.join(configDir, 'msd-core', 'agents'); fs.mkdirSync(commandsDir, { recursive: true }); fs.mkdirSync(agentsDir, { recursive: true }); const stalePath = path.join(commandsDir, 'removed-by-4132-test.md'); fs.writeFileSync(stalePath, '# stale\n'); fs.writeFileSync(path.join(agentsDir, 'msd-planner.md'), '# stale agent\n'); const manifestPath = path.join(configDir, 'msd-file-manifest.json'); const manifestBytes = JSON.stringify({ files: { 'msd-core/commands/msd/removed-by-4132-test.md': '0'.repeat(64), 'msd-core/agents/msd-planner.md': '0'.repeat(64), } }); fs.writeFileSync(manifestPath, manifestBytes); const originalRmSync = fs.rmSync; t.mock.method(fs, 'rmSync', (target, options) => { if (target === stalePath) throw new Error('injected stale corpus removal failure'); // eslint-disable-next-line local/no-raw-rmsync-in-tests -- delegate non-target production writes; fixture cleanup still uses cleanup(). return originalRmSync(target, options); }); assert.throws( () => installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE), /injected stale corpus removal failure/, ); assert.equal(fs.readFileSync(manifestPath, 'utf8'), manifestBytes, 'failed provisioning must not claim a new manifest'); const incompleteLayout = resolveRuntimeArtifactLayout('codex', configDir, 'global'); assert.throws( () => incompleteLayout.kinds.find((kind) => kind.kind === 'skills').stage(RESOLVED_CORE), /install or upgrade msd-core/, ); }); test('an empty-parent prune failure leaves a resolver-rejected corpus', (t) => { const configDir = createTempDir('msd-corpus-prune-failure-'); t.after(() => cleanup(configDir)); sandboxHome(t, configDir); const commandsDir = path.join(configDir, 'msd-core', 'commands', 'msd'); const agentsDir = path.join(configDir, 'msd-core', 'agents'); const staleParent = path.join(commandsDir, 'retired'); const stalePath = path.join(staleParent, 'removed-by-4132-test.md'); fs.mkdirSync(staleParent, { recursive: true }); fs.mkdirSync(agentsDir, { recursive: true }); fs.writeFileSync(stalePath, '# stale\n'); fs.writeFileSync(path.join(agentsDir, 'msd-planner.md'), '# stale agent\n'); const manifestPath = path.join(configDir, 'msd-file-manifest.json'); const manifestBytes = JSON.stringify({ files: { 'msd-core/commands/msd/retired/removed-by-4132-test.md': '0'.repeat(64), 'msd-core/agents/msd-planner.md': '0'.repeat(64), } }); fs.writeFileSync(manifestPath, manifestBytes); const originalRmdirSync = fs.rmdirSync; t.mock.method(fs, 'rmdirSync', (target, options) => { if (target === staleParent) throw new Error('injected corpus parent prune failure'); return originalRmdirSync(target, options); }); assert.throws( () => installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE), /injected corpus parent prune failure/, ); assert.equal(fs.existsSync(stalePath), false, 'owned stale file was removed before parent pruning failed'); assert.equal(fs.existsSync(staleParent), true, 'failed empty parent removal must leave the directory in place'); assert.equal(fs.readFileSync(manifestPath, 'utf8'), manifestBytes, 'failed provisioning must not claim a new manifest'); const incompleteLayout = resolveRuntimeArtifactLayout('codex', configDir, 'global'); assert.throws( () => incompleteLayout.kinds.find((kind) => kind.kind === 'skills').stage(RESOLVED_CORE), /install or upgrade msd-core/, ); }); for (const runtime of ['codex', 'claude']) { test(`full global ${runtime} manifest covers every installed corpus file with its hash`, (t) => { const installed = runMinimalInstall({ runtime, scope: 'global' }); t.after(() => cleanup(installed.root)); const corpusRoots = [ ['msd-core/commands/msd/', path.join(installed.configDir, 'msd-core', 'commands', 'msd')], ['msd-core/agents/', path.join(installed.configDir, 'msd-core', 'agents')], ]; for (const [prefix, root] of corpusRoots) { for (const file of walk(root)) { const key = prefix + path.relative(root, file).replace(/\\/g, '/'); const hash = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); assert.strictEqual(installed.manifest.files[key], hash, `${key} must be manifest-owned with the installed hash`); } } }); } }); const SKILLS_RUNTIMES_LAYOUT = [ 'claude', 'cursor', 'codex', 'antigravity', ]; const ALL_RUNTIMES_LAYOUT = [ 'claude', 'cursor', 'codex', 'antigravity', 'opencode', ]; function countPrefixedEntries(destDir, prefix) { if (!fs.existsSync(destDir)) return 0; return fs.readdirSync(destDir).filter(n => n.startsWith(prefix)).length; } function writeSkillEntry(destDir, prefix, stem) { const entryDir = path.join(destDir, `${prefix}${stem}`); fs.mkdirSync(entryDir, { recursive: true }); fs.writeFileSync(path.join(entryDir, 'SKILL.md'), `# ${stem}\n`); } function writeCommandEntry(destDir, prefix, stem) { fs.mkdirSync(destDir, { recursive: true }); fs.writeFileSync(path.join(destDir, `${prefix}${stem}.md`), `# ${stem}\n`); } function readAllSkillMd(dir) { const out = []; if (!fs.existsSync(dir)) return ''; const stack = [dir]; while (stack.length) { const cur = stack.pop(); for (const ent of fs.readdirSync(cur, { withFileTypes: true })) { const p = path.join(cur, ent.name); if (ent.isDirectory()) stack.push(p); else if (ent.name === 'SKILL.md') out.push(fs.readFileSync(p, 'utf8')); } } return out.join('\n'); } // Codex resolves its skills-kind destination via os.homedir() + a 'skills'-kind // 'home: ".agents"' layout override (ADR-1239 EoS upgrade 3, #2088), NOT // configDir/skills. Without sandboxing HOME, an in-process codex install would // write to (and an uninstall would mutate) the developer's REAL ~/.agents/skills. // Sandbox HOME/USERPROFILE to configDir before resolving the layout or invoking // install/uninstall so codex's resolved skills dir is configDir/.agents/skills. // // #3712: promoted to tests/helpers.cjs, from the byte-identical copy that used to // live here. It now also sets the sandbox marker src/real-home-guard.cts needs to // stay permissive on hosts with no readable passwd entry. const { sandboxHome } = require('./helpers.cjs'); describe('installRuntimeArtifacts — skills runtimes write msd-prefixed skill dirs', () => { for (const runtime of SKILLS_RUNTIMES_LAYOUT) { test(`${runtime}: msd-prefixed skill dirs in skills/`, (t) => { const configDir = createTempDir(`msd-ial-${runtime}-`); t.after(() => cleanup(configDir)); sandboxHome(t, configDir); assert.strictEqual(typeof installRuntimeArtifacts, 'function'); installRuntimeArtifacts(runtime, configDir, 'global', RESOLVED_CORE); const layout = resolveRuntimeArtifactLayout(runtime, configDir, 'global'); const skillsKind = layout.kinds.find(k => k.kind === 'skills'); assert.ok(skillsKind, `${runtime} must have skills kind`); const destDir = path.join(skillsKind.home || configDir, skillsKind.destSubpath); assert.ok(fs.existsSync(destDir)); assert.ok( fs.existsSync(path.join(destDir, `${skillsKind.prefix}help`, 'SKILL.md')), `${runtime}: ${skillsKind.prefix}help/SKILL.md must exist` ); if (RESOLVED_CORE.skills !== '*') { const prefixedCount = countPrefixedEntries(destDir, skillsKind.prefix || 'msd-'); assert.strictEqual(prefixedCount, RESOLVED_CORE.skills.size, `${runtime}: installed skill count must match profile`); } }); } }); describe('installRuntimeArtifacts — cursor skills-only layout (#2644)', () => { test('cursor: skills/ is created and commands/ is not materialized', (t) => { const configDir = createTempDir('msd-ial-cursor-cmds-'); t.after(() => cleanup(configDir)); installRuntimeArtifacts('cursor', configDir, 'global', RESOLVED_CORE); const skillsDir = path.join(configDir, 'skills'); assert.ok(fs.existsSync(skillsDir), 'skills/ must exist'); assert.ok(fs.existsSync(path.join(skillsDir, 'msd-help', 'SKILL.md')), 'skills/msd-help/SKILL.md must exist'); assert.ok(!fs.existsSync(path.join(configDir, 'commands')), 'Cursor must not materialize commands/ because skills already populate the slash menu'); }); }); describe('installRuntimeArtifacts — opencode flat commands', () => { // #2329: OpenCode discovers commands from the PLURAL `commands/` dir — the // singular `command/` made all /msd-* commands invisible to OpenCode // (see tests/opencode-command-dir-plural.test.cjs). const RUNTIME_COMMAND_DIRS = { opencode: 'commands' }; for (const runtime of ['opencode']) { test(`${runtime}: ${RUNTIME_COMMAND_DIRS[runtime]}/msd-help.md exists`, (t) => { const configDir = createTempDir(`msd-ial-${runtime}-`); t.after(() => cleanup(configDir)); installRuntimeArtifacts(runtime, configDir, 'global', RESOLVED_CORE); const commandDir = path.join(configDir, RUNTIME_COMMAND_DIRS[runtime]); assert.ok(fs.existsSync(commandDir)); assert.ok(fs.existsSync(path.join(commandDir, 'msd-help.md'))); }); } }); // ─── #784: installOpencodeFamilySkills — skills + path rewrite + preservation ─ // Stage the raw command set the way the installer's _stageSkills() does, so the // skills writer receives the same input as the flattened-command writer. function stageRawCommands(runtime, configDir) { const layout = resolveRuntimeArtifactLayout(runtime, configDir, 'global'); const commandsKind = layout.kinds.find((k) => k.kind === 'commands'); return commandsKind.stage(RESOLVED_CORE); } describe('installOpencodeFamilySkills — emits skills//SKILL.md (#784)', () => { for (const runtime of ['opencode']) { test(`${runtime}: writes msd-help/SKILL.md with name + description`, (t) => { const configDir = createTempDir(`msd-ocs-${runtime}-`); t.after(() => cleanup(configDir)); writePackageSourceMarkerFixture(configDir); const raw = stageRawCommands(runtime, configDir); const count = installOpencodeFamilySkills(runtime, configDir, raw, `${configDir}/`); assert.ok(count >= 1, 'should report installed skills'); const skillMd = path.join(configDir, 'skills', 'msd-help', 'SKILL.md'); assert.ok(fs.existsSync(skillMd), 'msd-help/SKILL.md must exist'); const content = fs.readFileSync(skillMd, 'utf8'); assert.match(content, /^name: msd-help$/m, 'name matches dir'); assert.match(content, /^description: /m, 'description present'); assert.ok(!/\/msd:/.test(content), 'no /msd: colon refs in body'); }); test(`${runtime}: rewrites body paths to the actual install target (#784 path fix)`, (t) => { const configDir = createTempDir(`msd-ocp-${runtime}-`); t.after(() => cleanup(configDir)); writePackageSourceMarkerFixture(configDir); // Simulate a custom/local install: pathPrefix points at configDir, NOT the // runtime's default global config dir. Body refs must use pathPrefix. const pathPrefix = `${configDir}/`; installOpencodeFamilySkills(runtime, configDir, stageRawCommands(runtime, configDir), pathPrefix); const defaultBase = '.config/opencode'; const help = fs.readFileSync(path.join(configDir, 'skills', 'msd-help', 'SKILL.md'), 'utf8'); // msd-help references msd-core workflow files via @/msd-core/... assert.ok( help.includes(`${configDir}/msd-core/`), 'msd-help body must reference the actual install target via pathPrefix', ); for (const skillName of fs.readdirSync(path.join(configDir, 'skills'))) { const body = fs.readFileSync(path.join(configDir, 'skills', skillName, 'SKILL.md'), 'utf8'); assert.ok( !body.includes(`~/${defaultBase}/`), `${skillName}: must not leak hardcoded ~/${defaultBase}/ — should use install target`, ); // Regression guard for the prefix-overlap double-rewrite (e.g. opencode-alt-alt). assert.ok( !new RegExp(`${escapeRegex(defaultBase)}-[^/\\s]*-`).test(body), `${skillName}: must not contain a doubled config-dir suffix`, ); } }); test(`${runtime}: preserves user-owned msd-dev-preferences across reinstall (#784)`, (t) => { const configDir = createTempDir(`msd-ocd-${runtime}-`); t.after(() => cleanup(configDir)); writePackageSourceMarkerFixture(configDir); const userSkill = path.join(configDir, 'skills', 'msd-dev-preferences'); fs.mkdirSync(userSkill, { recursive: true }); const marker = '---\nname: msd-dev-preferences\ndescription: mine\n---\nKEEP ME\n'; fs.writeFileSync(path.join(userSkill, 'SKILL.md'), marker); installOpencodeFamilySkills(runtime, configDir, stageRawCommands(runtime, configDir), `${configDir}/`); const after = fs.readFileSync(path.join(userSkill, 'SKILL.md'), 'utf8'); assert.ok(after.includes('KEEP ME'), 'user-owned dev-preferences must survive reinstall'); // MSD-managed skills should also be present. assert.ok(fs.existsSync(path.join(configDir, 'skills', 'msd-help', 'SKILL.md'))); }); } }); // ─── #2362: OpenCode combined-family INSTALL path drops the capability // registry — an installed+registered+surfaced+active third-party capability // skill never materializes ─────────────────────────────────────────────── // // installOpencodeFamilyArtifacts (called from installRuntimeArtifacts's // combinedFamilyInstall early return) never threaded capabilityRegistry into // installOpencodeFamilySkills, so the actual #2322 seam // (install-profiles.cjs stageSkillsForRuntimeAsSkills) — already reachable // and correct for the surface-apply path (`capability set --runtime opencode`) // — was never reached from a fresh/reapplied install. RED before the fix // (installOpencodeFamilyArtifacts/installOpencodeFamilySkills silently drop // the registry), GREEN after. /** Install a fake already-installed third-party capability skill under a sandboxed MSD_HOME. */ function install2362CapabilitySkill(msdHome, capId, stem, content) { const skillDir = path.join(msdHome, '.msd', 'capabilities', capId, 'skills', stem); fs.mkdirSync(skillDir, { recursive: true }); fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content, 'utf8'); } /** * A minimal capability-registry shape carrying one capId -> [stems] cluster. * `tier` (optional) additionally sets profileMembership so resolveProfile() * with a tiered (non-'*') mode unions this capability's stems into * resolvedProfile.skills — mirrors the seam's own __registryFor helper in * tests/runtime-artifact-layout-install-profiles.test.cjs. */ function registry2362For(capId, stems, tier) { const registry = { capabilityClusters: { [capId]: stems } }; if (tier) { registry.profileMembership = { [capId]: { tier, profiles: tier === 'core' ? ['core', 'standard', 'full'] : ['standard', 'full'] }, }; } return registry; } /** Run `fn` with MSD_HOME pointed at `msdHome`, always restoring the prior value. */ function with2362MsdHome(msdHome, fn) { const saved = process.env.MSD_HOME; process.env.MSD_HOME = msdHome; try { return fn(); } finally { if (saved === undefined) delete process.env.MSD_HOME; else process.env.MSD_HOME = saved; } } /** Recursively snapshot every FILE under dir into a Map. */ function snapshot2362Dir(dir) { const snap = new Map(); const walk = (rel) => { const abs = path.join(dir, rel); for (const entry of fs.readdirSync(abs, { withFileTypes: true })) { const relChild = path.join(rel, entry.name); if (entry.isDirectory()) walk(relChild); else if (entry.isFile()) snap.set(relChild, fs.readFileSync(path.join(dir, relChild), 'utf8')); } }; walk('.'); return snap; } /** Assert every entry captured in `before` (a snapshot2362Dir Map) still exists, byte-identical, under dir. */ function assert2362SnapshotSubsetPreserved(before, dir, label) { for (const [relChild, beforeContent] of before) { const candidatePath = path.join(dir, relChild); assert.ok(fs.existsSync(candidatePath), `${label}: ${relChild} must still exist`); assert.strictEqual(fs.readFileSync(candidatePath, 'utf8'), beforeContent, `${label}: ${relChild} must be byte-identical`); } } for (const __runtime2362 of ['opencode']) { describe(`installRuntimeArtifacts — #2362 ${__runtime2362} materializes an installed third-party capability skill`, () => { test(`${__runtime2362}: (1) registered capability skill materializes at skills/msd-/SKILL.md`, (t) => { const msdHome = createTempDir('msd-2362-home-'); const configDir = createTempDir(`msd-2362-${__runtime2362}-cfg-`); t.after(() => { cleanup(msdHome); cleanup(configDir); }); const AUTHORED = '---\nname: my-thing\ndescription: third-party test skill\n---\n\n# My Thing\nThird-party capability content.\n'; install2362CapabilitySkill(msdHome, 'my-thing', 'my-thing', AUTHORED); const registry = registry2362For('my-thing', ['my-thing']); const resolvedFull = resolveProfile({ modes: ['full'] }); with2362MsdHome(msdHome, () => { installRuntimeArtifacts(__runtime2362, configDir, 'global', resolvedFull, () => undefined, registry); }); const stagedPath = path.join(configDir, 'skills', 'msd-my-thing', 'SKILL.md'); assert.ok( fs.existsSync(stagedPath), `#2362: msd-my-thing/SKILL.md must exist after installRuntimeArtifacts('${__runtime2362}', ...) — registry says surfaced:true but the combined-family install path never materialized it`, ); // (2) prune parity: the staged directory carries the #2322 HIGH-3 marker // naming its declaring capId, matching the seam's guarantee. const markerPath = path.join(configDir, 'skills', 'msd-my-thing', CAPABILITY_SKILL_MARKER); assert.ok(fs.existsSync(markerPath), 'staged third-party skill must carry the CAPABILITY_SKILL_MARKER for prune parity'); assert.strictEqual(fs.readFileSync(markerPath, 'utf8').trim(), 'my-thing', 'marker must name the declaring capId'); }); test(`${__runtime2362}: (2) tiered (non-'*') profile — resolvedProfile.skills is a concrete Set and still materializes the third-party skill`, (t) => { const msdHome = createTempDir('msd-2362-home-'); const configDir = createTempDir(`msd-2362-${__runtime2362}-cfg-tiered-`); t.after(() => { cleanup(msdHome); cleanup(configDir); }); install2362CapabilitySkill(msdHome, 'my-thing', 'my-thing', '# my-thing (tiered)\n'); // tier='standard' -> profileMembership includes 'standard', so // resolveProfile({modes:['standard'], registry}) unions 'my-thing' into // the resolved Set BEFORE it ever reaches installOpencodeFamilySkills — // exercising the `(resolvedProfile && resolvedProfile.skills) || []` // (non-'*') branch of the new candidateStems ternary, never covered by // the mode=['full'] cases above. const registry = registry2362For('my-thing', ['my-thing'], 'standard'); const resolvedTiered = resolveProfile({ modes: ['standard'], registry }); assert.ok( resolvedTiered.skills instanceof Set, "sanity: a tiered (non-'*') profile mode must resolve to a concrete Set, not the full sentinel", ); assert.ok( resolvedTiered.skills.has('my-thing'), 'sanity: resolveProfile must union the registered capability skill into the tiered profile\'s Set (mirrors production callers)', ); with2362MsdHome(msdHome, () => { installRuntimeArtifacts(__runtime2362, configDir, 'global', resolvedTiered, () => undefined, registry); }); const stagedPath = path.join(configDir, 'skills', 'msd-my-thing', 'SKILL.md'); assert.ok( fs.existsSync(stagedPath), `#2362: a tiered profile (mode=standard) must also materialize a registered third-party capability skill via the non-'*' candidateStems branch`, ); const markerPath = path.join(configDir, 'skills', 'msd-my-thing', CAPABILITY_SKILL_MARKER); assert.ok(fs.existsSync(markerPath), 'staged third-party skill must carry the CAPABILITY_SKILL_MARKER for prune parity even on a tiered profile'); assert.strictEqual(fs.readFileSync(markerPath, 'utf8').trim(), 'my-thing', 'marker must name the declaring capId'); // Sanity: first-party skills that ARE part of the 'standard' base list // (e.g. 'help') must still stage too — the third-party fill-in must not // replace or crowd out the tiered profile's own first-party selection. assert.ok( fs.existsSync(path.join(configDir, 'skills', 'msd-help', 'SKILL.md')), "sanity: first-party 'standard' base skills must still stage alongside the third-party fill-in", ); }); test(`${__runtime2362}: (3) default full profile — resolvedProfile.skills === '*' still materializes the third-party skill (BLOCKER-2 parity)`, (t) => { const msdHome = createTempDir('msd-2362-home-'); const configDir = createTempDir(`msd-2362-${__runtime2362}-cfg-full-`); t.after(() => { cleanup(msdHome); cleanup(configDir); }); install2362CapabilitySkill(msdHome, 'my-thing', 'my-thing', '# my-thing (full)\n'); const registry = registry2362For('my-thing', ['my-thing']); const resolvedFull = resolveProfile({ modes: ['full'] }); assert.strictEqual(resolvedFull.skills, '*', "sanity: full mode resolves to the '*' sentinel"); with2362MsdHome(msdHome, () => { installRuntimeArtifacts(__runtime2362, configDir, 'global', resolvedFull, () => undefined, registry); }); assert.ok( fs.existsSync(path.join(configDir, 'skills', 'msd-my-thing', 'SKILL.md')), "BLOCKER-2 parity: the '*' full-profile sentinel must still stage a registered capability skill", ); }); test(`${__runtime2362}: (4) first-party wins on a stem collision`, (t) => { const msdHome = createTempDir('msd-2362-home-'); const configDir = createTempDir(`msd-2362-${__runtime2362}-cfg-collide-`); t.after(() => { cleanup(msdHome); cleanup(configDir); }); const EVIL = '# EVIL HELP — must never win over the first-party msd-help skill\n'; install2362CapabilitySkill(msdHome, 'evil-cap', 'help', EVIL); const registry = registry2362For('evil-cap', ['help']); const resolvedFull = resolveProfile({ modes: ['full'] }); with2362MsdHome(msdHome, () => { installRuntimeArtifacts(__runtime2362, configDir, 'global', resolvedFull, () => undefined, registry); }); const helpPath = path.join(configDir, 'skills', 'msd-help', 'SKILL.md'); assert.ok(fs.existsSync(helpPath), 'sanity: first-party msd-help must stage'); const helpContent = fs.readFileSync(helpPath, 'utf8'); assert.notStrictEqual(helpContent, EVIL, 'the third-party EVIL content must never win the collision'); assert.ok(!helpContent.includes('EVIL HELP'), 'first-party content must be staged on a stem collision, not the third-party body'); }); test(`${__runtime2362}: (5) graceful degradation — an unreadable capability SKILL.md does not throw and first-party staging is unaffected`, (t) => { const msdHome = createTempDir('msd-2362-home-'); const configDir = createTempDir(`msd-2362-${__runtime2362}-cfg-degrade-`); t.after(() => { cleanup(msdHome); cleanup(configDir); }); install2362CapabilitySkill(msdHome, 'my-thing', 'my-thing', '# my-thing\n'); const registry = registry2362For('my-thing', ['my-thing']); const resolvedFull = resolveProfile({ modes: ['full'] }); // Inject the IO failure deterministically by monkeypatching fs, scoped to // this one capability's SKILL.md path — never chmod 0o000 (root bypasses // mode bits; silently zero-coverage under root Docker/CI). const skillPath = path.join(msdHome, '.msd', 'capabilities', 'my-thing', 'skills', 'my-thing', 'SKILL.md'); const origReadFileSync = fs.readFileSync; fs.readFileSync = function injectedReadFileSync(p, ...rest) { if (p === skillPath) throw new Error('injected read failure (#2362 regression test)'); return origReadFileSync.call(fs, p, ...rest); }; try { with2362MsdHome(msdHome, () => { assert.doesNotThrow(() => { installRuntimeArtifacts(__runtime2362, configDir, 'global', resolvedFull, () => undefined, registry); }, `installRuntimeArtifacts('${__runtime2362}', ...) must not throw when a registry-referenced capability skill is unreadable`); }); } finally { fs.readFileSync = origReadFileSync; } assert.ok( !fs.existsSync(path.join(configDir, 'skills', 'msd-my-thing', 'SKILL.md')), 'an unreadable capability skill must be skipped, not partially staged', ); assert.ok( fs.existsSync(path.join(configDir, 'skills', 'msd-help', 'SKILL.md')), 'first-party skills must still materialize when a referenced capability skill is unreadable', ); }); test(`${__runtime2362}: (6) control — first-party skills are unperturbed (byte-identical) by an installed third-party capability skill`, (t) => { const msdHome = createTempDir('msd-2362-home-'); const configDir = createTempDir(`msd-2362-${__runtime2362}-cfg-control-`); t.after(() => { cleanup(msdHome); cleanup(configDir); }); const resolvedFull = resolveProfile({ modes: ['full'] }); // Baseline: no registry threaded through at all. installRuntimeArtifacts(__runtime2362, configDir, 'global', resolvedFull, () => undefined, undefined); const skillsDir = path.join(configDir, 'skills'); const baselineSnapshot = snapshot2362Dir(skillsDir); assert.ok(baselineSnapshot.size > 0, 'sanity: baseline install staged at least one first-party skill'); assert.ok(!baselineSnapshot.has(path.join('msd-my-thing', 'SKILL.md')), 'sanity: baseline has no msd-my-thing'); // Re-apply to the SAME configDir once a third-party capability is also // registered+installed — rebuilds nothing else, so any first-party byte // drift here is attributable only to the third-party fill-in pass. install2362CapabilitySkill(msdHome, 'my-thing', 'my-thing', '# my-thing\n'); const registry = registry2362For('my-thing', ['my-thing']); with2362MsdHome(msdHome, () => { installRuntimeArtifacts(__runtime2362, configDir, 'global', resolvedFull, () => undefined, registry); }); assert2362SnapshotSubsetPreserved(baselineSnapshot, skillsDir, `${__runtime2362} first-party skills`); assert.ok( fs.existsSync(path.join(skillsDir, 'msd-my-thing', 'SKILL.md')), 'the third-party capability skill must ALSO be present after the re-apply', ); }); }); } // ─── Section 7: uninstallRuntimeArtifacts — all runtimes ───────────────────── describe('uninstallRuntimeArtifacts — consumes Runtime Artifact Uninstall Plan Module', () => { test('removes returned plan destinations with layout kind metadata', (t) => { const configDir = createTempDir('msd-uninstall-plan-adapter-'); t.after(() => cleanup(configDir)); const commandsDir = path.join(configDir, 'custom-commands'); fs.mkdirSync(commandsDir, { recursive: true }); fs.writeFileSync(path.join(commandsDir, 'msd-help.md'), '# remove\n'); fs.writeFileSync(path.join(commandsDir, 'user-custom.md'), '# keep\n'); let planLayout; const { restore } = loadFreshInstallerWithPlanStubs({ uninstallStub(layout) { planLayout = layout; return { items: [ { kind: 'commands', destDir: commandsDir }, ], }; }, }); t.after(restore); // uninstallRuntimeArtifacts always lived in install-engine.cjs (bin/install.js's // #2876-retired re-export was an alias to the same singleton) — the direct // top-level import exercises the identical, already-monkeypatched instance. uninstallRuntimeArtifacts('opencode', configDir, 'global'); assert.strictEqual(planLayout.runtime, 'opencode'); assert.strictEqual(planLayout.configDir, configDir); assert.strictEqual(planLayout.scope, 'global'); assert.ok(!fs.existsSync(path.join(commandsDir, 'msd-help.md'))); assert.ok(fs.existsSync(path.join(commandsDir, 'user-custom.md'))); }); }); describe('uninstallRuntimeArtifacts — removes msd-owned entries, preserves foreign', () => { for (const runtime of ALL_RUNTIMES_LAYOUT) { test(`${runtime}: msd entries removed, foreign preserved`, (t) => { const configDir = createTempDir(`msd-ual-${runtime}-`); t.after(() => cleanup(configDir)); sandboxHome(t, configDir); const { uninstallRuntimeArtifacts } = require('../msd-core/bin/lib/install-engine.cjs'); assert.strictEqual(typeof uninstallRuntimeArtifacts, 'function'); const layout = resolveRuntimeArtifactLayout(runtime, configDir, 'global'); if (layout.kinds.length === 0) { const foreignDir = path.join(configDir, 'foreign-dir'); fs.mkdirSync(foreignDir, { recursive: true }); fs.writeFileSync(path.join(foreignDir, 'keep.md'), '# keep\n'); assert.doesNotThrow(() => uninstallRuntimeArtifacts(runtime, configDir, 'global')); assert.ok(fs.existsSync(path.join(foreignDir, 'keep.md'))); return; } for (const kind of layout.kinds) { const destDir = path.join(kind.home || configDir, kind.destSubpath); fs.mkdirSync(destDir, { recursive: true }); if (kind.kind === 'skills') { writeSkillEntry(destDir, kind.prefix, 'help'); writeSkillEntry(destDir, kind.prefix, 'phase'); const foreignDir = path.join(destDir, 'user-custom-skill'); fs.mkdirSync(foreignDir, { recursive: true }); fs.writeFileSync(path.join(foreignDir, 'SKILL.md'), '# user\n'); } else { writeCommandEntry(destDir, kind.prefix, 'help'); writeCommandEntry(destDir, kind.prefix, 'phase'); fs.writeFileSync(path.join(destDir, 'user-custom.md'), '# user\n'); } } uninstallRuntimeArtifacts(runtime, configDir, 'global'); for (const kind of layout.kinds) { const destDir = path.join(kind.home || configDir, kind.destSubpath); if (kind.kind === 'skills') { assert.ok(!fs.existsSync(path.join(destDir, `${kind.prefix}help`))); assert.ok(!fs.existsSync(path.join(destDir, `${kind.prefix}phase`))); assert.ok(fs.existsSync(path.join(destDir, 'user-custom-skill', 'SKILL.md'))); } else { assert.ok(!fs.existsSync(path.join(destDir, `${kind.prefix}help.md`))); assert.ok(!fs.existsSync(path.join(destDir, `${kind.prefix}phase.md`))); assert.ok(fs.existsSync(path.join(destDir, 'user-custom.md'))); } } }); } }); // ─── Section 8: Counter-test — unknown runtime is rejected (Contract 6) ────── describe('Contract 6: unknown runtime is rejected', () => { test('resolveRuntimeArtifactLayout throws TypeError for unknown runtime', () => { assert.throws( () => resolveRuntimeArtifactLayout('unknown-runtime-xyz', '/tmp/test', 'global'), (err) => { assert.ok(err instanceof TypeError, 'must be TypeError'); assert.ok(err.message.includes('Unknown runtime'), `message: ${err.message}`); return true; } ); }); test('parseRuntimeInput returns ["claude"] for unrecognised string (safe default)', () => { // parseRuntimeInput processes menu numbers, not runtime names directly; // an unrecognised token falls through to the default ["claude"]. const result = parseRuntimeInput('unknown-xyz'); assert.deepStrictEqual(result, ['claude']); }); test('allRuntimes does not include any unrecognised value', () => { // Every entry in allRuntimes must be recognised by resolveRuntimeArtifactLayout for (const runtime of allRuntimes) { assert.doesNotThrow( () => resolveRuntimeArtifactLayout(runtime, '/tmp/test', 'global'), `${runtime} must be a recognised runtime` ); } }); }); // ─── Section 12: Legacy migrations in installRuntimeArtifacts ──────────────── describe('installRuntimeArtifacts — legacy migrations run before layout copy', () => { test('claude: legacy commands/msd/dev-preferences.md migrated AND new skills written', (t) => { const configDir = createTempDir('msd-legacy-install-'); t.after(() => cleanup(configDir)); const legacyDir = path.join(configDir, 'commands', 'msd'); fs.mkdirSync(legacyDir, { recursive: true }); fs.writeFileSync(path.join(legacyDir, 'dev-preferences.md'), '# My dev prefs\n'); installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE); assert.ok(!fs.existsSync(legacyDir)); assert.ok(fs.existsSync(path.join(configDir, 'skills', 'msd-dev-preferences', 'SKILL.md'))); assert.ok(fs.existsSync(path.join(configDir, 'skills', 'msd-help', 'SKILL.md'))); }); }); describe('uninstallRuntimeArtifacts — legacy cleanup runs before layout removal', () => { test('claude: legacy commands/msd/ cleaned AND new skills/ entries removed', (t) => { const { uninstallRuntimeArtifacts } = require('../msd-core/bin/lib/install-engine.cjs'); const configDir = createTempDir('msd-legacy-uninstall-claude-'); t.after(() => cleanup(configDir)); const skillsDir = path.join(configDir, 'skills'); const msdHelp = path.join(skillsDir, 'msd-help'); fs.mkdirSync(msdHelp, { recursive: true }); fs.writeFileSync(path.join(msdHelp, 'SKILL.md'), '# help\n'); const legacyDir = path.join(configDir, 'commands', 'msd'); fs.mkdirSync(legacyDir, { recursive: true }); fs.writeFileSync(path.join(legacyDir, 'help.md'), '# legacy\n'); const userSkill = path.join(skillsDir, 'user-skill'); fs.mkdirSync(userSkill, { recursive: true }); fs.writeFileSync(path.join(userSkill, 'SKILL.md'), '# user\n'); uninstallRuntimeArtifacts('claude', configDir, 'global'); assert.ok(!fs.existsSync(msdHelp)); assert.ok(!fs.existsSync(legacyDir)); assert.ok(fs.existsSync(path.join(userSkill, 'SKILL.md'))); }); }); describe('skills wrapper threads install scope into converter isGlobal (regression: local installs must not leak global home paths)', () => { // Bug: the skills wrapper in runtime-artifact-layout passed `runtime` (a truthy // string) as the converter's 3rd positional arg. For antigravity that // param was `isGlobal`, so LOCAL installs always took the GLOBAL path branch and // leaked ~/.gemini/antigravity instead of the workspace path. for (const { runtime, globalMarker, localMarker } of [ { runtime: 'antigravity', globalMarker: '~/.gemini/antigravity', localMarker: '.agents' }, ]) { test(`${runtime}: local skill content uses workspace path, not global home`, (t) => { const globalDir = createTempDir(`msd-ial-g-${runtime}-`); const localDir = createTempDir(`msd-ial-l-${runtime}-`); t.after(() => { cleanup(globalDir); cleanup(localDir); }); // #3738: antigravity's global skills kind resolves its `home` override // from os.homedir(); sandbox HOME (with the #3712 marker) so the global // install writes inside globalDir instead of the runner's real home. sandboxHome(t, globalDir); installRuntimeArtifacts(runtime, globalDir, 'global', RESOLVED_CORE); installRuntimeArtifacts(runtime, localDir, 'local', RESOLVED_CORE); const gSkills = resolveRuntimeArtifactLayout(runtime, globalDir, 'global').kinds.find(k => k.kind === 'skills'); const lSkills = resolveRuntimeArtifactLayout(runtime, localDir, 'local').kinds.find(k => k.kind === 'skills'); assert.ok(gSkills && lSkills, `${runtime}: must resolve a skills kind for both scopes`); // #3738: the global skills tree may live under the kind `home` override // (antigravity → ~/.gemini/config), so honor it like the installer does. const gCombined = readAllSkillMd(path.join(gSkills.home ?? globalDir, gSkills.destSubpath)); const lCombined = readAllSkillMd(path.join(lSkills.home ?? localDir, lSkills.destSubpath)); // Precondition (non-vacuity guard): some core skill carries a ~/.claude // reference, so the GLOBAL install surfaces the global home marker. If this // assertion ever fails, the source skills lost their path references — fix // the fixture/source, do not delete this test. assert.ok(gCombined.includes(globalMarker), `${runtime}: precondition — global install should contain '${globalMarker}'`); // The actual regression: a LOCAL install must NOT leak the global home path… assert.ok(!lCombined.includes(globalMarker), `${runtime}: local install must NOT leak global home path '${globalMarker}'`); // …and SHOULD reference the workspace-relative path. assert.ok(lCombined.includes(localMarker), `${runtime}: local install must reference workspace path '${localMarker}'`); }); } }); // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-2418-antigravity-bare-path.test.cjs — consolidation epic #1969 (B1 #1970) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-2418-antigravity-bare-path (consolidation epic #1969 B1 #1970)", () => { /** * Bug #2418: Found unreplaced .claude path reference(s) in Antigravity install * * The Antigravity path converter handles ~/.claude/ (with trailing slash) but * misses bare ~/.claude (without trailing slash), leaving unreplaced references * that cause the installer to warn about leaked paths. * * Files affected: agents/msd-debugger.md (configDir = ~/.claude) and * msd-core/workflows/update.md (comment with e.g. ~/.claude). */ process.env.MSD_TEST_MODE = '1'; const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const { convertClaudeToAntigravityContent } = require('../bin/install.js'); describe('convertClaudeToAntigravityContent bare path replacement (#2418)', () => { describe('global install', () => { test('replaces ~/.claude (bare, no trailing slash) with ~/.gemini/antigravity', () => { const input = 'configDir = ~/.claude'; const result = convertClaudeToAntigravityContent(input, true); assert.ok( result.includes('~/.gemini/antigravity'), `Expected ~/.gemini/antigravity in output, got: ${result}` ); assert.ok( !result.includes('~/.claude'), `Expected ~/ .claude to be replaced, got: ${result}` ); }); test('replaces $HOME/.claude (bare, no trailing slash) with $HOME/.gemini/antigravity', () => { const input = 'export DIR=$HOME/.claude'; const result = convertClaudeToAntigravityContent(input, true); assert.ok( result.includes('$HOME/.gemini/antigravity'), `Expected $HOME/.gemini/antigravity in output, got: ${result}` ); assert.ok( !result.includes('$HOME/.claude'), `Expected $HOME/.claude to be replaced, got: ${result}` ); }); test('handles bare ~/.claude followed by comma (comment context)', () => { const input = '# e.g. ~/.claude, ~/.config/opencode'; const result = convertClaudeToAntigravityContent(input, true); assert.ok( !result.includes('~/.claude'), `Expected ~/ .claude to be replaced in comment context, got: ${result}` ); }); test('still replaces ~/.claude/ (with trailing slash) correctly', () => { const input = 'See ~/.claude/msd-core/workflows/'; const result = convertClaudeToAntigravityContent(input, true); assert.ok( result.includes('~/.gemini/antigravity/msd-core/workflows/'), `Expected path with trailing slash to be replaced, got: ${result}` ); assert.ok(!result.includes('~/.claude/'), `Expected ~/ .claude/ to be fully replaced, got: ${result}`); }); test('does not double-replace ~/.claude/ paths', () => { const input = 'See ~/.claude/msd-core/'; const result = convertClaudeToAntigravityContent(input, true); // Result should contain exactly one occurrence of the replacement path const count = (result.match(/~\/.gemini\/antigravity\//g) || []).length; assert.strictEqual(count, 1, `Expected exactly 1 replacement, got ${count} in: ${result}`); }); // #3738: global skills install under ~/.gemini/config/skills (the dir AGY // scans), while msd-core runtime references stay under configHome. A skills // path must therefore rewrite to the config root, not ~/.gemini/antigravity. test('replaces ~/.claude/skills/ with ~/.gemini/config/skills (#3738)', () => { const input = 'Skill dirs live at `~/.claude/skills/msd-*/`.'; const result = convertClaudeToAntigravityContent(input, true); assert.ok( result.includes('~/.gemini/config/skills/msd-*/'), `Expected ~/.gemini/config/skills rewrite, got: ${result}` ); assert.ok( !result.includes('~/.gemini/antigravity/skills'), `Skills must not point at the deprecated dir, got: ${result}` ); }); test('replaces $HOME/.claude/skills/ with $HOME/.gemini/config/skills (#3738)', () => { const input = 'ls $HOME/.claude/skills/'; const result = convertClaudeToAntigravityContent(input, true); assert.ok( result.includes('$HOME/.gemini/config/skills/'), `Expected $HOME/.gemini/config/skills rewrite, got: ${result}` ); assert.ok(!result.includes('$HOME/.claude/'), `Expected full replacement, got: ${result}`); }); test('replaces bare ~/.claude/skills (no trailing slash) with ~/.gemini/config/skills (#3738)', () => { const input = 'ls ~/.claude/skills'; const result = convertClaudeToAntigravityContent(input, true); assert.ok( result.includes('~/.gemini/config/skills'), `bare skills form must divert to the config root, got: ${result}` ); assert.ok( !result.includes('~/.gemini/antigravity/skills'), `bare skills form must not fall through to the retired configHome path, got: ${result}` ); }); test('keeps msd-core references under ~/.gemini/antigravity when a skills ref is present (#3738)', () => { const input = 'Read ~/.claude/msd-core/workflows/x.md then list ~/.claude/skills/.'; const result = convertClaudeToAntigravityContent(input, true); assert.ok( result.includes('~/.gemini/antigravity/msd-core/workflows/x.md'), `msd-core ref must stay under configHome, got: ${result}` ); assert.ok( result.includes('~/.gemini/config/skills/'), `skills ref must move to the config root, got: ${result}` ); }); }); describe('local install', () => { test('replaces ~/.claude (bare, no trailing slash) with .agents', () => { const input = 'configDir = ~/.claude'; const result = convertClaudeToAntigravityContent(input, false); assert.ok( result.includes('.agents'), `Expected .agents in output, got: ${result}` ); assert.ok( !result.includes('~/.claude'), `Expected ~/ .claude to be replaced, got: ${result}` ); }); test('replaces $HOME/.claude (bare, no trailing slash) with .agents', () => { const input = 'export DIR=$HOME/.claude'; const result = convertClaudeToAntigravityContent(input, false); assert.ok( result.includes('.agents'), `Expected .agents in output, got: ${result}` ); assert.ok( !result.includes('$HOME/.claude'), `Expected $HOME/.claude to be replaced, got: ${result}` ); }); test('does not double-replace ~/.claude/ paths', () => { const input = 'See ~/.claude/msd-core/'; const result = convertClaudeToAntigravityContent(input, false); // .agents/ should appear exactly once const count = (result.match(/\.agents\//g) || []).length; assert.strictEqual(count, 1, `Expected exactly 1 replacement, got ${count} in: ${result}`); }); }); describe('installed files contain no bare ~/.claude references after conversion', () => { const fs = require('fs'); const path = require('path'); const repoRoot = path.join(__dirname, '..'); // The scanner regex used by the installer to detect leaked paths const leakedPathRegex = /(?:~|\$HOME)\/\.claude\b/g; function convertFile(filePath, isGlobal) { const content = fs.readFileSync(filePath, 'utf8'); return convertClaudeToAntigravityContent(content, isGlobal); } test('msd-debugger.md has no leaked ~/.claude after global Antigravity conversion', () => { const debuggerPath = path.join(repoRoot, 'agents', 'msd-debugger.md'); if (!fs.existsSync(debuggerPath)) return; // skip if file doesn't exist const converted = convertFile(debuggerPath, true); const matches = converted.match(leakedPathRegex); assert.strictEqual( matches, null, `msd-debugger.md still contains leaked .claude paths after Antigravity conversion: ${matches}` ); }); test('update.md has no leaked ~/.claude after global Antigravity conversion', () => { const updatePath = path.join(repoRoot, 'msd-core', 'workflows', 'update.md'); if (!fs.existsSync(updatePath)) return; // skip if file doesn't exist const converted = convertFile(updatePath, true); const matches = converted.match(leakedPathRegex); assert.strictEqual( matches, null, `update.md still contains leaked .claude paths after Antigravity conversion: ${matches}` ); }); }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/enh-789-codebuddy-commands.test.cjs — consolidation epic #1969 (B1 #1970) // (the Cursor #2644 guard that lived in the same file is kept) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:enh-789-codebuddy-commands (consolidation epic #1969 B1 #1970)", () => { 'use strict'; const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { createTempDir, cleanup } = require('./helpers.cjs'); const { convertClaudeCommandToCursorSkill, } = require('../bin/install.js'); const { installRuntimeArtifacts, uninstallRuntimeArtifacts, } = require('../msd-core/bin/lib/install-engine.cjs'); const { resolveRuntimeArtifactLayout } = require('../msd-core/bin/lib/runtime-artifact-layout.cjs'); const { loadSkillsManifest, resolveProfile } = require('../msd-core/bin/lib/install-profiles.cjs'); const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'msd'); const MANIFEST = loadSkillsManifest(REAL_COMMANDS_DIR); const RESOLVED_CORE = resolveProfile({ modes: ['core'], manifest: MANIFEST }); // ─── #2644: Cursor skills are the one slash + model surface ────────────────── describe('fix-2644 — Cursor has one menu entry per MSD workflow', () => { test('convertClaudeCommandToCursorSkill emits only supported invocation metadata', () => { const src = [ '---', 'name: msd:help', 'description: Show help', '---', '', '# body', '', ].join('\n'); const out = convertClaudeCommandToCursorSkill(src, 'msd-help'); assert.ok(!/^user-invocable:/m.test(out), `Cursor does not support user-invocable; the field must not be emitted. Got:\n${out}`); assert.ok(!/^disable-model-invocation:/m.test(out), 'Cursor skill must remain available for contextual model invocation'); }); test('fresh install keeps the skill slash/model surface and omits commands', (t) => { const configDir = createTempDir('msd-fix2644-fresh-'); t.after(() => cleanup(configDir)); installRuntimeArtifacts('cursor', configDir, 'global', RESOLVED_CORE); const skill = fs.readFileSync(path.join(configDir, 'skills', 'msd-help', 'SKILL.md'), 'utf8'); assert.ok(!/^user-invocable:/m.test(skill)); assert.ok(!/^disable-model-invocation:/m.test(skill)); assert.ok(!fs.existsSync(path.join(configDir, 'commands', 'msd-help.md'))); }); test('reinstall removes manifest-proven legacy commands and preserves unknown files', (t) => { const configDir = createTempDir('msd-fix2644-upgrade-'); t.after(() => cleanup(configDir)); const commandsDir = path.join(configDir, 'commands'); fs.mkdirSync(commandsDir, { recursive: true }); const managedContent = '# old generated help\n'; fs.writeFileSync(path.join(commandsDir, 'msd-help.md'), managedContent); fs.writeFileSync(path.join(commandsDir, 'msd-my-custom.md'), '# user command\n'); fs.writeFileSync(path.join(configDir, 'msd-file-manifest.json'), JSON.stringify({ version: '1.8.0', timestamp: '2026-07-28T00:00:00.000Z', mode: 'full', files: { 'commands/msd-help.md': crypto.createHash('sha256').update(managedContent).digest('hex'), }, })); installRuntimeArtifacts('cursor', configDir, 'global', RESOLVED_CORE); assert.ok(!fs.existsSync(path.join(commandsDir, 'msd-help.md')), 'manifest-proven legacy command must be retired'); assert.ok(fs.existsSync(path.join(commandsDir, 'msd-my-custom.md')), 'unmanifested user command must be preserved'); }); test('profile surface apply also retires a manifest-proven legacy command', (t) => { const configDir = createTempDir('msd-fix2644-surface-'); t.after(() => cleanup(configDir)); const commandsDir = path.join(configDir, 'commands'); fs.mkdirSync(commandsDir, { recursive: true }); const content = '# old generated help\n'; fs.writeFileSync(path.join(commandsDir, 'msd-help.md'), content); fs.writeFileSync(path.join(configDir, 'msd-file-manifest.json'), JSON.stringify({ version: '1.8.0', timestamp: '2026-07-28T00:00:00.000Z', mode: 'core', files: { 'commands/msd-help.md': crypto.createHash('sha256').update(content).digest('hex') }, })); writePackageSourceMarkerFixture(configDir); const layout = resolveRuntimeArtifactLayout('cursor', configDir, 'global'); applySurface(configDir, layout, MANIFEST); assert.ok(!fs.existsSync(path.join(commandsDir, 'msd-help.md')), 'profile toggles must not leave or recreate the retired duplicate surface'); assert.ok(fs.existsSync(path.join(configDir, 'skills', 'msd-help', 'SKILL.md'))); }); test('uninstall also retires manifest-proven legacy commands', (t) => { const configDir = createTempDir('msd-fix2644-uninstall-'); t.after(() => cleanup(configDir)); const commandsDir = path.join(configDir, 'commands'); fs.mkdirSync(commandsDir, { recursive: true }); const managedContent = '# old generated help\n'; fs.writeFileSync(path.join(commandsDir, 'msd-help.md'), managedContent); fs.writeFileSync(path.join(commandsDir, 'user-command.md'), '# user command\n'); fs.writeFileSync(path.join(configDir, 'msd-file-manifest.json'), JSON.stringify({ version: '1.8.0', timestamp: '2026-07-28T00:00:00.000Z', mode: 'full', files: { 'commands/msd-help.md': crypto.createHash('sha256').update(managedContent).digest('hex'), }, })); uninstallRuntimeArtifacts('cursor', configDir, 'global'); assert.ok(!fs.existsSync(path.join(commandsDir, 'msd-help.md')), 'direct uninstall must remove a manifest-proven retired command'); assert.ok(fs.existsSync(path.join(commandsDir, 'user-command.md')), 'direct uninstall must preserve unknown user commands'); }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-2794-opencode-model-profile-overrides.test.cjs — consolidation epic #1969 (B1 #1970) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-2794-opencode-model-profile-overrides (consolidation epic #1969 B1 #1970)", () => { /** * Regression test for bug #2794 * * OpenCode generated agents ignored `model_profile_overrides.opencode.*`. * The agent install path called `readMsdEffectiveModelOverrides` (explicit * per-agent overrides) but never called `readMsdRuntimeProfileResolver` * (tier-based profile overrides). When a user configured: * * { runtime: "opencode", model_profile_overrides: { opencode: { sonnet: "..." } } } * * generated `.opencode/agents/msd-*.md` files contained no `model:` frontmatter. * * The fix adds a tier-resolver fallback in the OpenCode agent conversion block: * explicit `model_overrides[agent]` > `model_profile_overrides.opencode.` > omit. * * This test exercises: * 1. `readMsdRuntimeProfileResolver` correctly resolves OpenCode tier overrides. * 2. The agent install code path embeds the resolved model into OpenCode frontmatter. * 3. Explicit `model_overrides` still wins over tier-based resolution. * 4. Missing overrides produce no `model:` field (no regression on omit behavior). */ process.env.MSD_TEST_MODE = '1'; const { describe, test, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { install, } = require('../bin/install.js'); const { readMsdRuntimeProfileResolver, resolveAgentModelOverride } = require('../msd-core/bin/lib/install-model-override-resolver.cjs'); const { createTempDir, cleanup } = require('./helpers.cjs'); const makeTmp = (prefix) => createTempDir(`msd-2794-${prefix}-`); function writeJson(p, obj) { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(obj, null, 2), 'utf-8'); } describe('bug-2794: readMsdRuntimeProfileResolver resolves opencode tier overrides', () => { let projectDir; let homeDir; let origHome; let origUP; beforeEach(() => { projectDir = makeTmp('proj'); homeDir = makeTmp('home'); origHome = process.env.HOME; origUP = process.env.USERPROFILE; process.env.HOME = homeDir; process.env.USERPROFILE = homeDir; }); afterEach(() => { if (origHome === undefined) delete process.env.HOME; else process.env.HOME = origHome; if (origUP === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = origUP; cleanup(projectDir); cleanup(homeDir); }); test('resolves opencode sonnet tier to user-supplied model ID', () => { writeJson(path.join(projectDir, '.planning', 'config.json'), { runtime: 'opencode', model_profile: 'balanced', model_profile_overrides: { opencode: { sonnet: 'anthropic/claude-sonnet-4-7', }, }, }); const resolver = readMsdRuntimeProfileResolver(projectDir); assert.ok(resolver !== null, 'expected a resolver for opencode runtime'); // msd-roadmapper balanced tier = sonnet — should resolve to override const entry = resolver.resolve('msd-roadmapper'); assert.ok(entry !== null, 'expected entry for msd-roadmapper'); assert.strictEqual(entry.model, 'anthropic/claude-sonnet-4-7', 'sonnet override applied'); }); test('returns null resolver when runtime is not set', () => { writeJson(path.join(projectDir, '.planning', 'config.json'), { model_profile: 'balanced', model_profile_overrides: { opencode: { sonnet: 'x' } }, }); const resolver = readMsdRuntimeProfileResolver(projectDir); assert.strictEqual(resolver, null, 'no resolver without runtime field'); }); test('resolver returns null for agent not in MODEL_PROFILES', () => { writeJson(path.join(projectDir, '.planning', 'config.json'), { runtime: 'opencode', model_profile: 'balanced', model_profile_overrides: { opencode: { sonnet: 'x' } }, }); const resolver = readMsdRuntimeProfileResolver(projectDir); assert.ok(resolver !== null); const entry = resolver.resolve('msd-nonexistent-agent'); assert.strictEqual(entry, null, 'unknown agent name yields null'); }); }); // ──────────────────────────────────────────────────────────────────────── // #3705 — install-time bake honours model_policy, matching dispatch // ──────────────────────────────────────────────────────────────────────── // // The install-time chain read `runtime` / `model_profile` / // `model_profile_overrides` and nothing else — `model_policy` appeared ZERO // times in bin/install.js and in install-model-override-resolver.cts, against 16 // in the dispatch resolver. So a project configured for a non-Anthropic provider // had every agent's frontmatter rebaked to catalog `anthropic/claude-*` IDs on // each update, while `query resolve-model` returned the policy model. Only the // frontmatter is what a spawn uses, so the disagreement was silent — OpenCode // then falls back to the un-typed `general` subagent on the session model. // // Precedence here was MEASURED against dispatch on the same config, not assumed: // policy-only -> policy model; tier-overrides-only -> the override; BOTH -> // policy model. So `model_overrides` > `model_policy` > tier table > catalog. // // Home is sandboxed via HOME *and* USERPROFILE (the #2794 block's convention) — // `os.homedir()` reads both, and setting only HOME passes vacuously on Windows. describe('#3705: install-time bake honours model_policy', () => { let projectDir; let homeDir; let origHome; let origUP; const POLICY = { provider: 'generic', high: 'synthetic/hf:moonshotai/Kimi-K3', medium: 'synthetic/hf:zai-org/GLM-5.2', low: 'synthetic/hf:zai-org/GLM-5.2', }; const TIER_OVERRIDES = { opencode: { opus: 'TIEROVERRIDE-opus', sonnet: 'TIEROVERRIDE-sonnet' } }; beforeEach(() => { projectDir = makeTmp('proj-3705'); homeDir = makeTmp('home-3705'); origHome = process.env.HOME; origUP = process.env.USERPROFILE; process.env.HOME = homeDir; process.env.USERPROFILE = homeDir; }); afterEach(() => { if (origHome === undefined) delete process.env.HOME; else process.env.HOME = origHome; if (origUP === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = origUP; cleanup(projectDir); cleanup(homeDir); }); const projectConfig = (cfg) => writeJson(path.join(projectDir, '.planning', 'config.json'), cfg); const homeDefaults = (cfg) => writeJson(path.join(homeDir, '.msd', 'defaults.json'), cfg); const bake = (agent, overrides = null) => resolveAgentModelOverride(agent, overrides, readMsdRuntimeProfileResolver(projectDir)); // ── the defect ──────────────────────────────────────────────────────── test('a generic policy is baked instead of the catalog anthropic tier', () => { projectConfig({ runtime: 'opencode', model_policy: POLICY }); // msd-executor's balanced tier is sonnet -> the policy's `medium`. assert.strictEqual(bake('msd-executor'), POLICY.medium); assert.doesNotMatch(String(bake('msd-executor')), /^anthropic\/claude-/, 'the whole defect is the catalog Anthropic ID being baked over a configured provider'); }); test('tiers map to the policy the same way across agents', () => { projectConfig({ runtime: 'opencode', model_policy: POLICY }); // msd-planner is an opus-tier agent, msd-code-reviewer a sonnet-tier one. assert.strictEqual(bake('msd-planner'), POLICY.high); assert.strictEqual(bake('msd-code-reviewer'), POLICY.medium); }); test('policy outranks model_profile_overrides, matching dispatch', () => { projectConfig({ runtime: 'opencode', model_policy: POLICY, model_profile_overrides: TIER_OVERRIDES, }); assert.strictEqual(bake('msd-executor'), POLICY.medium, 'measured dispatch order: with both configured, policy wins'); }); test('an explicit per-agent model_overrides entry still outranks policy (#2256)', () => { projectConfig({ runtime: 'opencode', model_policy: POLICY, model_profile_overrides: TIER_OVERRIDES, }); assert.strictEqual(bake('msd-executor', { 'msd-executor': 'EXPLICIT' }), 'EXPLICIT'); }); // ── negative space: no policy must bake EXACTLY as before ───────────── test('without a policy, tier overrides bake exactly as they did', () => { projectConfig({ runtime: 'opencode', model_profile_overrides: TIER_OVERRIDES }); assert.strictEqual(bake('msd-executor'), 'TIEROVERRIDE-sonnet'); }); test('without a policy or overrides, the catalog tier bakes exactly as it did', () => { // The load-bearing control: this change adds a step to a chain that runs on // EVERY install for both static-frontmatter runtimes. If a project with no // policy bakes anything different, the fix has silently re-baked every // existing user. projectConfig({ runtime: 'opencode' }); assert.match(String(bake('msd-executor')), /^anthropic\/claude-/); }); test('model_profile: inherit still omits the key, policy notwithstanding', () => { // Policy must not resurrect a bake the user disabled (#3543's intent). projectConfig({ runtime: 'opencode', model_profile: 'inherit', model_policy: POLICY }); assert.strictEqual(bake('msd-executor'), null); }); test('a policy that resolves to nothing falls through to the tier table, never to null', () => { // Returning null here would OMIT a frontmatter key that used to be written — // a different regression than the one being fixed. projectConfig({ runtime: 'opencode', model_policy: { provider: 'not-a-real-provider' } }); assert.match(String(bake('msd-executor')), /^anthropic\/claude-/, 'unknown provider'); projectConfig({ runtime: 'opencode', model_policy: { provider: 'generic', medium: 'M', low: 'L' } }); assert.match(String(bake('msd-planner')), /^anthropic\/claude-/, 'policy declares no `high`, so the opus-tier agent falls through'); assert.strictEqual(bake('msd-executor'), 'M', 'while the tiers it DOES declare still resolve'); }); test('an agent absent from MODEL_PROFILES yields null, not a throw', () => { projectConfig({ runtime: 'opencode', model_policy: POLICY }); assert.strictEqual(bake('msd-nonexistent-agent'), null); }); // ── config precedence ──────────────────────────────────────────────── test('a policy in ~/.msd/defaults.json is honoured', () => { homeDefaults({ runtime: 'opencode', model_profile: 'balanced', model_policy: POLICY }); projectConfig({ runtime: 'opencode' }); assert.strictEqual(bake('msd-executor'), POLICY.medium); }); test('a project policy wins over a home-defaults policy', () => { homeDefaults({ runtime: 'opencode', model_profile: 'balanced', model_policy: POLICY }); projectConfig({ runtime: 'opencode', model_policy: { provider: 'generic', high: 'P-high', medium: 'P-medium', low: 'P-low' }, }); assert.strictEqual(bake('msd-executor'), 'P-medium'); }); // ── the rest of resolveModelPolicy's contract, reached through the bake ── test('a named provider preset resolves through the same owner', () => { projectConfig({ runtime: 'opencode', model_policy: { provider: 'openai' } }); const baked = bake('msd-executor'); assert.ok(baked, 'a known preset must resolve'); assert.doesNotMatch(String(baked), /^anthropic\/claude-/, 'the preset, not the catalog default'); }); test('the runtime_tiers escape hatch is honoured for the DOCUMENTED config shape', () => { // Review finding (blocker). The first version of this test put a `runtime` // key INSIDE `model_policy`, duplicating the top-level one. No real config // does that — docs/CONFIGURATION.md puts `runtime` at the top level and keeps // only `provider`/`runtime_tiers` inside the policy — and that one fabricated // key was the only reason the test passed. `resolveModelPolicy`'s // runtime_tiers branch reads `policy['runtime']`, which dispatch injects and // the bake did not, so runtime_tiers was silently skipped for every real // config. The fixture below is the documented shape verbatim. projectConfig({ runtime: 'opencode', model_policy: { provider: 'generic', runtime_tiers: { opencode: { sonnet: 'runtime-tiers-model' } }, medium: 'GENERIC-medium', }, }); assert.strictEqual(bake('msd-executor'), 'runtime-tiers-model', 'runtime_tiers must outrank the flat generic keys — and must be reached at all'); }); test('runtime_tiers accepts the object entry form the docs show', () => { // docs/CONFIGURATION.md's own example uses `{ model, reasoning_effort }` // rather than a bare string. projectConfig({ runtime: 'codex', model_policy: { provider: 'openai', runtime_tiers: { codex: { sonnet: { model: 'gpt-5.6-terra', reasoning_effort: 'medium' } } }, }, }); assert.strictEqual(bake('msd-executor'), 'gpt-5.6-terra'); }); test('a runtime_tiers block for a DIFFERENT runtime does not leak', () => { // The injected runtime is what selects the block; a mismatch must fall // through rather than borrow another runtime's pins. projectConfig({ runtime: 'opencode', model_policy: { provider: 'generic', runtime_tiers: { codex: { sonnet: 'CODEX-only' } }, medium: 'GENERIC-medium', }, }); assert.strictEqual(bake('msd-executor'), 'GENERIC-medium'); }); }); describe('bug-2794: OpenCode agent install embeds model_profile_overrides model', () => { let projectDir; let homeDir; let origHome; let origUP; let origCwd; beforeEach(() => { projectDir = makeTmp('proj'); homeDir = makeTmp('home'); origHome = process.env.HOME; origUP = process.env.USERPROFILE; origCwd = process.cwd(); process.env.HOME = homeDir; process.env.USERPROFILE = homeDir; process.chdir(projectDir); }); afterEach(() => { if (origHome === undefined) delete process.env.HOME; else process.env.HOME = origHome; if (origUP === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = origUP; process.chdir(origCwd); cleanup(projectDir); cleanup(homeDir); }); test('generated OpenCode agent frontmatter includes model from model_profile_overrides', () => { writeJson(path.join(projectDir, '.planning', 'config.json'), { runtime: 'opencode', model_profile: 'balanced', model_profile_overrides: { opencode: { sonnet: 'anthropic/claude-sonnet-4-7', opus: 'anthropic/claude-opus-4-7', haiku: 'anthropic/claude-haiku-4-5', }, }, }); const oldLog = console.log; console.log = () => {}; try { install(false, 'opencode'); } finally { console.log = oldLog; } const agentsDir = path.join(projectDir, '.opencode', 'agents'); assert.ok(fs.existsSync(agentsDir), 'agents directory should be created'); // msd-roadmapper is balanced -> sonnet tier const roadmapperPath = path.join(agentsDir, 'msd-roadmapper.md'); assert.ok(fs.existsSync(roadmapperPath), 'msd-roadmapper.md should exist'); const roadmapperContent = fs.readFileSync(roadmapperPath, 'utf-8'); assert.match( roadmapperContent, /^model: anthropic\/claude-sonnet-4-7$/m, 'msd-roadmapper should have sonnet model from model_profile_overrides' ); // msd-planner is balanced -> opus tier const plannerPath = path.join(agentsDir, 'msd-planner.md'); assert.ok(fs.existsSync(plannerPath), 'msd-planner.md should exist'); const plannerContent = fs.readFileSync(plannerPath, 'utf-8'); assert.match( plannerContent, /^model: anthropic\/claude-opus-4-7$/m, 'msd-planner should have opus model from model_profile_overrides' ); }); test('explicit model_overrides[agent] wins over model_profile_overrides tier', () => { writeJson(path.join(projectDir, '.planning', 'config.json'), { runtime: 'opencode', model_profile: 'balanced', model_overrides: { 'msd-roadmapper': 'explicit-winner-model', }, model_profile_overrides: { opencode: { sonnet: 'tier-model-that-should-lose', }, }, }); const oldLog = console.log; console.log = () => {}; try { install(false, 'opencode'); } finally { console.log = oldLog; } const roadmapperPath = path.join(projectDir, '.opencode', 'agents', 'msd-roadmapper.md'); assert.ok(fs.existsSync(roadmapperPath)); const content = fs.readFileSync(roadmapperPath, 'utf-8'); assert.match( content, /^model: explicit-winner-model$/m, 'explicit model_overrides must win over model_profile_overrides tier' ); assert.doesNotMatch( content, /tier-model-that-should-lose/, 'tier model must not appear when explicit override is present' ); }); test('no model field when neither model_overrides nor model_profile_overrides is set', () => { writeJson(path.join(projectDir, '.planning', 'config.json'), { runtime: 'opencode', model_profile: 'balanced', }); const oldLog = console.log; console.log = () => {}; try { install(false, 'opencode'); } finally { console.log = oldLog; } const roadmapperPath = path.join(projectDir, '.opencode', 'agents', 'msd-roadmapper.md'); if (fs.existsSync(roadmapperPath)) { const content = fs.readFileSync(roadmapperPath, 'utf-8'); // When no overrides, model field should either be absent or use built-in default // The key invariant: no model field if there are no user-supplied overrides // AND no built-in opencode defaults for this tier // (msd-roadmapper balanced = sonnet; opencode has built-in sonnet defaults) // So we only assert no crash and no tier-model-not-provided entries assert.ok(typeof content === 'string', 'agent file should be a string'); } // Key: no exception thrown (test passes = no crash on missing overrides) }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-2643-skill-frontmatter-name.test.cjs — consolidation epic #1969 (B1 #1970) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-2643-skill-frontmatter-name (consolidation epic #1969 B1 #1970)", () => { 'use strict'; process.env.MSD_TEST_MODE = '1'; /** * Bug #2643 / #2808: skill frontmatter name parity. * * Original (#2643): workflows emitted Skill(skill="msd:") and the * installer registered colon form in SKILL.md name: to match. * * Updated (#2808): workflows now use Skill(skill="msd-") (hyphen), * and the installer emits name: msd- (hyphen). Claude Code autocomplete * now shows the canonical hyphen form instead of the deprecated colon form. * The directory name (msd-) is unchanged. */ const { test, describe } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const ROOT = path.join(__dirname, '..'); const { convertClaudeCommandToClaudeSkill, skillFrontmatterName, } = require(path.join(ROOT, 'bin', 'install.js')); const WORKFLOWS_DIR = path.join(ROOT, 'msd-core', 'workflows'); const COMMANDS_DIR = path.join(ROOT, 'commands', 'msd'); function collectFiles(dir, results) { if (!results) results = []; let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return results; } for (const e of entries) { const full = path.join(dir, e.name); if (e.isDirectory()) collectFiles(full, results); else if (e.name.endsWith('.md')) results.push(full); } return results; } /** * Extract every `Skill(skill="")` invocation as a structured record. * * Per project test rigor (`feedback_no_source_grep_tests.md`), this parses * each call as a unit instead of leaning on a single regex over raw bytes. * The flow is: * * 1. Strip HTML comments so commented-out examples don't count as drift. * 2. Walk the content for `Skill(` openers; for each, find the matching * `)` closer (Skill bodies are simple kwarg lists, no nesting). * 3. Parse the call body for the `skill = "..."` keyword argument. * Permissive whitespace around the keyword and `=`, permissive * single/double quoting (with optional `\` escapes from string- * embedded examples), permissive name body — so malformed drift like * `Skill(skill="msd:extract_learnings")` is surfaced rather than * silently skipped by an over-strict character class. * * Returns `[{ name, raw }]` per call. Filtering by namespace (msd- vs msd:) * happens at the call site so the extractor stays neutral. */ function extractSkillCalls(content) { // regex-free HTML-comment stripper (CodeQL: avoid incomplete-multi-character-sanitization) let stripped = ''; { let rest = content; let idx; while ((idx = rest.indexOf('', idx + 4); if (end === -1) { rest = ''; break; } rest = rest.slice(end + 3); } stripped += rest; } const calls = []; // Body class excludes backslash so the extractor doesn't include an // escape character that precedes the closing quote in embedded examples // (e.g. `Skill(skill=\"msd-plan-phase\", …)` written inside a string // context). A trailing `\` is permitted on the closing-quote side via the // optional `\\?` so both `\"` and `"` close the value cleanly. const argRe = /^\s*skill\s*=\s*\\?(['"])([^'"\\]+)\\?\1/i; let i = 0; while (i < stripped.length) { const open = stripped.indexOf('Skill(', i); if (open === -1) break; const close = stripped.indexOf(')', open); if (close === -1) break; const body = stripped.slice(open + 'Skill('.length, close); const match = body.match(argRe); if (match) calls.push({ name: match[2], raw: stripped.slice(open, close + 1) }); i = close + 1; } return calls; } function extractSkillNamesHyphen(content) { return new Set( extractSkillCalls(content) .map((c) => c.name) .filter((n) => n.startsWith('msd-')), ); } function extractSkillNamesColon(content) { return new Set( extractSkillCalls(content) .map((c) => c.name) .filter((n) => n.startsWith('msd:')), ); } describe('skill frontmatter name parity (#2643 / #2808)', () => { test('skillFrontmatterName helper emits hyphen form (#2808)', () => { assert.strictEqual(typeof skillFrontmatterName, 'function'); assert.strictEqual(skillFrontmatterName('msd-execute-phase'), 'msd-execute-phase'); assert.strictEqual(skillFrontmatterName('msd-plan-phase'), 'msd-plan-phase'); assert.strictEqual(skillFrontmatterName('msd-next'), 'msd-next'); }); test('convertClaudeCommandToClaudeSkill emits name: msd- (hyphen)', () => { const input = '---\nname: old\ndescription: test\n---\n\nBody.'; const result = convertClaudeCommandToClaudeSkill(input, 'msd-execute-phase'); // Parse the frontmatter block structurally: extract the name: field value. const frontmatterMatch = result.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(frontmatterMatch, 'output must have a frontmatter block delimited by ---'); const frontmatterLines = frontmatterMatch[1].split(/\r?\n/); const nameEntry = frontmatterLines.find((l) => l.startsWith('name:')); assert.ok(nameEntry, 'frontmatter must contain a name: field'); const nameValue = nameEntry.replace(/^name:\s*/, '').trim(); assert.strictEqual( nameValue, 'msd-execute-phase', `frontmatter name: must be 'msd-execute-phase' (hyphen form), got '${nameValue}'` ); }); test('no workflow uses deprecated Skill(skill="msd:") colon form', () => { const workflowFiles = collectFiles(WORKFLOWS_DIR); const colonRefs = []; for (const f of workflowFiles) { const src = fs.readFileSync(f, 'utf-8'); for (const n of extractSkillNamesColon(src)) { colonRefs.push(path.basename(f) + ': ' + n); } } assert.deepStrictEqual( colonRefs, [], 'deprecated colon-form Skill() calls found (update to hyphen): ' + colonRefs.join(', ') ); }); test('every workflow Skill(skill="msd-") resolves to an emitted skill name', () => { const workflowFiles = collectFiles(WORKFLOWS_DIR); const referenced = new Set(); const templatedSkipped = []; for (const f of workflowFiles) { const src = fs.readFileSync(f, 'utf-8'); for (const n of extractSkillNamesHyphen(src)) { // Skip template expressions (e.g. `msd-${ref.skill}`): these are // capability-dispatched — the skill stem is resolved at runtime from // the `loop render-hooks` registry output (ADR-857 phase 6), so there // is no single literal skill file to validate against here. // The capability registry's own validateStep gate (gen-capability-registry.cjs) // is responsible for ensuring each `steps[].ref.skill` corresponds to a // real skill declared in the capability's `skills` array. if (n.includes('${')) { templatedSkipped.push(path.basename(f) + ': ' + n); } else { referenced.add(n); } } } assert.ok( referenced.size > 0, `expected at least one literal Skill(skill="msd-") reference in workflows under ${WORKFLOWS_DIR}` ); const emitted = new Set(); const cmdFiles = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md')); for (const cmd of cmdFiles) { const base = cmd.replace(/\.md$/, ''); const skillDirName = 'msd-' + base; const src = fs.readFileSync(path.join(COMMANDS_DIR, cmd), 'utf-8'); const out = convertClaudeCommandToClaudeSkill(src, skillDirName); const m = out.match(/^---\r?\nname:\s*(.+)$/m); if (m) emitted.add(m[1].trim()); } const missing = []; for (const r of referenced) if (!emitted.has(r)) missing.push(r); assert.deepStrictEqual( missing, [], 'workflow refs not emitted as skill names: ' + missing.join(', '), ); // Informational: report how many templated dispatches were intentionally skipped. // (Templated names are validated by the capability registry, not statically here.) if (templatedSkipped.length > 0) { // Not a failure — just a note for test output transparency. // Use a diagnostic comment: node:test does not have a skip-within-test API. } }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/enh-769-context-fork-effort.install.test.cjs — consolidation epic #1969 (B1 #1970) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:enh-769-context-fork-effort.install (consolidation epic #1969 B1 #1970)", () => { // allow-test-rule: integration-test-input (see #769) // Exercises install() as a black-box by inspecting produced SKILL.md output // in a temp dir. Source command .md files are inputs whose installed // transformation is asserted — not inspected for string presence. /** * #769 — effort: frontmatter on heavy workflow skills. * #921 — spawning orchestrators must NOT carry context: fork. * * Context: context:fork was added by #769 to protect context budget, but * plan-phase, execute-phase, and autonomous are spawning orchestrators — a * forked subagent has no Agent/Task tool, breaking their core function. * effort: high is clamped from max (thinking-disabled safety, #3039); context: fork is removed from these three. * The converter still passes context: fork through if a source file has it * (for any future leaf skill that legitimately needs isolation). * * Verifies: * 1. Source commands/msd/autonomous.md does NOT have context: fork, has effort: max * 2. Source commands/msd/execute-phase.md does NOT have context: fork, has effort: max * 3. Source commands/msd/plan-phase.md does NOT have context: fork, has effort: max * 4. Source commands/msd/progress.md has effort: low * 5. Source commands/msd/stats.md has effort: low * 6. Claude global install: SKILL.md for autonomous has effort: high (clamped), NOT context: fork * 7. Claude global install: SKILL.md for execute-phase has effort: high (clamped), NOT context: fork * 8. Claude global install: SKILL.md for plan-phase has effort: high (clamped), NOT context: fork * 9. Claude global install: SKILL.md for progress has effort: low * 10. Claude global install: SKILL.md for stats has effort: low * 11. convertClaudeCommandToClaudeSkill still passes context: fork through (for non-orchestrator skills) * 12. convertClaudeCommandToClaudeSkill emits portable effort: field values */ 'use strict'; process.env.MSD_TEST_MODE = '1'; const { describe, test, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const { install, convertClaudeCommandToClaudeSkill } = require('../bin/install.js'); const { cleanup } = require('./helpers.cjs'); // #924: Claude global install is now FLAT — concrete skills are at the top level. // flatSkillPath returns: /msd-/SKILL.md function flatSkillPath(skillsRoot, stem) { return path.join(skillsRoot, `msd-${stem}`, 'SKILL.md'); } const REPO_ROOT = path.resolve(__dirname, '..'); const SOURCE_COMMANDS_DIR = path.join(REPO_ROOT, 'commands', 'msd'); // ─── helpers ────────────────────────────────────────────────────────────────── function makeTmpDir(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); } function readFrontmatter(mdPath) { const content = fs.readFileSync(mdPath, 'utf8'); if (!content.startsWith('---')) return ''; const end = content.indexOf('---', 3); if (end === -1) return ''; return content.substring(3, end); } /** * Run a global install for Claude, redirecting its home dir to tmpHome. * Returns the tmpHome for inspection. */ function runClaudeGlobalInstall(claudeHome) { const isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-769-home-')); const prevCwd = process.cwd(); const prevClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR; const prevHome = process.env.HOME; const prevUserProfile = process.env.USERPROFILE; const prevSkipStale = process.env.MSD_SKIP_STALE_SDK_CHECK; process.env.CLAUDE_CONFIG_DIR = claudeHome; process.env.HOME = isolatedHome; process.env.USERPROFILE = isolatedHome; process.env.MSD_SKIP_STALE_SDK_CHECK = '1'; process.chdir(REPO_ROOT); try { install(true, 'claude'); } finally { process.chdir(prevCwd); if (prevClaudeConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR; else process.env.CLAUDE_CONFIG_DIR = prevClaudeConfigDir; if (prevHome === undefined) delete process.env.HOME; else process.env.HOME = prevHome; if (prevUserProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = prevUserProfile; if (prevSkipStale === undefined) delete process.env.MSD_SKIP_STALE_SDK_CHECK; else process.env.MSD_SKIP_STALE_SDK_CHECK = prevSkipStale; cleanup(isolatedHome); } return claudeHome; } // ─── describe 1: Source command files have correct frontmatter ──────────────── // #921/#922: spawning orchestrators must NOT carry context: fork — a forked // subagent has no Agent/Task tool, making it impossible for orchestrators to // spawn their required subagents. context: fork is appropriate only for leaf // skills that do not themselves dispatch agents. effort: max in source; clamped to high in emitted SKILL.md (#3039: max rejected when thinking disabled). describe('#769/#921/#1319 source commands: spawning orchestrators have effort: max but NOT context: fork', () => { test('commands/msd/autonomous.md does NOT have context: fork (#921)', () => { const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'autonomous.md')); assert.doesNotMatch(fm, /^context:[ \t]*fork$/m, `autonomous.md is a spawning orchestrator and must NOT have context: fork (#921)\nActual:\n${fm}`); }); test('commands/msd/autonomous.md has effort: max (#1319)', () => { const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'autonomous.md')); assert.match(fm, /^effort:[ \t]*max$/m, `autonomous.md SOURCE frontmatter must have effort: max (#1319)\nActual:\n${fm}`); assert.doesNotMatch(fm, /^effort:[ \t]*xhigh$/m, `autonomous.md frontmatter must not have rejected effort: xhigh (#1319)\nActual:\n${fm}`); }); test('commands/msd/execute-phase.md does NOT have context: fork (#921)', () => { const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'execute-phase.md')); assert.doesNotMatch(fm, /^context:[ \t]*fork$/m, `execute-phase.md is a spawning orchestrator and must NOT have context: fork (#921)\nActual:\n${fm}`); }); test('commands/msd/execute-phase.md has effort: max (#1319)', () => { const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'execute-phase.md')); assert.match(fm, /^effort:[ \t]*max$/m, `execute-phase.md frontmatter must have effort: max\nActual:\n${fm}`); assert.doesNotMatch(fm, /^effort:[ \t]*xhigh$/m, `execute-phase.md frontmatter must not have rejected effort: xhigh (#1319)\nActual:\n${fm}`); }); test('commands/msd/plan-phase.md does NOT have context: fork (#921)', () => { const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'plan-phase.md')); assert.doesNotMatch(fm, /^context:[ \t]*fork$/m, `plan-phase.md is a spawning orchestrator and must NOT have context: fork (#921)\nActual:\n${fm}`); }); test('commands/msd/plan-phase.md has effort: max (#1319)', () => { const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'plan-phase.md')); assert.match(fm, /^effort:[ \t]*max$/m, `plan-phase.md frontmatter must have effort: max\nActual:\n${fm}`); assert.doesNotMatch(fm, /^effort:[ \t]*xhigh$/m, `plan-phase.md frontmatter must not have rejected effort: xhigh (#1319)\nActual:\n${fm}`); }); }); describe('#769 source commands: quick-status skills have effort: low', () => { test('commands/msd/progress.md has effort: low', () => { const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'progress.md')); assert.match(fm, /^effort:[ \t]*low$/m, `progress.md frontmatter must have effort: low\nActual:\n${fm}`); }); test('commands/msd/stats.md has effort: low', () => { const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'stats.md')); assert.match(fm, /^effort:[ \t]*low$/m, `stats.md frontmatter must have effort: low\nActual:\n${fm}`); }); }); // ─── describe 2: convertClaudeCommandToClaudeSkill preserves new fields ─────── describe('#769/#1319 convertClaudeCommandToClaudeSkill: preserves context and emits portable effort fields', () => { test('preserves context: fork in emitted SKILL.md frontmatter', () => { const input = [ '---', 'name: msd:test-heavy', 'description: Test heavy skill', 'context: fork', 'effort: xhigh', 'allowed-tools:', ' - Read', ' - Bash', '---', '', 'Heavy skill body.', ].join('\n'); const result = convertClaudeCommandToClaudeSkill(input, 'test-heavy'); const end = result.indexOf('---', 3); const fm = result.substring(3, end); assert.match(fm, /^context:[ \t]*fork$/m, `SKILL.md frontmatter must include context: fork\nActual frontmatter:\n${fm}`); }); test('#3151: does NOT emit effort: into SKILL.md frontmatter (a static effort value invalidates the caller\'s prompt cache)', () => { // Whatever effort the source command declares, the installed SKILL.md must // NOT carry it: Claude Code applies SKILL.md `effort:` as output_config.effort, // and any change from the session baseline invalidates the prompt cache at both // scope boundaries (entry + exit). Verified by the reporter's owned measurement. const inputs = [ ['xhigh source', '---\nname: msd:test-heavy\ndescription: Heavy\ncontext: fork\neffort: xhigh\nallowed-tools:\n - Read\n---\n\nBody.\n'], ['low source', '---\nname: msd:test-light\ndescription: Light\neffort: low\nallowed-tools:\n - Read\n---\n\nBody.\n'], ]; for (const [label, input] of inputs) { const result = convertClaudeCommandToClaudeSkill(input, label === 'xhigh source' ? 'test-heavy' : 'test-light'); const end = result.indexOf('---', 3); const fm = result.substring(3, end); assert.doesNotMatch(fm, /^effort:/m, `SKILL.md frontmatter must NOT include effort: for ${label} (#3151)\nActual frontmatter:\n${fm}`); } }); test('does NOT emit context: or effort: when absent from source', () => { const input = [ '---', 'name: msd:test-plain', 'description: Plain skill without context or effort', 'allowed-tools:', ' - Read', '---', '', 'Plain skill body.', ].join('\n'); const result = convertClaudeCommandToClaudeSkill(input, 'test-plain'); const end = result.indexOf('---', 3); const fm = result.substring(3, end); assert.doesNotMatch(fm, /^context:/m, `SKILL.md must not emit context: when absent from source\nActual:\n${fm}`); assert.doesNotMatch(fm, /^effort:/m, `SKILL.md must not emit effort: when absent from source\nActual:\n${fm}`); }); }); // ─── describe 3: Claude global install — SKILL.md files include new fields ──── // #921/#922: after install, spawning orchestrators must NOT carry context: fork // in their emitted SKILL.md. #1319: heavyweight skills must use portable max effort. describe('#769/#921/#3151 Claude global install: SKILL.md files emit NO effort (#3151 — static effort invalidates caller cache) and NOT context: fork', () => { let tmpDir; let claudeHome; beforeEach(() => { tmpDir = makeTmpDir('msd-769-claude-'); claudeHome = path.join(tmpDir, 'claude-home'); fs.mkdirSync(claudeHome, { recursive: true }); }); afterEach(() => { cleanup(tmpDir); }); test('msd-autonomous SKILL.md does NOT have context: fork after global install (#921)', () => { runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'autonomous'); const fm = readFrontmatter(skillPath); assert.doesNotMatch(fm, /^context:[ \t]*fork$/m, `msd-autonomous is a spawning orchestrator; its SKILL.md must NOT have context: fork (#921)\nActual:\n${fm}`); }); test('msd-autonomous SKILL.md does NOT emit effort: after global install (#3151)', () => { runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'autonomous'); const fm = readFrontmatter(skillPath); assert.doesNotMatch(fm, /^effort:/m, `msd-autonomous SKILL.md must NOT emit effort: (#3151 — a static value invalidates the caller's prompt cache)\nActual:\n${fm}`); }); test('msd-execute-phase SKILL.md does NOT have context: fork after global install (#921)', () => { runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'execute-phase'); const fm = readFrontmatter(skillPath); assert.doesNotMatch(fm, /^context:[ \t]*fork$/m, `msd-execute-phase is a spawning orchestrator; its SKILL.md must NOT have context: fork (#921)\nActual:\n${fm}`); }); test('msd-execute-phase SKILL.md does NOT emit effort: after global install (#3151)', () => { runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'execute-phase'); const fm = readFrontmatter(skillPath); assert.doesNotMatch(fm, /^effort:/m, `msd-execute-phase SKILL.md must NOT emit effort: (#3151)\nActual:\n${fm}`); }); test('msd-plan-phase SKILL.md does NOT have context: fork after global install (#921)', () => { runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'plan-phase'); const fm = readFrontmatter(skillPath); assert.doesNotMatch(fm, /^context:[ \t]*fork$/m, `msd-plan-phase is a spawning orchestrator; its SKILL.md must NOT have context: fork (#921)\nActual:\n${fm}`); }); test('msd-plan-phase SKILL.md does NOT emit effort: after global install (#3151)', () => { runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'plan-phase'); const fm = readFrontmatter(skillPath); assert.doesNotMatch(fm, /^effort:/m, `msd-plan-phase SKILL.md must NOT emit effort: (#3151)\nActual:\n${fm}`); }); test('msd-progress SKILL.md does NOT emit effort: after global install (#3151)', () => { runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'progress'); const fm = readFrontmatter(skillPath); assert.doesNotMatch(fm, /^effort:/m, `msd-progress SKILL.md must NOT emit effort: (#3151)\nActual:\n${fm}`); }); test('msd-stats SKILL.md does NOT emit effort: after global install (#3151)', () => { runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'stats'); const fm = readFrontmatter(skillPath); assert.doesNotMatch(fm, /^effort:/m, `msd-stats SKILL.md must NOT emit effort: (#3151)\nActual:\n${fm}`); }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/feat-443-effort-install-wiring.install.test.cjs — consolidation epic #1969 (B1 #1970) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:feat-443-effort-install-wiring.install (consolidation epic #1969 B1 #1970)", () => { // allow-test-rule: integration-test-input (see #443) // Exercises install() + generateCodexAgentToml() as a black-box by inspecting // produced output files in temp dirs. Source agent .md files are inputs whose // installed transformation is asserted — not inspected for string presence. /** * #443 — Effort per-runtime wiring at install time. * * Verifies: * 1. Claude global install injects `effort:` into agent .md frontmatter. * 2. Codex inherited-model installs omit `model_reasoning_effort` so model * and effort are not partially pinned (#838). * 3. Config-driven proof: effort.agent_overrides wins over tier defaults * for Claude .md and for Codex .toml when runtime:"codex" pins a model. * 4. Source agents/msd-planner.md has NO effort: key (injection is * install-only, source markdown carries no effort: key). * * #1928: the gemini runtime (and its "Gemini install does NOT inject effort:" * coverage) was removed — Google sunset Gemini CLI 2026-06-18. */ 'use strict'; process.env.MSD_TEST_MODE = '1'; const { describe, test, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const { install } = require('../bin/install.js'); const { cleanup } = require('./helpers.cjs'); const REPO_ROOT = path.resolve(__dirname, '..'); const SOURCE_AGENTS_DIR = path.join(REPO_ROOT, 'agents'); // ─── helpers ───────────────────────────────────────────────────────────────── function makeTmpDir(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); } function readFrontmatter(mdPath) { const content = fs.readFileSync(mdPath, 'utf8'); if (!content.startsWith('---')) return ''; const end = content.indexOf('---', 3); if (end === -1) return ''; return content.substring(3, end); } /** * Run a global install for the given runtime, redirecting its home dir to * tmpHome. Returns the tmpHome for inspection. * * Env-var redirection: * claude → CLAUDE_CONFIG_DIR * codex → CODEX_HOME * * HOME is also redirected to an isolated temp dir for the duration of the * install call. This prevents any install.js code that uses os.homedir() * directly (e.g. ~/.cache/msd update-check deletion, ~/.msd/defaults.json * reads, stale-SDK npm subprocess writes to ~/.npm) from touching the real * HOME and polluting the test environment for other concurrently-running * test files (e.g. runtime-launcher-parity test (D) checks that * $HOME/.claude/msd-core/bin/msd-tools.cjs is absent). * * MSD_SKIP_STALE_SDK_CHECK=1 is set to suppress the `npm ls -g` subprocess * that the installer spawns for global installs — that subprocess is slow, * writes to ~/.npm cache, and is irrelevant to effort-wiring assertions. * * The working directory is set to REPO_ROOT so install() can find the source * agents/. For config-driven tests, place tmpHome inside the project dir * so that readMsdEffectiveEffortConfig(targetDir) can walk up from tmpHome * and find .planning/config.json. */ function runGlobalInstall(runtime, tmpHome) { const envVarMap = { claude: 'CLAUDE_CONFIG_DIR', codex: 'CODEX_HOME', }; const envVar = envVarMap[runtime]; if (!envVar) throw new Error(`Unsupported runtime in test: ${runtime}`); // Isolate HOME to a fresh temp dir so install.js code that calls // os.homedir() (cache deletion, defaults.json reads, npm subprocess) // never touches the real $HOME/.claude / $HOME/.cache / $HOME/.msd. const isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-443-home-')); const prev = process.env[envVar]; const prevCwd = process.cwd(); const prevHome = process.env.HOME; const prevUserProfile = process.env.USERPROFILE; const prevSkipStale = process.env.MSD_SKIP_STALE_SDK_CHECK; process.env[envVar] = tmpHome; process.env.HOME = isolatedHome; process.env.USERPROFILE = isolatedHome; process.env.MSD_SKIP_STALE_SDK_CHECK = '1'; process.chdir(REPO_ROOT); try { install(true, runtime); } finally { process.chdir(prevCwd); if (prev === undefined) delete process.env[envVar]; else process.env[envVar] = prev; if (prevHome === undefined) delete process.env.HOME; else process.env.HOME = prevHome; if (prevUserProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = prevUserProfile; if (prevSkipStale === undefined) delete process.env.MSD_SKIP_STALE_SDK_CHECK; else process.env.MSD_SKIP_STALE_SDK_CHECK = prevSkipStale; // Clean up the isolated HOME dir cleanup(isolatedHome); } return tmpHome; } // ─── Tier default expectations ──────────────────────────────────────────────── // light → low, standard → high, heavy → xhigh (catalog defaults) // msd-planner: heavy → xhigh // msd-codebase-mapper: light → low // msd-executor: standard → high // ─── describe 1: Claude install injects effort: ─────────────────────────────── describe('#443 Claude install: effort: injected into frontmatter', () => { let tmpDir; let claudeHome; beforeEach(() => { tmpDir = makeTmpDir('msd-443-claude-'); claudeHome = path.join(tmpDir, 'claude-home'); fs.mkdirSync(claudeHome, { recursive: true }); }); afterEach(() => { cleanup(tmpDir); }); test('msd-planner.md contains effort: xhigh (heavy tier default)', () => { runGlobalInstall('claude', claudeHome); const fm = readFrontmatter(path.join(claudeHome, 'agents', 'msd-planner.md')); assert.match(fm, /^effort:\s*xhigh$/m, `msd-planner frontmatter should have effort: xhigh\nActual:\n${fm}`); }); test('msd-codebase-mapper.md contains effort: low (light tier default)', () => { runGlobalInstall('claude', claudeHome); const fm = readFrontmatter(path.join(claudeHome, 'agents', 'msd-codebase-mapper.md')); assert.match(fm, /^effort:\s*low$/m, `msd-codebase-mapper frontmatter should have effort: low\nActual:\n${fm}`); }); test('msd-executor.md contains effort: high (standard tier default)', () => { runGlobalInstall('claude', claudeHome); const fm = readFrontmatter(path.join(claudeHome, 'agents', 'msd-executor.md')); assert.match(fm, /^effort:\s*high$/m, `msd-executor frontmatter should have effort: high\nActual:\n${fm}`); }); }); // ─── describe 3: Codex inherited-model install omits model_reasoning_effort ── describe('#838 Codex install: inherited model omits model_reasoning_effort', () => { let tmpDir; let codexHome; beforeEach(() => { tmpDir = makeTmpDir('msd-443-codex-'); codexHome = path.join(tmpDir, 'codex-home'); fs.mkdirSync(codexHome, { recursive: true }); }); afterEach(() => { cleanup(tmpDir); }); test('msd-planner.toml omits both model and model_reasoning_effort when model is inherited', () => { runGlobalInstall('codex', codexHome); const tomlContent = fs.readFileSync( path.join(codexHome, 'agents', 'msd-planner.toml'), 'utf8' ); assert.doesNotMatch(tomlContent, /^model\s*=/m, `msd-planner.toml should omit model when inheriting Codex chat model\nActual:\n${tomlContent.slice(0, 500)}`); assert.doesNotMatch(tomlContent, /^model_reasoning_effort\s*=/m, `msd-planner.toml should omit model_reasoning_effort when model is inherited\nActual:\n${tomlContent.slice(0, 500)}`); }); }); // ─── describe 4: Config-driven proof ───────────────────────────────────────── // // The runtime home dir must be INSIDE (or a sibling of) the project root so // that readMsdEffectiveEffortConfig(targetDir) can walk up from the runtime // home and find .planning/config.json. We put .claude/ and .codex/ as siblings // of .planning/ inside the project dir — this is the natural local-install shape. describe('#443 Config-driven: effort.agent_overrides drives install-time effort', () => { let tmpDir; let claudeHome; let codexHome; beforeEach(() => { // Layout: tmpDir/project/ <-- project root (cwd for install) // .planning/config.json // .claude/ <-- claudeHome (CLAUDE_CONFIG_DIR) // .codex/ <-- codexHome (CODEX_HOME) tmpDir = makeTmpDir('msd-443-cfg-'); const projectDir = path.join(tmpDir, 'project'); claudeHome = path.join(projectDir, '.claude'); codexHome = path.join(projectDir, '.codex'); fs.mkdirSync(claudeHome, { recursive: true }); fs.mkdirSync(codexHome, { recursive: true }); fs.mkdirSync(path.join(projectDir, '.planning'), { recursive: true }); // Write a project config with effort.agent_overrides overriding msd-planner to 'low'. // #3241: runtime:"codex" alone (with no model_overrides) no longer auto-pins a // per-tier model — the resolver-only embed was removed (D1). These tests add an // explicit model_overrides pin below so a real Codex model id survives (#3241 // row 4, "unchanged"), which keeps emitting model_reasoning_effort valid under // the #838 model/effort coupling rule. const config = { runtime: 'codex', model_overrides: { 'msd-planner': 'gpt-5.6-sol', }, effort: { agent_overrides: { 'msd-planner': 'low', }, }, }; fs.writeFileSync( path.join(projectDir, '.planning', 'config.json'), JSON.stringify(config, null, 2) ); }); afterEach(() => { cleanup(tmpDir); }); test('Claude .md gets effort: low when agent_overrides.msd-planner=low', () => { // projectDir is the cwd for install — chdir handled inside runGlobalInstall. // claudeHome is inside projectDir, so walking up from claudeHome finds .planning/config.json. runGlobalInstall('claude', claudeHome); const fm = readFrontmatter(path.join(claudeHome, 'agents', 'msd-planner.md')); assert.match(fm, /^effort:\s*low$/m, `msd-planner should have effort: low from config override\nActual:\n${fm}`); }); test('Codex .toml gets model_reasoning_effort = "low" when agent_overrides.msd-planner=low', () => { runGlobalInstall('codex', codexHome); const tomlContent = fs.readFileSync( path.join(codexHome, 'agents', 'msd-planner.toml'), 'utf8' ); assert.match(tomlContent, /^model\s*=\s*"gpt-5.6-sol"$/m, `msd-planner.toml should pin Codex model when runtime:"codex" is configured\nActual:\n${tomlContent.slice(0, 500)}`); assert.match(tomlContent, /^model_reasoning_effort\s*=\s*"low"$/m, `msd-planner.toml should have model_reasoning_effort = "low" from config override\nActual:\n${tomlContent.slice(0, 500)}`); }); // #3007: corrected — Codex's gpt-5.6-sol advertises 'max' in its own // supported_reasoning_levels, so install-time rendering now passes 'max' // through instead of clamping it to 'xhigh'. test('Codex .toml renders effort max → max when agent_overrides.msd-planner=max', () => { const projectDir = path.dirname(codexHome); // Overwrite config with max override. #3241: include an explicit // model_overrides pin (D1 removed the resolver-only auto-embed). const config = { runtime: 'codex', model_overrides: { 'msd-planner': 'gpt-5.6-sol', }, effort: { agent_overrides: { 'msd-planner': 'max', }, }, }; fs.writeFileSync( path.join(projectDir, '.planning', 'config.json'), JSON.stringify(config, null, 2) ); runGlobalInstall('codex', codexHome); const tomlContent = fs.readFileSync( path.join(codexHome, 'agents', 'msd-planner.toml'), 'utf8' ); assert.match(tomlContent, /^model\s*=\s*"gpt-5.6-sol"$/m, `msd-planner.toml should pin Codex model when runtime:"codex" is configured\nActual:\n${tomlContent.slice(0, 500)}`); // gpt-5.6-sol advertises 'max' -> renders through unchanged, no clamp. assert.match(tomlContent, /^model_reasoning_effort\s*=\s*"max"$/m, `msd-planner.toml should render max for Codex on a model that advertises it\nActual:\n${tomlContent.slice(0, 500)}`); assert.doesNotMatch(tomlContent, /model_reasoning_effort\s*=\s*"xhigh"/, 'Codex .toml must not clamp "max" down to "xhigh" for a model that advertises max'); }); }); // ─── describe 4b: #3241 — deprecation warning when a resolver-sourced model is dropped ─ // // Phase 1 (#3241) removes the automatic per-tier Codex model embed sourced from // readMsdRuntimeProfileResolver. These tests assert the observable side of that // removal at full-install granularity — a one-time deprecation warning on // stderr, never on stdout, emitted exactly once across an install even though // every Codex agent hits the same "resolver would have pinned a model" branch // simultaneously (the design's Rejected #2: "warn per agent"). // // RED (pre-fix): no such warning exists anywhere in bin/install.js today, so // captured stderr is empty for this config shape and every assertion below // that looks for warning text fails. // // Pinned wording (maintainer-confirmed, so the implementer matches it): // - single line, on stderr // - begins "msd: notice — " (deliberately distinct from the existing // "msd: warning — " dropped-override text — this is a notice about an // intentional behavior change, not a malformed value) // - contains the literal substring "model_overrides" (the recovery path) // - contains the literal substring "session model" (what the agent gets // instead) // - names neither a specific agent nor a specific model — it is a // whole-install condition, not a per-agent one // Tests assert on these substrings plus "exactly one matching line", not on // the full sentence, so the prose can improve without breaking the test. describe('#3241 Codex install: deprecation warning when a resolver-sourced model is dropped', () => { let tmpDir; let projectDir; let codexHome; let stderrChunks; let stdoutChunks; let origStderrWrite; let origStdoutWrite; beforeEach(() => { tmpDir = makeTmpDir('msd-3241-codex-warn-'); projectDir = path.join(tmpDir, 'project'); codexHome = path.join(projectDir, '.codex'); fs.mkdirSync(codexHome, { recursive: true }); fs.mkdirSync(path.join(projectDir, '.planning'), { recursive: true }); // runtime:"codex" + default model_profile:"balanced", no model_overrides — // the exact shipping shape (per 50-test-matrix.md's "Altitude" note) that // resolves a tier model per-agent via readMsdRuntimeProfileResolver (#2517). fs.writeFileSync( path.join(projectDir, '.planning', 'config.json'), JSON.stringify({ runtime: 'codex' }, null, 2) ); stderrChunks = []; stdoutChunks = []; origStderrWrite = process.stderr.write; origStdoutWrite = process.stdout.write; process.stderr.write = (chunk) => { stderrChunks.push(String(chunk)); return true; }; process.stdout.write = (chunk) => { stdoutChunks.push(String(chunk)); return true; }; }); afterEach(() => { process.stderr.write = origStderrWrite; process.stdout.write = origStdoutWrite; cleanup(tmpDir); }); test('warns exactly once on stderr with the pinned notice wording, and never on stdout (#3241)', () => { runGlobalInstall('codex', codexHome); const stderr = stderrChunks.join(''); const stdout = stdoutChunks.join(''); const noticeLines = stderr.split(/\r?\n/).filter((line) => line.startsWith('msd: notice — ')); assert.strictEqual(noticeLines.length, 1, `expected exactly one matching notice line, got ${noticeLines.length}\nstderr:\n${stderr}`); assert.match(noticeLines[0], /model_overrides/, 'the notice must name model_overrides as the recovery path'); assert.match(noticeLines[0], /session model/, 'the notice must name the session model as what the agent gets instead'); assert.doesNotMatch(stdout, /msd: notice — /, 'stdout must never carry the notice text — stdout carries installer result data'); }); test('the deprecation notice fires exactly once per install across every Codex agent, not once per agent, and no agent .toml pins a model by default (#3241)', () => { // This test rides two properties deliberately: the notice's per-install // dedupe (its original subject) AND the actual emitted-file content. The // three describe-4 tests above (#443 Config-driven) all supply an // explicit model_overrides pin to keep exercising unrelated effort- // resolution behavior, so none of them prove the default (no // model_overrides) install path actually stops pinning a model — only // the unit-level generateCodexAgentToml tests in codex-config.test.cjs // did. This is therefore the only install()-altitude coverage of Phase // 1's headline behavior until Phase 4's health-check/sync land. runGlobalInstall('codex', codexHome); const stderr = stderrChunks.join(''); const installedTomls = fs.readdirSync(path.join(codexHome, 'agents')) .filter((name) => name.endsWith('.toml')); // Sanity check: this config shape must actually install more than one // Codex agent — otherwise "exactly once, not once per agent" is untestable. assert.ok(installedTomls.length > 1, `sanity check: install must produce more than one Codex agent .toml, got ${installedTomls.length}`); const noticeLines = stderr.split(/\r?\n/).filter((line) => line.startsWith('msd: notice — ')); assert.strictEqual(noticeLines.length, 1, `expected the notice exactly once across ${installedTomls.length} installed agents, saw ${noticeLines.length}\nstderr:\n${stderr}`); // Emission check. Trap (same one ADR-2313 flags for Phase 3's sync): // `developer_instructions` is a `'''`-quoted TOML multiline block holding // the agent's raw prompt text, and MSD agent prompts discuss "model" // constantly — a bare `content.includes('model =')` would match prose // inside that block and false-fail. Guard against it by only scanning the // lines BEFORE the `developer_instructions = '''` marker (generateCodexAgentToml // always emits model / model_reasoning_effort, if present, ahead of that // marker — bin/install.js's `lines` array construction), and by anchoring // each check on a line that STARTS a TOML key (`^model\s*=`), never a bare // substring match. for (const tomlName of installedTomls) { const tomlPath = path.join(codexHome, 'agents', tomlName); const content = fs.readFileSync(tomlPath, 'utf8'); const lines = content.split(/\r?\n/); const bodyStart = lines.findIndex((line) => line.startsWith("developer_instructions = '''")); assert.notStrictEqual(bodyStart, -1, `${tomlName}: expected a developer_instructions = ''' marker to scope the header scan against\nActual:\n${content.slice(0, 500)}`); const header = lines.slice(0, bodyStart); const modelLine = header.find((line) => /^model\s*=/.test(line)); const effortLine = header.find((line) => /^model_reasoning_effort\s*=/.test(line)); assert.strictEqual(modelLine, undefined, `${tomlName} must not pin a model by default (#3241 — no model_overrides configured) — found line: ${JSON.stringify(modelLine)}`); assert.strictEqual(effortLine, undefined, `${tomlName} must not emit model_reasoning_effort with no model pinned (#838 coupling) — found line: ${JSON.stringify(effortLine)}`); } }); }); // ─── describe 5b: Invalid effort tokens fall through (Codex adversarial finding #2) ─ // // These tests FAIL before the fix: resolveInstallTimeEffort returns the raw // invalid string without validating it against VALID_EFFORTS. describe('#443 resolveInstallTimeEffort: invalid tokens fall through to valid effort', () => { let tmpDir; let claudeHome; let codexHome; beforeEach(() => { // Layout: tmpDir/project/ <-- project root // .planning/config.json // .claude/ <-- claudeHome // .codex/ <-- codexHome tmpDir = makeTmpDir('msd-443-invalid-effort-'); const projectDir = path.join(tmpDir, 'project'); claudeHome = path.join(projectDir, '.claude'); codexHome = path.join(projectDir, '.codex'); fs.mkdirSync(claudeHome, { recursive: true }); fs.mkdirSync(codexHome, { recursive: true }); fs.mkdirSync(path.join(projectDir, '.planning'), { recursive: true }); }); afterEach(() => { cleanup(tmpDir); }); function writeProjectConfig(config) { const projectDir = path.dirname(claudeHome); fs.writeFileSync( path.join(projectDir, '.planning', 'config.json'), JSON.stringify(config, null, 2) ); } const VALID_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max']; test('effort.default="ultra" (invalid) -> Claude .md effort: is a VALID value (falls through to high)', () => { // BUG before fix: resolveInstallTimeEffort returns "ultra" verbatim writeProjectConfig({ effort: { default: 'ultra' } }); runGlobalInstall('claude', claudeHome); const fm = readFrontmatter(path.join(claudeHome, 'agents', 'msd-planner.md')); const match = fm.match(/^effort:\s*(\S+)$/m); assert.ok(match, `effort: must be present in frontmatter\nActual:\n${fm}`); assert.ok(VALID_EFFORTS.includes(match[1]), `effort: must be a VALID effort string, got: "${match[1]}"\nActual frontmatter:\n${fm}`); }); test('effort.agent_overrides.msd-planner="bogus" (invalid) with valid default -> falls through to valid default', () => { // BUG before fix: "bogus" is returned and written verbatim writeProjectConfig({ effort: { agent_overrides: { 'msd-planner': 'bogus' }, default: 'medium', }, }); runGlobalInstall('claude', claudeHome); const fm = readFrontmatter(path.join(claudeHome, 'agents', 'msd-planner.md')); const match = fm.match(/^effort:\s*(\S+)$/m); assert.ok(match, `effort: must be present in frontmatter\nActual:\n${fm}`); assert.ok(VALID_EFFORTS.includes(match[1]), `effort: must be a VALID effort string, got: "${match[1]}"\nActual frontmatter:\n${fm}`); // Falls through invalid "bogus" -> valid tier default or "medium" default // "medium" is valid, so it should appear (or tier default if medium is invalid, but medium is valid) }); test('effort.default="ultra" (invalid) + runtime:"codex" -> Codex .toml model_reasoning_effort is VALID', () => { // BUG before fix: "ultra" written into .toml verbatim // #3241: include an explicit model_overrides pin — D1 removed the // resolver-only auto-embed, and model_reasoning_effort is only emitted // when a model is pinned (#838 coupling), so a pin is required to // exercise this test's actual target (effort-token fallback validity). writeProjectConfig({ runtime: 'codex', model_overrides: { 'msd-planner': 'gpt-5.6-sol' }, effort: { default: 'ultra' }, }); runGlobalInstall('codex', codexHome); const tomlContent = fs.readFileSync( path.join(codexHome, 'agents', 'msd-planner.toml'), 'utf8' ); assert.match(tomlContent, /^model\s*=\s*"gpt-5.6-sol"$/m, `msd-planner.toml should pin Codex model when runtime:"codex" is configured\nActual:\n${tomlContent.slice(0, 500)}`); // eslint-disable-next-line local/no-unbounded-quantifier -- parses output of an actual install run against test-controlled config, bounded, not adversarial input const match = tomlContent.match(/^model_reasoning_effort\s*=\s*"([^"]+)"/m); assert.ok(match, `model_reasoning_effort must be present in .toml\nActual:\n${tomlContent.slice(0, 500)}`); assert.ok(VALID_EFFORTS.includes(match[1]), `model_reasoning_effort must be VALID, got: "${match[1]}"\nActual:\n${tomlContent.slice(0, 500)}`); }); }); // ─── describe 5d: #3533 (10d) — inherit omits the effort key at install ────── describe('#3533 inherit: install writes NO effort key when the agent resolves to inherit', () => { let tmpDir; let claudeHome; let codexHome; beforeEach(() => { tmpDir = makeTmpDir('msd-3533-inherit-'); const projectDir = path.join(tmpDir, 'project'); claudeHome = path.join(projectDir, '.claude'); codexHome = path.join(projectDir, '.codex'); fs.mkdirSync(claudeHome, { recursive: true }); fs.mkdirSync(codexHome, { recursive: true }); fs.mkdirSync(path.join(projectDir, '.planning'), { recursive: true }); }); afterEach(() => { cleanup(tmpDir); }); function writeProjectConfig(config) { const projectDir = path.dirname(claudeHome); fs.writeFileSync( path.join(projectDir, '.planning', 'config.json'), JSON.stringify(config, null, 2) ); } test('claude .md frontmatter has no effort: key for an inherit-resolving agent', () => { writeProjectConfig({ effort: { agent_overrides: { 'msd-planner': 'inherit' } } }); runGlobalInstall('claude', claudeHome); const fm = readFrontmatter(path.join(claudeHome, 'agents', 'msd-planner.md')); assert.doesNotMatch(fm, /^effort:/m, `an inherit-resolving agent must carry NO effort key\nActual:\n${fm}`); // A non-inherit agent still gets its concrete value. const fmExecutor = readFrontmatter(path.join(claudeHome, 'agents', 'msd-executor.md')); const m = fmExecutor.match(/^effort:\s*(\S+)$/m); assert.ok(m && m[1] === 'high', `msd-executor keeps its concrete tier value, got: ${m && m[1]}`); }); test('codex .toml omits model_reasoning_effort for an inherit-resolving pinned agent', () => { writeProjectConfig({ runtime: 'codex', model_overrides: { 'msd-planner': 'gpt-5.6-sol' }, effort: { agent_overrides: { 'msd-planner': 'inherit' } }, }); runGlobalInstall('codex', codexHome); const toml = fs.readFileSync(path.join(codexHome, 'agents', 'msd-planner.toml'), 'utf8'); assert.match(toml, /^model\s*=\s*"gpt-5.6-sol"$/m, 'the model pin stays'); assert.doesNotMatch(toml, /^model_reasoning_effort\s*=/m, `inherit must not pin a literal effort level\nActual:\n${toml.slice(0, 400)}`); }); }); // ─── describe 5c: #3531 (10c) — a partial effort block must not discard manifest tier defaults ── // // Regression fixture from issue #3160: adding four agent_overrides produced a // 23-agent dry-run diff that DOWNGRADED xhigh-tier agents to high and UPGRADED // low-tier agents to high — because an effort block without // routing_tier_defaults disabled the built-in tier ladder (manifest tier // defaults were consulted only when the whole effort block was absent). describe('#3531 resolveInstallTimeEffort: agent_overrides-only effort block keeps manifest tier defaults', () => { let tmpDir; let claudeHome; beforeEach(() => { tmpDir = makeTmpDir('msd-3531-tier-merge-'); const projectDir = path.join(tmpDir, 'project'); claudeHome = path.join(projectDir, '.claude'); fs.mkdirSync(claudeHome, { recursive: true }); fs.mkdirSync(path.join(projectDir, '.planning'), { recursive: true }); }); afterEach(() => { cleanup(tmpDir); }); function writeProjectConfig(config) { const projectDir = path.dirname(claudeHome); fs.writeFileSync( path.join(projectDir, '.planning', 'config.json'), JSON.stringify(config, null, 2) ); } function readInstalledEffort(agentFile) { const fm = readFrontmatter(path.join(claudeHome, 'agents', agentFile)); const match = fm.match(/^effort:\s*(\S+)$/m); assert.ok(match, `effort: must be present in ${agentFile} frontmatter\nActual:\n${fm}`); return match[1]; } test('agent_overrides-only block leaves untiered agents on their manifest tier defaults', () => { // The exact #3160 shape: one agent overridden, no routing_tier_defaults. writeProjectConfig({ effort: { agent_overrides: { 'msd-code-reviewer': 'medium' } } }); runGlobalInstall('claude', claudeHome); // Before #3531: every non-overridden agent collapsed to the manifest // default 'high' — downgrading planner (xhigh) and upgrading mapper (low). assert.strictEqual(readInstalledEffort('msd-planner.md'), 'xhigh'); assert.strictEqual(readInstalledEffort('msd-executor.md'), 'high'); assert.strictEqual(readInstalledEffort('msd-codebase-mapper.md'), 'low'); // The one agent the user actually named still gets its override. assert.strictEqual(readInstalledEffort('msd-code-reviewer.md'), 'medium'); }); test('partial routing_tier_defaults fills gaps from the manifest at install', () => { writeProjectConfig({ effort: { routing_tier_defaults: { heavy: 'medium' } } }); runGlobalInstall('claude', claudeHome); assert.strictEqual(readInstalledEffort('msd-planner.md'), 'medium'); assert.strictEqual(readInstalledEffort('msd-executor.md'), 'high'); assert.strictEqual(readInstalledEffort('msd-codebase-mapper.md'), 'low'); }); }); // ─── describe 5: Source stays clean ────────────────────────────────────────── describe('#443 Source purity: agents/msd-planner.md has no effort: key', () => { test('source agents/msd-planner.md frontmatter does not contain effort:', () => { const fm = readFrontmatter(path.join(SOURCE_AGENTS_DIR, 'msd-planner.md')); assert.doesNotMatch(fm, /^effort:/m, `Source agents/msd-planner.md must NOT contain effort: (injection is install-only)`); }); test('source agents/msd-executor.md frontmatter does not contain effort:', () => { const fm = readFrontmatter(path.join(SOURCE_AGENTS_DIR, 'msd-executor.md')); assert.doesNotMatch(fm, /^effort:/m, `Source agents/msd-executor.md must NOT contain effort: (injection is install-only)`); }); test('source agents/msd-codebase-mapper.md frontmatter does not contain effort:', () => { const fm = readFrontmatter(path.join(SOURCE_AGENTS_DIR, 'msd-codebase-mapper.md')); assert.doesNotMatch(fm, /^effort:/m, `Source agents/msd-codebase-mapper.md must NOT contain effort: (injection is install-only)`); }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/enh-1510-rewrite-engine-helper-relocation.test.cjs — consolidation epic #1969 (B1 #1970) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:enh-1510-rewrite-engine-helper-relocation (consolidation epic #1969 B1 #1970)", () => { 'use strict'; // Enhancement #1510 (epic #1507, ADR-1508 Phase 1): behavior-preserving // relocation of pure rewrite-engine helpers out of hand-authored bin/install.js. // - getDirName -> msd-core/bin/lib/runtime-name-policy.cjs // - processAttribution -> msd-core/bin/lib/runtime-artifact-conversion.cjs // getCommitAttribution stays in install.js (impure install-time config I/O). // These tests exercise the REAL relocated functions at their new home (the // generated .cjs). #2876 (epic #2866 Phase 7) retired install.js's re-export // of both names — a repo-wide audit found zero production consumers of the // pass-through, so the "existing consumers" beneficiary ADR-1508 cited was // the test suite alone. The re-export absence is asserted below instead. const { test, describe } = require('node:test'); const assert = require('node:assert'); const runtimeNamePolicy = require('../msd-core/bin/lib/runtime-name-policy.cjs'); const conversion = require('../msd-core/bin/lib/runtime-artifact-conversion.cjs'); const installer = require('../bin/install.js'); // ── Slice A: getDirName relocated to runtime-name-policy ────────────────────── describe('getDirName (relocated to runtime-name-policy)', () => { const EXPECTED = { claude: '.claude', opencode: '.opencode', codex: '.codex', antigravity: '.agents', cursor: '.cursor', }; for (const [runtime, dir] of Object.entries(EXPECTED)) { test(`maps '${runtime}' to '${dir}'`, () => { assert.strictEqual(runtimeNamePolicy.getDirName(runtime), dir); }); } test('falls back to .claude for an unknown runtime', () => { assert.strictEqual(runtimeNamePolicy.getDirName('definitely-not-a-runtime'), '.claude'); }); test('falls back to .claude for empty input', () => { assert.strictEqual(runtimeNamePolicy.getDirName(''), '.claude'); }); test('bin/install.js no longer re-exports getDirName (#2876 retired the pass-through)', () => { assert.strictEqual(installer.getDirName, undefined); }); }); // ── Slice B: processAttribution relocated to runtime-artifact-conversion ─────── describe('processAttribution (relocated to runtime-artifact-conversion)', () => { test('null removes the Co-Authored-By line and its preceding blank line', () => { const input = 'Commit body line.\n\nCo-Authored-By: Someone '; assert.strictEqual(conversion.processAttribution(input, null), 'Commit body line.'); }); test('undefined leaves content unchanged', () => { const input = 'Commit body.\n\nCo-Authored-By: Someone '; assert.strictEqual(conversion.processAttribution(input, undefined), input); }); test('a string replaces the attribution value', () => { const input = 'Body\n\nCo-Authored-By: Old Name '; assert.strictEqual( conversion.processAttribution(input, 'New Name '), 'Body\n\nCo-Authored-By: New Name ', ); }); test('escapes $ in the attribution to prevent backreference injection', () => { const input = 'Body\n\nCo-Authored-By: x'; // "$1" must survive literally, not be interpreted as a regex backreference. assert.strictEqual( conversion.processAttribution(input, 'A $1 B'), 'Body\n\nCo-Authored-By: A $1 B', ); }); test('handles CRLF when removing (null)', () => { const input = 'Body\r\n\r\nCo-Authored-By: Someone '; assert.strictEqual(conversion.processAttribution(input, null), 'Body'); }); test('replaces every Co-Authored-By line (global)', () => { const input = 'Body\nCo-Authored-By: A \nCo-Authored-By: B '; assert.strictEqual( conversion.processAttribution(input, 'Z '), 'Body\nCo-Authored-By: Z \nCo-Authored-By: Z ', ); }); test('bin/install.js no longer re-exports processAttribution (#2876 retired the pass-through)', () => { assert.strictEqual(installer.processAttribution, undefined); }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/enh-1511-rewrite-engine-relocation.test.cjs — consolidation epic #1969 (B1 #1970) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:enh-1511-rewrite-engine-relocation (consolidation epic #1969 B1 #1970)", () => { 'use strict'; /** * Tests for ADR-1508 Phase 2: rewrite engine relocation to runtime-artifact-conversion. * Issue #1511 — verifies the deep public seam signatures and behavior. * * Tests are behavioral (no source-grep). All filesystem operations use tmp dirs. */ const { describe, test, before } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { cleanup } = require('./helpers.cjs'); let conversion; before(() => { process.env['MSD_TEST_MODE'] = '1'; conversion = require('../msd-core/bin/lib/runtime-artifact-conversion.cjs'); }); // --------------------------------------------------------------------------- // _computePathPrefix unit tests // --------------------------------------------------------------------------- describe('_computePathPrefix', () => { test('global under home → $HOME/... form', () => { const prefix = conversion._computePathPrefix({ isGlobal: true, isOpencode: false, isWindowsHost: false, resolvedTarget: '/home/u/.cursor', homeDir: '/home/u', }); assert.equal(prefix, '$HOME/.cursor/'); }); test('non-global → resolvedTarget/ form', () => { const prefix = conversion._computePathPrefix({ isGlobal: false, isOpencode: false, isWindowsHost: false, resolvedTarget: '/project/.cursor', homeDir: '/home/u', }); assert.equal(prefix, '/project/.cursor/'); }); test('global opencode skips $HOME shorthand', () => { // OpenCode uses ~/.config/opencode which breaks $HOME shorthand in content const prefix = conversion._computePathPrefix({ isGlobal: true, isOpencode: true, isWindowsHost: false, resolvedTarget: '/home/u/.config/opencode', homeDir: '/home/u', }); assert.equal(prefix, '/home/u/.config/opencode/'); }); test('global target outside home → resolvedTarget/ form', () => { const prefix = conversion._computePathPrefix({ isGlobal: true, isOpencode: false, isWindowsHost: false, resolvedTarget: '/opt/custom-cursor', homeDir: '/home/u', }); assert.equal(prefix, '/opt/custom-cursor/'); }); test('isWindowsHost tripwire — Windows paths collapse to $HOME/ same as POSIX (no-op today)', () => { // Documents CURRENT behavior: isWindowsHost is accepted but not branched on. // Both win32=true and win32=false return '$HOME/.cursor/' for a home-relative target. // If a future Windows-specific branch is added, this tripwire fails and forces // an explicit decision about what to return on Windows. const withWindows = conversion._computePathPrefix({ isGlobal: true, isOpencode: false, isWindowsHost: true, resolvedTarget: 'C:/Users/matte/.cursor', homeDir: 'C:/Users/matte', }); const withoutWindows = conversion._computePathPrefix({ isGlobal: true, isOpencode: false, isWindowsHost: false, resolvedTarget: 'C:/Users/matte/.cursor', homeDir: 'C:/Users/matte', }); assert.equal(withWindows, '$HOME/.cursor/'); assert.strictEqual(withWindows, withoutWindows); }); test('backslash-style resolvedTarget is normalized to forward slashes (#1615 regression)', () => { // path.join on Windows produces backslashes; the returned prefix is // substituted into markdown @-references which must use POSIX paths. // Without normalization the backslashes leak into workflow file content // and break substring checks on Windows CI. const prefix = conversion._computePathPrefix({ isGlobal: false, isOpencode: false, isWindowsHost: true, resolvedTarget: 'C:\\Users\\runner\\AppData\\Local\\Temp\\msd-1615-fixture', homeDir: 'C:\\Users\\runner', }); assert.strictEqual(prefix, 'C:/Users/runner/AppData/Local/Temp/msd-1615-fixture/'); assert.ok(!prefix.includes('\\'), `prefix must not contain backslashes: ${prefix}`); }); }); // --------------------------------------------------------------------------- // _applyRuntimeRewrites with injected attribution // --------------------------------------------------------------------------- describe('_applyRuntimeRewrites — attribution injection', () => { const PREFIX = '$HOME/.cursor/'; test('attribution=null removes Co-Authored-By line', () => { const content = '# Hello\n\nSome text\n\nCo-Authored-By: Claude\n'; const result = conversion._applyRuntimeRewrites(content, 'cursor', PREFIX, true, null); assert.ok(!result.includes('Co-Authored-By:'), 'Co-Authored-By should be removed'); }); test('attribution=undefined leaves Co-Authored-By unchanged', () => { const content = '# Hello\n\nCo-Authored-By: Claude\n'; const result = conversion._applyRuntimeRewrites(content, 'cursor', PREFIX, true, undefined); assert.ok(result.includes('Co-Authored-By: Claude'), 'Co-Authored-By should be preserved when attribution=undefined'); }); test('attribution=string replaces Co-Authored-By value', () => { const content = '# Hello\n\nCo-Authored-By: OldName\n'; const result = conversion._applyRuntimeRewrites(content, 'cursor', PREFIX, true, 'NewName '); assert.ok(result.includes('Co-Authored-By: NewName '), 'Co-Authored-By should be replaced'); }); test('cursor runtime replaces ~/.claude/ paths', () => { const content = 'See ~/.claude/skills/ for more info\n'; const result = conversion._applyRuntimeRewrites(content, 'cursor', '/home/u/.cursor/', false, undefined); assert.ok(result.includes('/home/u/.cursor/skills/'), 'cursor should replace ~/.claude/ with pathPrefix'); }); }); // --------------------------------------------------------------------------- // rewriteStagedSkillBodies — behavioral filesystem test // --------------------------------------------------------------------------- describe('rewriteStagedSkillBodies', () => { test('rewrites .md files in-place for cursor runtime', () => { const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-test-staged-')); const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-test-config-')); try { // Create a skill dir with a SKILL.md referencing ~/.claude/skills/foo // NOTE: the rewrite engine handles path replacement and attribution only. // Bash→Shell conversion is done by the stage-1 skill converter, not the engine. const skillDir = path.join(stagedDir, 'msd-test-skill'); fs.mkdirSync(skillDir, { recursive: true }); const content = '# Test\n\nSee ~/.claude/skills/foo\n\nAlso ~/.cursor/skills/bar\n'; fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content); // Call with injected homedir + platform for determinism conversion.rewriteStagedSkillBodies(stagedDir, { runtime: 'cursor', configDir, scope: 'global', homedir: () => '/home/u', platform: 'linux', }); const result = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8'); // cursor rewrites ~/.claude/ → pathPrefix // configDir is a tmpdir, not under /home/u, so prefix = resolvedTarget + '/' // Mirror the engine's backslash→slash normalization so the assertion holds on Windows. const resolvedTarget = path.resolve(configDir).replace(/\\/g, '/'); assert.ok(result.includes(`${resolvedTarget}/skills/foo`), `Should replace ~/.claude/skills/ with ${resolvedTarget}/skills/`); // cursor also rewrites ~/.cursor/ → pathPrefix assert.ok(result.includes(`${resolvedTarget}/skills/bar`), `Should replace ~/.cursor/skills/ with ${resolvedTarget}/skills/`); } finally { cleanup(stagedDir); cleanup(configDir); } }); test('with injected homedir: global under home uses $HOME prefix', () => { // Real absolute path so Windows path.resolve does not re-root a POSIX literal onto a drive. // The dir need not exist — the engine only string-processes it. const HOME = path.resolve(os.tmpdir(), 'msd-1511-fake-home'); const configDir = path.join(HOME, '.cursor'); const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-test-staged-')); try { const skillDir = path.join(stagedDir, 'msd-help'); fs.mkdirSync(skillDir, { recursive: true }); fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'Use ~/.claude/skills/ here\n'); conversion.rewriteStagedSkillBodies(stagedDir, { runtime: 'cursor', configDir, scope: 'global', homedir: () => HOME, platform: process.platform, }); const result = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8'); assert.ok(result.includes('$HOME/.cursor/skills/'), 'Should use $HOME shorthand when configDir is under homedir'); } finally { cleanup(stagedDir); } }); test('non-existent stagedDir is a no-op', () => { assert.doesNotThrow(() => { conversion.rewriteStagedSkillBodies('/nonexistent/dir', { runtime: 'cursor', configDir: '/tmp/fake', scope: 'global', }); }); }); }); // --------------------------------------------------------------------------- // rewriteStagedCommandBodies — returns temp dir, does not mutate source // --------------------------------------------------------------------------- describe('rewriteStagedCommandBodies', () => { test('returns a temp dir (not the source dir) with rewritten content', () => { const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-test-cmd-')); const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-test-config-')); let tempDir; try { // NOTE: rewrite engine handles path replacement + attribution, NOT tool renames. fs.writeFileSync(path.join(stagedDir, 'help.md'), '# Help\n\nSee ~/.claude/skills/\n\nSee ~/.cursor/skills/\n'); tempDir = conversion.rewriteStagedCommandBodies(stagedDir, { runtime: 'cursor', configDir, scope: 'global', homedir: () => '/home/u', platform: 'linux', }); assert.notEqual(tempDir, stagedDir, 'must return a different dir, never the source'); assert.ok(fs.existsSync(tempDir), 'returned tempDir should exist'); const result = fs.readFileSync(path.join(tempDir, 'help.md'), 'utf8'); // Source dir should be unchanged const source = fs.readFileSync(path.join(stagedDir, 'help.md'), 'utf8'); assert.ok(source.includes('~/.claude/skills/'), 'source file must not be mutated'); // configDir is /tmp/... (not under /home/u), so prefix = resolvedTarget + '/' const resolvedTarget = path.resolve(configDir).replace(/\\/g, '/'); assert.ok(result.includes(`${resolvedTarget}/skills/`), 'output should have cursor path rewrite applied'); // ~/.cursor/ also rewrites to prefix assert.ok(!result.includes('~/.cursor/'), 'output should have ~/.cursor/ replaced too'); } finally { cleanup(stagedDir); cleanup(configDir); if (tempDir && tempDir !== stagedDir) { cleanup(tempDir); } } }); test('non-existent stagedDir returns stagedDir unchanged (safe)', () => { const result = conversion.rewriteStagedCommandBodies('/nonexistent/dir', { runtime: 'cursor', configDir: '/tmp/fake', scope: 'global', }); assert.equal(result, '/nonexistent/dir', 'should return input path unchanged for missing dir'); }); }); // --------------------------------------------------------------------------- // #4377: project-relative includes for local installs // --------------------------------------------------------------------------- describe('#4377 _computePathPrefix — project-relative local includes', () => { // This block sits inside the enh-1511 fold, whose scope has its own // `conversion` binding and does NOT see the outer file's // `runtimeNamePolicy` (that one belongs to the fold that closed above). // require() is cached, so this is a lookup, not a second load. const namePolicy = require('../msd-core/bin/lib/runtime-name-policy.cjs'); // A local install baked the install-time absolute path into every generated // @ include. Worked from several git worktrees, that means each worktree // runs its own engine but reads its workflow prose out of ONE checkout — // and updating that checkout breaks every other worktree at once, with no // way to stage it. The relative form lets each worktree read its own copy. // // Every arm below passes `projectRelative` explicitly rather than leaning on // the environment default, so these assertions cannot flip on an ambient // MSD_RELATIVE_INCLUDES leaking in from the runner. const prefix = (over) => conversion._computePathPrefix({ isGlobal: false, isOpencode: false, isWindowsHost: false, resolvedTarget: '/project/.cursor', homeDir: '/home/u', ...over, }); test('opted in, a local install emits the descriptor dir, not the checkout path', () => { assert.equal(prefix({ projectRelative: true, localDirName: '.cursor' }), '.cursor/'); }); test('opted OUT, a local install is byte-identical to the pre-#4377 behavior', () => { // The default must not change for the single-checkout majority. assert.equal(prefix({ projectRelative: false, localDirName: '.cursor' }), '/project/.cursor/'); }); test('a GLOBAL install ignores the opt-in entirely', () => { // The $HOME-shorthand branch is the global contract and #4377 does not // touch it — a relative include in a global install would resolve against // whatever project the user happens to be sitting in. assert.equal( conversion._computePathPrefix({ isGlobal: true, isOpencode: false, isWindowsHost: false, resolvedTarget: '/home/u/.cursor', homeDir: '/home/u', projectRelative: true, localDirName: '.cursor', }), '$HOME/.cursor/', ); }); test('a nested descriptor dir survives as a nested relative prefix', () => { assert.equal( prefix({ projectRelative: true, localDirName: '.config/opencode', resolvedTarget: '/project/.config/opencode' }), '.config/opencode/', ); }); test('an explicit local target derives its prefix from the resolved target, not the runtime default', () => { assert.equal( prefix({ projectRelative: true, projectRoot: '/project', resolvedTarget: '/project/.custom/claude', localDirName: '.claude' }), '.custom/claude/', ); }); test('a Windows-style descriptor value is normalized to POSIX', () => { // The prefix is substituted into markdown @-references, which are POSIX // universally — a backslash here leaks into shipped content (#1615). assert.equal(prefix({ projectRelative: true, localDirName: '.claude\\nested' }), '.claude/nested/'); }); test('a trailing slash in the descriptor does not double up', () => { assert.equal(prefix({ projectRelative: true, localDirName: '.cursor/' }), '.cursor/'); }); // ── fail-safe arms: anything unexpressible falls back to absolute ────────── // A wrong-but-absolute include still points at a real file. A wrong RELATIVE // one silently resolves against whatever the reader's cwd happens to be, // which is a worse failure than the one being fixed. for (const [label, localDirName] of [ ['the no-local-config-dir sentinel (vscode)', namePolicy.NO_LOCAL_CONFIG_DIR_SENTINEL], ['an absolute descriptor value', '/etc/msd'], ['a Windows-absolute descriptor value', 'C:/msd'], ['a value climbing out of the project', '../outside'], ['a value climbing out of the project mid-path', 'nested/../../outside'], ['a bare ..', '..'], ['an empty value', ''], ['a missing value', undefined], ]) { test(`falls back to the absolute prefix for ${label}`, () => { assert.equal(prefix({ projectRelative: true, localDirName }), '/project/.cursor/'); }); } }); describe('#4377 _relativeIncludesEnabled — the opt-in is off unless asked for', () => { test('reads MSD_RELATIVE_INCLUDES=1 from the injected environment', () => { assert.equal(conversion._relativeIncludesEnabled({ MSD_RELATIVE_INCLUDES: '1' }), true); }); test('anything other than the exact string 1 is off', () => { // No truthiness coercion: 'true'/'0'/'' must not silently opt a user in. for (const value of ['true', 'yes', '0', '', 'TRUE', ' 1']) { assert.equal(conversion._relativeIncludesEnabled({ MSD_RELATIVE_INCLUDES: value }), false, `value=${JSON.stringify(value)}`); } }); test('an absent variable is off', () => { assert.equal(conversion._relativeIncludesEnabled({}), false); }); test('the opt-in is true for exactly the string 1 over arbitrary JSON values', () => { fc.assert(fc.property(fc.jsonValue(), (value) => { assert.equal( conversion._relativeIncludesEnabled({ MSD_RELATIVE_INCLUDES: value }), value === '1', ); })); }); }); describe('#4377 project-relative prefix properties', () => { const segment = fc.array( fc.constantFrom(...'abcdefghijklmnopqrstuvwxyz0123456789_-'), { minLength: 1, maxLength: 12 }, ).map((chars) => chars.join('')); test('safe descriptor segments normalize to one POSIX prefix', () => { fc.assert(fc.property( fc.array(segment, { minLength: 1, maxLength: 5 }), fc.constantFrom('/', '\\'), fc.boolean(), (segments, separator, trailingSlash) => { const localDirName = segments.join(separator) + (trailingSlash ? separator : ''); assert.equal(conversion._projectRelativePrefix(localDirName), `${segments.join('/')}/`); }, )); }); test('a traversal segment is rejected at every generated depth', () => { fc.assert(fc.property( fc.array(segment, { maxLength: 4 }), fc.array(segment, { maxLength: 4 }), (before, after) => { assert.equal(conversion._projectRelativePrefix([...before, '..', ...after].join('/')), ''); }, )); }); }); describe('#4377 relative rewrites preserve every runtime launcher shell default', () => { test('the shared mask preserves a complete nested shell default as one unit', () => { const nested = '${OUTER:-${INNER:-$HOME/.claude}/msd-core}'; const input = `outside=$HOME/.claude inside=${nested}`; const rewritten = conversion._withShellDefaultsPreserved( input, (body) => body.replace(/\$HOME\/\.claude/g, '.claude'), ); assert.equal(rewritten, `outside=.claude inside=${nested}`); }); test('all emitted runtime conversions leave ${VAR:-default} probes verbatim', () => { const launcher = fs.readFileSync( path.join(__dirname, '..', 'msd-core', 'workflows', '_runtime-launcher.snippet.sh'), 'utf8', ); const defaults = []; for (let start = launcher.indexOf('${'); start !== -1; start = launcher.indexOf('${', start + 2)) { let depth = 1; let end = start + 2; while (end < launcher.length && depth > 0) { if (launcher.startsWith('${', end)) { depth += 1; end += 2; } else { if (launcher[end] === '}') depth -= 1; end += 1; } } if (depth !== 0) break; const expansion = launcher.slice(start, end); if (/^\$\{[A-Za-z_][A-Za-z0-9_]*:-/.test(expansion)) defaults.push(expansion); start = end - 2; } assert.ok(defaults.length > 0, 'the launcher fixture must contain shell-default probes'); for (const runtime of ['claude', ...conversion.NON_CLAUDE_RUNTIMES]) { const rewritten = conversion._applyRuntimeRewrites( launcher, runtime, `.${runtime}/`, ); for (const shellDefault of defaults) { assert.ok( rewritten.includes(shellDefault), `${runtime} relative rewrite must preserve ${shellDefault}`, ); } } }); }); // --------------------------------------------------------------------------- // Error-path: applyRuntimeContentRewritesForCommandsInPlace must rm the tempDir // on any exception and NOT leave an orphaned msd-cmd-rewrites-* directory. // --------------------------------------------------------------------------- describe('applyRuntimeContentRewritesForCommandsInPlace — error-path tempDir cleanup', () => { test('rmSync is called on the tempDir when readFileSync throws (deterministic monkeypatch)', () => { // Asserting the injected error propagates proves the throw happens AFTER the tempDir is // created (the function creates tempDir, then reads .md), so the catch's rmSync cleanup // is genuinely exercised — deterministic on every platform/uid. const stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-test-error-path-')); fs.writeFileSync(path.join(stagedDir, 'x.md'), '# test\n'); // Capture the EXACT tempDir THIS invocation creates (via the function's own // fs.mkdtempSync call) instead of diffing the shared os.tmpdir() listing. // The old diff-and-sweep approach raced any concurrently-running test file // that mkdtemps its own msd-cmd-rewrites-* dir under --test-concurrency: it // misattributed a sibling's live dir as this test's leak AND force-deleted // it mid-use (the #1575 ENOENT on graphify.md). Owning a single, // self-generated fixture makes this Independent + Repeatable under any // parallelism. const origMkdtempSync = fs.mkdtempSync; const origReadFileSync = fs.readFileSync; let capturedTempDir = null; try { fs.mkdtempSync = (...args) => (capturedTempDir = origMkdtempSync.apply(fs, args)); fs.readFileSync = () => { throw new Error('injected read failure'); }; assert.throws( () => conversion.applyRuntimeContentRewritesForCommandsInPlace(stagedDir, 'cursor', '/tmp/x/', false), /injected read failure/, ); // Restore before any further fs use so the existsSync check is trustworthy. fs.mkdtempSync = origMkdtempSync; fs.readFileSync = origReadFileSync; assert.ok(capturedTempDir, 'function under test must create a tempDir before failing'); assert.equal( fs.existsSync(capturedTempDir), false, `tempDir not cleaned up on error: ${capturedTempDir}`, ); } finally { // Idempotent restore — guard against early-throw paths above. fs.mkdtempSync = origMkdtempSync; fs.readFileSync = origReadFileSync; // Clean up only OUR OWN fixture — never sweep the shared os.tmpdir(). cleanup(stagedDir); } }); }); // --------------------------------------------------------------------------- // Guard: runtime-artifact-layout no longer exports getInstallExports // --------------------------------------------------------------------------- describe('layout module no longer exports getInstallExports', () => { test('getInstallExports is not on the layout module export', () => { process.env['MSD_TEST_MODE'] = '1'; const layout = require('../msd-core/bin/lib/runtime-artifact-layout.cjs'); assert.equal( typeof layout.getInstallExports, 'undefined', 'getInstallExports should have been removed from runtime-artifact-layout exports (ADR-1508 Phase 2)', ); }); }); // --------------------------------------------------------------------------- // DEFECT.GENERATIVE-FIX: single-owner duplicate-body guard (#1511), // re-armed AST-based after #2876 (epic #2866 Phase 7) retired the exports // the original reference-identity check compared against. // // #1511 proved install.js bound to the conversion module's implementation // rather than a duplicate local copy — the single-owner property held via // `assert.strictEqual(install.X, conversion.X)` (reference identity). #2876 // retired these names from bin/install.js's module.exports entirely because // nothing consumed the re-export. That broke the reference-identity form // (there is no `install.X` left to compare), and a same-shaped // `install.X === undefined` replacement is NOT an equivalent guard: it only // inspects the EXPORT surface, so a duplicate, unexported, re-implemented // copy of one of these functions/consts added directly to bin/install.js // would satisfy `install.X === undefined` trivially while still being // exactly the drift-hazard duplicate this guard exists to catch. // // Fix: parse bin/install.js's own top-level AST (espree) and assert directly // on what is actually declared there, instead of on what is exported. // --------------------------------------------------------------------------- describe('single-owner duplicate-body guard (ADR-1508 / #1511 Phase 2, AST-based since #2876)', () => { let install; let conversionCjs; let installTopLevel; // Collect every top-level `function (...) {}` declaration and every // top-level `const/let/var = ;` declarator in bin/install.js, // keyed by name. A duplicate body re-introduced under EITHER shape (a real // function, or a const bound to something other than a bare // runtimeArtifactConversion. reference) is visible here regardless of // whether it is ever exported. function collectTopLevelBindings(ast) { const functionNames = new Set(); const variableInits = new Map(); for (const node of ast.body) { if (node.type === 'FunctionDeclaration' && node.id) { functionNames.add(node.id.name); } if (node.type === 'VariableDeclaration') { for (const decl of node.declarations) { if (decl.type === 'VariableDeclarator' && decl.id && decl.id.type === 'Identifier') { variableInits.set(decl.id.name, decl.init); } } } } return { functionNames, variableInits }; } // True iff `node` is exactly `.` — a bare // single-hop member-expression reference, not a call, not a duplicated // function/object body. function isMemberReferenceTo(node, objectName, propertyName) { return ( !!node && node.type === 'MemberExpression' && !node.computed && node.object && node.object.type === 'Identifier' && node.object.name === objectName && node.property && node.property.type === 'Identifier' && node.property.name === propertyName ); } before(() => { process.env['MSD_TEST_MODE'] = '1'; install = require('../bin/install.js'); conversionCjs = require('../msd-core/bin/lib/runtime-artifact-conversion.cjs'); // bin/install.js opens with a `#!/usr/bin/env node` shebang line, which // is not valid top-level JS syntax for espree's parser — strip it before // parsing (mirrors how Node's own module loader strips it at runtime). // Uses splitLines() (src/text-lines.cts, the repo's sole `\r?\n` split // seam) rather than a readFileSync-content regex, per // local/no-unbounded-quantifier and local/no-crlf-fragile-split. const installSrcRaw = fs.readFileSync(INSTALL_SCRIPT, 'utf8'); const installSrcLines = splitLines(installSrcRaw); if (installSrcLines[0] && installSrcLines[0].startsWith('#!')) { installSrcLines[0] = ''; } const installSrc = joinLines(installSrcLines, '\n'); const ast = espree.parse(installSrc, { ecmaVersion: 2022, sourceType: 'script', range: true, loc: true }); installTopLevel = collectTopLevelBindings(ast); }); // Names #2876 retired from bin/install.js entirely (no export, no internal // caller). Regression shape this guards against: someone re-adds one of // these as either a real `function NAME(...) {...}` OR a // `const NAME = ;` — under a plain `install.X === undefined` // check, an unexported re-add of either shape passes silently. const RETIRED_NAMES = [ ['applyRuntimeContentRewritesInPlace', 'applyRuntimeContentRewritesInPlace'], ['applyRuntimeContentRewritesForCommandsInPlace', 'applyRuntimeContentRewritesForCommandsInPlace'], ]; for (const [name, conversionProp] of RETIRED_NAMES) { test(`${name}: retired from bin/install.js with zero top-level presence (#2876); conversion.${conversionProp} remains the single implementation`, () => { assert.strictEqual(install[name], undefined, `bin/install.js must no longer export ${name}`); assert.strictEqual( installTopLevel.functionNames.has(name), false, `bin/install.js must not declare a top-level function named ${name} — a re-added unexported copy is the #1511/#2876 duplicate-body regression this guard exists to catch` ); assert.strictEqual( installTopLevel.variableInits.has(name), false, `bin/install.js must not declare a top-level const/let/var named ${name} — a re-added unexported copy is the #1511/#2876 duplicate-body regression this guard exists to catch` ); assert.strictEqual(typeof conversionCjs[conversionProp], 'function', `${conversionProp} remains available from the conversion module`); }); } // Names #2876 kept as a bare single-hop reference (`const X = // runtimeArtifactConversion.;`) because bin/install.js still calls them // internally (computePathPrefix, _applyRuntimeRewrites, // applyClaudeCodeBrandSwap — #2931). The // regression this half guards against: the reference gets replaced with an // actual re-implemented body instead of staying a pointer to the single // conversion-module owner. const REFERENCE_ONLY_NAMES = [ ['computePathPrefix', '_computePathPrefix'], ['_applyRuntimeRewrites', '_applyRuntimeRewrites'], ['applyClaudeCodeBrandSwap', 'applyClaudeCodeBrandSwap'], ]; for (const [local, conversionProp] of REFERENCE_ONLY_NAMES) { test(`${local}: still a bare runtimeArtifactConversion.${conversionProp} reference, not a re-implemented body`, () => { assert.strictEqual(install[local], undefined, `bin/install.js must no longer export ${local}`); const init = installTopLevel.variableInits.get(local); assert.ok(init, `bin/install.js must still declare a top-level const named ${local} (has an internal caller)`); assert.ok( isMemberReferenceTo(init, 'runtimeArtifactConversion', conversionProp), `bin/install.js's ${local} binding must be a bare \`runtimeArtifactConversion.${conversionProp}\` reference — anything else (a real function/object body) is the #1511/#2876 duplicate-body regression this guard exists to catch` ); assert.strictEqual(typeof conversionCjs[conversionProp], 'function'); }); } }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-190-bridge-collapse.test.cjs — consolidation epic #1969 (B3 #1972) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-190-bridge-collapse (consolidation epic #1969 B3 #1972)", () => { 'use strict'; const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const ROOT = path.join(__dirname, '..'); function read(rel) { return fs.readFileSync(path.join(ROOT, rel), 'utf8'); } test('bridge collapse removes cjs-sdk-bridge and runtime-bridge-sync seam', () => { const bridgePath = path.join(ROOT, 'msd-core', 'bin', 'lib', 'cjs-sdk-bridge.cjs'); const sdkDir = path.join(ROOT, 'sdk'); assert.equal(fs.existsSync(bridgePath), false, 'cjs-sdk-bridge.cjs must be removed'); assert.equal(fs.existsSync(sdkDir), false, 'sdk directory must be removed'); const routers = [ 'msd-core/bin/lib/init-command-router.cjs', 'msd-core/bin/lib/roadmap-command-router.cjs', 'msd-core/bin/lib/state-command-router.cjs', 'msd-core/bin/lib/validate-command-router.cjs', 'msd-core/bin/lib/verify-command-router.cjs', 'msd-core/bin/lib/phases-command-router.cjs', ]; for (const rel of routers) { const src = read(rel); assert.equal( src.includes('cjs-sdk-bridge.cjs'), false, `${rel} must not import cjs-sdk-bridge.cjs`, ); } const rootPkg = JSON.parse(read('package.json')); assert.equal( Object.prototype.hasOwnProperty.call(rootPkg.dependencies || {}, 'synckit'), false, 'package.json must not include synckit', ); }); }); } // ──────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-2973-profile-user-skills-path.test.cjs — consolidation epic #1969 (B5 #1974) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-2973-profile-user-skills-path (consolidation epic #1969 B5 #1974)", () => { 'use strict'; // allow-test-rule: source-text-is-the-product. profile-user.md IS the (see #2973) // shipped workflow product; the `Display:` line at line 356 IS the // user-visible artifact-name message. This test parses the markdown's // structured `Display: "..."` line via a regex (not source-grep) to // extract the path argument as a typed value, then asserts on the // typed value. The .includes() at the end is a structural absence-check // against the legacy path literal — the same shape the bug-2470 // installer-leak test uses to enforce a known-pattern invariant. process.env.MSD_TEST_MODE = '1'; /** * Bug #2973: /msd-profile-user --refresh writes dev-preferences.md to the * legacy commands/msd subdirectory, contradicting v1.39.0's skills-only * migration claim that "Legacy commands/msd directory removed * (replaced by skills/)". * * Root cause: the writer at msd-core/bin/lib/profile-output.cjs * fell back to commands/msd/dev-preferences.md when no --output was passed. * The /msd-profile-user workflow does not pass --output, so every refresh * deterministically re-creates the legacy directory. * * Fix: * 1. profile-output.cjs default targets skills/msd-dev-preferences/SKILL.md * 2. profile-user.md confirmation message references the new path * 3. install.js migrates any existing legacy file into the new skill * location during install (no-op if SKILL.md already exists) * * This test exercises the runtime behavior of the writer (writes to the * skills path) and the structural shape of the workflow message. No * source-grep on the .cjs body — assertions go against the writer's * actual output and the parsed workflow message. */ const { test, describe } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const { cleanup, TEST_ENV_BASE } = require('./helpers.cjs'); const ROOT = path.join(__dirname, '..'); const PROFILE_OUTPUT = path.join(ROOT, 'msd-core', 'bin', 'lib', 'profile-output.cjs'); const WORKFLOW = path.join(ROOT, 'msd-core', 'workflows', 'profile-user.md'); const installEngine = require('../msd-core/bin/lib/install-engine.cjs'); describe('Bug #2973: dev-preferences default writer path is skills/msd-dev-preferences/SKILL.md', () => { test('exercise the writer in a subprocess with HOME pointed at a tmp dir; assert the artifact lands at the skills path', () => { // Subprocess so fs.writeSync(1, ...) in core.cjs goes to a pipe we can // capture (the parent process's fd 1 bypasses any in-process stubbing). const cp = require('node:child_process'); const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2973-')); try { const analysisPath = path.join(tmpHome, 'analysis.json'); fs.writeFileSync(analysisPath, JSON.stringify({ data_source: 'questionnaire', dimensions: { rigor: { score: 7 } }, })); const driver = path.join(tmpHome, 'driver.js'); fs.writeFileSync(driver, ` const m = require(${JSON.stringify(PROFILE_OUTPUT)}); m.cmdGenerateDevPreferences(${JSON.stringify(tmpHome)}, { analysis: ${JSON.stringify(analysisPath)} }, false); `); const result = cp.spawnSync(process.execPath, [driver], { // #2665: TEST_ENV_BASE must be merged in explicitly here. This is a RAW // spawn, not runMsdTools, so nothing scrubs the config-location vars for // it -- and the writer under test resolves them env-FIRST. Sandboxing // HOME alone let an ambient CLAUDE_CONFIG_DIR win, and the SKILL.md // landed in the developer's live config dir instead of tmpHome. env: Object.assign({}, process.env, TEST_ENV_BASE, { HOME: tmpHome, USERPROFILE: tmpHome }), encoding: 'utf-8', // See WRITER_SUBPROCESS_TIMEOUT_MS's own doc comment (top of file) // for the full rationale. timeout: WRITER_SUBPROCESS_TIMEOUT_MS, }); assert.equal(result.signal, null, `writer subprocess was killed by signal ${result.signal} (likely timeout): ${result.stderr}`); assert.equal(result.status, 0, `writer subprocess failed: ${result.stderr}`); const parsed = JSON.parse(result.stdout); const expectedPath = path.join(tmpHome, '.claude', 'skills', 'msd-dev-preferences', 'SKILL.md'); assert.equal(parsed.command_path, expectedPath, `writer emitted ${parsed.command_path}; expected skills path ${expectedPath} (#2973)`); assert.equal(fs.existsSync(expectedPath), true, `expected SKILL.md at ${expectedPath} after writer ran`); const legacyPath = path.join(tmpHome, '.claude', 'commands', 'msd', 'dev-preferences.md'); assert.equal(fs.existsSync(legacyPath), false, `writer must not create ${legacyPath} (#2973)`); } finally { cleanup(tmpHome); } }); }); describe('Bug #2973: profile-user.md confirmation message references the skills path', () => { test('the Display message points at $HOME/.claude/skills/msd-dev-preferences/SKILL.md', () => { const md = fs.readFileSync(WORKFLOW, 'utf-8'); // Match the structured Display: line; capture the path value. // eslint-disable-next-line local/no-unbounded-quantifier -- parses maintainer-authored profile-user.md workflow, bounded prose, not adversarial input const m = md.match(/Display:\s*"[^"]*Generated\s*\/msd-dev-preferences\s*at\s*([^"]+)"/); assert.notEqual(m, null, 'expected a Display: "Generated /msd-dev-preferences at " line'); const referencedPath = m[1].trim(); assert.equal(referencedPath, '$HOME/.claude/skills/msd-dev-preferences/SKILL.md', `workflow references ${referencedPath}; expected skills path (#2973)`); }); test('no occurrence of the legacy commands/msd/dev-preferences.md path remains in profile-user.md', () => { const md = fs.readFileSync(WORKFLOW, 'utf-8'); assert.equal(md.includes('commands/msd/dev-preferences.md'), false, 'profile-user.md still references legacy commands/msd/dev-preferences.md (#2973)'); }); }); describe('Bug #2973: installer migrates existing legacy dev-preferences.md to skills/msd-dev-preferences/SKILL.md', () => { test('migrateLegacyDevPreferencesToSkill is exported and writes to the skills path', () => { const inst = installEngine; // Module exports the migration helper for direct testing. // Note: this is the structural assertion — the helper exists with the // documented signature. End-to-end install testing is covered by // tests/install-*.test.cjs which already exercise legacy preservation. assert.equal(typeof inst.migrateLegacyDevPreferencesToSkill, 'function', 'expected migrateLegacyDevPreferencesToSkill in install-engine.cjs exports (#2973)'); }); test('migration writes to skills/msd-dev-preferences/SKILL.md when no skill exists yet', () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2973-mig-')); try { const inst = installEngine; const saved = new Map([['dev-preferences.md', '# my legacy preferences\n']]); const migrated = inst.migrateLegacyDevPreferencesToSkill(tmpDir, saved); assert.equal(migrated, true, 'expected migration to succeed when no SKILL.md exists'); const skillFile = path.join(tmpDir, 'skills', 'msd-dev-preferences', 'SKILL.md'); assert.equal(fs.existsSync(skillFile), true, `expected SKILL.md at ${skillFile}`); assert.equal(fs.readFileSync(skillFile, 'utf-8'), '# my legacy preferences\n'); } finally { cleanup(tmpDir); } }); test('migration is a no-op when a SKILL.md already exists at the new location (do not clobber user-customized skill content)', () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2973-skip-')); try { const inst = installEngine; const skillDir = path.join(tmpDir, 'skills', 'msd-dev-preferences'); const skillFile = path.join(skillDir, 'SKILL.md'); fs.mkdirSync(skillDir, { recursive: true }); fs.writeFileSync(skillFile, '# user-customized skill\n'); const saved = new Map([['dev-preferences.md', '# legacy content\n']]); const migrated = inst.migrateLegacyDevPreferencesToSkill(tmpDir, saved); assert.equal(migrated, false, 'expected migration to skip when SKILL.md exists'); // Existing content untouched. assert.equal(fs.readFileSync(skillFile, 'utf-8'), '# user-customized skill\n'); } finally { cleanup(tmpDir); } }); }); // ─── #2911: migrateLegacyDevPreferencesToSkill is the latent instance of the ── // ─── same installer-vs-surface destination-root defect ─────────────────────── // // migrateLegacyDevPreferencesToSkill (src/install-engine.cts) resolved the // skill dir as `assertDestWithinConfigHome(targetDir, skillsKindEntry.destSubpath)` // — always against targetDir (configDir), ignoring skillsKindEntry.home. For // codex/global (the only current `home`-override runtime/scope), a legacy // dev-preferences.md migration would have landed under $CODEX_HOME/skills // instead of the canonical $HOME/.agents/skills tree used by both the // installer's _copyStaged and (post-#2911-fix) applySurface. Fixed the same // way: `skillsKindEntry.home ?? targetDir`. describe('Bug #2911: migrateLegacyDevPreferencesToSkill honors the skills-kind home override (codex)', () => { const { resolveRuntimeArtifactLayout } = require('../msd-core/bin/lib/runtime-artifact-layout.cjs'); function withFakeHome(fakeHome, fn) { const savedHome = process.env.HOME; const savedUserProfile = process.env.USERPROFILE; // #3712: record WHICH home this sandboxed to. src/real-home-guard.cts fails // closed on hosts with no readable passwd entry, and this is what proves a // genuinely-sandboxed caller there. Without it these calls would be refused. const savedMarker = process.env.MSD_TEST_HOME_SANDBOX; process.env.HOME = fakeHome; process.env.USERPROFILE = fakeHome; process.env.MSD_TEST_HOME_SANDBOX = fakeHome; try { return fn(); } finally { if (savedHome === undefined) delete process.env.HOME; else process.env.HOME = savedHome; if (savedUserProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = savedUserProfile; if (savedMarker === undefined) delete process.env.MSD_TEST_HOME_SANDBOX; else process.env.MSD_TEST_HOME_SANDBOX = savedMarker; } } test('codex + global: migration writes SKILL.md under $HOME/.agents/skills, NOT under $CODEX_HOME/skills', () => { const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2911-mig-home-')); const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2911-mig-codexhome-')); try { withFakeHome(fakeHome, () => { const inst = installEngine; const layout = resolveRuntimeArtifactLayout('codex', codexHome, 'global'); const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills'); assert.equal(skillsKindEntry.home, path.join(fakeHome, '.agents'), 'pre-condition: codex global skills kind declares the $HOME/.agents override'); const saved = new Map([['dev-preferences.md', '# my legacy preferences\n']]); const migrated = inst.migrateLegacyDevPreferencesToSkill(codexHome, saved, 'codex', 'global'); assert.equal(migrated, true, 'expected migration to succeed when no SKILL.md exists'); const correctSkillFile = path.join(fakeHome, '.agents', 'skills', 'msd-dev-preferences', 'SKILL.md'); assert.equal(fs.existsSync(correctSkillFile), true, `expected SKILL.md at ${correctSkillFile}`); assert.equal(fs.readFileSync(correctSkillFile, 'utf-8'), '# my legacy preferences\n'); const legacySkillFile = path.join(codexHome, 'skills', 'msd-dev-preferences', 'SKILL.md'); assert.equal(fs.existsSync(legacySkillFile), false, `migration must NOT also write a second copy at the legacy location ${legacySkillFile}`); }); } finally { cleanup(fakeHome); cleanup(codexHome); } }); test('codex + local: no home override — migration destination is unchanged ($CODEX_HOME/skills)', () => { const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2911-mig-local-')); try { const inst = installEngine; const layout = resolveRuntimeArtifactLayout('codex', codexHome, 'local'); const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills'); assert.equal(skillsKindEntry.home, undefined, 'pre-condition: codex local scope declares NO home override'); const saved = new Map([['dev-preferences.md', '# my legacy preferences\n']]); const migrated = inst.migrateLegacyDevPreferencesToSkill(codexHome, saved, 'codex', 'local'); assert.equal(migrated, true); const skillFile = path.join(codexHome, 'skills', 'msd-dev-preferences', 'SKILL.md'); assert.equal(fs.existsSync(skillFile), true, `expected SKILL.md at ${skillFile} (unchanged, no home override)`); } finally { cleanup(codexHome); } }); }); // ─── #3003 CR follow-up: installRuntimeArtifacts preserves user-owned skills ── // // Production install() calls installRuntimeArtifacts() without a prior // uninstallRuntimeArtifacts(). This means _copyStaged overlays new skills // on top of the existing skills/ directory — it does NOT wipe first. // As a result, user-owned msd-dev-preferences/SKILL.md is preserved across // a plain install because _copyStaged only cpSync's newly staged skill dirs. // // NOTE: If callers run uninstallRuntimeArtifacts() before installRuntimeArtifacts() // (e.g. full reinstall), msd-dev-preferences IS wiped by uninstall and NOT // restored by install (#3664 production gap — tracked separately). describe('Bug #2973 (#3003 CR): installRuntimeArtifacts preserves user-owned msd-dev-preferences across install', () => { test('user-customized skills/msd-dev-preferences/SKILL.md survives a plain install (no pre-uninstall)', () => { // Production install() does NOT call uninstallRuntimeArtifacts() first. // installRuntimeArtifacts → _copyStaged overlays only staged skill dirs; // msd-dev-preferences (not in source) is left untouched. const inst = installEngine; const { loadSkillsManifest, resolveProfile } = require(path.join(ROOT, 'msd-core', 'bin', 'lib', 'install-profiles.cjs')); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2973-wipe-')); try { const configDir = path.join(tmp, 'config'); fs.mkdirSync(configDir, { recursive: true }); // Set up a minimal source dir (plan-phase only; no dev-preferences). const srcDir = path.join(tmp, 'src-commands'); fs.mkdirSync(srcDir, { recursive: true }); fs.writeFileSync(path.join(srcDir, 'plan-phase.md'), '---\nname: msd:plan-phase\ndescription: Plan\n---\n\nPlan body.\n'); fs.writeFileSync(path.join(configDir, '.msd-source'), srcDir + '\n'); const skillsDir = path.join(configDir, 'skills'); const userSkillDir = path.join(skillsDir, 'msd-dev-preferences'); fs.mkdirSync(userSkillDir, { recursive: true }); const userContent = '# my customized dev preferences\n\nstack: rust\n'; fs.writeFileSync(path.join(userSkillDir, 'SKILL.md'), userContent); // Plain install (matching production install() call site). const manifest = loadSkillsManifest(); const resolvedProfile = resolveProfile({ modes: [], manifest }); inst.installRuntimeArtifacts('claude', configDir, 'global', resolvedProfile); const skillFile = path.join(userSkillDir, 'SKILL.md'); assert.equal(fs.existsSync(skillFile), true, 'msd-dev-preferences/SKILL.md must survive a plain install (#3003 CR)'); assert.equal(fs.readFileSync(skillFile, 'utf-8'), userContent, 'user content must be byte-identical after the install'); } finally { cleanup(tmp); } }); test('non-user-owned msd-* skills are wiped and recreated via uninstall+install cycle', () => { // Stale artifacts (e.g. STALE-MARKER.txt left from a previous version) // are removed when the caller runs uninstallRuntimeArtifacts() before // installRuntimeArtifacts() — the full uninstall+reinstall cycle. // uninstallRuntimeArtifacts removes all msd-* entries; installRuntimeArtifacts // then writes fresh ones from source. const inst = installEngine; const { loadSkillsManifest, resolveProfile } = require(path.join(ROOT, 'msd-core', 'bin', 'lib', 'install-profiles.cjs')); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2973-wipe-shipped-')); try { const configDir = path.join(tmp, 'config'); fs.mkdirSync(configDir, { recursive: true }); const srcDir = path.join(tmp, 'src-commands'); fs.mkdirSync(srcDir, { recursive: true }); fs.writeFileSync(path.join(srcDir, 'plan-phase.md'), '---\nname: msd:plan-phase\ndescription: Plan fresh\n---\n\nFresh body.\n'); fs.writeFileSync(path.join(configDir, '.msd-source'), srcDir + '\n'); const skillsDir = path.join(configDir, 'skills'); const staleSkillDir = path.join(skillsDir, 'msd-plan-phase'); fs.mkdirSync(staleSkillDir, { recursive: true }); fs.writeFileSync(path.join(staleSkillDir, 'STALE-MARKER.txt'), 'wipe me'); const manifest = loadSkillsManifest(); const resolvedProfile = resolveProfile({ modes: [], manifest }); // Full uninstall+install cycle (e.g. --reinstall flow) inst.uninstallRuntimeArtifacts('claude', configDir, 'global'); inst.installRuntimeArtifacts('claude', configDir, 'global', resolvedProfile); assert.equal(fs.existsSync(path.join(staleSkillDir, 'STALE-MARKER.txt')), false, 'stale shipped-skill content must be wiped by uninstall (preservation is opt-in by name)'); assert.equal(fs.existsSync(path.join(staleSkillDir, 'SKILL.md')), true, 'fresh SKILL.md from source must be installed after wipe'); } finally { cleanup(tmp); } }); }); }); } // ──────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-2808-skill-hyphen-name.test.cjs — consolidation epic #1969 (B6 #1975) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-2808-skill-hyphen-name (consolidation epic #1969 B6 #1975)", () => { // allow-test-rule: source-text-is-the-product (see #2808) // Reads .md/.json/.yml product files whose deployed text IS what the // runtime loads — testing text content tests the deployed contract. /** * Regression test for bug #2808 * * All 85 MSD SKILL.md files declared `name: msd:` (colon), the deprecated * form. Claude Code surfaces the `name:` frontmatter field in autocomplete, so * users saw `/msd:add-phase` suggestions instead of the canonical `/msd-add-phase`. * * Root cause: skillFrontmatterName() in bin/install.js converted hyphenated * skill dir names to colon form (msd-add-phase → msd:add-phase) because * workflows called Skill(skill="msd:"). That was the original fix for * #2643. Since then, workflows have been updated to use hyphen form (#2808). * * Fix: skillFrontmatterName() now returns the hyphen form unchanged. * Workflow Skill() colon calls are updated to hyphen. * * This test verifies: * 1. skillFrontmatterName returns hyphen form (not colon). * 2. Installed SKILL.md would emit name: msd- (not msd:). * 3. No workflow contains a Skill(skill="msd:") colon call. */ 'use strict'; process.env.MSD_TEST_MODE = '1'; const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { cleanup, createTempDir } = require('./helpers.cjs'); const ROOT = path.join(__dirname, '..'); const { convertClaudeCommandToClaudeSkill, skillFrontmatterName } = require(path.join(ROOT, 'bin', 'install.js')); const { installRuntimeArtifacts } = require(path.join(ROOT, 'msd-core', 'bin', 'lib', 'install-engine.cjs')); const { loadSkillsManifest, resolveProfile, } = require(path.join(ROOT, 'msd-core', 'bin', 'lib', 'install-profiles.cjs')); // Full resolved profile — installs all available skills from the source dir const _manifest = loadSkillsManifest(); const resolvedProfileFull = resolveProfile({ modes: [], manifest: _manifest }); const WORKFLOWS_DIR = path.join(ROOT, 'msd-core', 'workflows'); const COMMANDS_DIR = path.join(ROOT, 'commands', 'msd'); function walkMd(dir) { const files = []; try { for (const e of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, e.name); if (e.isDirectory()) files.push(...walkMd(full)); else if (e.name.endsWith('.md')) files.push(full); } } catch (err) { assert.fail(`failed to read markdown files from ${dir}: ${err.message}`); } return files; } describe('bug-2808: SKILL.md name: uses hyphen form', () => { test('skillFrontmatterName returns hyphen form (not colon)', () => { assert.strictEqual(skillFrontmatterName('msd-add-phase'), 'msd-add-phase'); assert.strictEqual(skillFrontmatterName('msd-plan-phase'), 'msd-plan-phase'); assert.strictEqual(skillFrontmatterName('msd-autonomous'), 'msd-autonomous'); }); test('generated SKILL.md contains name: msd- (not msd:)', () => { const cmdFiles = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md')); assert.ok(cmdFiles.length > 0, 'expected MSD command files'); for (const cmd of cmdFiles) { const base = cmd.replace(/\.md$/, ''); const skillDirName = 'msd-' + base; const src = fs.readFileSync(path.join(COMMANDS_DIR, cmd), 'utf-8'); const skillContent = convertClaudeCommandToClaudeSkill(src, skillDirName); // Parse frontmatter structurally: extract name: line from the --- block. const fmMatch = skillContent.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(fmMatch, `${cmd}: generated skill content must have a frontmatter block`); const fmLines = fmMatch[1].split(/\r?\n/); const nameEntry = fmLines.find((l) => l.startsWith('name:')); assert.ok(nameEntry, `${cmd}: generated SKILL.md is missing required name: field`); const name = nameEntry.replace(/^name:\s*/, '').trim(); assert.ok( !name.includes(':'), `${cmd}: SKILL.md name should be hyphen form, got "${name}"` ); assert.ok( name.startsWith('msd-'), `${cmd}: SKILL.md name should start with msd-, got "${name}"` ); // #3583 regression guard: the *body* must not leak retired colon-form // command references (e.g. /msd:plan-phase or msd:review). The converter // now uses transformContentToHyphen from the shared transformer. // // We explicitly scope to the body (after stripping the leading frontmatter // block) so that descriptions or other frontmatter fields containing example // msd: references do not cause spurious failures. // // msd:sdk and msd:tools are intentionally excluded: they are not slash commands // (no commands/msd/sdk.md or tools.md exist), so the transformer correctly leaves // them alone. They are benign and should not trigger this assertion. const bodyContent = skillContent.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, ''); const colonRefs = (bodyContent.match(/\bmsd:[a-z][a-z0-9-]*\b/g) || []) .filter(r => !/msd:(sdk|tools)/.test(r)); assert.strictEqual( colonRefs.length, 0, `${cmd}: generated SKILL.md body must not contain msd: command references (found: ${colonRefs.join(', ')})` ); } }); test('no workflow contains Skill(skill="msd:") colon form', () => { const workflowFiles = walkMd(WORKFLOWS_DIR); assert.ok( workflowFiles.length > 0, `expected workflow markdown files under ${WORKFLOWS_DIR}` ); const colonCalls = []; for (const f of workflowFiles) { const src = fs.readFileSync(f, 'utf-8'); // Strip HTML comments to avoid matching commented-out examples. // regex-free HTML-comment stripper (CodeQL: avoid incomplete-multi-character-sanitization) let stripped = ''; { let rest = src; let idx; while ((idx = rest.indexOf('', idx + 4); if (end === -1) { rest = ''; break; } rest = rest.slice(end + 3); } stripped += rest; } // Scan each line for Skill() calls using the colon form. // Parsing line-by-line is more precise than a multi-line regex // and avoids false positives from incidental matches in prose. for (const line of stripped.split(/\r?\n/)) { // Tolerate whitespace around the parenthesis, the `skill` keyword, // and the `=` so variants like `Skill( skill = "msd:foo" )` are still // flagged. Without the `\s*` allowances, drift slips through this guard. // // The local-name capture must be permissive (`[^'"\s)]+`, not // `[a-z0-9-]+`) — the whole purpose of this guard is to surface // *malformed* drift, including legacy underscore-form names like // `msd:extract_learnings`. A character-class that excludes the very // characters we need to flag would silently let drift through. const colonCallRe = /Skill\(\s*skill\s*=\s*\\?['"]msd:([^'"\s)]+)\\?['"]/gi; let m; while ((m = colonCallRe.exec(line)) !== null) { colonCalls.push(`${path.basename(f)}: Skill(skill="msd:${m[1]}")`); } } } assert.deepStrictEqual( colonCalls, [], 'deprecated colon-form Skill() calls found — update to msd-: ' + colonCalls.join(', ') ); }); test('generated autocomplete skill surface uses hyphen names without underscores', (t) => { const tmp = createTempDir('msd-autocomplete-surface-'); t.after(() => cleanup(tmp)); // Use the real COMMANDS_DIR as the source via .msd-source marker. // installRuntimeArtifacts('claude', configDir, 'global') writes to // configDir/skills/ using the same converter as the shim did. // With the full profile (#924 fix), skills are FLAT: msd-/SKILL.md // (nested layout reverted for Claude — Claude Code scans only one level). const configDir = path.join(tmp, 'config'); fs.mkdirSync(configDir, { recursive: true }); fs.writeFileSync(path.join(configDir, '.msd-source'), COMMANDS_DIR + '\n'); installRuntimeArtifacts('claude', configDir, 'global', resolvedProfileFull); const skillsDir = path.join(configDir, 'skills'); // Recursively collect all SKILL.md files under skills/ (handles both flat and // nested layouts). Don't filter any paths — that would silently hide exactly // the kind of drift this test exists to catch (a `msd:extract-learnings` // colon variant or a bare `extract-learnings` without the namespace prefix // would never be collected, and the loop below would never see them). function collectSkillMds(dir) { const results = []; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { results.push(...collectSkillMds(full)); } else if (entry.name === 'SKILL.md') { results.push(full); } } return results; } const allSkillMdPaths = collectSkillMds(skillsDir); assert.ok(allSkillMdPaths.length > 0, 'expected generated SKILL.md files under skillsDir'); // Validate every SKILL.md's name: field (the consumer-facing name used in // autocomplete). We also check that the containing dir name doesn't use // banned characters at any level of nesting. const allNames = []; for (const skillMdPath of allSkillMdPaths) { const relPath = path.relative(skillsDir, skillMdPath); const skillContent = fs.readFileSync(skillMdPath, 'utf-8'); // Scope the name: lookup to the YAML frontmatter block so a stray // `name:` line in the body cannot satisfy the assertion. // eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own generated SKILL.md frontmatter, fixed-size author-controlled content const fmMatch = skillContent.match(/^---\r?\n([\s\S]*?)\r?\n---/); assert.ok(fmMatch, `${relPath}: generated SKILL.md must include frontmatter`); const nameLine = fmMatch[1].split(/\r?\n/).find((l) => /^name:\s*/.test(l)); assert.ok(nameLine, `${relPath}: generated SKILL.md is missing name: frontmatter`); const name = nameLine.replace(/^name:\s*/, '').trim(); assert.ok(name.startsWith('msd-'), `${relPath}: autocomplete name must start with msd-, got ${name}`); assert.ok(!name.includes(':'), `${relPath}: autocomplete name must not contain colon, got ${name}`); assert.ok(!name.includes('_'), `${relPath}: autocomplete name must not contain underscore, got ${name}`); allNames.push(name); // Also validate each path segment (dir name) in the relative path doesn't // contain the banned characters — catches mislabeled directory names. const segments = relPath.split(path.sep).slice(0, -1); // exclude 'SKILL.md' filename for (const seg of segments) { assert.ok(!seg.includes(':'), `${relPath}: dir segment "${seg}" must not contain colon`); assert.ok(!seg.includes('_'), `${relPath}: dir segment "${seg}" must use hyphens, not underscores`); } } assert.ok(allNames.includes('msd-extract-learnings'), 'autocomplete surface must include msd-extract-learnings'); assert.ok(!allNames.includes('msd-extract_learnings'), 'autocomplete surface must not include msd-extract_learnings'); }); test('transformContentToHyphen (from fix-slash-commands.cjs) rewrites colon to hyphen for known commands', () => { const transformer = require(path.join(ROOT, 'scripts', 'fix-slash-commands.cjs')); const { transformContentToHyphen, readCmdNames } = transformer; const liveCmdNames = readCmdNames(); const input = 'Run /msd:plan-phase then msd:execute-phase. Also see /msd:review and msd-sdk query.'; const out = transformContentToHyphen(input, liveCmdNames); assert.ok(out.includes('/msd-plan-phase'), 'leading-/ colon form must become hyphen'); assert.ok(out.includes('msd-execute-phase'), 'bare colon form must become hyphen'); assert.ok(out.includes('/msd-review'), 'another command reference must be rewritten'); assert.ok(out.includes('msd-sdk'), 'non-command msd-sdk must be left untouched'); assert.ok(!out.match(/\bmsd:[a-z]/), 'no colon-form command reference may survive'); }); test('respects word boundary — does not rewrite msd:plan-phase-extra (partial match guard)', () => { const transformer = require(path.join(ROOT, 'scripts', 'fix-slash-commands.cjs')); const { transformContentToHyphen, readCmdNames } = transformer; const liveCmdNames = readCmdNames(); const out = transformContentToHyphen('msd:plan-phase-extra and /msd:execute-phase-extra', liveCmdNames); assert.strictEqual(out, 'msd:plan-phase-extra and /msd:execute-phase-extra', 'word-boundary lookahead must prevent partial matches on the reverse transform'); }); test('respects left word boundary — does not rewrite inside larger tokens (e.g. mymsd:cmd)', () => { const transformer = require(path.join(ROOT, 'scripts', 'fix-slash-commands.cjs')); const { transformContentToHyphen, readCmdNames } = transformer; const liveCmdNames = readCmdNames(); const input = 'See mymsd:plan-phase or prefix-msd:execute in the docs.'; const out = transformContentToHyphen(input, liveCmdNames); assert.strictEqual(out, input, 'negative lookbehind must prevent left-side in-word matches'); }); test('leaves already-hyphen-form references untouched (idempotent on output)', () => { const transformer = require(path.join(ROOT, 'scripts', 'fix-slash-commands.cjs')); const { transformContentToHyphen, readCmdNames } = transformer; const liveCmdNames = readCmdNames(); const input = 'Run msd-plan-phase and /msd-execute-phase then msd:review.'; // mixed, only colon should change const out = transformContentToHyphen(input, liveCmdNames); assert.ok(out.includes('msd-plan-phase'), 'pre-existing hyphen stays'); assert.ok(out.includes('/msd-execute-phase'), 'pre-existing hyphen stays'); assert.ok(out.includes('msd-review'), 'colon form was normalized'); assert.ok(!out.includes('msd:review'), 'no colon form remains'); }); }); }); } // ──────────────────────────────────────────────────────────────────────── // Folded from tests/fix-1920-installer-ships-capability-generators.test.cjs — consolidation epic #1969 (B6 #1975) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:fix-1920-installer-ships-capability-generators (consolidation epic #1969 B6 #1975)", () => { 'use strict'; /** * Regression tests for #1920: the installer must produce a capability-ecosystem- * complete flattened layout, and the capability loader must resolve the real host * version in that layout. * * Two gaps broke third-party capabilities on installed (flattened) layouts: * * Gap 1 — host version read as 0.0.0. `readHostVersion()` resolved the running MSD * version via require('../../../package.json'), which in the installed layout is the * marker package.json ({"type":"commonjs"}, no version) → the fail-closed fallback * reported 0.0.0, so `capability install` rejected any manifest with a real * engines.msd range as "incompatible with MSD 0.0.0". Worse, for runtimes that get * no marker and for local installs, that walked-up package.json could be the USER's * own project, reporting a wrong version. Fix: readHostVersion() prefers the * authoritative msd-core/VERSION the installer writes for EVERY runtime. * * Gap 2 — the registry generator was never shipped. The loader composes overlays via * require('../../../scripts/gen-capability-registry.cjs'); the installer never copied * it (nor its sibling gen-loop-host-contract.cjs), so the never-crash invariant * discarded EVERY overlay and fell back to the frozen first-party registry — * installed third-party capabilities were silently inert. Same class of gap as #1223 * (scripts/fix-slash-commands.cjs). * * These tests are RED before the fix (loader/install.js) and GREEN after. */ process.env.MSD_TEST_MODE = '1'; const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const { cleanup } = require('./helpers.cjs'); const ROOT = path.join(__dirname, '..'); const INSTALL = path.join(ROOT, 'bin', 'install.js'); const MANIFEST_NAME = 'msd-file-manifest.json'; // The generator scripts the capability loader requires by relative path. const GENERATORS = ['gen-capability-registry.cjs', 'gen-loop-host-contract.cjs']; // --------------------------------------------------------------------------- // Gap 1 — readHostVersion() prefers msd-core/VERSION (the installer-written, // all-runtime authoritative source) over an ambient/absent package.json. // --------------------------------------------------------------------------- describe('Gap 1: readHostVersion resolves the real host version in an installed layout (#1920)', () => { const { readHostVersion } = require('../msd-core/bin/lib/capability-loader.cjs'); /** Build a fake installed tree and return its msd-core/bin/lib dir (the module libDir). */ function fakeTree({ version, pkg }) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1920-ver-')); const libDir = path.join(root, 'msd-core', 'bin', 'lib'); fs.mkdirSync(libDir, { recursive: true }); if (version !== undefined) fs.writeFileSync(path.join(root, 'msd-core', 'VERSION'), version); if (pkg !== undefined) fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(pkg)); return { root, libDir }; } test('prefers msd-core/VERSION over a wrong ambient package.json version', () => { // The walked-up package.json belongs to the user's project (wrong version) — must be ignored. const { root, libDir } = fakeTree({ version: '9.9.9\n', pkg: { name: 'user-app', version: '1.2.3', type: 'commonjs' } }); try { assert.strictEqual(readHostVersion(libDir), '9.9.9'); } finally { cleanup(root); } }); test('falls back to the runtime-root package.json when no VERSION file (dev/source tree)', () => { const { root, libDir } = fakeTree({ pkg: { version: '2.3.4' } }); try { assert.strictEqual(readHostVersion(libDir), '2.3.4'); } finally { cleanup(root); } }); test('fail-closes to 0.0.0 when neither VERSION nor a package.json version resolves', () => { const { root, libDir } = fakeTree({}); try { assert.strictEqual(readHostVersion(libDir), '0.0.0'); } finally { cleanup(root); } }); test('a real global install writes msd-core/VERSION carrying the host version', () => { const dir = realInstall(); try { const vfile = path.join(dir, 'msd-core', 'VERSION'); assert.ok(fs.existsSync(vfile), 'installer must write msd-core/VERSION'); assert.strictEqual( fs.readFileSync(vfile, 'utf8').trim(), require('../package.json').version, 'msd-core/VERSION must carry the real host version readHostVersion() reads', ); } finally { cleanup(dir); } }); }); // --------------------------------------------------------------------------- // Gap 2 — the installer ships (and uninstalls / manifest-tracks) the capability // registry generator scripts. // --------------------------------------------------------------------------- /** Run a real global install into a fresh temp config dir; return that dir. */ function realInstall() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1920-')); // The module-level MSD_TEST_MODE=1 gates the installer's main() off entirely — // strip it from the child env for the spawned REAL install. const childEnv = { ...process.env }; delete childEnv.MSD_TEST_MODE; const res = spawnSync( process.execPath, [INSTALL, '--claude', '--global', '--config-dir', dir], { encoding: 'utf8', timeout: INSTALL_TIMEOUT_MS, env: childEnv }, ); assert.strictEqual(res.status, 0, `install --claude failed: ${res.stderr || res.stdout}`); return dir; } // --------------------------------------------------------------------------- // Gap 1 (end-to-end CLI) — the actual repro: `msd-tools capability install` on an // INSTALLED layout must resolve the real host version for the engines.msd gate, not // 0.0.0. The dev tree always has a versioned package.json two levels up, so this only // reproduces against a real install (where ../../package.json is the versionless marker // and msd-core/VERSION carries the truth). The CLI computes hostVersion itself, so this // covers capHostVersion() in msd-tools.cjs — a path the loader unit test does not touch. // --------------------------------------------------------------------------- describe('Gap 1 (end-to-end CLI): installed capability install uses the real host version (#1920)', () => { const HOST_MAJOR = require('../package.json').version.split('.')[0]; function writeProbeCapability(engines) { const src = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1920-cap-')); const cap = { id: 'p1920-probe', role: 'feature', version: '1.0.0', title: 'probe', description: 'test capability', tier: 'standard', requires: [], runtimeCompat: { supported: ['*'], unsupported: [] }, skills: [], agents: [], hooks: [], config: {}, steps: [], contributions: [], gates: [], engines, }; fs.writeFileSync(path.join(src, 'capability.json'), JSON.stringify(cap, null, 2)); return src; } test('a capability requiring engines.msd ">=.0.0" is not rejected as MSD 0.0.0', () => { const dir = realInstall(); const home = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1920-home-')); const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1920-cwd-')); fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true }); fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}'); const src = writeProbeCapability({ msd: `>=${HOST_MAJOR}.0.0` }); try { const installedTools = path.join(dir, 'msd-core', 'bin', 'msd-tools.cjs'); const env = { ...process.env, MSD_HOME: home, MSD_WORKSTREAM: '', MSD_PROJECT: '', MSD_SESSION_KEY: '', CLAUDE_SESSION_ID: '' }; delete env.MSD_TEST_MODE; const res = spawnSync( process.execPath, [installedTools, 'capability', 'install', src, '--scope', 'global', '--yes', '--json'], { cwd, env, encoding: 'utf8', timeout: CAPABILITY_INSTALL_TIMEOUT_MS }, ); const combined = `${res.stdout || ''}\n${res.stderr || ''}`; assert.doesNotMatch( combined, /incompatible with MSD 0\.0\.0/, `installed CLI saw host version 0.0.0 — the engines gate read the versionless marker: ${combined}`, ); assert.strictEqual(res.status, 0, `capability install failed on the installed layout: ${combined}`); } finally { cleanup(dir); cleanup(home); cleanup(cwd); cleanup(src); } }); }); describe('Gap 2: installer ships the capability registry generator scripts (#1920)', () => { test('the generator scripts are copied into scripts/', () => { const dir = realInstall(); try { for (const gen of GENERATORS) { const dest = path.join(dir, 'scripts', gen); assert.ok(fs.existsSync(dest), `installer must ship scripts/${gen}`); assert.ok(fs.statSync(dest).size > 0, `scripts/${gen} must not be empty`); } } finally { cleanup(dir); } }); test('the shipped generator scripts are tracked in the file manifest', () => { const dir = realInstall(); try { const manifest = JSON.parse(fs.readFileSync(path.join(dir, MANIFEST_NAME), 'utf8')); for (const gen of GENERATORS) { assert.ok( manifest.files[`scripts/${gen}`], `manifest must track scripts/${gen} for drift/uninstall accounting`, ); } } finally { cleanup(dir); } }); }); }); } // ─── #2218 cross-scope shadowing — coexistence gate (C1) + 4b guard pair // (E13/E14, #2873, epic #2866 Phase 4a) ──────────────────────────────────── // // Moved here from the now-deleted tests/install-cross-scope-shadowing.test.cjs: // `scripts/lint-test-file-count.allowlist.json` grandfathers the `install` // prefix at 8 files, and that suite's own `_doc` says adding a 9th file to a // capped module is a novel offender, not a fix — this gate is folded into // the emitted-artifact suite instead, which is already allowlisted and, // per #2873's acceptance criteria, is the correct home ("written against the // existing `runMinimalInstall` harness"). // // Implements the coexistence gate (`C1`) and the 4b behavioral pair // (`E13`/`E14`) from // `.msd/phase/feat-2873-cross-scope-shadowing/50-test-matrix.md`. Per that // matrix's "Red-first order": C1 must go RED against `next` (no // `install-shadow-report.cjs` report exists today), E14 must go RED today // (the global skill's spec-root include points at the global tree even when // a coexisting local install has its own project-local copy of that // workflow file), and E13 must stay GREEN both before and after — it is the // guard that phase 4b does not break today's global-only case. // // This section does NOT implement the 4b spec-root emission transform // (E1-E12, a separate matrix section) — that transform // (`resolveSpecRootReference`, `runtime-artifact-conversion.cts`) landed // separately and is exercised here only via its INSTALLED OUTPUT. #2873 // Task 3 (2026-08-14): re-verified against a real global+local double // install — 4b has landed and E14 below is GREEN, not the known-RED case // this comment block originally described. The "Red-first order" paragraph // above is left as-is: it accurately records the matrix's ORIGINAL red-first // plan, not a live claim about E14's current state. /** * Extract the `@`-include lines from an emitted markdown body — structural * parsing, never substring/regex matching on the whole body (CONTRIBUTING.md * "Prohibited: Raw Text Matching on Test Outputs"). Splits on newlines * (CRLF-tolerant) and keeps only lines whose first character is `@`. * * @param {string} content * @returns {string[]} */ function extractAtIncludeLines(content) { return content.split(/\r?\n/).filter((line) => line.startsWith('@')); } describe('#2218 cross-scope shadowing', () => { let root; let projectDir; let globalInstallResult; let localInstallResult; before(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2218-shadow-')); projectDir = path.join(root, 'myrepo'); fs.mkdirSync(projectDir, { recursive: true }); // Global half: cannot use runMinimalInstall here — its scope:'global' // path pushes `--config-dir `, which pins the install AT `` // itself (manifest at `/msd-file-manifest.json`), not at // `/.claude`. That is not the shape #2218 describes: the reporter's // configuration is a HOME-resolved global install (no --config-dir) // sitting alongside a project-local one. Spawn the installer directly, // with HOME=root and no --config-dir, so it resolves its own config home // the way a real global install does. Must run BEFORE the local half — // order matters for this fixture (a separate test covers order-independence). globalInstallResult = runNode([INSTALL_SCRIPT, '--claude', '--global'], { cwd: root, env: installerEnv({ HOME: root, USERPROFILE: root }), timeoutMs: INSTALL_TIMEOUT_MS, }); assert.strictEqual(globalInstallResult.exitCode, 0, `global install exited with status ${globalInstallResult.exitCode} ` + `(outcome=${globalInstallResult.outcome})\n` + `stdout: ${globalInstallResult.stdout}\nstderr: ${globalInstallResult.stderr}`); // Local half: runMinimalInstall cannot be reused for this either — for // scope:'local' it sets cwd=root, which would install into // `/.claude` and collide with the global install above. Spawn the // installer directly instead, with cwd pinned at the project dir. localInstallResult = runNode([INSTALL_SCRIPT, '--claude', '--local'], { cwd: projectDir, env: installerEnv({ HOME: root, USERPROFILE: root }), timeoutMs: INSTALL_TIMEOUT_MS, }); assert.strictEqual(localInstallResult.exitCode, 0, `local install exited with status ${localInstallResult.exitCode} ` + `(outcome=${localInstallResult.outcome})\n` + `stdout: ${localInstallResult.stdout}\nstderr: ${localInstallResult.stderr}`); }); after(() => { cleanup(root); }); test('both installs land their own manifest', () => { const globalManifestPath = path.join(root, '.claude', MANIFEST_NAME); const localManifestPath = path.join(projectDir, '.claude', MANIFEST_NAME); assert.ok(fs.existsSync(globalManifestPath), 'global manifest should exist'); assert.ok(fs.statSync(globalManifestPath).isFile(), 'global manifest should be a file'); assert.ok(fs.existsSync(localManifestPath), 'local manifest should exist'); assert.ok(fs.statSync(localManifestPath).isFile(), 'local manifest should be a file'); const globalManifest = JSON.parse(fs.readFileSync(globalManifestPath, 'utf8')); const localManifest = JSON.parse(fs.readFileSync(localManifestPath, 'utf8')); assert.strictEqual(globalManifest.scope, 'global'); assert.strictEqual(localManifest.scope, 'local'); }); test('the local install reports the shadowing it causes', () => { // #2218/#2873: install-shadow-report.cjs does not exist yet — this // require is the intended RED. buildShadowReport is the pure IR builder // described in .msd/phase/feat-2873-cross-scope-shadowing/40-design.md // (row 3): claude installed at both G and L reports N triggers shadowed, // winner skills@global, loser commands@local. const { buildShadowReport } = require('../msd-core/bin/lib/install-shadow-report.cjs'); const report = buildShadowReport('claude', { home: root, cwd: projectDir }); assert.strictEqual(report.shadowed, true); assert.strictEqual(report.winner.kind, 'skills'); assert.strictEqual(report.winner.scope, 'global'); assert.strictEqual(report.shadowedSide.kind, 'commands'); assert.strictEqual(report.shadowedSide.scope, 'local'); assert.ok(report.triggers.length > 0, 'expected at least one shadowed trigger'); }); test('global-only install resolves the same spec file it does today', () => { const skillPath = path.join(root, '.claude', 'skills', 'msd-plan-phase', 'SKILL.md'); const content = fs.readFileSync(skillPath, 'utf8'); const atLines = extractAtIncludeLines(content); assert.ok( atLines.includes('@~/.claude/msd-core/references/ui-brand.md'), `expected the ui-brand reference @-line among: ${JSON.stringify(atLines)}`, ); }); // #2218 / phase #2873: before 4b, the global SKILL.md's spec-root include // was a static `@~/.claude/msd-core/workflows/plan-phase.md` reference, // which always resolved against the GLOBAL tree even when a coexisting // local install has its own project-local copy of that workflow file. // Phase 4b (`resolveSpecRootReference`, `runtime-artifact-conversion.cts`) // replaces that static include with a two-step imperative form that names // both candidate paths and lets the runtime prefer the local one when it // exists. #2873 Task 3 (2026-08-14): re-verified GREEN against a real // global+local double install — 4b landed after this test package was // authored, so this is no longer the known-RED case the original comment // above it described. test('the winning global skill points at the project-local spec tree (E14)', () => { const skillPath = path.join(root, '.claude', 'skills', 'msd-plan-phase', 'SKILL.md'); const content = fs.readFileSync(skillPath, 'utf8'); const atLines = extractAtIncludeLines(content); assert.ok( !atLines.includes('@~/.claude/msd-core/workflows/plan-phase.md'), `expected the static global workflow @-line to be replaced, but found it among: ${JSON.stringify(atLines)}`, ); // The reference @-include (a DIFFERENT spec root, row E4) survives // untouched — structural proof 4b did not over-fire on this file. assert.ok( atLines.includes('@~/.claude/msd-core/references/ui-brand.md'), `expected the ui-brand reference @-line to survive among: ${JSON.stringify(atLines)}`, ); const localSpecPath = path.join(projectDir, '.claude', 'msd-core', 'workflows', 'plan-phase.md'); assert.ok(fs.existsSync(localSpecPath), 'local spec-root workflow file should exist on disk'); // Positive assertion, not just absence-of-the-old-include: the emitted // body must actually NAME the project-local candidate path. Exact-string // presence check on the literal candidate path `resolveSpecRootReference` // emits (never a substring-scan for prose wording — CONTRIBUTING → // "Prohibited: Raw Text Matching on Test Outputs"; this checks for the // PATH token, not sentence phrasing). assert.ok( content.includes('.claude/msd-core/workflows/plan-phase.md'), `expected the emitted body to name the project-local candidate path, got: ${JSON.stringify(content)}`, ); }); }); // ─── #2873 matrix section C — install-time report (spawned installer) ───── // // Implements rows C1-C6 from // `.msd/phase/feat-2873-cross-scope-shadowing/50-test-matrix.md`. The // `#2218 cross-scope shadowing` suite above calls `buildShadowReport` // DIRECTLY — real coverage of the pure IR, but it proves nothing about the // INSTALLER'S OWN WIRING at bin/install.js's writeManifest-adjacent // try/catch block (the only call site that ever prints a report). These // rows instead spawn the real installer and inspect its own stdout/stderr // and exit code — the actual product surface #2218 reported a gap in. describe('#2873 C1-C6 — install-time shadow report (spawned installer wiring)', () => { const { buildShadowReport, renderShadowReport, SHADOW_REASON } = require('../msd-core/bin/lib/install-shadow-report.cjs'); const SHADOW_THROWS_PRELOAD = path.join(__dirname, 'helpers', 'shadow-report-throws-preload.cjs'); function spawnInstall(args, cwd, root, nodeFlags = []) { return runNode([...nodeFlags, INSTALL_SCRIPT, ...args], { cwd, env: installerEnv({ HOME: root, USERPROFILE: root }), timeoutMs: INSTALL_TIMEOUT_MS, }); } /** * Assert `stderr` (the installer's own `console.warn` shadow-report * output) actually carries `expectedReport`'s rendered lines, verbatim. * `expectedReport`/its lines are computed by CALLING the module's own * pure `buildShadowReport`/`renderShadowReport` against the SAME on-disk * fixture the spawned installer just produced — never a guessed/hardcoded * literal. This is the typed-count-plus-content route the review brief * asks for: structural comparison against a pure function's own computed * output (mirrors this file's own `extractAtIncludeLines`/E14 pattern * above), not prose matching. */ function assertReportRendered(stderr, expectedReport) { const lines = renderShadowReport(expectedReport); assert.ok(lines.length > 0, 'fixture must actually be shadowed for this to be a meaningful positive assertion'); const stripped = stripAnsi(stderr); for (const line of lines) { assert.ok(stripped.includes(line), `expected installer stderr to carry the typed report line ${JSON.stringify(line)}\nstderr: ${stderr}`); } } /** * Negative-proof counterpart to `assertReportRendered`, for fixtures where * no report is expected. `expectedReport` is computed by calling the * module's own pure `buildShadowReport` against the SAME on-disk fixture * the spawned installer just produced. Asserts the typed IR itself is * `not_shadowed`, that `renderShadowReport` therefore computes ZERO lines * for it, and then — for every line it WOULD have computed had the IR been * shadowed (structurally empty here) — that none of them appear in * `stderr`. This replaces matching a hardcoded literal fragment * (`' shadowed: the '`) with a structural comparison against * `renderShadowReport`'s own (empty) output, so there is no longer a * guessed string for `local/no-source-grep`/`allow-test-rule` to flag. */ function assertReportAbsent(stderr, expectedReport) { assert.strictEqual(expectedReport.shadowed, false, 'fixture must not be shadowed for this to be a meaningful negative assertion'); assert.strictEqual(expectedReport.reason, SHADOW_REASON.NOT_SHADOWED, `expected reason ${SHADOW_REASON.NOT_SHADOWED}, got ${expectedReport.reason}`); const lines = renderShadowReport(expectedReport); assert.deepStrictEqual(lines, [], 'renderShadowReport must compute zero lines for a not_shadowed report'); const stripped = stripAnsi(stderr); for (const line of lines) { assert.ok(!stripped.includes(line), `expected installer stderr NOT to carry the typed report line ${JSON.stringify(line)}\nstderr: ${stderr}`); } } test('C1: global-then-local double install reports shadowing on the second install, exit 0', (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2873-c1-')); const projectDir = path.join(root, 'myrepo'); fs.mkdirSync(projectDir, { recursive: true }); t.after(() => cleanup(root)); const g = spawnInstall(['--claude', '--global'], root, root); assert.strictEqual(g.exitCode, 0, `global install failed: ${g.stdout}\n${g.stderr}`); const l = spawnInstall(['--claude', '--local'], projectDir, root); assert.strictEqual(l.exitCode, 0, `local install failed: ${l.stdout}\n${l.stderr}`); const expectedReport = buildShadowReport('claude', { home: root, cwd: projectDir }); assert.strictEqual(expectedReport.shadowed, true); assertReportRendered(l.stderr, expectedReport); }); test('C2: local-then-global double install reports shadowing symmetrically, exit 0', (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2873-c2-')); const projectDir = path.join(root, 'myrepo'); fs.mkdirSync(projectDir, { recursive: true }); t.after(() => cleanup(root)); const l = spawnInstall(['--claude', '--local'], projectDir, root); assert.strictEqual(l.exitCode, 0, `local install failed: ${l.stdout}\n${l.stderr}`); // The global install's own production `buildShadowReport(runtime)` call // (bin/install.js) takes no injected opts — it defaults to // `process.cwd()` to detect a coexisting LOCAL scope. Run it with cwd // INSIDE the already-locally-installed project (the real #2218 shape: a // developer running the global install from inside an existing // project), or it structurally cannot see the local scope at all — // verified empirically: cwd=root (a global install's usual cwd) never // reports, cwd=projectDir does. const g = spawnInstall(['--claude', '--global'], projectDir, root); assert.strictEqual(g.exitCode, 0, `global install failed: ${g.stdout}\n${g.stderr}`); const expectedReport = buildShadowReport('claude', { home: root, cwd: projectDir }); assert.strictEqual(expectedReport.shadowed, true); assertReportRendered(g.stderr, expectedReport); }); test('C3: global-only install stays quiet, exit 0 (negative proof)', (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2873-c3-')); t.after(() => cleanup(root)); const g = spawnInstall(['--claude', '--global'], root, root); assert.strictEqual(g.exitCode, 0, `global install failed: ${g.stdout}\n${g.stderr}`); // Typed control: a single-scope fixture can never be shadowed by // construction (buildShadowReport requires two installed scopes) — // confirms this negative-proof fixture is not accidentally shadowed. const controlReport = buildShadowReport('claude', { home: root, cwd: root }); assertReportAbsent(g.stderr, controlReport); }); test('C4: an install that fails before writeManifest never emits a report', (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2873-c4-')); t.after(() => cleanup(root)); // Structural failure, never chmod (chmod 0o000 no-ops under root — // CONTRIBUTING.md): pre-create the global config dir's OWN path as a // plain file. installerMigrations' lock-acquisition mkdirSync (which // runs before ANY artifact copy, long before writeManifest at // bin/install.js) then throws ENOTDIR/EEXIST — verified empirically, // and works identically whether or not the test runner is root. fs.writeFileSync(path.join(root, '.claude'), 'blocker'); const g = spawnInstall(['--claude', '--global'], root, root); assert.notStrictEqual(g.exitCode, 0, `expected the structural collision to fail the install: ${g.stdout}\n${g.stderr}`); const manifestPath = path.join(root, '.claude', MANIFEST_NAME); assert.ok(!fs.existsSync(manifestPath), 'writeManifest must never have run'); // Same fixture the failed install just left on disk: nothing was ever // written, so the typed IR is not_shadowed by construction — the failure // path never reaches the report call site at all (it runs strictly after // writeManifest). const expectedReport = buildShadowReport('claude', { home: root, cwd: root }); assertReportAbsent(g.stdout + g.stderr, expectedReport); }); test('C5: a throwing report builder never fails the install, report suppressed', (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2873-c5-')); t.after(() => cleanup(root)); const g = spawnInstall(['--claude', '--global'], root, root, ['--require', SHADOW_THROWS_PRELOAD]); assert.strictEqual(g.exitCode, 0, `a throwing buildShadowReport must never fail the install: ${g.stdout}\n${g.stderr}`); const manifestPath = path.join(root, '.claude', MANIFEST_NAME); assert.ok(fs.existsSync(manifestPath), 'writeManifest must still have run — the report call happens strictly after it'); // Same single-scope fixture as C3 (writeManifest ran, but the report // builder was preloaded to throw): the typed IR built from the real // installer's own scope is still not_shadowed, and — because the // injected throw is caught before renderShadowReport ever runs — no // report text should reach stderr either. const expectedReport = buildShadowReport('claude', { home: root, cwd: root }); assertReportAbsent(g.stderr, expectedReport); }); test('C6: re-running the same scope twice produces the same report', (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2873-c6-')); const projectDir = path.join(root, 'myrepo'); fs.mkdirSync(projectDir, { recursive: true }); t.after(() => cleanup(root)); const g = spawnInstall(['--claude', '--global'], root, root); assert.strictEqual(g.exitCode, 0, `global install failed: ${g.stdout}\n${g.stderr}`); const l1 = spawnInstall(['--claude', '--local'], projectDir, root); assert.strictEqual(l1.exitCode, 0, `first local install failed: ${l1.stdout}\n${l1.stderr}`); const l2 = spawnInstall(['--claude', '--local'], projectDir, root); assert.strictEqual(l2.exitCode, 0, `second local install failed: ${l2.stdout}\n${l2.stderr}`); const expectedReport = buildShadowReport('claude', { home: root, cwd: projectDir }); assert.strictEqual(expectedReport.shadowed, true); assertReportRendered(l1.stderr, expectedReport); assertReportRendered(l2.stderr, expectedReport); }); }); // ─── #3543: an unverifiable model_profile must bake no tier model ─── // // readMsdRuntimeProfileResolver probes for the project's // .planning/config.json by walking up from the install's targetDir. A GLOBAL // OpenCode install (targetDir ~/.config/) can never reach the // consuming project, and ~/.msd/defaults.json never carries model_profile // (writeNonClaudeDefaults writes only resolve_model_ids + runtime), so the // resolver silently fell back to 'balanced' and baked e.g. // anthropic/claude-opus-4-8 into the emitted agent frontmatter — defeating a // project's explicit model_profile:"inherit" (OpenCode subagents use the // static frontmatter model, which overrides the live /model selection). // // The contract under test (issue #3543, maintainer Agent Brief): // - "no project config found" is NOT "profile absent": the profile is // UNVERIFIABLE, and an unverifiable profile bakes no model key. // - a found config keeps the documented 'balanced' default (local installs). // - a profile (or model_overrides pin) declared in ~/.msd/defaults.json is // machine-level and still bakes on a global install. { const { describe: __d3543, test: __t3543, beforeEach: __be3543, afterEach: __ae3543 } = require('node:test'); const { install: __install3543 } = require('../bin/install.js'); const { readMsdRuntimeProfileResolver: __resolver3543 } = require('../msd-core/bin/lib/install-model-override-resolver.cjs'); const { captureConsole: __capture3543 } = require('./helpers.cjs'); // Installer-written shape for a non-Claude runtime (writeNonClaudeDefaults). const __INSTALLER_DEFAULTS_3543 = { resolve_model_ids: 'omit', runtime: 'opencode' }; // Tier ids from msd-core/bin/shared/model-catalog.json runtimeTierDefaults. // msd-roadmapper distinguishes profiles: balanced → sonnet, quality → opus. const __SONNET_3543 = 'anthropic/claude-sonnet-5'; const __OPUS_3543 = 'anthropic/claude-opus-4-8'; function __writeJson3543(p, obj) { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(obj, null, 2), 'utf-8'); } function __agentsDir3543(configHome, runtime) { return path.join(configHome, '.config', runtime, 'agents'); } function __listAgents3543(agentsDir) { return fs.readdirSync(agentsDir).filter((f) => f.startsWith('msd-') && f.endsWith('.md')); } // Extract the baked model line (or null) — assertions compare it for // equality against the expected literal rather than building a RegExp // from the model id (CodeQL: incomplete backslash escaping). function __modelLine3543(content) { const m = content.match(/^model:.*$/m); return m ? m[0] : null; } __d3543('#3543 unverifiable model_profile bakes no tier model', () => { let __root3543; let __home3543; let __project3543; let __prevEnv3543; let __prevCwd3543; // opencode global config homes resolve through an XDG descriptor whose // env chain is [_CONFIG_DIR, _CONFIG, XDG_CONFIG_HOME] // before falling back to /.config/. CI runners export // XDG_CONFIG_HOME, which would route a "global" install into the runner's // REAL config home (the live-config guard fails the job on exactly that); // clearing the whole chain pins resolution to the isolated HOME fallback. const __XDG_ENV_3543 = [ 'OPENCODE_CONFIG_DIR', 'OPENCODE_CONFIG', 'XDG_CONFIG_HOME', ]; __be3543(() => { __root3543 = createTempDir('msd-3543-'); __home3543 = path.join(__root3543, 'home'); __project3543 = path.join(__root3543, 'project'); fs.mkdirSync(__project3543, { recursive: true }); __writeJson3543(path.join(__home3543, '.msd', 'defaults.json'), __INSTALLER_DEFAULTS_3543); __writeJson3543(path.join(__project3543, '.planning', 'config.json'), { runtime: 'opencode', model_profile: 'inherit', }); __prevEnv3543 = { HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE, SKIP: process.env.MSD_SKIP_STALE_SDK_CHECK, XDG: Object.fromEntries(__XDG_ENV_3543.map((k) => [k, process.env[k]])), }; __prevCwd3543 = process.cwd(); process.env.HOME = __home3543; process.env.USERPROFILE = __home3543; process.env.MSD_SKIP_STALE_SDK_CHECK = '1'; for (const k of __XDG_ENV_3543) delete process.env[k]; process.chdir(__project3543); }); __ae3543(() => { process.chdir(__prevCwd3543); if (__prevEnv3543.HOME === undefined) delete process.env.HOME; else process.env.HOME = __prevEnv3543.HOME; if (__prevEnv3543.USERPROFILE === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = __prevEnv3543.USERPROFILE; if (__prevEnv3543.SKIP === undefined) delete process.env.MSD_SKIP_STALE_SDK_CHECK; else process.env.MSD_SKIP_STALE_SDK_CHECK = __prevEnv3543.SKIP; for (const [k, v] of Object.entries(__prevEnv3543.XDG)) { if (v === undefined) delete process.env[k]; else process.env[k] = v; } cleanup(__root3543); }); function runInstall3543(isGlobal, runtime) { __capture3543(() => __install3543(isGlobal, runtime)); } // Row 1 — unit half: the resolver as a GLOBAL install invokes it. __t3543('resolver returns null when the only inherit declaration lives in a project the global probe cannot reach', () => { const resolver = __resolver3543(path.join(__home3543, '.config', 'opencode')); assert.equal(resolver, null, 'a global install cannot verify a profile — it must not resolve tier models'); }); // Row 1 — install half (criterion 1 + 5): the packaged defaults file // containing only resolve_model_ids + runtime, project declaring inherit. __t3543('global OpenCode install bakes no model line for any msd-* agent when the profile is unverifiable', () => { runInstall3543(true, 'opencode'); const agentsDir = __agentsDir3543(__home3543, 'opencode'); assert.ok(fs.existsSync(agentsDir), 'global install should create the agents directory'); const files = __listAgents3543(agentsDir); assert.ok(files.includes('msd-planner.md'), `msd-planner.md should be emitted (found: ${files.slice(0, 5).join(', ')}…)`); for (const f of files) { const content = fs.readFileSync(path.join(agentsDir, f), 'utf-8'); assert.doesNotMatch(content, /^model:/m, `${f} must carry no baked model — the profile is unverifiable at global scope`); } }); // Row 2 — control: a LOCAL install's probe reaches the project's inherit. __t3543('local OpenCode install keeps honoring a reachable model_profile inherit', () => { runInstall3543(false, 'opencode'); const agentsDir = path.join(__project3543, '.opencode', 'agents'); assert.ok(fs.existsSync(agentsDir), 'local install should create the agents directory'); for (const f of __listAgents3543(agentsDir)) { const content = fs.readFileSync(path.join(agentsDir, f), 'utf-8'); assert.doesNotMatch(content, /^model:/m, `${f} must carry no baked model under inherit`); } }); // Row 3 — boundary: a FOUND config with an absent profile key keeps the // documented 'balanced' default ("profile absent" ≠ "not found"). __t3543('local install with found config and absent profile key still bakes the balanced default', () => { __writeJson3543(path.join(__project3543, '.planning', 'config.json'), { runtime: 'opencode', }); runInstall3543(false, 'opencode'); const roadmapper = fs.readFileSync( path.join(__project3543, '.opencode', 'agents', 'msd-roadmapper.md'), 'utf-8'); assert.equal(__modelLine3543(roadmapper), `model: ${__SONNET_3543}`, 'msd-roadmapper balanced → sonnet tier must still bake on a local install'); }); // Row 4 — a machine-declared profile is verifiable and still bakes globally. __t3543('global install bakes the tier of a model_profile declared in ~/.msd/defaults.json', () => { __writeJson3543(path.join(__home3543, '.msd', 'defaults.json'), { resolve_model_ids: 'omit', runtime: 'opencode', model_profile: 'quality', }); runInstall3543(true, 'opencode'); const roadmapper = fs.readFileSync( path.join(__agentsDir3543(__home3543, 'opencode'), 'msd-roadmapper.md'), 'utf-8'); assert.equal(__modelLine3543(roadmapper), `model: ${__OPUS_3543}`, 'msd-roadmapper quality → opus tier must bake when the profile is machine-declared'); }); // Row 5 — explicit model_overrides pins keep working at any scope. __t3543('global install still bakes an explicit model_overrides pin from ~/.msd/defaults.json', () => { __writeJson3543(path.join(__home3543, '.msd', 'defaults.json'), { resolve_model_ids: 'omit', runtime: 'opencode', model_overrides: { 'msd-roadmapper': 'explicit-global-pin-3543' }, }); runInstall3543(true, 'opencode'); const roadmapper = fs.readFileSync( path.join(__agentsDir3543(__home3543, 'opencode'), 'msd-roadmapper.md'), 'utf-8'); assert.equal(__modelLine3543(roadmapper), 'model: explicit-global-pin-3543', 'explicit model_overrides pins are the highest precedence and must bake'); }); // Row 6 — inherit declared at machine level. __t3543('global install bakes nothing when ~/.msd/defaults.json itself declares inherit', () => { __writeJson3543(path.join(__home3543, '.msd', 'defaults.json'), { resolve_model_ids: 'omit', runtime: 'opencode', model_profile: 'inherit', }); runInstall3543(true, 'opencode'); for (const f of __listAgents3543(__agentsDir3543(__home3543, 'opencode'))) { const content = fs.readFileSync(path.join(__agentsDir3543(__home3543, 'opencode'), f), 'utf-8'); assert.doesNotMatch(content, /^model:/m, `${f} must carry no baked model under inherit`); } }); // Row 7 — doc contract: targetDir null consults only the global defaults. __t3543('null targetDir with runtime-only home defaults resolves null (unverifiable)', () => { assert.equal(__resolver3543(null), null, 'with no project to probe and no declared profile, the resolver must be inert'); }); // Row 8 — falsy home model_profile values count as undeclared, matching // the existing || merge semantics. __t3543('falsy model_profile in ~/.msd/defaults.json counts as undeclared', () => { const defaultsPath = path.join(__home3543, '.msd', 'defaults.json'); const globalDir = path.join(__home3543, '.config', 'opencode'); for (const falsy of ['', null]) { __writeJson3543(defaultsPath, { resolve_model_ids: 'omit', runtime: 'opencode', model_profile: falsy, }); assert.equal(__resolver3543(globalDir), null, `model_profile ${JSON.stringify(falsy)} must be treated as undeclared`); } }); // Row 9 — a local install into a tree with no .planning anywhere is just // as unverifiable as a global one: bake nothing, crash nowhere. __t3543('local install without .planning bakes no model and does not crash', () => { cleanup(path.join(__project3543, '.planning')); runInstall3543(false, 'opencode'); const agentsDir = path.join(__project3543, '.opencode', 'agents'); assert.ok(fs.existsSync(agentsDir), 'local install should create the agents directory'); const planner = fs.readFileSync(path.join(agentsDir, 'msd-planner.md'), 'utf-8'); assert.doesNotMatch(planner, /^model:/m, 'with no reachable project config the profile is unverifiable — no bake'); }); }); } // ─── #2874 (epic #2866 Phase 5) — G1/G3: the additive-contract guard ──────── // Governed by ADR-58 (docs/adr/58-runtime-install-policy-module.md). // Design: .msd/phase/feat-2874-executed-plan-return/40-design.md // Test matrix: .msd/phase/feat-2874-executed-plan-return/50-test-matrix.md // // G1 and G3 must be GREEN both BEFORE and AFTER the executed-plan return // lands — they are the guard proving the return value is additive (AC4), // never a behavior change. No production code is touched by this file. /** * Recursively hash a directory tree into a stable, order-independent digest. * `stripDir`, if given, is textually removed from each UTF-8-decodable * file's content before hashing, so two installs into DIFFERENT temp * directories (whose absolute paths get baked into rewritten skill bodies) * can still be compared for content-identity. * * Both `stripDir` and the file content are normalized to forward slashes * before the strip, unconditionally (never gated on `path.sep`) — production * (`posixNormalize` in shell-command-projection.cts) rewrites `\` -> `/` in * the resolved configDir before baking it into skill bodies, so on Windows * `stripDir` (a raw fs.mkdtempSync path, backslash-separated) would never * match the posix-normalized text actually written, leaving each install's * unique temp-dir suffix embedded and making every file's hash diverge. */ function hashDirTree(rootDir, stripDir) { const entries = []; const stripDirPosix = stripDir ? stripDir.replace(/\\/g, '/') : stripDir; const walk = (relPath, absPath) => { for (const entry of fs.readdirSync(absPath, { withFileTypes: true }) .sort((a, b) => a.name.localeCompare(b.name))) { const childRel = relPath ? `${relPath}/${entry.name}` : entry.name; const childAbs = path.join(absPath, entry.name); if (entry.isDirectory()) { walk(childRel, childAbs); } else if (entry.isFile()) { const buf = fs.readFileSync(childAbs); const normalized = stripDirPosix ? buf.toString('utf8').replace(/\\/g, '/').split(stripDirPosix).join('') : buf; entries.push(`${childRel}:${crypto.createHash('sha256').update(normalized).digest('hex')}`); } } }; if (fs.existsSync(rootDir)) walk('', rootDir); return entries.sort().join('\n'); } describe('installRuntimeArtifacts — G1: void-ignoring caller is unaffected (AC4)', () => { test('writes are byte-identical whether or not the caller uses the return value', (t) => { const configDirIgnored = createTempDir('msd-g1-ignored-'); const configDirCaptured = createTempDir('msd-g1-captured-'); t.after(() => cleanup(configDirIgnored)); t.after(() => cleanup(configDirCaptured)); // Caller A: discards the return value entirely — today's every call site // (bin/install.js, both existing adapter test doubles). installRuntimeArtifacts('claude', configDirIgnored, 'global', RESOLVED_CORE); // Caller B: captures the return value. Its shape is not asserted here — // section E owns that — only that capturing it changes nothing about // what gets written, and that capturing never itself throws. const captured = installRuntimeArtifacts('claude', configDirCaptured, 'global', RESOLVED_CORE); assert.ok( captured === undefined || (captured !== null && typeof captured === 'object'), 'G1: the return value, whatever its shape, must be undefined (today) or a plain object ' + '(after) — never something a capturing caller could not safely ignore', ); assert.strictEqual( hashDirTree(configDirCaptured, configDirCaptured), hashDirTree(configDirIgnored, configDirIgnored), 'G1: writes must be byte-identical regardless of whether the caller captures the return value', ); }); }); describe('installRuntimeArtifacts — G3: adapter calling-convention regression guard', () => { // tests/adapter-declarative-equivalence.test.cjs:52 and // tests/adapter-imperative.test.cjs:80 pin their OWN behavior via a // module-ref monkeypatch of installRuntimeArtifacts — neither file ever // invokes the real function, and neither is read or modified here. This // row proves those two files' SUBJECT — the real installRuntimeArtifacts, // called with the exact positional shape each adapter uses — still // behaves: a real, successful, byte-on-disk install. test('declarative-adapter call shape (5 positional args, no capabilityRegistry) still installs', (t) => { const configDir = createTempDir('msd-g3-declarative-'); t.after(() => cleanup(configDir)); // Matches tests/adapter-declarative-equivalence.test.cjs:62-66's // captured shape: [runtime, configDir, scope, resolvedProfile, resolveAttribution]. const resolveAttribution = () => 'attr-claude'; installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE, resolveAttribution); assert.ok( fs.existsSync(path.join(configDir, 'skills', 'msd-help', 'SKILL.md')), 'G3: the declarative adapter\'s calling convention must still produce a real install', ); }); test('imperative-adapter call shape (6 positional args incl. composed capability registry) still installs', (t) => { const configDir = createTempDir('msd-g3-imperative-'); t.after(() => cleanup(configDir)); // Matches tests/adapter-imperative.test.cjs:88's captured shape: // [runtime, configDir, scope, resolvedProfile, resolveAttribution, capabilityRegistry]. const capabilityRegistry = { capabilityClusters: {} }; installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE, undefined, capabilityRegistry); assert.ok( fs.existsSync(path.join(configDir, 'skills', 'msd-help', 'SKILL.md')), 'G3: the imperative adapter\'s calling convention must still produce a real install', ); }); }); // --------------------------------------------------------------------------- // #2875 (epic #2866 Phase 6): User Artifact Staging — call-site integration. // (.msd/phase/feat-2875-materialization-primitives/50-test-matrix.md) // // C7 (anti-inertness): recovery must be reachable from a REAL `bin/install.js` // run, not merely callable — the #1879-F15 failure mode this whole phase // exists to avoid. This spawns the real installer twice against the SAME // sandbox HOME, with an orphaned staged artifact manually planted between the // two runs (simulating a prior run that died between the wipe and its own // restore/discard), and asserts the SECOND real installer invocation recovers // it — proving the wiring in bin/install.js's `install()`, not just that the // module's own function works when called directly. // --------------------------------------------------------------------------- describe('#2875: user-artifact-staging — call-site integration (C7 anti-inertness)', () => { test('an orphaned USER-PROFILE.md staged under the real msd-core destDir is recovered by a subsequent real install() run', (t) => { const first = runMinimalInstall({ runtime: 'claude', scope: 'global' }); t.after(() => cleanup(first.root)); const msdCoreDir = path.join(first.configDir, 'msd-core'); const profilePath = path.join(msdCoreDir, 'USER-PROFILE.md'); const customContent = '# My Profile\n\nOrphaned content from a crashed install run.\n'; fs.writeFileSync(profilePath, customContent, 'utf8'); // Manually plant the orphan: stage USER-PROFILE.md durably (the commit // point — record.json — lands), then simulate the crash by deleting the // file WITHOUT restoring or discarding. This is exactly the state a // process death between the wipe and the restore leaves behind. const stagingRoot = _resolveUserArtifactStagingRoot(first.configDir); const staged = stageUserArtifacts(msdCoreDir, ['USER-PROFILE.md'], stagingRoot, { runId: '999999' }); assert.deepEqual(staged.names, ['USER-PROFILE.md'], 'precondition: the orphan really did stage'); fs.unlinkSync(profilePath); assert.ok(!fs.existsSync(profilePath), 'precondition: the file is genuinely gone before the second run'); // Second REAL install, same HOME. If recovery is wired at a reachable // production entry point, USER-PROFILE.md is repopulated with the // orphaned content BEFORE the ordinary preserve/wipe/restore cycle runs // (which has nothing to preserve on its own — the file was deleted, not // merely staged, before this run started). runMinimalInstall({ runtime: 'claude', scope: 'global', root: first.root }); assert.ok(fs.existsSync(profilePath), 'C7: USER-PROFILE.md must be recovered by the second real install run'); assert.equal( fs.readFileSync(profilePath, 'utf8'), customContent, 'recovered content must be byte-identical to the orphaned staged copy', ); // The staging entry is consumed by the recovery step itself. const entries = fs.existsSync(stagingRoot) ? fs.readdirSync(stagingRoot) : []; assert.equal(entries.length, 0, 'the orphan is discarded once recovered — not left for a third run to find again'); }); }); // --------------------------------------------------------------------------- // #3719: applyAgentPathRewrites (install-profiles.cts:964, the agents/ // staging pipeline's pre-converter path-rewrite step) never calls // restoreClaudeGlobalAtRefTilde — the same restore #3133/#3544 wired into // the skill-staging and msd-core/ spec-tree emit paths. A real global Claude // install therefore ships every `agents/msd-*.md` `@`-include as // `@$HOME/.claude/...`, which Claude Code does not expand: the include // silently loads nothing (planner guidance, the untrusted-input boundary, // the agent-skills bootstrap, the mandatory initial read). // // Uses the real spawned installer (runMinimalInstall, `bin/install.js` // subprocess) so a green run here proves the fix reaches the actual emit // path, not just the pure function tested in tests/path-replacement.test.cjs. // --------------------------------------------------------------------------- describe('#3719: real global Claude install — agents/*.md @-refs must resolve on tilde', () => { let claudeGlobal; let projectLocal; before(() => { claudeGlobal = runMinimalInstall({ runtime: 'claude', scope: 'global' }); projectLocal = runMinimalInstall({ runtime: 'claude', scope: 'local' }); }); after(() => { cleanup(claudeGlobal.root); cleanup(projectLocal.root); }); // A live `@`-include is BARE markdown. An occurrence inside an inline-code span is // PROSE ABOUT one — `msd-core/CHANGELOG.md` ships the #3133/#3544 entries, which // quote `@$HOME/...` verbatim while describing the very defect this row guards. // // Dropping the line-start anchor (correct: it hid 48 mid-line refs) made those // entries visible to this scan, so the row went red on a CORRECT tree. The fix is // to strip inline-code spans, NOT to restore the anchor — restoring it would trade // a false positive for the false negative that let this bug ship in the first place. const INLINE_CODE_SPAN_RE = /`[^`]*`/g; function collectAtHomeLines(rootDir) { const failures = []; for (const file of walk(rootDir)) { if (!file.endsWith('.md')) continue; const relative = path.relative(claudeGlobal.configDir, file).replace(/\\/g, '/'); // The installation-owned Runtime Surface corpus is raw package input, // not an emitted runtime artifact. It must remain byte-identical to the // package and is validated separately by the corpus parity matrix. if (relative.startsWith('msd-core/commands/msd/') || relative.startsWith('msd-core/agents/')) continue; const content = fs.readFileSync(file, 'utf8'); for (const line of splitLines(content)) { if (/@\$HOME\//.test(line.replace(INLINE_CODE_SPAN_RE, ''))) failures.push({ file, line }); } } return failures; } test('row 3 [RED] — a real global Claude install emits ZERO @$HOME/ lines across agents/*.md', () => { const agentsDir = path.join(claudeGlobal.configDir, 'agents'); assert.ok(fs.existsSync(agentsDir), `expected ${agentsDir} to exist`); const failures = []; for (const file of walk(agentsDir)) { if (!file.endsWith('.md')) continue; const content = fs.readFileSync(file, 'utf8'); for (const line of splitLines(content)) { if (/@\$HOME\//.test(line.replace(INLINE_CODE_SPAN_RE, ''))) failures.push(`${path.relative(agentsDir, file)}: ${line}`); } } assert.deepStrictEqual(failures, [], `agents/*.md files with a broken @$HOME/ include:\n${failures.join('\n')}`); }); test('row 4 [RED, non-vacuity] — that same install DOES emit @~/ includes in agents/*.md', () => { const agentsDir = path.join(claudeGlobal.configDir, 'agents'); const planner = fs.readFileSync(path.join(agentsDir, 'msd-planner.md'), 'utf8'); const tildeLines = splitLines(planner).filter((l) => l.startsWith('@~/')); assert.ok( tildeLines.length > 0, `expected at least one @~/ line in msd-planner.md (proves the file is not empty/absent), got: ${JSON.stringify(splitLines(planner).filter((l) => l.startsWith('@')))}`, ); assert.ok( tildeLines.some((l) => l.includes('mandatory-initial-read.md')), `expected the mandatory-initial-read.md @-include specifically to survive on tilde, got: ${JSON.stringify(tildeLines)}`, ); }); test('row 5 [CONTROL] — skills and workflows emit paths still emit @~/ (no #3133/#3544 regression)', () => { const msdCoreDir = path.join(claudeGlobal.configDir, 'msd-core'); assert.ok(fs.existsSync(msdCoreDir), `expected ${msdCoreDir} to exist`); const failures = collectAtHomeLines(msdCoreDir); assert.deepStrictEqual(failures, [], `#3544 regression — @$HOME/ lines under msd-core/:\n${failures.map(f => `${f.file}: ${f.line}`).join('\n')}`); const skillsDir = path.join(claudeGlobal.configDir, 'skills'); assert.ok(fs.existsSync(skillsDir), `expected ${skillsDir} to exist`); let sawTilde = false; for (const file of walk(skillsDir)) { if (!file.endsWith('.md')) continue; if (splitLines(fs.readFileSync(file, 'utf8')).some((l) => l.startsWith('@~/'))) sawTilde = true; } assert.ok(sawTilde, 'expected at least one skills/ SKILL.md to still carry an @~/ include (#3133 not regressed)'); }); // THE KEY ROW: walk the ENTIRE emitted tree — not an enumerated list of known // paths — so a new emit path added later is covered automatically by landing // in the same tree. Reports every offending file so a future failure is // diagnosable without re-deriving the failing path by hand. test('row 6 [RED, PARITY] — no file anywhere in the emitted global Claude install tree contains @$HOME/', () => { const failures = collectAtHomeLines(claudeGlobal.configDir); assert.deepStrictEqual( failures, [], `found @$HOME/ lines under the entire emitted tree (${claudeGlobal.configDir}):\n` + failures.map((f) => ` ${path.relative(claudeGlobal.configDir, f.file)}: ${f.line}`).join('\n'), ); }); test('row 8 [CONTROL] — a project-scoped (non-global) Claude install is unaffected', () => { const agentsDir = path.join(projectLocal.configDir, 'agents'); assert.ok(fs.existsSync(agentsDir), `expected ${agentsDir} to exist`); // Local install's pathPrefix is absolute (not $HOME-form) — restoreClaudeGlobalAtRefTilde // is a documented no-op for it, and there must be no @$HOME/ or bare @~/.claude leak either. const failures = []; for (const file of walk(agentsDir)) { if (!file.endsWith('.md')) continue; const content = fs.readFileSync(file, 'utf8'); for (const line of splitLines(content)) { if (/^@\$HOME\//.test(line) || /^@~\/\.claude\//.test(line)) failures.push(`${path.relative(agentsDir, file)}: ${line}`); } } assert.deepStrictEqual(failures, [], `local install must not leak @$HOME/ or @~/.claude/:\n${failures.join('\n')}`); }); }); // ── #3738: antigravity global artifacts install under ~/.gemini/config ──────── describe('#3738: antigravity global artifacts install under ~/.gemini/config', () => { test('global skills and agents dest dirs resolve under /.gemini/config, not configHome', (t) => { const configDir = createTempDir('msd-3738-antigravity-'); t.after(() => cleanup(configDir)); sandboxHome(t, configDir); const layout = resolveRuntimeArtifactLayout('antigravity', configDir, 'global'); const skillsKind = layout.kinds.find(k => k.kind === 'skills'); const agentsKind = layout.kinds.find(k => k.kind === 'agents'); assert.ok(skillsKind, 'antigravity must have a skills kind'); assert.ok(agentsKind, 'antigravity must have an agents kind'); const expectedHome = path.join(configDir, '.gemini', 'config'); assert.strictEqual(skillsKind.home, expectedHome, 'skills home override must be ~/.gemini/config'); assert.strictEqual(agentsKind.home, expectedHome, 'agents home override must be ~/.gemini/config'); installRuntimeArtifacts('antigravity', configDir, 'global', RESOLVED_CORE); assert.ok( fs.existsSync(path.join(expectedHome, 'skills', 'msd-help', 'SKILL.md')), 'a msd-* skill must exist under ~/.gemini/config/skills' ); const agentsDir = path.join(expectedHome, 'agents'); assert.ok(fs.existsSync(agentsDir), '~/.gemini/config/agents must exist'); assert.ok( fs.readdirSync(agentsDir).some(n => n.startsWith('msd-')), 'at least one msd-* agent must exist under ~/.gemini/config/agents' ); assert.ok( !fs.existsSync(path.join(configDir, 'skills')), 'no skills dir may be created under the configHome (~/.gemini/antigravity)' ); assert.ok( !fs.existsSync(path.join(configDir, 'agents')), 'no agents dir may be created under the configHome (~/.gemini/antigravity)' ); }); test('local (workspace) layout is unchanged: .agents/skills and .agents/agents', (t) => { const configDir = createTempDir('msd-3738-antigravity-local-'); t.after(() => cleanup(configDir)); sandboxHome(t, configDir); const layout = resolveRuntimeArtifactLayout('antigravity', configDir, 'local'); for (const kind of layout.kinds) { assert.strictEqual(kind.home, undefined, `local ${kind.kind} must not carry a home override`); } }); // ── #3747: pin the CLI-ONLY probe branch ──────────────────────────────────── // The #3738 tests above hand installRuntimeArtifacts an arbitrary configDir. // The reporter's machine resolved the configHome to ~/.gemini/antigravity-cli // (the only sibling present, carrying the msd-core/VERSION marker). This test // reproduces exactly that resolution and asserts global skills/agents still // land under ~/.gemini/config — the dir `agy` scans — and NOTHING lands under // the resolved configHome. This is the "path-asserting test pinned to the CLI // branch" the issue asked for: a regression that re-couples the global skills // root to the resolved config home goes red here. test('#3747: CLI-only install (configHome resolves to ~/.gemini/antigravity-cli) still lands global skills/agents under ~/.gemini/config', (t) => { const { resolveAntigravityGlobalDir } = require('../msd-core/bin/lib/runtime-homes.cjs'); const home = createTempDir('msd-3747-antigravity-cli-'); t.after(() => cleanup(home)); sandboxHome(t, home); // CLI-only environment: ~/.gemini/antigravity-cli is the sole sibling and // carries the MSD marker — resolution must pick it (not the IDE default). const cliConfigHome = path.join(home, '.gemini', 'antigravity-cli'); fs.mkdirSync(path.join(cliConfigHome, 'msd-core'), { recursive: true }); fs.writeFileSync(path.join(cliConfigHome, 'msd-core', 'VERSION'), '1.12.0\n'); for (const sibling of ['antigravity', 'antigravity-ide']) { assert.ok( !fs.existsSync(path.join(home, '.gemini', sibling)), `test premise: ~/.gemini/${sibling} must not exist (CLI-only install)`, ); } const resolved = resolveAntigravityGlobalDir({ home, env: {}, existsSync: fs.existsSync }); assert.strictEqual(resolved, cliConfigHome, 'configHome must resolve to the CLI dir (CLI-only probe branch)'); installRuntimeArtifacts('antigravity', resolved, 'global', RESOLVED_CORE); const discoveryHome = path.join(home, '.gemini', 'config'); assert.ok( fs.existsSync(path.join(discoveryHome, 'skills', 'msd-help', 'SKILL.md')), 'a msd-* skill must exist under ~/.gemini/config/skills (the dir agy scans, #3747)' ); assert.ok( fs.readdirSync(path.join(discoveryHome, 'agents')).some((n) => n.startsWith('msd-')), 'at least one msd-* agent must exist under ~/.gemini/config/agents' ); assert.ok( !fs.existsSync(path.join(resolved, 'skills')), 'no skills dir may be created under the CLI configHome — agy never reads it (#3747 silent drop)' ); assert.ok( !fs.existsSync(path.join(resolved, 'agents')), 'no agents dir may be created under the CLI configHome — agy never reads it (#3747 silent drop)' ); }); }); // ── #4667 — codex installs must not keep @~/.claude includes ────────────────── // The @-include form points Codex at the CLAUDE install: silent wrong-copy on // dual-runtime machines, nothing at all on codex-only ones (#570 cause 2 // residue). Every target ships in the codex install, so the installer rewrites // the `@~/.claude/msd-core/` include form in manifest-tracked artifacts; // the `_MSD_RUNTIME_ROOT` fallback chains and prose `.claude` mentions are a // must-NOT-rewrite group (issue evidence table) and stay byte-identical. const { test: __test4667, describe: __describe4667, before: __before4667, after: __after4667 } = require('node:test'); const assert4667 = require('node:assert/strict'); const fs4667 = require('node:fs'); const { spawnSync: __spawnSync4667 } = require('node:child_process'); const { createTempDir: __createTempDir4667, cleanup: __cleanup4667, isolatedNpmEnv: __isolatedNpmEnv4667 } = require('./helpers.cjs'); describe('install() global codex — @~/.claude include rewrite (#4667)', () => { const path4667 = path; let tmpCodexHome; let configDir; // Install through the real CLI as a SUBPROCESS with a fully controlled env — // the same shape test G uses. In-process installs share the test process's // real HOME and the installer's own exit paths; the subprocess isolates both // (#3712's real-home guard + MSD_TEST_MODE/npm isolation in one place). function codexInstall() { const result = __spawnSync4667(process.execPath, [ path.join(__dirname, '..', 'bin', 'install.js'), '--codex', '--global', '--config-dir', configDir, ], { cwd: tmpCodexHome, env: { ...process.env, ...__isolatedNpmEnv4667(), HOME: tmpCodexHome, USERPROFILE: tmpCodexHome, MSD_TEST_MODE: '', NO_UPDATE_NOTIFIER: '1', npm_config_update_notifier: 'false', }, encoding: 'utf8', timeout: INSTALL_TIMEOUT_MS, }); assert.equal(result.status, 0, `codex install failed:\n${result.stdout}\n${result.stderr}`); } function installedMdFiles() { // Mirror the installer's own leak-scanner scope: manifest-tracked .md // artifacts under the msd-core payload PLUS the codex skills staging root // ($HOME/.agents/skills — the "skills" kind declares a global home // override), CHANGELOG excluded. const roots = [ path4667.join(configDir, 'msd-core'), path4667.join(tmpCodexHome, '.agents', 'skills'), ]; const out = []; const walk = (dir) => { if (!fs4667.existsSync(dir)) return; for (const entry of fs4667.readdirSync(dir, { withFileTypes: true })) { const p = path4667.join(dir, entry.name); if (entry.isDirectory()) walk(p); else if (entry.name.endsWith('.md') && entry.name !== 'CHANGELOG.md') out.push(p); } }; for (const root of roots) walk(root); return out; } before(() => { tmpCodexHome = __createTempDir4667('msd-codex-4667-'); configDir = path4667.join(tmpCodexHome, 'codex-config'); fs4667.mkdirSync(configDir, { recursive: true }); codexInstall(); }); after(() => { __cleanup4667(tmpCodexHome); }); test('codex install leaves zero @~/.claude includes in MSD-owned .md artifacts (#4667)', () => { const leaks = installedMdFiles().filter((file) => fs4667.readFileSync(file, 'utf8').includes('@~/.claude/')); assert4667.equal( leaks.length, 0, `files still carrying @~/.claude includes:\n${leaks.join('\n')}` ); }); test('codex install rewrites agent @ includes to the codex root (#4667)', () => { const agentFile = path4667.join(configDir, 'msd-core', 'agents', 'msd-advisor-researcher.md'); assert4667.ok(fs.existsSync(agentFile), 'the advisor-researcher agent must be installed'); const content = fs4667.readFileSync(agentFile, 'utf8'); assert4667.ok( content.includes('@~/.codex/msd-core/references/untrusted-input-boundary.md'), 'the include must point at the codex install' ); assert4667.ok(!content.includes('@~/.claude/'), 'no @~/.claude include may survive'); }); test('codex install rewrites $HOME-anchored @ includes too (#4667)', () => { const cmdFile = path4667.join(configDir, 'msd-core', 'commands', 'msd', 'plan-review-convergence.md'); assert4667.ok(fs4667.existsSync(cmdFile), 'the plan-review-convergence command must be installed'); const content = fs4667.readFileSync(cmdFile, 'utf8'); assert4667.ok( content.includes('@$HOME/.codex/msd-core/workflows/plan-review-convergence.md'), 'the $HOME-anchored include must point at the codex install' ); assert4667.ok(!content.includes('@$HOME/.claude/'), 'no @$HOME/.claude include may survive'); }); test('codex install keeps the _MSD_RUNTIME_ROOT .claude fallbacks (#4667)', () => { const workflowsDir = path4667.join(configDir, 'msd-core', 'workflows'); let fallbacks = 0; for (const file of fs4667.readdirSync(workflowsDir)) { if (!file.endsWith('.md')) continue; const content = fs4667.readFileSync(path4667.join(workflowsDir, file), 'utf8'); fallbacks += (content.match(/_MSD_RUNTIME_ROOT\}\/\.claude\//g) || []).length; } assert4667.ok( fallbacks > 0, 'the _MSD_RUNTIME_ROOT .claude fallback chains must survive the rewrite (must-NOT-rewrite group)' ); }); });