Files
msd-core/tests/install-runtime-artifacts.test.cjs
Jakub Zych 6cfa0c55d2 refactor: drop 12 runtimes, keep Claude, Codex, OpenCode, Cursor, ZCode, Antigravity
Removes kilo, kimi, kimi-code, copilot, windsurf, augment, trae, qwen, hermes,
cline, codebuddy and pi end to end: capability descriptors, installer branches
and converters (bin/install.js 14.9k -> 11.2k lines), TypeScript converters,
hook surfaces and runtime homes, review lanes qwen/kimi-code, the two pi
migrations, Kimi payload normalization in the hook guards, dead hostBehaviors
vocabulary, launcher home probes, fixtures, runtime-specific tests and the
prose that presented them as supported.

Installer output for the six kept runtimes is byte-identical to before the
prune. The Kimi tool-vocabulary tests in workflow-guard, read-guard and
read-injection-scanner are left in place pending a decision.
2026-10-06 20:02:40 +02:00

6047 lines
280 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// docs-guard-exempt: codebuddy.ai/docs/... is an external URL and docs/adr/58-...md is a comment citation; neither is 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/<name>/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 @<configDir>/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. kilo-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/Kilo 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<relPath, content>. */
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-<stem>/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)
// (codebuddy runtime retired; 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.<tier>` > 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:<cmd>") and the
* installer registered colon form in SKILL.md name: to match.
*
* Updated (#2808): workflows now use Skill(skill="msd-<cmd>") (hyphen),
* and the installer emits name: msd-<cmd> (hyphen). Claude Code autocomplete
* now shows the canonical hyphen form instead of the deprecated colon form.
* The directory name (msd-<cmd>) 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="<name>")` 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('<!--')) !== -1) {
stripped += rest.slice(0, idx);
const end = 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-<cmd> (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:<cmd>") 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-<cmd>") 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-<cmd>") 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: <skillsRoot>/msd-<stem>/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); the
// convertClaudeToAugmentMarkdown duplicate dedup is deferred to Phase 2's cleanup
// (entangled converter cluster; not required to unblock Phase 2).
// 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 <s@example.com>';
assert.strictEqual(conversion.processAttribution(input, null), 'Commit body line.');
});
test('undefined leaves content unchanged', () => {
const input = 'Commit body.\n\nCo-Authored-By: Someone <s@example.com>';
assert.strictEqual(conversion.processAttribution(input, undefined), input);
});
test('a string replaces the attribution value', () => {
const input = 'Body\n\nCo-Authored-By: Old Name <old@example.com>';
assert.strictEqual(
conversion.processAttribution(input, 'New Name <new@example.com>'),
'Body\n\nCo-Authored-By: New Name <new@example.com>',
);
});
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 <s@example.com>';
assert.strictEqual(conversion.processAttribution(input, null), 'Body');
});
test('replaces every Co-Authored-By line (global)', () => {
const input = 'Body\nCo-Authored-By: A <a@x>\nCo-Authored-By: B <b@x>';
assert.strictEqual(
conversion.processAttribution(input, 'Z <z@x>'),
'Body\nCo-Authored-By: Z <z@x>\nCo-Authored-By: Z <z@x>',
);
});
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-windsurf',
homeDir: 'C:\\Users\\runner',
});
assert.strictEqual(prefix, 'C:/Users/runner/AppData/Local/Temp/msd-1615-windsurf/');
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 <new@example.com>');
assert.ok(result.includes('Co-Authored-By: NewName <new@example.com>'), '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('/')), '');
},
));
});
});
test('#4377: a project-root local target falls back to absolute includes', () => {
// No shipped runtime installs at the project root any more, so the `undefined` branch is not
// reachable through a real descriptor; only the descriptor-directory branch is asserted.
assert.equal(conversion._localIncludeDirName('claude'), '.claude');
});
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 <name>(...) {}` declaration and every
// top-level `const/let/var <name> = <init>;` 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.<Y> 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 `<objectName>.<propertyName>` — 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 = <anything>;` — 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.<Y>;`) 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 <path>" 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:<cmd>` (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:<cmd>"). 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-<cmd> (not msd:<cmd>).
* 3. No workflow contains a Skill(skill="msd:<cmd>") 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-<cmd> (not msd:<cmd>)', () => {
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:<cmd>") 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('<!--')) !== -1) {
stripped += rest.slice(0, idx);
const end = 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-<cmd>: ' + 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-<stem>/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 ">=<host major>.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 <root>`, which pins the install AT `<root>`
// itself (manifest at `<root>/msd-file-manifest.json`), not at
// `<root>/.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
// `<root>/.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/Kilo install (targetDir ~/.config/<runtime>) 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.
// - Kilo mirrors OpenCode (static-frontmatter twin, #2093).
{
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 [<RUNTIME>_CONFIG_DIR, <RUNTIME>_CONFIG, XDG_CONFIG_HOME]
// before falling back to <HOME>/.config/<name>. 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('<CONFIGDIR>')
: 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 <home>/.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)'
);
});
});