'use strict'; /** * Consolidated tests for the Runtime Artifact Layout Module (ADR-3660) — layout seam. * * Covers: * - resolveRuntimeArtifactLayout — structural shape per runtime * - resolveRuntimeArtifactLayout edge-cases (error paths, invalid input) * - kind.stage() invocations per kind type * * Sources consolidated (3 files deleted): * tests/runtime-artifact-layout-resolve.test.cjs * tests/runtime-artifact-layout-edge-cases.test.cjs * tests/runtime-artifact-layout-stage.test.cjs * * See also: * runtime-artifact-layout-surface.test.cjs — surface seam * runtime-artifact-layout-install-profiles.test.cjs — install-profiles seam */ const { test, describe, beforeEach, afterEach, mock } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const path = require('path'); const crypto = require('node:crypto'); const { resolveRuntimeArtifactLayout, findInstallSourceRoot } = require('../msd-core/bin/lib/runtime-artifact-layout.cjs'); const capabilityRegistry = require('../msd-core/bin/lib/capability-registry.cjs'); const { withInstallFs } = require('../msd-core/bin/lib/install-fs-adapter.cjs'); const { install } = require('../bin/install.js'); const { createTempDir, cleanup, scrubConfigLocationEnv, writePackageSourceMarkerFixture } = require('./helpers.cjs'); const REPO_ROOT = path.join(__dirname, '..'); const FAKE_DIR = '/tmp/fake-config-dir'; // ─── resolveRuntimeArtifactLayout — structural shape ──────────────────────── describe('resolveRuntimeArtifactLayout — claude local', () => { test('returns correct layout for claude scope=local', () => { const layout = resolveRuntimeArtifactLayout('claude', FAKE_DIR, 'local'); assert.strictEqual(layout.runtime, 'claude'); assert.strictEqual(layout.configDir, FAKE_DIR); assert.strictEqual(layout.kinds.length, 2); assert.strictEqual(layout.kinds[0].kind, 'commands'); assert.strictEqual(layout.kinds[0].destSubpath, 'commands'); // #1367: flat msd-.md layout assert.strictEqual(layout.kinds[0].prefix, 'msd-'); assert.strictEqual(typeof layout.kinds[0].stage, 'function'); assert.strictEqual(layout.kinds[1].kind, 'agents'); assert.strictEqual(layout.kinds[1].destSubpath, 'agents'); assert.strictEqual(layout.kinds[1].prefix, 'msd-'); assert.strictEqual(typeof layout.kinds[1].stage, 'function'); }); }); describe('resolveRuntimeArtifactLayout — claude global', () => { // #2875 Part 2: claude/global gained an `agents` kind — NOT new on-disk // behavior. A `claude --global` install always wrote // `/agents/msd-*.md` via the now-deleted inline agent-staging // loop in bin/install.js (claude was never in the deleted // `_DESCRIPTOR_AGENTS_RUNTIMES` set, and that loop ran unconditionally, // independent of `_isSkillsRuntime`/scope). The descriptor previously never // modeled that write; it now does, matching what was always materialized — // which is also why the golden install-tree fixtures never moved (see // tests/fixtures/install-tree/**): the on-disk bytes were unchanged, only // the descriptor's own metadata became complete. test('returns correct layout for claude scope=global', () => { const layout = resolveRuntimeArtifactLayout('claude', FAKE_DIR, 'global'); assert.strictEqual(layout.runtime, 'claude'); assert.strictEqual(layout.configDir, FAKE_DIR); assert.strictEqual(layout.kinds.length, 2); assert.strictEqual(layout.kinds[0].kind, 'skills'); assert.strictEqual(layout.kinds[0].destSubpath, 'skills'); assert.strictEqual(layout.kinds[0].prefix, 'msd-'); assert.strictEqual(typeof layout.kinds[0].stage, 'function'); assert.strictEqual(layout.kinds[1].kind, 'agents'); assert.strictEqual(layout.kinds[1].destSubpath, 'agents'); assert.strictEqual(layout.kinds[1].prefix, 'msd-'); assert.strictEqual(typeof layout.kinds[1].stage, 'function'); }); }); describe('resolveRuntimeArtifactLayout — cursor', () => { test('returns correct layout for cursor — skills + agents only (#2644)', () => { const layout = resolveRuntimeArtifactLayout('cursor', FAKE_DIR); assert.strictEqual(layout.runtime, 'cursor'); assert.strictEqual(layout.configDir, FAKE_DIR); assert.strictEqual(layout.kinds.length, 2); const skillsKind = layout.kinds.find(k => k.kind === 'skills'); assert.ok(skillsKind, 'must have a skills kind'); assert.strictEqual(skillsKind.destSubpath, 'skills'); assert.strictEqual(skillsKind.prefix, 'msd-'); assert.strictEqual(typeof skillsKind.stage, 'function'); assert.equal(layout.kinds.find(k => k.kind === 'commands'), undefined, 'Cursor skills are the sole slash-menu surface; commands would duplicate them (#2644)'); const agentsKind = layout.kinds.find(k => k.kind === 'agents'); assert.ok(agentsKind, 'must have an agents kind (ADR-1235 §1 descriptor cutover)'); assert.strictEqual(agentsKind.destSubpath, 'agents'); assert.strictEqual(agentsKind.prefix, 'msd-'); assert.strictEqual(typeof agentsKind.stage, 'function'); }); }); describe('resolveRuntimeArtifactLayout — codex', () => { // #2875 Part 2: agents kind added — codex was never in the deleted // `_DESCRIPTOR_AGENTS_RUNTIMES` set, so the inline loop wrote codex agents // too; the descriptor now models it. Codex's separate config.toml // `[agents.msd-*]` strip on a full→minimal downgrade is untouched. test('returns correct layout for codex', () => { const layout = resolveRuntimeArtifactLayout('codex', FAKE_DIR); assert.strictEqual(layout.runtime, 'codex'); assert.strictEqual(layout.configDir, FAKE_DIR); assert.strictEqual(layout.kinds.length, 2); assert.strictEqual(layout.kinds[0].kind, 'skills'); assert.strictEqual(layout.kinds[0].destSubpath, 'skills'); assert.strictEqual(layout.kinds[0].prefix, 'msd-'); assert.strictEqual(typeof layout.kinds[0].stage, 'function'); assert.strictEqual(layout.kinds[1].kind, 'agents'); assert.strictEqual(layout.kinds[1].destSubpath, 'agents'); assert.strictEqual(layout.kinds[1].prefix, 'msd-'); assert.strictEqual(typeof layout.kinds[1].stage, 'function'); }); test('#2429: codex local scope does not set $HOME/.agents skills home override', () => { const layout = resolveRuntimeArtifactLayout('codex', FAKE_DIR, 'local'); const skills = layout.kinds.find(k => k.kind === 'skills'); assert.ok(skills, 'codex local must have a skills kind'); assert.strictEqual(skills.home, undefined, 'codex local skills must NOT have a home override (would redirect to $HOME/.agents instead of project-local)'); }); test('#2429: codex global scope DOES set $HOME/.agents skills home override', () => { const layout = resolveRuntimeArtifactLayout('codex', FAKE_DIR, 'global'); const skills = layout.kinds.find(k => k.kind === 'skills'); assert.ok(skills, 'codex global must have a skills kind'); assert.ok(typeof skills.home === 'string' && skills.home.length > 0, 'codex global skills MUST have a home override ($HOME/.agents)'); assert.ok(skills.home.includes('.agents'), 'codex global skills home should point to .agents directory'); }); }); test('keeps every built-in local artifact layout project-scoped (#2777)', () => { for (const [runtime, descriptor] of Object.entries(capabilityRegistry.runtimes)) { const localEntries = descriptor.runtime?.artifactLayout?.local ?? []; for (const entry of localEntries) { assert.strictEqual( Object.prototype.hasOwnProperty.call(entry, 'home'), false, `${runtime} local artifact layout entry '${entry.kind}' must not declare home`, ); } } }); describe('resolveRuntimeArtifactLayout — antigravity', () => { test('returns correct layout for antigravity', () => { const layout = resolveRuntimeArtifactLayout('antigravity', FAKE_DIR); assert.strictEqual(layout.runtime, 'antigravity'); assert.strictEqual(layout.configDir, FAKE_DIR); // #1575: agents kind added (antigravity cutover) assert.strictEqual(layout.kinds.length, 2); assert.strictEqual(layout.kinds[0].kind, 'skills'); assert.strictEqual(layout.kinds[0].destSubpath, 'skills'); assert.strictEqual(layout.kinds[0].prefix, 'msd-'); assert.strictEqual(typeof layout.kinds[0].stage, 'function'); assert.strictEqual(layout.kinds[1].kind, 'agents'); assert.strictEqual(layout.kinds[1].destSubpath, 'agents'); assert.strictEqual(layout.kinds[1].prefix, 'msd-'); assert.strictEqual(typeof layout.kinds[1].stage, 'function'); }); }); describe('resolveRuntimeArtifactLayout — opencode', () => { // #2875 Part 2: agents kind added — opencode's agents were previously // written by a code path entirely separate from this layout // (installOpencodeFamilyArtifacts's bespoke writers never called // resolveRuntimeArtifactLayout for agents); installAgentsKindStandalone now // covers them through the SAME descriptor this layout resolves. test('returns commands + skills + agents layout for opencode (#784)', () => { const layout = resolveRuntimeArtifactLayout('opencode', FAKE_DIR); assert.strictEqual(layout.runtime, 'opencode'); assert.strictEqual(layout.configDir, FAKE_DIR); assert.strictEqual(layout.kinds.length, 3); const commands = layout.kinds.find((k) => k.kind === 'commands'); assert.ok(commands, 'should have a commands kind'); // #2329: OpenCode discovers commands from the PLURAL `commands/` dir — the // singular `command/` made all /msd-* commands invisible to OpenCode. assert.strictEqual(commands.destSubpath, 'commands'); assert.strictEqual(commands.prefix, 'msd-'); assert.strictEqual(typeof commands.stage, 'function'); const skills = layout.kinds.find((k) => k.kind === 'skills'); assert.ok(skills, 'should have a skills kind'); assert.strictEqual(skills.destSubpath, 'skills'); assert.strictEqual(skills.prefix, 'msd-'); assert.strictEqual(typeof skills.stage, 'function'); const agents = layout.kinds.find((k) => k.kind === 'agents'); assert.ok(agents, 'should have an agents kind'); assert.strictEqual(agents.destSubpath, 'agents'); assert.strictEqual(agents.prefix, 'msd-'); assert.strictEqual(typeof agents.stage, 'function'); }); }); // ─── resolveRuntimeArtifactLayout — edge-cases ────────────────────────────── describe('resolveRuntimeArtifactLayout edge-cases', () => { test('claude local has both commands and agents kinds', () => { const layout = resolveRuntimeArtifactLayout('claude', '/tmp/x', 'local'); const kindNames = layout.kinds.map(k => k.kind); assert.ok(kindNames.includes('commands'), 'should have commands kind'); assert.ok(kindNames.includes('agents'), 'should have agents kind'); }); test('cursor has a skills kind and no commands kind (#2644)', () => { const layout = resolveRuntimeArtifactLayout('cursor', '/tmp/x'); const kindNames = layout.kinds.map(k => k.kind); assert.ok(kindNames.includes('skills'), 'cursor must have skills kind'); assert.ok(!kindNames.includes('commands'), 'cursor commands kind would duplicate skill menu entries'); }); // #2875 Part 2: claude/global now also declares an `agents` kind (see the // 'resolveRuntimeArtifactLayout — claude global' describe block above for // the full "was this new on-disk behavior?" determination — it is not). test('claude global has skills and agents kinds', () => { const layout = resolveRuntimeArtifactLayout('claude', '/tmp/x', 'global'); assert.strictEqual(layout.kinds.length, 2); assert.strictEqual(layout.kinds[0].kind, 'skills'); assert.strictEqual(layout.kinds[1].kind, 'agents'); }); test('unknown runtime grok throws TypeError containing runtime name', () => { assert.throws( () => resolveRuntimeArtifactLayout('grok', '/tmp/x'), (err) => { assert.ok(err instanceof TypeError); assert.ok(err.message.includes('grok'), 'error message must contain the runtime name'); return true; } ); }); test('unknown runtime xyzunknown throws TypeError', () => { assert.throws( () => resolveRuntimeArtifactLayout('xyzunknown', '/tmp/x'), TypeError ); }); test('empty configDir throws TypeError', () => { assert.throws( () => resolveRuntimeArtifactLayout('claude', ''), TypeError ); }); test('non-string configDir throws TypeError', () => { assert.throws( () => resolveRuntimeArtifactLayout('claude', null), TypeError ); }); test('bad scope throws TypeError', () => { assert.throws( () => resolveRuntimeArtifactLayout('claude', '/x', 'invalid'), TypeError ); }); }); // ─── kind.stage() invocations ──────────────────────────────────────────────── const CORE_SKILLS = new Set(['help', 'phase', 'new-project']); const CORE_AGENTS = new Set(['msd-planner']); const PROFILE_CORE = { skills: CORE_SKILLS, agents: CORE_AGENTS }; const PROFILE_FULL = { skills: '*', agents: new Set() }; function createStageConfigDir(t) { const configDir = writePackageSourceMarkerFixture(createTempDir('msd-layout-stage-')); t.after(() => cleanup(configDir)); return configDir; } describe('stage — agents kind (claude local)', () => { test('stage returns a valid directory for the agents kind', (t) => { const layout = resolveRuntimeArtifactLayout('claude', createStageConfigDir(t), 'local'); const agentsKind = layout.kinds.find(k => k.kind === 'agents'); assert.ok(agentsKind, 'should have an agents kind'); const stagedDir = agentsKind.stage(PROFILE_CORE); assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist'); assert.ok(fs.statSync(stagedDir).isDirectory(), 'stagedDir must be a directory'); }); }); describe('stage — skills kind (claude global)', () => { test('#4132: an independent source checkout can replace an invalid installed corpus', (t) => { const configDir = createTempDir('msd-4132-installer-source-'); t.after(() => cleanup(configDir)); const installedCommands = path.join(configDir, 'msd-core', 'commands', 'msd'); const installedAgents = path.join(configDir, 'msd-core', 'agents'); fs.mkdirSync(installedCommands, { recursive: true }); fs.mkdirSync(installedAgents, { recursive: true }); fs.writeFileSync(path.join(installedCommands, 'help.md'), 'OLD INSTALLED COMMAND\n'); fs.writeFileSync(path.join(installedAgents, 'msd-planner.md'), 'OLD INSTALLED AGENT\n'); fs.writeFileSync(path.join(configDir, '.msd-source'), path.join(REPO_ROOT, 'commands', 'msd') + '\n'); const layout = resolveRuntimeArtifactLayout('claude', configDir, 'global'); const skillsKind = layout.kinds.find(k => k.kind === 'skills'); const agentsKind = layout.kinds.find(k => k.kind === 'agents'); assert.ok(skillsKind); assert.ok(agentsKind); const stagedSkills = skillsKind.stage(PROFILE_CORE); const stagedAgents = agentsKind.stage(PROFILE_CORE); t.after(() => cleanup(stagedSkills)); t.after(() => cleanup(stagedAgents)); assert.match( fs.readFileSync(path.join(stagedSkills, 'msd-help', 'SKILL.md'), 'utf8'), /Show available MSD commands and usage guide/, ); assert.doesNotMatch( fs.readFileSync(path.join(stagedAgents, 'msd-planner.md'), 'utf8'), /OLD INSTALLED AGENT/, ); }); test('stage returns a directory containing msd-/SKILL.md entries', (t) => { const layout = resolveRuntimeArtifactLayout('claude', createStageConfigDir(t), 'global'); const skillsKind = layout.kinds.find(k => k.kind === 'skills'); assert.ok(skillsKind, 'should have a skills kind'); const stagedDir = skillsKind.stage(PROFILE_CORE); assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist'); const entries = fs.readdirSync(stagedDir); for (const entry of entries) { assert.ok(entry.startsWith('msd-'), `entry should start with msd-: ${entry}`); const skillMd = path.join(stagedDir, entry, 'SKILL.md'); assert.ok(fs.existsSync(skillMd), `SKILL.md must exist in ${entry}`); } assert.ok(entries.length >= 1, 'at least one skill dir should be staged'); }); test('stage with skills="*" produces flat layout for claude (#924: reverted from nested)', (t) => { const layout = resolveRuntimeArtifactLayout('claude', createStageConfigDir(t), 'global'); const skillsKind = layout.kinds.find(k => k.kind === 'skills'); assert.ok(skillsKind, 'should have a skills kind'); const stagedDir = skillsKind.stage(PROFILE_FULL); assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist'); // #924: Claude is reverted to FLAT. Full profile produces >= 60 top-level msd-* dirs. // (Previously nested: exactly 6 msd-ns-* router dirs. That broke Skill-tool discovery.) const topEntries = fs.readdirSync(stagedDir); assert.ok( topEntries.length >= 60, `full profile should have >= 60 top-level skill dirs (flat layout, #924), got ${topEntries.length}`, ); for (const entry of topEntries) { assert.ok(entry.startsWith('msd-'), `entry should start with msd-: ${entry}`); // Each skill dir has its own SKILL.md at the top level. const skillMd = path.join(stagedDir, entry, 'SKILL.md'); assert.ok(fs.existsSync(skillMd), `SKILL.md must exist at top level in ${entry}`); // No nested skills/ subdirectory: flat layout means no nesting. const skillsSubdir = path.join(stagedDir, entry, 'skills'); assert.ok(!fs.existsSync(skillsSubdir), `skills/ subdir must NOT exist in ${entry} (flat layout, #924)`); } }); }); describe('stage — opencode commands kind', () => { test('opencode stage returns directory with .md files for selected skills', (t) => { const layout = resolveRuntimeArtifactLayout('opencode', createStageConfigDir(t)); const commandsKind = layout.kinds.find(k => k.kind === 'commands'); assert.ok(commandsKind, 'should have a commands kind'); const stagedDir = commandsKind.stage(PROFILE_CORE); assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist'); const entries = fs.readdirSync(stagedDir).filter(f => f.endsWith('.md')); for (const entry of entries) { const stem = entry.slice(0, -3); assert.ok(CORE_SKILLS.has(stem), `unexpected skill staged: ${stem}`); } }); }); describe('stage — opencode skills kind (#784)', () => { for (const runtime of ['opencode']) { test(`${runtime} skills stage writes msd-/SKILL.md with name + description`, (t) => { const layout = resolveRuntimeArtifactLayout(runtime, createStageConfigDir(t)); const skillsKind = layout.kinds.find(k => k.kind === 'skills'); assert.ok(skillsKind, 'should have a skills kind'); const stagedDir = skillsKind.stage(PROFILE_CORE); assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist'); const entries = fs.readdirSync(stagedDir); assert.ok(entries.length >= 1, 'at least one skill dir should be staged'); for (const entry of entries) { assert.ok(entry.startsWith('msd-'), `entry should start with msd-: ${entry}`); const skillMd = path.join(stagedDir, entry, 'SKILL.md'); assert.ok(fs.existsSync(skillMd), `SKILL.md must exist in ${entry}`); const content = fs.readFileSync(skillMd, 'utf8'); // OpenCode skill spec: name must match the dir, description required. assert.ok(content.startsWith('---\n'), 'SKILL.md must open with frontmatter'); assert.match(content, new RegExp(`^name: ${entry}$`, 'm'), `name must equal dir ${entry}`); assert.match(content, /^description: /m, 'description frontmatter required'); // No colon-namespace command leaks in the converted body. assert.ok(!/\/msd:/.test(content), 'body must not contain /msd: colon refs'); } }); } }); describe('stage — cursor retired commands kind (#2644)', () => { test('cursor layout exposes no commands staging surface', () => { const layout = resolveRuntimeArtifactLayout('cursor', FAKE_DIR); const commandsKind = layout.kinds.find(k => k.kind === 'commands'); assert.equal(commandsKind, undefined); }); }); // ─── #1477: .msd-source marker provisioning ─────────────────────────────────── // // Regression for #1477: the Claude Code global skills layout ships // msd-core/{bin,contexts,references,templates,workflows} but no commands/msd // source tree, and _runLegacyUninstallCleanup removes any commands/msd/ for that // scope. At runtime findInstallSourceRoot's walk-up from msd-core/bin/lib had // nothing to find and /msd-surface threw for every subcommand (list/status // included); the marker reader added in #1476 never fired because nothing wrote // the marker. // // Fix: bin/install.js writes /.msd-source pointing at a resolvable // commands/msd, and findInstallSourceRoot prefers that marker over its walk-up. // // (The original #1477 also covered a deployed-install MODULE_NOT_FOUND from the // surface path's relative require('../../../bin/install.js'); ADR-1508 / #1511 // removed that getInstallExports relay entirely — surface.cjs now calls the // shipped runtime-artifact-conversion sibling directly — so that half no longer // applies and is covered by the #1511 relocation suite.) describe('#1477 .msd-source marker provisioning', () => { let tmpRoot; let savedHome; let savedUserProfile; let savedExplicitConfigDir; let savedTestMode; let restoreConfigLocationEnv; function silenceConsole(fn) { const orig = { log: console.log, warn: console.warn, error: console.error }; console.log = () => {}; console.warn = () => {}; console.error = () => {}; try { return fn(); } finally { console.log = orig.log; console.warn = orig.warn; console.error = orig.error; } } // Guard against process.exit killing the runner mid-install. function runInstall(isGlobal, runtime) { const origExit = process.exit; let exitCalled = false; process.exit = (code) => { exitCalled = true; throw new Error(`process.exit(${code}) during install — should not happen`); }; try { return silenceConsole(() => install(isGlobal, runtime)); } catch (e) { if (exitCalled) assert.fail(`install() called process.exit — unexpected: ${e.message}`); throw e; } finally { process.exit = origExit; } } beforeEach(() => { tmpRoot = createTempDir('msd-1477-'); savedHome = process.env.HOME; // os.homedir() reads USERPROFILE on win32, HOME elsewhere; redirect both so // install() targets the fixture regardless of platform. savedUserProfile = process.env.USERPROFILE; process.env.HOME = tmpRoot; process.env.USERPROFILE = tmpRoot; savedExplicitConfigDir = process.env.MSD_EXPLICIT_CONFIG_DIR; delete process.env.MSD_EXPLICIT_CONFIG_DIR; savedTestMode = process.env.MSD_TEST_MODE; process.env.MSD_TEST_MODE = '1'; // #2665: this block calls install(true, 'claude') IN-PROCESS. Redirecting // HOME/USERPROFILE is not enough, because getGlobalConfigDir is env-FIRST: // an ambient CLAUDE_CONFIG_DIR wins over the fixture and a full global // install lands in the developer's live config dir. Found by the post-suite // hermeticity guard (scripts/live-config-guard.cjs), not by inspection. restoreConfigLocationEnv = scrubConfigLocationEnv(); }); afterEach(() => { 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 (savedExplicitConfigDir === undefined) delete process.env.MSD_EXPLICIT_CONFIG_DIR; else process.env.MSD_EXPLICIT_CONFIG_DIR = savedExplicitConfigDir; if (savedTestMode === undefined) delete process.env.MSD_TEST_MODE; else process.env.MSD_TEST_MODE = savedTestMode; restoreConfigLocationEnv(); cleanup(tmpRoot); }); // ── Failure 1: the installer provisions a valid marker ────────────────────── test('global claude install writes a .msd-source marker pointing at a real commands/msd', () => { const claudeDir = path.join(tmpRoot, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); runInstall(true /* isGlobal */, 'claude'); const markerPath = path.join(claudeDir, '.msd-source'); assert.ok(fs.existsSync(markerPath), `.msd-source marker must be written at ${markerPath}`); const markerSrc = fs.readFileSync(markerPath, 'utf8').trim(); assert.ok(path.isAbsolute(markerSrc), `marker must contain an absolute path, got: ${markerSrc}`); assert.ok(fs.existsSync(markerSrc), `marker target must exist on disk: ${markerSrc}`); assert.equal(path.basename(markerSrc), 'msd', 'marker must point at a commands/msd directory'); assert.equal(path.basename(path.dirname(markerSrc)), 'commands'); }); // ── Guard: marker is scoped to claude-global ONLY (#1477 PR-scope) ─────────── // Locks the `runtime === 'claude' && isGlobal` write guard. Every other layout // (non-claude runtimes, and claude *local*) ships a commands/msd source tree, so // findInstallSourceRoot's walk-up already resolves and the marker must not appear. function findMsdSourceMarkers(root) { const found = []; const walk = (dir) => { let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; } for (const e of entries) { const full = path.join(dir, e.name); if (e.isDirectory()) walk(full); else if (e.name === '.msd-source') found.push(full); } }; walk(root); return found; } test('non-claude global install writes no .msd-source marker (locks runtime === claude)', () => { runInstall(true /* isGlobal */, 'cursor'); assert.deepEqual( findMsdSourceMarkers(tmpRoot), [], 'a non-claude runtime must not provision the claude-global marker', ); }); test('claude local install writes no .msd-source marker (locks isGlobal)', () => { // Local installs target process.cwd()/.claude — chdir into the fixture so the // install is contained within tmpRoot rather than polluting the repo. const savedCwd = process.cwd(); process.chdir(tmpRoot); try { runInstall(false /* isGlobal */, 'claude'); } finally { process.chdir(savedCwd); } assert.deepEqual( findMsdSourceMarkers(tmpRoot), [], 'a claude local install must not provision the marker — local ships commands/msd', ); }); // ── Writer fault-injection: marker write failure is non-fatal + warns ──────── // CONTRIBUTING.md filesystem-write QA matrix: prove the marker-writer catch // branch (bin/install.js) is reachable. A failed write (e.g. read-only target) // must not abort the install, and — because walk-up also fails on this layout — // must surface a diagnostic so the broken /msd-surface is traceable. test('marker write failure does not abort install and warns (locks the writer catch)', () => { const claudeDir = path.join(tmpRoot, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); const markerPath = path.join(claudeDir, '.msd-source'); const realWriteFileSync = fs.writeFileSync; // Fault-inject ONLY the marker write; every other install write proceeds. mock.method(fs, 'writeFileSync', (file, ...rest) => { if (path.resolve(String(file)) === path.resolve(markerPath)) { throw new Error('EACCES: read-only target (injected)'); } return realWriteFileSync(file, ...rest); }); // Capture console.warn around the install (runInstall's silenceConsole would // otherwise swallow it); still guard process.exit. const warnings = []; const origExit = process.exit; const origWarn = console.warn; const origLog = console.log; process.exit = (code) => { throw new Error(`process.exit(${code}) during install`); }; console.warn = (msg) => { warnings.push(String(msg)); }; console.log = () => {}; try { assert.doesNotThrow(() => install(true /* isGlobal */, 'claude'), 'a marker write failure must be non-fatal — install proceeds via the catch'); } finally { process.exit = origExit; console.warn = origWarn; console.log = origLog; mock.restoreAll(); } assert.ok(!fs.existsSync(markerPath), 'the injected fault must leave no marker on disk'); assert.ok( warnings.some((w) => /\.msd-source marker/.test(w)), `the writer catch must warn for diagnosability; got: ${JSON.stringify(warnings)}`, ); }); // ── Failure 1 end-to-end: resolution succeeds FROM the deployed tree ───────── // Fixed installs own a raw corpus below the deployed msd-core tree. The // marker remains compatible, but offline resolution no longer depends on // the executing package path surviving. test('deployed global layout resolves its installation-owned source corpus', () => { const claudeDir = path.join(tmpRoot, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); runInstall(true /* isGlobal */, 'claude'); // Sanity: the global layout genuinely ships no commands/msd source tree. assert.ok( !fs.existsSync(path.join(claudeDir, 'commands', 'msd')), 'precondition: global claude install must not ship commands/msd', ); const deployedLayoutPath = path.join(claudeDir, 'msd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs'); assert.ok(fs.existsSync(deployedLayoutPath), 'deployed runtime-artifact-layout.cjs must exist'); delete require.cache[deployedLayoutPath]; const deployed = require(deployedLayoutPath); const installedCorpus = path.join(claudeDir, 'msd-core', 'commands', 'msd'); assert.ok(fs.existsSync(installedCorpus), 'fixed install must own commands/msd below msd-core'); assert.equal(fs.realpathSync(deployed.findInstallSourceRoot()), fs.realpathSync(installedCorpus)); // With the marker (configDir provided), list/status resolution succeeds. let resolved; assert.doesNotThrow(() => { resolved = deployed.findInstallSourceRoot(claudeDir); }, 'findInstallSourceRoot must resolve via the .msd-source marker'); assert.equal(fs.realpathSync(resolved), fs.realpathSync(installedCorpus)); }); test('#4132: deployed findInstallSourceRoot rejects an installed commands corpus whose hash changed', () => { const claudeDir = path.join(tmpRoot, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); runInstall(true /* isGlobal */, 'claude'); const manifest = JSON.parse(fs.readFileSync(path.join(claudeDir, 'msd-file-manifest.json'), 'utf8')); const commandKey = Object.keys(manifest.files).find((key) => key.startsWith('msd-core/commands/msd/')); assert.ok(commandKey, 'precondition: install manifest must own at least one command corpus file'); const commandPath = path.join(claudeDir, ...commandKey.split('/')); fs.appendFileSync(commandPath, '\n# corrupted after install\n'); const deployedLayoutPath = path.join(claudeDir, 'msd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs'); delete require.cache[deployedLayoutPath]; const deployed = require(deployedLayoutPath); assert.throws( () => deployed.findInstallSourceRoot(claudeDir), /install or upgrade msd-core/, ); }); test('#4132: deployed finder rejects an installed corpus reached through an ancestor symlink', (t) => { const claudeDir = path.join(tmpRoot, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); runInstall(true /* isGlobal */, 'claude'); const commandsParent = path.join(claudeDir, 'msd-core', 'commands'); const outsideParent = path.join(tmpRoot, 'outside-commands'); fs.renameSync(commandsParent, outsideParent); try { fs.symlinkSync(outsideParent, commandsParent, process.platform === 'win32' ? 'junction' : 'dir'); } catch (error) { fs.renameSync(outsideParent, commandsParent); if (['EPERM', 'EACCES', 'ENOSYS'].includes(error.code)) { t.skip(`directory symlink creation unavailable: ${error.code || error.message}`); return; } throw error; } const deployedLayoutPath = path.join(claudeDir, 'msd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs'); delete require.cache[deployedLayoutPath]; const deployed = require(deployedLayoutPath); const priorOptIn = process.env.MSD_ALLOW_SYMLINKED_DEST; process.env.MSD_ALLOW_SYMLINKED_DEST = '1'; t.after(() => { if (priorOptIn === undefined) delete process.env.MSD_ALLOW_SYMLINKED_DEST; else process.env.MSD_ALLOW_SYMLINKED_DEST = priorOptIn; }); assert.throws( () => deployed.findInstallSourceRoot(claudeDir), /install or upgrade msd-core/, ); }); test('#4132: deployed agents layout rejects an installed agents corpus whose hash changed', () => { const claudeDir = path.join(tmpRoot, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); runInstall(true /* isGlobal */, 'claude'); const manifest = JSON.parse(fs.readFileSync(path.join(claudeDir, 'msd-file-manifest.json'), 'utf8')); const agentKey = Object.keys(manifest.files).find((key) => key.startsWith('msd-core/agents/')); assert.ok(agentKey, 'precondition: install manifest must own at least one agent corpus file'); fs.appendFileSync(path.join(claudeDir, ...agentKey.split('/')), '\n# corrupted after install\n'); const deployedLayoutPath = path.join(claudeDir, 'msd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs'); delete require.cache[deployedLayoutPath]; const deployed = require(deployedLayoutPath); assert.throws( () => deployed.resolveRuntimeArtifactLayout('claude', claudeDir, 'global') .kinds.find((kind) => kind.kind === 'agents').stage(PROFILE_CORE), /install or upgrade msd-core/, ); }); // ── Adversarial marker-reader cases (no full install needed) ───────────────── describe('findInstallSourceRoot marker handling', () => { let cfgDir; beforeEach(() => { cfgDir = createTempDir('msd-1477-marker-'); }); afterEach(() => { cleanup(cfgDir); }); test('marker pointing at a valid commands/msd takes precedence over walk-up', () => { const fakeSrc = path.join(cfgDir, 'pkg', 'commands', 'msd'); fs.mkdirSync(fakeSrc, { recursive: true }); fs.writeFileSync(path.join(fakeSrc, 'msd-test.md'), '# marker source\n'); fs.writeFileSync(path.join(cfgDir, '.msd-source'), fakeSrc + '\n', 'utf8'); const resolved = findInstallSourceRoot(cfgDir); assert.equal(path.resolve(resolved), path.resolve(fakeSrc), 'marker target must win over the repo walk-up'); }); test('marker pointing at a non-existent path is ignored (falls through to walk-up)', () => { const ghost = path.join(cfgDir, 'does', 'not', 'exist', 'commands', 'msd'); fs.writeFileSync(path.join(cfgDir, '.msd-source'), ghost + '\n', 'utf8'); const resolved = findInstallSourceRoot(cfgDir); assert.notEqual(path.resolve(resolved), path.resolve(ghost)); assert.equal(path.resolve(resolved), path.resolve(REPO_ROOT, 'commands', 'msd')); }); test('#4132: a missing installed leaf still rejects a marker containing it through a symlinked ancestor', (t) => { const outsideCore = path.join(cfgDir, 'outside-core'); const markerCommands = path.join(outsideCore, 'commands'); const markerAgents = path.join(cfgDir, 'agents'); fs.mkdirSync(markerCommands, { recursive: true }); fs.mkdirSync(markerAgents, { recursive: true }); fs.writeFileSync(path.join(markerCommands, 'help.md'), '# marker command\n'); fs.writeFileSync(path.join(markerAgents, 'msd-planner.md'), '# marker agent\n'); try { fs.symlinkSync( outsideCore, path.join(cfgDir, 'msd-core'), process.platform === 'win32' ? 'junction' : 'dir', ); } catch (error) { t.skip(`directory symlink creation unavailable: ${error.code || error.message}`); return; } fs.writeFileSync(path.join(cfgDir, '.msd-source'), markerCommands + '\n'); assert.throws( () => resolveRuntimeArtifactLayout('claude', cfgDir, 'global') .kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE), /install or upgrade msd-core/, ); }); test('empty / whitespace-only marker is ignored', () => { fs.writeFileSync(path.join(cfgDir, '.msd-source'), ' \n', 'utf8'); const resolved = findInstallSourceRoot(cfgDir); assert.equal(path.resolve(resolved), path.resolve(REPO_ROOT, 'commands', 'msd')); }); test('one complete installed provider wins over a different complete marker provider', (t) => { const installedCommands = path.join(cfgDir, 'msd-core', 'commands', 'msd'); const installedAgents = path.join(cfgDir, 'msd-core', 'agents'); fs.mkdirSync(installedCommands, { recursive: true }); fs.mkdirSync(installedAgents, { recursive: true }); const installedCommandPath = path.join(installedCommands, 'help.md'); const installedAgentPath = path.join(installedAgents, 'msd-planner.md'); fs.writeFileSync(installedCommandPath, '# installed command\n'); fs.writeFileSync(installedAgentPath, '# installed agent\n'); fs.writeFileSync(path.join(cfgDir, 'msd-file-manifest.json'), JSON.stringify({ files: { 'msd-core/commands/msd/help.md': crypto.createHash('sha256').update(fs.readFileSync(installedCommandPath)).digest('hex'), 'msd-core/agents/msd-planner.md': crypto.createHash('sha256').update(fs.readFileSync(installedAgentPath)).digest('hex'), } })); const markerCommands = path.join(cfgDir, 'legacy-package', 'commands', 'msd'); const markerAgents = path.join(cfgDir, 'legacy-package', 'agents'); fs.mkdirSync(markerCommands, { recursive: true }); fs.mkdirSync(markerAgents, { recursive: true }); fs.writeFileSync(path.join(markerCommands, 'help.md'), '# marker command\n'); fs.writeFileSync(path.join(markerAgents, 'msd-planner.md'), '# marker agent\n'); fs.writeFileSync(path.join(cfgDir, '.msd-source'), markerCommands + '\n'); const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global'); const stagedSkills = layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE); const stagedAgents = layout.kinds.find((kind) => kind.kind === 'agents').stage(PROFILE_CORE); t.after(() => cleanup(stagedSkills)); t.after(() => cleanup(stagedAgents)); assert.match(fs.readFileSync(path.join(stagedSkills, 'msd-help', 'SKILL.md'), 'utf8'), /installed command/); assert.match(fs.readFileSync(path.join(stagedAgents, 'msd-planner.md'), 'utf8'), /installed agent/); }); test('a partial installed corpus is not mixed with a complete marker provider', (t) => { const installedCommands = path.join(cfgDir, 'msd-core', 'commands', 'msd'); fs.mkdirSync(installedCommands, { recursive: true }); fs.writeFileSync(path.join(installedCommands, 'help.md'), '# installed command\n'); const markerCommands = path.join(cfgDir, 'legacy-package', 'commands', 'msd'); const markerAgents = path.join(cfgDir, 'legacy-package', 'agents'); fs.mkdirSync(markerCommands, { recursive: true }); fs.mkdirSync(markerAgents, { recursive: true }); fs.writeFileSync(path.join(markerCommands, 'help.md'), '# marker command\n'); fs.writeFileSync(path.join(markerAgents, 'msd-planner.md'), '# marker agent\n'); fs.writeFileSync(path.join(cfgDir, '.msd-source'), markerCommands + '\n'); const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global'); const stagedSkills = layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE); const stagedAgents = layout.kinds.find((kind) => kind.kind === 'agents').stage(PROFILE_CORE); t.after(() => cleanup(stagedSkills)); t.after(() => cleanup(stagedAgents)); assert.match(fs.readFileSync(path.join(stagedSkills, 'msd-help', 'SKILL.md'), 'utf8'), /marker command/); assert.match(fs.readFileSync(path.join(stagedAgents, 'msd-planner.md'), 'utf8'), /marker agent/); }); test('partial providers are not mixed when package fallback is disabled', () => { const installedCommands = path.join(cfgDir, 'msd-core', 'commands', 'msd'); fs.mkdirSync(installedCommands, { recursive: true }); fs.writeFileSync(path.join(installedCommands, 'msd-test.md'), '# installed command\n'); const markerCommands = path.join(cfgDir, 'legacy-package', 'commands', 'msd'); fs.mkdirSync(markerCommands, { recursive: true }); fs.writeFileSync(path.join(markerCommands, 'msd-test.md'), '# marker command\n'); fs.writeFileSync(path.join(cfgDir, '.msd-source'), markerCommands + '\n'); const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global'); assert.throws(() => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE), /install or upgrade msd-core/); }); test('ordinary Runtime Surface staging never falls back to the source checkout package', () => { const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global'); const skills = layout.kinds.find((kind) => kind.kind === 'skills'); assert.ok(skills); assert.throws( () => skills.stage(PROFILE_CORE), /install or upgrade msd-core/, ); }); test('#4132: caller-supplied stage context cannot select installer source authority', () => { const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global'); const skills = layout.kinds.find((kind) => kind.kind === 'skills'); assert.throws( () => skills.stage(PROFILE_CORE, { [Symbol.for('@open-gsd/runtime-artifact-installer-source-authority')]: true, }), /install or upgrade msd-core/, ); assert.throws( () => withInstallFs(undefined, () => skills.stage(PROFILE_CORE)), /install or upgrade msd-core/, ); }); test('an installed corpus without a manifest is not a working fallback', () => { const installedCommands = path.join(cfgDir, 'msd-core', 'commands', 'msd'); const installedAgents = path.join(cfgDir, 'msd-core', 'agents'); fs.mkdirSync(installedCommands, { recursive: true }); fs.mkdirSync(installedAgents, { recursive: true }); fs.writeFileSync(path.join(installedCommands, 'help.md'), '# unverified command\n'); fs.writeFileSync(path.join(installedAgents, 'msd-planner.md'), '# unverified agent\n'); const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global'); assert.throws( () => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE), /install or upgrade msd-core/, ); }); test('an installed corpus whose bytes do not match its manifest is not a working fallback', () => { const installedCommands = path.join(cfgDir, 'msd-core', 'commands', 'msd'); const installedAgents = path.join(cfgDir, 'msd-core', 'agents'); fs.mkdirSync(installedCommands, { recursive: true }); fs.mkdirSync(installedAgents, { recursive: true }); fs.writeFileSync(path.join(installedCommands, 'help.md'), '# corrupted command\n'); fs.writeFileSync(path.join(installedAgents, 'msd-planner.md'), '# corrupted agent\n'); fs.writeFileSync(path.join(cfgDir, 'msd-file-manifest.json'), JSON.stringify({ files: { 'msd-core/commands/msd/help.md': '0'.repeat(64), 'msd-core/agents/msd-planner.md': '0'.repeat(64), } })); const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global'); assert.throws( () => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE), /install or upgrade msd-core/, ); }); test('a hash-mismatched installed corpus falls back to an independent complete marker provider', (t) => { const installedCommands = path.join(cfgDir, 'msd-core', 'commands', 'msd'); const installedAgents = path.join(cfgDir, 'msd-core', 'agents'); fs.mkdirSync(installedCommands, { recursive: true }); fs.mkdirSync(installedAgents, { recursive: true }); fs.writeFileSync(path.join(installedCommands, 'help.md'), '# corrupted command\n'); fs.writeFileSync(path.join(installedAgents, 'msd-planner.md'), '# corrupted agent\n'); fs.writeFileSync(path.join(cfgDir, 'msd-file-manifest.json'), JSON.stringify({ files: { 'msd-core/commands/msd/help.md': '0'.repeat(64), 'msd-core/agents/msd-planner.md': '0'.repeat(64), } })); const markerCommands = path.join(cfgDir, 'legacy-package', 'commands', 'msd'); const markerAgents = path.join(cfgDir, 'legacy-package', 'agents'); fs.mkdirSync(markerCommands, { recursive: true }); fs.mkdirSync(markerAgents, { recursive: true }); fs.writeFileSync(path.join(markerCommands, 'help.md'), '# marker command\n'); fs.writeFileSync(path.join(markerAgents, 'msd-planner.md'), '# marker agent\n'); fs.writeFileSync(path.join(cfgDir, '.msd-source'), markerCommands + '\n'); const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global'); const stagedSkills = layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE); const stagedAgents = layout.kinds.find((kind) => kind.kind === 'agents').stage(PROFILE_CORE); t.after(() => cleanup(stagedSkills)); t.after(() => cleanup(stagedAgents)); assert.match(fs.readFileSync(path.join(stagedSkills, 'msd-help', 'SKILL.md'), 'utf8'), /marker command/); assert.match(fs.readFileSync(path.join(stagedAgents, 'msd-planner.md'), 'utf8'), /marker agent/); }); test('#4132: a marker aliasing any rejected installed root is not an independent provider', (t) => { const installedCommandsParent = path.join(cfgDir, 'msd-core', 'commands'); const installedCommands = path.join(installedCommandsParent, 'msd'); const installedAgents = path.join(cfgDir, 'msd-core', 'agents'); fs.mkdirSync(installedCommands, { recursive: true }); fs.mkdirSync(installedAgents, { recursive: true }); fs.writeFileSync(path.join(installedCommands, 'help.md'), '# corrupted installed command\n'); const installedAgentPath = path.join(installedAgents, 'msd-planner.md'); fs.writeFileSync(installedAgentPath, '# installed agent\n'); fs.writeFileSync(path.join(cfgDir, 'msd-file-manifest.json'), JSON.stringify({ files: { 'msd-core/commands/msd/help.md': '0'.repeat(64), 'msd-core/agents/msd-planner.md': crypto.createHash('sha256').update(fs.readFileSync(installedAgentPath)).digest('hex'), } })); const markerRoot = path.join(cfgDir, 'hybrid-marker'); fs.mkdirSync(markerRoot, { recursive: true }); try { fs.symlinkSync( installedCommandsParent, path.join(markerRoot, 'commands'), process.platform === 'win32' ? 'junction' : 'dir', ); } catch (error) { t.skip(`directory symlink creation unavailable: ${error.code || error.message}`); return; } const markerCommands = path.join(markerRoot, 'commands', 'msd'); const markerAgents = path.join(markerRoot, 'agents'); fs.mkdirSync(markerAgents, { recursive: true }); fs.writeFileSync(path.join(markerAgents, 'msd-planner.md'), '# independent marker agent\n'); fs.writeFileSync(path.join(cfgDir, '.msd-source'), markerCommands + '\n'); assert.equal(fs.realpathSync(markerCommands), fs.realpathSync(installedCommands)); assert.notEqual(fs.realpathSync(markerAgents), fs.realpathSync(installedAgents)); const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global'); const skills = layout.kinds.find((kind) => kind.kind === 'skills'); let stagedSkills; t.after(() => { if (stagedSkills) cleanup(stagedSkills); }); assert.throws( () => { stagedSkills = skills.stage(PROFILE_CORE); }, /install or upgrade msd-core/, ); }); test('#4132: a marker containing a rejected installed root is not an independent provider', () => { const installedCommandsParent = path.join(cfgDir, 'msd-core', 'commands'); const installedCommands = path.join(installedCommandsParent, 'msd'); const installedAgents = path.join(cfgDir, 'msd-core', 'agents'); const markerAgents = path.join(cfgDir, 'agents'); fs.mkdirSync(installedCommands, { recursive: true }); fs.mkdirSync(installedAgents, { recursive: true }); fs.mkdirSync(markerAgents, { recursive: true }); fs.writeFileSync(path.join(installedCommands, 'help.md'), '# corrupted installed command\n'); const installedAgentPath = path.join(installedAgents, 'msd-planner.md'); fs.writeFileSync(installedAgentPath, '# installed agent\n'); fs.writeFileSync(path.join(installedCommandsParent, 'rogue.md'), 'ROGUE\n'); fs.writeFileSync(path.join(markerAgents, 'msd-planner.md'), '# marker agent\n'); fs.writeFileSync(path.join(cfgDir, 'msd-file-manifest.json'), JSON.stringify({ files: { 'msd-core/commands/msd/help.md': '0'.repeat(64), 'msd-core/agents/msd-planner.md': crypto.createHash('sha256').update(fs.readFileSync(installedAgentPath)).digest('hex'), } })); fs.writeFileSync(path.join(cfgDir, '.msd-source'), installedCommandsParent + '\n'); const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global'); assert.throws( () => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE), /install or upgrade msd-core/, ); }); test('#4132: an agents descendant of a rejected installed root rejects the whole provider', () => { const installedCommands = path.join(cfgDir, 'msd-core', 'commands', 'msd'); const installedAgents = path.join(cfgDir, 'msd-core', 'agents'); fs.mkdirSync(installedCommands, { recursive: true }); fs.mkdirSync(installedAgents, { recursive: true }); const installedCommandPath = path.join(installedCommands, 'help.md'); fs.writeFileSync(installedCommandPath, '# installed command\n'); fs.writeFileSync(path.join(installedAgents, 'msd-planner.md'), '# corrupted installed agent\n'); fs.writeFileSync(path.join(cfgDir, 'msd-file-manifest.json'), JSON.stringify({ files: { 'msd-core/commands/msd/help.md': '0'.repeat(64), 'msd-core/agents/msd-planner.md': '0'.repeat(64), } })); const nestedProviderRoot = path.join(installedAgents, 'nested'); const markerCommands = path.join(nestedProviderRoot, 'commands', 'msd'); const markerAgents = path.join(nestedProviderRoot, 'agents'); fs.mkdirSync(markerCommands, { recursive: true }); fs.mkdirSync(markerAgents, { recursive: true }); fs.writeFileSync(path.join(markerCommands, 'help.md'), '# marker command\n'); fs.writeFileSync(path.join(markerAgents, 'msd-planner.md'), '# marker agent\n'); fs.writeFileSync(path.join(cfgDir, '.msd-source'), markerCommands + '\n'); assert.equal( fs.realpathSync(findInstallSourceRoot(cfgDir)), fs.realpathSync(markerCommands), 'commands-only resolution must ignore overlap in the non-required agents class', ); const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global'); assert.throws( () => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE), /install or upgrade msd-core/, ); }); test('manifest-expected missing corpus file fails closed without reusing its marker alias', () => { const installedCommands = path.join(cfgDir, 'msd-core', 'commands', 'msd'); const installedAgents = path.join(cfgDir, 'msd-core', 'agents'); fs.mkdirSync(installedCommands, { recursive: true }); fs.mkdirSync(installedAgents, { recursive: true }); fs.writeFileSync(path.join(installedCommands, 'msd-test.md'), '# installed command\n'); fs.writeFileSync(path.join(installedAgents, 'msd-test-agent.md'), '# installed agent\n'); fs.writeFileSync(path.join(cfgDir, 'msd-file-manifest.json'), JSON.stringify({ files: { 'msd-core/commands/msd/msd-test.md': '0'.repeat(64), 'msd-core/agents/msd-test-agent.md': '0'.repeat(64), 'msd-core/agents/removed.md': '0'.repeat(64), } })); fs.writeFileSync(path.join(cfgDir, '.msd-source'), installedCommands + '\n'); const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global'); assert.throws(() => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE), /install or upgrade msd-core/); }); test('a symlink inside the installed corpus is not a confined source', (t) => { const installedCommands = path.join(cfgDir, 'msd-core', 'commands', 'msd'); const installedAgents = path.join(cfgDir, 'msd-core', 'agents'); fs.mkdirSync(installedCommands, { recursive: true }); fs.mkdirSync(installedAgents, { recursive: true }); fs.writeFileSync(path.join(installedCommands, 'msd-test.md'), '# installed command\n'); const outside = path.join(cfgDir, 'outside.md'); fs.writeFileSync(outside, '# outside\n'); try { fs.symlinkSync(outside, path.join(installedAgents, 'msd-test-agent.md')); } catch (error) { t.skip(`symlink creation unavailable: ${error.code || error.message}`); return; } const layout = resolveRuntimeArtifactLayout('claude', cfgDir, 'global'); assert.throws(() => layout.kinds.find((kind) => kind.kind === 'skills').stage(PROFILE_CORE), /install or upgrade msd-core/); assert.strictEqual(fs.readFileSync(outside, 'utf8'), '# outside\n'); }); }); }); // ─── #2624: stale .msd-source marker read before rewrite on upgrade ────────── // // Regression for #2624: a Claude-global upgrade silently installed skill content // from the PREVIOUS version. findInstallSourceRoot prefers /.msd-source, // and install() used to REWRITE that marker AFTER staging had already read it — so // on an upgrade the marker still pointed at the prior version's source (an npx // cache dir that still exists on disk) and every converted skill was generated from // the OLD commands/msd. Fix: write the marker BEFORE staging reads it. // // These exercise the real install(true, 'claude') with HOME redirected to a tmp dir, // pre-seeding a STALE marker that points at a different (still-existing) source — // the exact upgrade condition. No live npx / network. describe('#2624 .msd-source marker is rewritten before staging reads it', () => { let tmpRoot; let savedHome; let savedUserProfile; let savedExplicitConfigDir; let savedTestMode; let restoreConfigLocationEnv; // Run install() with process.exit and console output mocked via t.mock (auto-restored), // per CONTRIBUTING.md test rules (no manual monkeypatch / try-finally in test bodies). // process.exit during install is a hard failure — surface it, don't let it kill the runner. function runInstall(t, isGlobal, runtime) { t.mock.method(process, 'exit', (code) => { throw new Error(`process.exit(${code}) during install — should not happen`); }); t.mock.method(console, 'log', () => {}); t.mock.method(console, 'warn', () => {}); t.mock.method(console, 'error', () => {}); return install(isGlobal, runtime); } beforeEach(() => { tmpRoot = createTempDir('msd-2624-'); savedHome = process.env.HOME; savedUserProfile = process.env.USERPROFILE; process.env.HOME = tmpRoot; process.env.USERPROFILE = tmpRoot; savedExplicitConfigDir = process.env.MSD_EXPLICIT_CONFIG_DIR; delete process.env.MSD_EXPLICIT_CONFIG_DIR; savedTestMode = process.env.MSD_TEST_MODE; process.env.MSD_TEST_MODE = '1'; // #2665: same in-process hazard as the block above. This one calls // install(true, 'claude') via runInstall(), and HOME/USERPROFILE alone do // not contain it — getGlobalConfigDir is env-FIRST, so an ambient // CLAUDE_CONFIG_DIR beats the fixture, the install lands in the developer's // live config dir, and these tests then fail looking for a marker under // tmpRoot that was never written there. restoreConfigLocationEnv = scrubConfigLocationEnv(); }); afterEach(() => { 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 (savedExplicitConfigDir === undefined) delete process.env.MSD_EXPLICIT_CONFIG_DIR; else process.env.MSD_EXPLICIT_CONFIG_DIR = savedExplicitConfigDir; if (savedTestMode === undefined) delete process.env.MSD_TEST_MODE; else process.env.MSD_TEST_MODE = savedTestMode; restoreConfigLocationEnv(); cleanup(tmpRoot); }); const installedSource = (claudeDir) => path.join(claudeDir, 'msd-core', 'commands', 'msd'); test('claude-global install overwrites a stale .msd-source marker before staging reads it', (t) => { const claudeDir = path.join(tmpRoot, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); // Simulate the PREVIOUS install's marker: a different source dir that STILL EXISTS // on disk (mirroring a coexisting npx per-version cache dir). findInstallSourceRoot // returns this immediately if it is read before the marker is rewritten. const staleSource = path.join(tmpRoot, 'old-npx-cache', 'commands', 'msd'); fs.mkdirSync(staleSource, { recursive: true }); fs.writeFileSync(path.join(claudeDir, '.msd-source'), staleSource + '\n', 'utf8'); // Spy on findInstallSourceRoot to capture WHICH source staging actually resolves. // The marker ends up correct either way (the late write corrected it on the old code), // so the marker content alone cannot prove the ordering — the resolved-source captures // can. Before the fix, staging resolves the stale path; after, the current path. // install-engine.cjs calls runtimeArtifactLayout.findInstallSourceRoot via the module // object (not a captured ref), so mocking the property on the shared module instance // intercepts the staging call. Same pattern as tests/phase.test.cjs (capture original, // delegate). const runtimeArtifactLayout = require('../msd-core/bin/lib/runtime-artifact-layout.cjs'); const realFindInstallSourceRoot = runtimeArtifactLayout.findInstallSourceRoot; const resolvedSources = []; t.mock.method(runtimeArtifactLayout, 'findInstallSourceRoot', function (configDir) { const result = realFindInstallSourceRoot.call(this, configDir); resolvedSources.push(path.resolve(result)); return result; }); runInstall(t, true /* isGlobal */, 'claude'); const markerPath = path.join(claudeDir, '.msd-source'); assert.ok(fs.existsSync(markerPath), 'marker must exist after install'); const finalMarker = path.resolve(fs.readFileSync(markerPath, 'utf8').trim()); assert.equal(finalMarker, path.resolve(installedSource(claudeDir)), `final marker must point at the installed current-version corpus, not ${finalMarker}`); // The decisive assertion: staging must NEVER have resolved the stale source. Before the // fix, at least one staging resolution returned the stale path (read before rewrite). const resolvedStale = resolvedSources.filter((p) => p === path.resolve(staleSource)); assert.equal(resolvedStale.length, 0, `staging must not resolve the stale source during install, but did ${resolvedStale.length} time(s). ` + `Resolved: ${JSON.stringify(resolvedSources)}`); }); test('fresh claude-global install (no prior marker) still writes the correct marker', (t) => { const claudeDir = path.join(tmpRoot, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); // No pre-existing marker — fresh install (negative space, must stay correct). runInstall(t, true /* isGlobal */, 'claude'); const markerPath = path.join(claudeDir, '.msd-source'); assert.ok(fs.existsSync(markerPath), 'fresh claude-global install must write the marker'); const resolved = fs.readFileSync(markerPath, 'utf8').trim(); assert.equal( path.resolve(resolved), path.resolve(installedSource(claudeDir)), 'fresh install marker must point at the installed corpus', ); }); test('claude-global install rewrites a marker pointing at a deleted (ghost) source', (t) => { const claudeDir = path.join(tmpRoot, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); // A ghost path that does NOT exist on disk — findInstallSourceRoot ignores it and // falls through to walk-up, then install() rewrites the marker to the current source. const ghost = path.join(tmpRoot, 'gone', 'commands', 'msd'); fs.writeFileSync(path.join(claudeDir, '.msd-source'), ghost + '\n', 'utf8'); runInstall(t, true /* isGlobal */, 'claude'); const markerPath = path.join(claudeDir, '.msd-source'); assert.ok(fs.existsSync(markerPath), 'marker must exist after install'); const resolved = fs.readFileSync(markerPath, 'utf8').trim(); assert.equal( path.resolve(resolved), path.resolve(installedSource(claudeDir)), 'ghost marker must be rewritten to the installed corpus', ); }); });