Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
1820 lines
82 KiB
JavaScript
1820 lines
82 KiB
JavaScript
'use strict';
|
|
/**
|
|
* Runtime Artifact Layout Module (ADR-3660) — surface seam.
|
|
* Consolidated from: surface-apply, surface-resolve, surface-state,
|
|
* surface-clusters, surface-list (5 files deleted).
|
|
* See also: runtime-artifact-layout.test.cjs, runtime-artifact-layout-install-profiles.test.cjs
|
|
*/
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
|
|
const { writeSurface, readSurface, resolveSurface, listSurface, applySurface } = require('../msd-core/bin/lib/surface.cjs');
|
|
const { loadSkillsManifest, writeActiveProfile, resolveProfile } = require('../msd-core/bin/lib/install-profiles.cjs');
|
|
const { resolveRuntimeArtifactLayout } = require('../msd-core/bin/lib/runtime-artifact-layout.cjs');
|
|
const { CLUSTERS, allClusteredSkills } = require('../msd-core/bin/lib/clusters.cjs');
|
|
const { createTempDir, cleanup, sandboxHome, writePackageSourceMarkerFixture } = require('./helpers.cjs');
|
|
const { runMinimalInstall } = require('./helpers/install-shared.cjs');
|
|
|
|
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'msd');
|
|
|
|
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
|
|
function tmpDir(prefix) {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix || 'msd-ral-surf-'));
|
|
}
|
|
|
|
function createFixtureRuntime() {
|
|
const base = createTempDir('msd-surface-apply-');
|
|
const runtimeConfigDir = base;
|
|
// #1367: claude local uses flat commands/ (not commands/msd/) — commandsDir is commands/.
|
|
const commandsDir = path.join(runtimeConfigDir, 'commands');
|
|
const agentsDir = path.join(runtimeConfigDir, 'agents');
|
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
fs.writeFileSync(path.join(runtimeConfigDir, '.msd-source'), REAL_COMMANDS_DIR + '\n');
|
|
return { base, runtimeConfigDir, commandsDir, agentsDir };
|
|
}
|
|
|
|
function realManifest() {
|
|
return loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
}
|
|
|
|
function readFrontmatterDescription(markdown) {
|
|
const lines = markdown.split('\n');
|
|
if (lines[0].trim() !== '---') return '';
|
|
for (let i = 1; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (line.trim() === '---') break;
|
|
const sep = line.indexOf(':');
|
|
if (sep === -1) continue;
|
|
const key = line.slice(0, sep).trim();
|
|
if (key !== 'description') continue;
|
|
return line.slice(sep + 1).trim();
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function surfaceBytesForTest(state) {
|
|
return JSON.stringify(state, null, 2) + '\n';
|
|
}
|
|
|
|
// ─── applySurface ────────────────────────────────────────────────────────────
|
|
|
|
describe('applySurface', () => {
|
|
test('#4132: an unmanifested installed command cannot become a live instruction', (t) => {
|
|
const installed = runMinimalInstall({ runtime: 'claude', scope: 'global' });
|
|
const configDir = installed.configDir;
|
|
t.after(() => cleanup(installed.root));
|
|
const rogueSource = path.join(configDir, 'msd-core', 'commands', 'msd', 'rogue.md');
|
|
const rogueSkill = path.join(configDir, 'skills', 'msd-rogue', 'SKILL.md');
|
|
fs.writeFileSync(rogueSource, '<instructions>ROGUE</instructions>\n');
|
|
|
|
const layout = resolveRuntimeArtifactLayout('claude', configDir, 'global');
|
|
assert.throws(
|
|
() => applySurface(configDir, layout, realManifest(), CLUSTERS, undefined, {
|
|
surfaceState: { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] },
|
|
}),
|
|
/install or upgrade msd-core/,
|
|
);
|
|
assert.equal(fs.existsSync(rogueSkill), false, 'unmanifested Markdown must not reach the live skill surface');
|
|
});
|
|
|
|
test('#4132: a marker below rejected installed commands cannot promote instructions', (t) => {
|
|
const installed = runMinimalInstall({ runtime: 'claude', scope: 'global' });
|
|
const configDir = installed.configDir;
|
|
t.after(() => cleanup(installed.root));
|
|
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(configDir, '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 a command corpus file');
|
|
fs.appendFileSync(path.join(configDir, ...commandKey.split('/')), '\n# corrupted after install\n');
|
|
|
|
const installedCommands = path.join(configDir, 'msd-core', 'commands', 'msd');
|
|
const nestedMarkerCommands = path.join(installedCommands, 'nested');
|
|
const markerAgents = path.resolve(path.dirname(nestedMarkerCommands), '..', 'agents');
|
|
fs.mkdirSync(nestedMarkerCommands, { recursive: true });
|
|
fs.mkdirSync(markerAgents, { recursive: true });
|
|
fs.writeFileSync(path.join(nestedMarkerCommands, 'rogue.md'), '<instructions>ROGUE</instructions>\n');
|
|
fs.copyFileSync(path.join(__dirname, '..', 'agents', 'msd-planner.md'), path.join(markerAgents, 'msd-planner.md'));
|
|
fs.appendFileSync(path.join(markerAgents, 'msd-planner.md'), '\nMARKER_AGENT_USED\n');
|
|
fs.writeFileSync(path.join(configDir, '.msd-source'), nestedMarkerCommands + '\n');
|
|
|
|
const rogueSkill = path.join(configDir, 'skills', 'msd-rogue', 'SKILL.md');
|
|
const liveAgent = path.join(configDir, 'agents', 'msd-planner.md');
|
|
const oldAgent = fs.readFileSync(liveAgent);
|
|
const oldState = { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] };
|
|
writeSurface(configDir, oldState);
|
|
const oldSurface = fs.readFileSync(path.join(configDir, '.msd-surface.json'));
|
|
|
|
const layout = resolveRuntimeArtifactLayout('claude', configDir, 'global');
|
|
assert.throws(
|
|
() => applySurface(configDir, layout, realManifest(), CLUSTERS, undefined, { surfaceState: oldState }),
|
|
/install or upgrade msd-core/,
|
|
);
|
|
assert.equal(fs.existsSync(rogueSkill), false, 'nested marker Markdown must not reach the live skill surface');
|
|
assert.deepEqual(fs.readFileSync(liveAgent), oldAgent, 'marker agents must not replace live agents');
|
|
assert.deepEqual(fs.readFileSync(path.join(configDir, '.msd-surface.json')), oldSurface);
|
|
});
|
|
|
|
test('core profile: only core skills appear in commandsDir', (t) => {
|
|
// #1367: claude local uses flat msd-<stem>.md files at commands/ (not commands/msd/<stem>.md).
|
|
const { base, runtimeConfigDir, commandsDir } = createFixtureRuntime();
|
|
t.after(() => cleanup(base));
|
|
writeActiveProfile(runtimeConfigDir, 'core');
|
|
writeSurface(runtimeConfigDir, {
|
|
baseProfile: 'core',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
|
const resolved = applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
|
|
|
// After #1367: files are msd-<stem>.md (not bare stem.md). Strip the msd- prefix
|
|
// to check against the REAL_COMMANDS_DIR (which still uses bare names).
|
|
const files = fs.readdirSync(commandsDir).filter(f => f.startsWith('msd-') && f.endsWith('.md'));
|
|
for (const file of files) {
|
|
const bareName = file.slice('msd-'.length); // msd-help.md → help.md
|
|
assert.ok(fs.existsSync(path.join(REAL_COMMANDS_DIR, bareName)), `unexpected file: ${file} (no source: ${bareName})`);
|
|
}
|
|
const expectedCore = [...resolved.skills].map(stem => `msd-${stem}.md`).sort();
|
|
assert.deepStrictEqual(
|
|
[...files].sort(),
|
|
expectedCore,
|
|
'commandsDir should contain exactly core commands'
|
|
);
|
|
});
|
|
|
|
test('removes superseded files when profile shrinks', (t) => {
|
|
const { base, runtimeConfigDir, commandsDir } = createFixtureRuntime();
|
|
t.after(() => cleanup(base));
|
|
writeActiveProfile(runtimeConfigDir, 'standard');
|
|
writeSurface(runtimeConfigDir, {
|
|
baseProfile: 'standard',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
|
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
|
|
|
// #1367: files are msd-<stem>.md in flat commands/
|
|
const afterStandard = new Set(fs.readdirSync(commandsDir).filter(f => f.startsWith('msd-') && f.endsWith('.md')));
|
|
|
|
writeSurface(runtimeConfigDir, {
|
|
baseProfile: 'core',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
const resolvedCore = applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
|
|
|
const afterCore = new Set(fs.readdirSync(commandsDir).filter(f => f.startsWith('msd-') && f.endsWith('.md')));
|
|
|
|
assert.ok(afterCore.size <= afterStandard.size, 'core should have fewer or equal files than standard');
|
|
|
|
const expectedCore = [...resolvedCore.skills].map(stem => `msd-${stem}.md`).sort();
|
|
assert.deepStrictEqual(
|
|
[...afterCore].sort(),
|
|
expectedCore,
|
|
'afterCore should contain exactly core commands'
|
|
);
|
|
|
|
for (const file of afterCore) {
|
|
const bareName = file.slice('msd-'.length);
|
|
assert.ok(
|
|
fs.existsSync(path.join(REAL_COMMANDS_DIR, bareName)),
|
|
`file in commandsDir not a real skill: ${file}`
|
|
);
|
|
}
|
|
});
|
|
|
|
test('leaves non-msd .md files alone in agentsDir', (t) => {
|
|
const { base, runtimeConfigDir, agentsDir } = createFixtureRuntime();
|
|
t.after(() => cleanup(base));
|
|
const foreignAgent = path.join(agentsDir, 'my-custom-agent.md');
|
|
fs.writeFileSync(foreignAgent, '# custom agent\n', 'utf8');
|
|
|
|
writeActiveProfile(runtimeConfigDir, 'core');
|
|
writeSurface(runtimeConfigDir, {
|
|
baseProfile: 'core',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
|
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
|
|
|
assert.ok(fs.existsSync(foreignAgent), 'non-msd agent file should not be touched');
|
|
});
|
|
|
|
test('adds missing skill files from install source', (t) => {
|
|
const { base, runtimeConfigDir, commandsDir } = createFixtureRuntime();
|
|
t.after(() => cleanup(base));
|
|
writeActiveProfile(runtimeConfigDir, 'core');
|
|
writeSurface(runtimeConfigDir, {
|
|
baseProfile: 'core',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
|
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
|
|
|
// #1367: flat msd-<stem>.md files at commands/ (not commands/msd/<stem>.md)
|
|
assert.ok(
|
|
fs.existsSync(path.join(commandsDir, 'msd-help.md')),
|
|
'msd-help.md should be copied from install source (#1367: flat hyphen layout)'
|
|
);
|
|
assert.ok(
|
|
fs.existsSync(path.join(commandsDir, 'msd-new-project.md')),
|
|
'msd-new-project.md should be copied from install source (#1367: flat hyphen layout)'
|
|
);
|
|
});
|
|
|
|
test('_syncMsdDir skills kind: adds missing skill dirs, removes stale prefix-matched dirs, preserves foreign dirs', (t) => {
|
|
const { _syncMsdDir } = require('../msd-core/bin/lib/surface.cjs');
|
|
|
|
const base = createTempDir('msd-surface-skills-');
|
|
t.after(() => cleanup(base));
|
|
const stagedDir = path.join(base, 'staged');
|
|
const destDir = path.join(base, 'dest');
|
|
fs.mkdirSync(destDir, { recursive: true });
|
|
|
|
const stem1 = 'msd-help';
|
|
const stem2 = 'msd-update';
|
|
fs.mkdirSync(path.join(stagedDir, stem1), { recursive: true });
|
|
fs.writeFileSync(path.join(stagedDir, stem1, 'SKILL.md'), '# help\n', 'utf8');
|
|
fs.mkdirSync(path.join(stagedDir, stem2), { recursive: true });
|
|
fs.writeFileSync(path.join(stagedDir, stem2, 'SKILL.md'), '# update\n', 'utf8');
|
|
|
|
const staleDir = path.join(destDir, 'msd-old-skill');
|
|
fs.mkdirSync(staleDir, { recursive: true });
|
|
fs.writeFileSync(path.join(staleDir, 'SKILL.md'), '# old\n', 'utf8');
|
|
|
|
const foreignDir = path.join(destDir, 'my-custom-skill');
|
|
fs.mkdirSync(foreignDir, { recursive: true });
|
|
fs.writeFileSync(path.join(foreignDir, 'SKILL.md'), '# custom\n', 'utf8');
|
|
|
|
const skillsKind = { kind: 'skills', destSubpath: 'skills', prefix: 'msd-', stage: () => stagedDir };
|
|
|
|
// Build a minimal manifest that includes the MSD-owned stems so that the
|
|
// manifest-membership gate (Finding 1 fix) correctly identifies msd-old-skill
|
|
// as MSD-owned and prunes it. Without a manifest the new code conservatively
|
|
// preserves all msd-* dirs it cannot confirm are MSD-owned.
|
|
const manifest = new Map([
|
|
['help', []],
|
|
['update', []],
|
|
['old-skill', []], // MSD-owned stale stem — must be pruned when not in staged set
|
|
]);
|
|
|
|
_syncMsdDir(stagedDir, destDir, skillsKind, manifest);
|
|
|
|
assert.ok(fs.existsSync(path.join(destDir, stem1, 'SKILL.md')), 'msd-help/SKILL.md should be copied');
|
|
assert.ok(fs.existsSync(path.join(destDir, stem2, 'SKILL.md')), 'msd-update/SKILL.md should be copied');
|
|
// stale msd- dir removed (it's in the manifest so it is MSD-owned, but not in staged set)
|
|
assert.ok(!fs.existsSync(staleDir), 'stale msd-old-skill dir should be removed');
|
|
assert.ok(fs.existsSync(foreignDir), 'my-custom-skill dir should be preserved');
|
|
});
|
|
|
|
test('applySurface recreates missing destination directories', (t) => {
|
|
const base = createTempDir('msd-surface-missing-dest-');
|
|
t.after(() => cleanup(base));
|
|
const runtimeConfigDir = base;
|
|
writeActiveProfile(runtimeConfigDir, 'core');
|
|
writeSurface(runtimeConfigDir, {
|
|
baseProfile: 'core',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const layout = resolveRuntimeArtifactLayout('claude', runtimeConfigDir, 'local');
|
|
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
|
|
|
// #1367: claude local uses flat commands/ (not commands/msd/)
|
|
const commandsDir = path.join(runtimeConfigDir, 'commands');
|
|
assert.ok(fs.existsSync(commandsDir), 'commands/ dir should be created even if initially absent');
|
|
const files = fs.readdirSync(commandsDir).filter(f => f.startsWith('msd-') && f.endsWith('.md'));
|
|
assert.ok(files.length > 0, 'commands/ should contain staged skill files (msd-*.md)');
|
|
assert.ok(files.includes('msd-help.md'), 'msd-help.md should be present after applySurface on missing dest');
|
|
});
|
|
|
|
test('Hermes profile shrink: stale MSD skill dirs are removed; user skills preserved', (t) => {
|
|
const { _syncMsdDir } = require('../msd-core/bin/lib/surface.cjs');
|
|
|
|
const base = createTempDir('msd-surface-hermes-shrink-');
|
|
t.after(() => cleanup(base));
|
|
const stagedDir = path.join(base, 'staged');
|
|
const destDir = path.join(base, 'dest');
|
|
fs.mkdirSync(destDir, { recursive: true });
|
|
|
|
fs.mkdirSync(path.join(stagedDir, 'msd-executor'), { recursive: true });
|
|
fs.writeFileSync(path.join(stagedDir, 'msd-executor', 'SKILL.md'), '# executor\n', 'utf8');
|
|
|
|
fs.mkdirSync(path.join(destDir, 'msd-executor'), { recursive: true });
|
|
fs.writeFileSync(path.join(destDir, 'msd-executor', 'SKILL.md'), '# executor\n', 'utf8');
|
|
fs.mkdirSync(path.join(destDir, 'msd-planner'), { recursive: true });
|
|
fs.writeFileSync(path.join(destDir, 'msd-planner', 'SKILL.md'), '# planner\n', 'utf8');
|
|
fs.mkdirSync(path.join(destDir, 'user-skill'), { recursive: true });
|
|
fs.writeFileSync(path.join(destDir, 'user-skill', 'SKILL.md'), '# user\n', 'utf8');
|
|
|
|
const manifest = new Map([
|
|
['msd-executor', []],
|
|
['msd-planner', []],
|
|
]);
|
|
|
|
const hermesKind = { kind: 'skills', destSubpath: 'skills/msd', prefix: '', stage: () => stagedDir };
|
|
_syncMsdDir(stagedDir, destDir, hermesKind, manifest);
|
|
|
|
assert.ok(
|
|
fs.existsSync(path.join(destDir, 'msd-executor', 'SKILL.md')),
|
|
'msd-executor should be kept (in staged set)'
|
|
);
|
|
assert.ok(
|
|
!fs.existsSync(path.join(destDir, 'msd-planner')),
|
|
'msd-planner should be removed (in manifest but not in staged set — stale MSD skill)'
|
|
);
|
|
assert.ok(
|
|
fs.existsSync(path.join(destDir, 'user-skill', 'SKILL.md')),
|
|
'user-skill should be preserved (not in manifest — user-owned)'
|
|
);
|
|
});
|
|
|
|
test('_syncMsdDir skills kind (hermes): preserves non-MSD user dir under skills/msd/ when kindPrefix is empty', (t) => {
|
|
const { _syncMsdDir } = require('../msd-core/bin/lib/surface.cjs');
|
|
|
|
const base = createTempDir('msd-surface-hermes-');
|
|
t.after(() => cleanup(base));
|
|
const stagedDir = path.join(base, 'staged');
|
|
const destDir = path.join(base, 'dest');
|
|
fs.mkdirSync(destDir, { recursive: true });
|
|
|
|
const stem1 = 'help';
|
|
fs.mkdirSync(path.join(stagedDir, stem1), { recursive: true });
|
|
fs.writeFileSync(path.join(stagedDir, stem1, 'SKILL.md'), '# help\n', 'utf8');
|
|
|
|
const userDir = path.join(destDir, 'user-custom-skill');
|
|
fs.mkdirSync(userDir, { recursive: true });
|
|
fs.writeFileSync(path.join(userDir, 'SKILL.md'), '# user custom\n', 'utf8');
|
|
|
|
const hermesKind = { kind: 'skills', destSubpath: 'skills/msd', prefix: '', stage: () => stagedDir };
|
|
_syncMsdDir(stagedDir, destDir, hermesKind);
|
|
|
|
assert.ok(fs.existsSync(userDir), 'user-custom-skill dir must be preserved when kindPrefix is empty (Hermes)');
|
|
assert.ok(fs.existsSync(path.join(destDir, stem1, 'SKILL.md')), 'MSD help/SKILL.md must be copied');
|
|
});
|
|
|
|
// Regression guard for #816: applySurface must write msd-prefixed command files
|
|
// (matching installRuntimeArtifacts/_copyStaged behaviour) and must NOT prune
|
|
// user-created command files that install would preserve.
|
|
//
|
|
// Affected runtimes have a FLAT command dir (opencode `commands/` — #2329,
|
|
// cursor `commands/`, augment `commands/`, kilo `command/`) with kind.prefix='msd-'.
|
|
// _copyStaged names files `msd-<stem>.md` but the buggy _syncMsdDir copies
|
|
// them as `<stem>.md` (unprefixed) and also deletes ALL .md files not in the
|
|
// staged set, including user files.
|
|
test('applySurface writes msd-prefixed command files matching install and preserves user commands (#816)', (t) => {
|
|
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-surface-816-'));
|
|
t.after(() => cleanup(configDir));
|
|
writePackageSourceMarkerFixture(configDir);
|
|
|
|
writeActiveProfile(configDir, 'standard');
|
|
writeSurface(configDir, {
|
|
baseProfile: 'standard',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
|
|
// Determine the command dest dir for opencode: commandsKind destSubpath='commands' (#2329)
|
|
const layout = resolveRuntimeArtifactLayout('opencode', configDir, 'global');
|
|
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
|
|
assert.ok(commandsKind, 'opencode layout must have a commands kind');
|
|
const commandDir = path.join(configDir, commandsKind.destSubpath);
|
|
|
|
// Pre-seed a user command file BEFORE applySurface — install would preserve it
|
|
fs.mkdirSync(commandDir, { recursive: true });
|
|
fs.writeFileSync(path.join(commandDir, 'my-user-cmd.md'), '# user custom command\n', 'utf8');
|
|
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
applySurface(configDir, layout, manifest, CLUSTERS);
|
|
|
|
const files = fs.readdirSync(commandDir).filter(f => f.endsWith('.md'));
|
|
|
|
// (a) At least one msd-prefixed command file must exist — on buggy code only
|
|
// unprefixed files like 'help.md' are written, so this assertion fails.
|
|
assert.ok(
|
|
files.some(f => f.startsWith('msd-') && f.endsWith('.md')),
|
|
'#816: applySurface must write at least one msd-prefixed command file ' +
|
|
'(e.g. msd-help.md) to match installRuntimeArtifacts/_copyStaged behaviour. ' +
|
|
`Actual files: [${files.join(', ')}]`
|
|
);
|
|
|
|
// (b) Every MSD-owned command file must be prefixed — no bare <stem>.md files
|
|
// allowed among MSD-owned output (excluding the user file).
|
|
const msdFiles = files.filter(f => f !== 'my-user-cmd.md');
|
|
const unprefixed = msdFiles.filter(f => !f.startsWith('msd-'));
|
|
assert.deepStrictEqual(
|
|
unprefixed,
|
|
[],
|
|
'#816: all MSD-owned command files must start with msd- to match install. ' +
|
|
`Found unprefixed: [${unprefixed.join(', ')}]`
|
|
);
|
|
|
|
// (c) The pre-seeded user file must survive applySurface — on buggy code the
|
|
// commands-kind pruning loop deletes ALL .md files not in the staged set,
|
|
// which wipes user files that installRuntimeArtifacts would never touch.
|
|
assert.ok(
|
|
files.includes('my-user-cmd.md'),
|
|
'#816: applySurface must preserve user command file my-user-cmd.md that was ' +
|
|
'present before sync — _syncMsdDir must not prune files not owned by MSD. ' +
|
|
`Actual files: [${files.join(', ')}]`
|
|
);
|
|
});
|
|
|
|
// Parity regression guard for #816: applySurface command-dir filenames must
|
|
// match a fresh installRuntimeArtifacts for every command runtime. Guards
|
|
// against future drift between _syncMsdDir (surface) and _copyStaged (install)
|
|
// command-naming logic.
|
|
//
|
|
// Matrix: opencode/cursor/augment = flat commands/ + prefix msd- (#2329: opencode
|
|
// moved from singular command/ to commands/); kilo = flat command/ + prefix msd-.
|
|
// For each runtime we: run install into installDir, run applySurface into
|
|
// surfaceDir (same 'standard' profile both sides), then compare sorted .md
|
|
// filename sets in the commands dest dir. On a fresh dir (no superseded files)
|
|
// both paths must produce identical sets.
|
|
test('applySurface command-dir filenames match a fresh install for every command runtime (#816 parity)', async (t) => {
|
|
process.env.MSD_TEST_MODE = '1';
|
|
const { installRuntimeArtifacts } = require('../msd-core/bin/lib/install-engine.cjs');
|
|
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
// Build the resolved profile once. Both install and surface sides must use
|
|
// the same skill set so any filename difference is purely a naming bug.
|
|
const resolvedProfile = resolveProfile({ modes: ['standard'], manifest });
|
|
|
|
const PARITY_RUNTIMES = ['opencode', 'cursor', 'augment', 'kilo'];
|
|
|
|
for (const runtime of PARITY_RUNTIMES) {
|
|
// Create two independent temp dirs — one for install, one for surface.
|
|
const installDir = fs.mkdtempSync(path.join(os.tmpdir(), `msd-816-install-${runtime}-`));
|
|
const surfaceDir = fs.mkdtempSync(path.join(os.tmpdir(), `msd-816-surface-${runtime}-`));
|
|
t.after(() => { cleanup(installDir); cleanup(surfaceDir); });
|
|
writePackageSourceMarkerFixture(surfaceDir);
|
|
|
|
// --- Install path ---
|
|
installRuntimeArtifacts(runtime, installDir, 'global', resolvedProfile);
|
|
|
|
// --- Surface path ---
|
|
writeActiveProfile(surfaceDir, 'standard');
|
|
writeSurface(surfaceDir, {
|
|
baseProfile: 'standard',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
const layout = resolveRuntimeArtifactLayout(runtime, surfaceDir, 'global');
|
|
applySurface(surfaceDir, layout, manifest, CLUSTERS);
|
|
|
|
// --- Find commands kind ---
|
|
const cmdKind = layout.kinds.find(k => k.kind === 'commands');
|
|
if (!cmdKind) {
|
|
// Runtime has no commands kind at global scope — skip gracefully.
|
|
continue;
|
|
}
|
|
|
|
// --- Compare sorted .md filename sets ---
|
|
const installCmdDir = path.join(installDir, cmdKind.destSubpath);
|
|
const surfaceCmdDir = path.join(surfaceDir, cmdKind.destSubpath);
|
|
|
|
const installFiles = fs.existsSync(installCmdDir)
|
|
? fs.readdirSync(installCmdDir).filter(f => f.endsWith('.md')).sort()
|
|
: [];
|
|
const surfaceFiles = fs.existsSync(surfaceCmdDir)
|
|
? fs.readdirSync(surfaceCmdDir).filter(f => f.endsWith('.md')).sort()
|
|
: [];
|
|
|
|
assert.deepStrictEqual(
|
|
surfaceFiles,
|
|
installFiles,
|
|
`#816 parity: command filenames for ${runtime} (${cmdKind.destSubpath}) must match a fresh install.\n` +
|
|
` install: [${installFiles.slice(0, 5).join(', ')}${installFiles.length > 5 ? '...' : ''}]\n` +
|
|
` surface: [${surfaceFiles.slice(0, 5).join(', ')}${surfaceFiles.length > 5 ? '...' : ''}]`
|
|
);
|
|
}
|
|
});
|
|
|
|
// Regression test for #813: applySurface must apply per-runtime path rewrites
|
|
// (applyRuntimeContentRewritesInPlace) just as installRuntimeArtifacts does.
|
|
// Without the fix, skill bodies retain the converter's default ~/.claude/ paths
|
|
// instead of being rewritten to the install target (pathPrefix).
|
|
//
|
|
// Both 'cursor' and 'codex' use skillsKind AND have a path-rewrite case in
|
|
// _applyRuntimeRewrites — so the regression guard covers both.
|
|
for (const runtime of ['cursor', 'codex']) {
|
|
test(`applySurface rewrites ${runtime} skill bodies to the install pathPrefix, not the converter default ~/.claude path (#813)`, (t) => {
|
|
// Use mkdtempSync under os.tmpdir() — NOT under the user's home dir — so that
|
|
// computePathPrefix returns an ABSOLUTE prefix `${configDir}/` for local installs,
|
|
// clearly distinguishable from the `~/.claude/` converter default.
|
|
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), `msd-surface-813-${runtime}-`));
|
|
t.after(() => cleanup(configDir));
|
|
|
|
writeActiveProfile(configDir, 'standard');
|
|
writeSurface(configDir, {
|
|
baseProfile: 'standard',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
// skills/msd-<name>/SKILL.md (destSubpath 'skills', prefix 'msd-')
|
|
// scope='local' ensures computePathPrefix returns an absolute configDir prefix
|
|
// rather than the home-relative default (e.g. ~/.cursor/ or ~/.codex/).
|
|
const layout = resolveRuntimeArtifactLayout(runtime, configDir, 'local');
|
|
applySurface(configDir, layout, manifest, CLUSTERS);
|
|
|
|
// Collect every SKILL.md body under ${configDir}/skills/
|
|
const skillsRoot = path.join(configDir, 'skills');
|
|
const skillBodies = [];
|
|
if (fs.existsSync(skillsRoot)) {
|
|
for (const dirEntry of fs.readdirSync(skillsRoot)) {
|
|
const skillMd = path.join(skillsRoot, dirEntry, 'SKILL.md');
|
|
if (fs.existsSync(skillMd)) {
|
|
skillBodies.push(fs.readFileSync(skillMd, 'utf8'));
|
|
}
|
|
}
|
|
}
|
|
|
|
// (a) Sanity: at least one SKILL.md must have been staged
|
|
assert.ok(
|
|
skillBodies.length > 0,
|
|
`applySurface must stage at least one ${runtime} SKILL.md under ${skillsRoot}/ but found none`
|
|
);
|
|
|
|
// (b) BUG SYMPTOM (#813): after the rewrite, no body should contain '~/.claude/'
|
|
// or '$HOME/.claude/' — both are converter-default forms that must be eliminated.
|
|
// This assertion FAILS on unpatched code — applySurface does not call
|
|
// applyRuntimeContentRewritesInPlace, so the converter's default ~/.claude/
|
|
// paths are left verbatim in the staged files.
|
|
const bodiesWithTildeClaude = skillBodies.filter(b => b.includes('~/.claude/') || b.includes('$HOME/.claude/'));
|
|
assert.strictEqual(
|
|
bodiesWithTildeClaude.length,
|
|
0,
|
|
`#813 regression: ${bodiesWithTildeClaude.length} ${runtime} SKILL.md(s) still contain '~/.claude/' or '$HOME/.claude/' after applySurface — ` +
|
|
`applyRuntimeContentRewritesInPlace was not applied (mirrors installRuntimeArtifacts' rewrite step)`
|
|
);
|
|
|
|
// (c) The rewrite must inject the real install target path, not just remove the tilde.
|
|
// This also fails on unpatched code for the same reason.
|
|
// NOTE: this assertion depends on the command corpus emitting rewritable
|
|
// '~/.claude/'-style paths in at least one skill body. If a future corpus
|
|
// change removes all such paths, this assertion will become vacuously true
|
|
// (no body will contain configDirPrefix either) — update the test rather than
|
|
// treating a silent zero-match as a pass.
|
|
// Production derives pathPrefix as `path.resolve(configDir).replace(/\\/g, '/')`
|
|
// (mirrors installRuntimeArtifacts), so on Windows the rewritten body uses
|
|
// forward slashes. Normalize the expected prefix the same way so this assertion
|
|
// is cross-platform (Windows CI leg is not covered by local msd-test) (#813).
|
|
const configDirPrefix = `${path.resolve(configDir).replace(/\\/g, '/')}/`;
|
|
const bodiesWithAbsolutePrefix = skillBodies.filter(b => b.includes(configDirPrefix));
|
|
assert.ok(
|
|
bodiesWithAbsolutePrefix.length > 0,
|
|
`#813 regression: no ${runtime} SKILL.md contains the absolute configDir prefix '${configDirPrefix}' — ` +
|
|
`the path rewrite was not applied by applySurface. ` +
|
|
`(If the command corpus no longer emits any '~/.claude/'-style paths, update this test.)`
|
|
);
|
|
});
|
|
}
|
|
|
|
test('#4132: a later kind staging failure leaves exact all-old state and artifacts', (t) => {
|
|
const root = createTempDir('msd-surface-stage-failure-');
|
|
t.after(() => cleanup(root));
|
|
const firstDest = path.join(root, 'first');
|
|
const secondDest = path.join(root, 'second');
|
|
fs.mkdirSync(firstDest, { recursive: true });
|
|
fs.mkdirSync(secondDest, { recursive: true });
|
|
fs.writeFileSync(path.join(firstDest, 'msd-alpha.md'), 'old alpha\n');
|
|
fs.writeFileSync(path.join(secondDest, 'msd-beta.md'), 'old beta\n');
|
|
const stagedFirst = path.join(root, 'staged-first');
|
|
fs.mkdirSync(stagedFirst);
|
|
fs.writeFileSync(path.join(stagedFirst, 'alpha.md'), 'new alpha\n');
|
|
const oldSurface = surfaceBytesForTest({ baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] });
|
|
fs.writeFileSync(path.join(root, '.msd-surface.json'), oldSurface);
|
|
const layout = {
|
|
runtime: 'claude', configDir: root, scope: 'local', kinds: [
|
|
{ kind: 'commands', destSubpath: 'first', prefix: 'msd-', stage: () => stagedFirst },
|
|
{ kind: 'commands', destSubpath: 'second', prefix: 'msd-', stage: () => { throw new Error('later stage failed'); } },
|
|
],
|
|
};
|
|
|
|
assert.throws(() => applySurface(root, layout, new Map([['alpha', []], ['beta', []]])), /later stage failed/);
|
|
assert.equal(fs.readFileSync(path.join(firstDest, 'msd-alpha.md'), 'utf8'), 'old alpha\n');
|
|
assert.equal(fs.readFileSync(path.join(secondDest, 'msd-beta.md'), 'utf8'), 'old beta\n');
|
|
assert.equal(fs.readFileSync(path.join(root, '.msd-surface.json'), 'utf8'), oldSurface);
|
|
});
|
|
|
|
test('#4132: a hash-mismatched installed corpus leaves exact all-old state and artifacts', (t) => {
|
|
const root = createTempDir('msd-surface-corrupt-corpus-');
|
|
t.after(() => cleanup(root));
|
|
const installedCommands = path.join(root, 'msd-core', 'commands', 'msd');
|
|
const installedAgents = path.join(root, '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(root, '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 skillPath = path.join(root, 'skills', 'msd-help', 'SKILL.md');
|
|
const agentPath = path.join(root, 'agents', 'msd-planner.md');
|
|
fs.mkdirSync(path.dirname(skillPath), { recursive: true });
|
|
fs.mkdirSync(path.dirname(agentPath), { recursive: true });
|
|
fs.writeFileSync(skillPath, 'old skill\n');
|
|
fs.writeFileSync(agentPath, 'old agent\n');
|
|
const oldState = { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] };
|
|
const candidate = { ...oldState, explicitRemoves: ['help'] };
|
|
const oldSurface = surfaceBytesForTest(oldState);
|
|
fs.writeFileSync(path.join(root, '.msd-surface.json'), oldSurface);
|
|
|
|
const layout = resolveRuntimeArtifactLayout('claude', root, 'global');
|
|
assert.throws(
|
|
() => applySurface(root, layout, realManifest(), CLUSTERS, undefined, { surfaceState: candidate }),
|
|
/install or upgrade msd-core/,
|
|
);
|
|
assert.equal(fs.readFileSync(skillPath, 'utf8'), 'old skill\n');
|
|
assert.equal(fs.readFileSync(agentPath, 'utf8'), 'old agent\n');
|
|
assert.equal(fs.readFileSync(path.join(root, '.msd-surface.json'), 'utf8'), oldSurface);
|
|
});
|
|
|
|
test('#4132: candidate surface state is published after materialization', (t) => {
|
|
const root = createTempDir('msd-surface-state-last-');
|
|
t.after(() => cleanup(root));
|
|
const dest = path.join(root, 'commands');
|
|
const staged = path.join(root, 'staged');
|
|
fs.mkdirSync(dest, { recursive: true });
|
|
fs.mkdirSync(staged);
|
|
fs.writeFileSync(path.join(dest, 'msd-alpha.md'), 'old alpha\n');
|
|
fs.writeFileSync(path.join(staged, 'alpha.md'), 'new alpha\n');
|
|
const oldState = { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] };
|
|
const newState = { ...oldState, disabledClusters: ['ui'] };
|
|
writeSurface(root, oldState);
|
|
const layout = {
|
|
runtime: 'claude', configDir: root, scope: 'local', kinds: [
|
|
{ kind: 'commands', destSubpath: 'commands', prefix: 'msd-', stage: () => {
|
|
assert.deepEqual(readSurface(root), oldState, 'candidate must not be visible while staging');
|
|
return staged;
|
|
} },
|
|
],
|
|
};
|
|
|
|
applySurface(root, layout, new Map([['alpha', []]]), undefined, undefined, { surfaceState: newState });
|
|
|
|
assert.equal(fs.readFileSync(path.join(dest, 'msd-alpha.md'), 'utf8'), 'new alpha\n');
|
|
assert.deepEqual(readSurface(root), newState);
|
|
});
|
|
|
|
test('#4132: reset removes surface state after materialization', (t) => {
|
|
const root = createTempDir('msd-surface-reset-state-last-');
|
|
t.after(() => cleanup(root));
|
|
const dest = path.join(root, 'commands');
|
|
const staged = path.join(root, 'staged');
|
|
fs.mkdirSync(dest, { recursive: true });
|
|
fs.mkdirSync(staged);
|
|
fs.writeFileSync(path.join(staged, 'alpha.md'), 'reset alpha\n');
|
|
writeActiveProfile(root, 'full');
|
|
writeSurface(root, { baseProfile: 'core', disabledClusters: ['ui'], explicitAdds: [], explicitRemoves: [] });
|
|
const layout = {
|
|
runtime: 'claude', configDir: root, scope: 'local', kinds: [
|
|
{ kind: 'commands', destSubpath: 'commands', prefix: 'msd-', stage: () => staged },
|
|
],
|
|
};
|
|
|
|
applySurface(root, layout, new Map([['alpha', []]]), undefined, undefined, { surfaceState: null });
|
|
|
|
assert.equal(fs.existsSync(path.join(root, '.msd-surface.json')), false);
|
|
assert.equal(fs.readFileSync(path.join(dest, 'msd-alpha.md'), 'utf8'), 'reset alpha\n');
|
|
});
|
|
|
|
});
|
|
|
|
// ─── resolveSurface ──────────────────────────────────────────────────────────
|
|
|
|
describe('resolveSurface', () => {
|
|
test('no surface state + core base profile → identical to resolveProfile core', () => {
|
|
const dir = tmpDir('msd-surface-resolve-');
|
|
try {
|
|
writeActiveProfile(dir, 'core');
|
|
const manifest = realManifest();
|
|
const surfaceResolved = resolveSurface(dir, manifest, CLUSTERS);
|
|
const profileResolved = resolveProfile({ modes: ['core'], manifest });
|
|
|
|
assert.ok(surfaceResolved.skills instanceof Set);
|
|
assert.ok(profileResolved.skills instanceof Set);
|
|
assert.deepStrictEqual(
|
|
[...surfaceResolved.skills].sort(),
|
|
[...profileResolved.skills].sort(),
|
|
'surface with no state should equal profile resolution'
|
|
);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('standard base + disabledClusters:["utility"] removes utility skills', () => {
|
|
const dir = tmpDir('msd-surface-resolve-');
|
|
try {
|
|
writeActiveProfile(dir, 'standard');
|
|
writeSurface(dir, {
|
|
baseProfile: 'standard',
|
|
disabledClusters: ['utility'],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
const manifest = realManifest();
|
|
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
|
|
|
assert.ok(resolved.skills instanceof Set);
|
|
for (const stem of CLUSTERS.utility) {
|
|
const standardResolved = resolveProfile({ modes: ['standard'], manifest });
|
|
if (standardResolved.skills.has(stem)) {
|
|
assert.ok(
|
|
!resolved.skills.has(stem),
|
|
`"${stem}" should be removed by disabling utility cluster`
|
|
);
|
|
}
|
|
}
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('explicitAdds:["sketch"] adds sketch to a core install', () => {
|
|
const dir = tmpDir('msd-surface-resolve-');
|
|
try {
|
|
writeActiveProfile(dir, 'core');
|
|
writeSurface(dir, {
|
|
baseProfile: 'core',
|
|
disabledClusters: [],
|
|
explicitAdds: ['sketch'],
|
|
explicitRemoves: [],
|
|
});
|
|
const manifest = realManifest();
|
|
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
|
|
|
assert.ok(resolved.skills instanceof Set);
|
|
assert.ok(resolved.skills.has('sketch'), 'sketch must be in resolved skills');
|
|
|
|
const sketchRequires = manifest.get('sketch') || [];
|
|
for (const dep of sketchRequires) {
|
|
assert.ok(resolved.skills.has(dep), `transitive dep "${dep}" of sketch must be present`);
|
|
}
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('explicitRemoves removes individual skill stems', () => {
|
|
const dir = tmpDir('msd-surface-resolve-');
|
|
try {
|
|
writeSurface(dir, {
|
|
baseProfile: 'standard',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: ['progress'],
|
|
});
|
|
writeActiveProfile(dir, 'standard');
|
|
const manifest = realManifest();
|
|
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
|
|
|
assert.ok(!resolved.skills.has('progress'), '"progress" must be removed by explicitRemoves');
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('result is a Set<string> with name property and agents Set', () => {
|
|
const dir = tmpDir('msd-surface-resolve-');
|
|
try {
|
|
writeActiveProfile(dir, 'core');
|
|
const manifest = realManifest();
|
|
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
|
|
|
assert.ok(resolved.skills instanceof Set);
|
|
assert.ok(typeof resolved.name === 'string');
|
|
assert.ok(resolved.agents instanceof Set);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('surface with baseProfile overrides .msd-profile marker', () => {
|
|
const dir = tmpDir('msd-surface-resolve-');
|
|
try {
|
|
writeActiveProfile(dir, 'core');
|
|
writeSurface(dir, {
|
|
baseProfile: 'standard',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
const manifest = realManifest();
|
|
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
|
const standardResolved = resolveProfile({ modes: ['standard'], manifest });
|
|
|
|
assert.deepStrictEqual(
|
|
[...resolved.skills].sort(),
|
|
[...standardResolved.skills].sort(),
|
|
'surface baseProfile takes precedence over marker'
|
|
);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('disabled cluster + explicitAdds can re-add specific skills from disabled cluster', () => {
|
|
const dir = tmpDir('msd-surface-resolve-');
|
|
try {
|
|
writeSurface(dir, {
|
|
baseProfile: 'standard',
|
|
disabledClusters: ['workspace_state'],
|
|
explicitAdds: ['capture'],
|
|
explicitRemoves: [],
|
|
});
|
|
writeActiveProfile(dir, 'standard');
|
|
const manifest = realManifest();
|
|
const resolved = resolveSurface(dir, manifest, CLUSTERS);
|
|
|
|
assert.ok(resolved.skills.has('capture'), '"capture" must be present via explicitAdds');
|
|
const standardResolved = resolveProfile({ modes: ['standard'], manifest });
|
|
for (const stem of CLUSTERS.workspace_state) {
|
|
if (stem === 'capture') continue;
|
|
if (standardResolved.skills.has(stem)) {
|
|
assert.ok(
|
|
!resolved.skills.has(stem),
|
|
`"${stem}" should be removed (workspace_state disabled, not explicitly re-added)`
|
|
);
|
|
}
|
|
}
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── readSurface / writeSurface ──────────────────────────────────────────────
|
|
|
|
describe('readSurface / writeSurface', () => {
|
|
test('round-trips a complete surface state', () => {
|
|
const dir = tmpDir('msd-surface-state-');
|
|
try {
|
|
const state = {
|
|
baseProfile: 'standard',
|
|
disabledClusters: ['utility'],
|
|
explicitAdds: ['sketch'],
|
|
explicitRemoves: [],
|
|
};
|
|
writeSurface(dir, state);
|
|
const read = readSurface(dir);
|
|
assert.deepStrictEqual(read, state);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('round-trips empty arrays', () => {
|
|
const dir = tmpDir('msd-surface-state-');
|
|
try {
|
|
const state = {
|
|
baseProfile: 'core',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
};
|
|
writeSurface(dir, state);
|
|
assert.deepStrictEqual(readSurface(dir), state);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('round-trips composed base profile', () => {
|
|
const dir = tmpDir('msd-surface-state-');
|
|
try {
|
|
const state = {
|
|
baseProfile: 'core,audit',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: ['health'],
|
|
};
|
|
writeSurface(dir, state);
|
|
assert.deepStrictEqual(readSurface(dir), state);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('missing file returns null', () => {
|
|
const dir = tmpDir('msd-surface-state-');
|
|
try {
|
|
const result = readSurface(dir);
|
|
assert.strictEqual(result, null);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('non-existent directory returns null', () => {
|
|
const ghost = path.join(os.tmpdir(), 'msd-surface-no-exist-' + Date.now());
|
|
const result = readSurface(ghost);
|
|
assert.strictEqual(result, null);
|
|
});
|
|
|
|
test('corrupt JSON returns null', () => {
|
|
const dir = tmpDir('msd-surface-state-');
|
|
try {
|
|
fs.writeFileSync(path.join(dir, '.msd-surface.json'), '{not valid json', 'utf8');
|
|
const result = readSurface(dir);
|
|
assert.strictEqual(result, null);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('JSON missing baseProfile field returns null', () => {
|
|
const dir = tmpDir('msd-surface-state-');
|
|
try {
|
|
fs.writeFileSync(
|
|
path.join(dir, '.msd-surface.json'),
|
|
JSON.stringify({ disabledClusters: [], explicitAdds: [], explicitRemoves: [] }),
|
|
'utf8'
|
|
);
|
|
const result = readSurface(dir);
|
|
assert.strictEqual(result, null);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('JSON with non-array disabledClusters returns null', () => {
|
|
const dir = tmpDir('msd-surface-state-');
|
|
try {
|
|
fs.writeFileSync(
|
|
path.join(dir, '.msd-surface.json'),
|
|
JSON.stringify({ baseProfile: 'standard', disabledClusters: 'utility', explicitAdds: [], explicitRemoves: [] }),
|
|
'utf8'
|
|
);
|
|
const result = readSurface(dir);
|
|
assert.strictEqual(result, null);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('atomic write: result file is never a partial tmp file', () => {
|
|
const dir = tmpDir('msd-surface-state-');
|
|
try {
|
|
const state = { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] };
|
|
writeSurface(dir, state);
|
|
const files = fs.readdirSync(dir);
|
|
const tmpFiles = files.filter(f => f.includes('.tmp.'));
|
|
assert.deepStrictEqual(tmpFiles, [], 'no tmp files should remain after write');
|
|
assert.ok(files.includes('.msd-surface.json'));
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('second write overwrites first', () => {
|
|
const dir = tmpDir('msd-surface-state-');
|
|
try {
|
|
writeSurface(dir, { baseProfile: 'core', disabledClusters: [], explicitAdds: [], explicitRemoves: [] });
|
|
writeSurface(dir, { baseProfile: 'standard', disabledClusters: ['utility'], explicitAdds: [], explicitRemoves: [] });
|
|
const read = readSurface(dir);
|
|
assert.strictEqual(read.baseProfile, 'standard');
|
|
assert.deepStrictEqual(read.disabledClusters, ['utility']);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('writeSurface creates directory if it does not exist', () => {
|
|
const base = tmpDir('msd-surface-state-');
|
|
const nested = path.join(base, 'skills', 'subdir');
|
|
try {
|
|
writeSurface(nested, { baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] });
|
|
assert.ok(fs.existsSync(nested));
|
|
assert.ok(readSurface(nested) !== null);
|
|
} finally {
|
|
cleanup(base);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── CLUSTERS data structure ─────────────────────────────────────────────────
|
|
|
|
describe('CLUSTERS data structure', () => {
|
|
test('no cluster is empty', () => {
|
|
for (const [name, members] of Object.entries(CLUSTERS)) {
|
|
assert.ok(members.length > 0, `cluster ${name} must not be empty`);
|
|
}
|
|
});
|
|
|
|
test('every cluster member is a real skill stem in commands/msd/', () => {
|
|
const entries = fs.readdirSync(REAL_COMMANDS_DIR, { withFileTypes: true });
|
|
const realStems = new Set(
|
|
entries
|
|
.filter(e => e.isFile() && e.name.endsWith('.md'))
|
|
.map(e => e.name.slice(0, -3))
|
|
);
|
|
const mismatches = [];
|
|
for (const [cluster, members] of Object.entries(CLUSTERS)) {
|
|
for (const stem of members) {
|
|
if (!realStems.has(stem)) {
|
|
mismatches.push(`${cluster}: "${stem}" not found in commands/msd/`);
|
|
}
|
|
}
|
|
}
|
|
assert.deepStrictEqual(mismatches, [], `Cluster members missing from disk:\n${mismatches.join('\n')}`);
|
|
});
|
|
|
|
test('union of all clusters covers every skill in commands/msd/', () => {
|
|
const entries = fs.readdirSync(REAL_COMMANDS_DIR, { withFileTypes: true });
|
|
const realStems = new Set(
|
|
entries
|
|
.filter(e => e.isFile() && e.name.endsWith('.md'))
|
|
.map(e => e.name.slice(0, -3))
|
|
);
|
|
const clustered = allClusteredSkills();
|
|
const uncategorized = [];
|
|
for (const stem of realStems) {
|
|
if (!clustered.has(stem)) uncategorized.push(stem);
|
|
}
|
|
assert.deepStrictEqual(
|
|
uncategorized,
|
|
[],
|
|
`Uncategorized skills (not in any cluster):\n${uncategorized.sort().join('\n')}`
|
|
);
|
|
});
|
|
|
|
test('CLUSTERS is frozen (immutable)', () => {
|
|
assert.ok(Object.isFrozen(CLUSTERS), 'CLUSTERS must be frozen');
|
|
for (const [name, members] of Object.entries(CLUSTERS)) {
|
|
assert.ok(Object.isFrozen(members), `CLUSTERS.${name} must be frozen`);
|
|
}
|
|
});
|
|
|
|
test('cluster names contain the expected set from research memo §3.2', () => {
|
|
const expectedClusterNames = new Set([
|
|
'core_loop',
|
|
'audit_review',
|
|
'milestone',
|
|
'research_ideate',
|
|
'workspace_state',
|
|
'docs',
|
|
'ui',
|
|
'ai_eval',
|
|
'ns_meta',
|
|
'utility',
|
|
]);
|
|
const actualClusterNames = new Set(Object.keys(CLUSTERS));
|
|
for (const name of expectedClusterNames) {
|
|
assert.ok(actualClusterNames.has(name), `expected cluster "${name}" missing from CLUSTERS`);
|
|
}
|
|
});
|
|
|
|
test('allClusteredSkills returns a Set containing all cluster members', () => {
|
|
const result = allClusteredSkills();
|
|
assert.ok(result instanceof Set, 'allClusteredSkills() must return a Set');
|
|
for (const members of Object.values(CLUSTERS)) {
|
|
for (const stem of members) {
|
|
assert.ok(result.has(stem), `allClusteredSkills() missing "${stem}"`);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── listSurface ─────────────────────────────────────────────────────────────
|
|
|
|
describe('listSurface', () => {
|
|
test('accepts parsed msd-file-manifest JSON objects without crashing (#322)', () => {
|
|
const dir = tmpDir('msd-surface-list-');
|
|
try {
|
|
fs.writeFileSync(path.join(dir, '.msd-source'), REAL_COMMANDS_DIR, 'utf8');
|
|
writeActiveProfile(dir, 'core');
|
|
const diskManifestShape = {
|
|
version: '1.0.0',
|
|
timestamp: new Date().toISOString(),
|
|
mode: 'core',
|
|
files: {},
|
|
};
|
|
const result = listSurface(dir, diskManifestShape, CLUSTERS);
|
|
|
|
assert.ok(Array.isArray(result.enabled), 'enabled must be array');
|
|
assert.ok(Array.isArray(result.disabled), 'disabled must be array');
|
|
assert.ok(typeof result.tokenCost === 'number', 'tokenCost must be number');
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('returns { enabled, disabled, tokenCost } structure', () => {
|
|
const dir = tmpDir('msd-surface-list-');
|
|
try {
|
|
fs.writeFileSync(path.join(dir, '.msd-source'), REAL_COMMANDS_DIR, 'utf8');
|
|
writeActiveProfile(dir, 'core');
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const result = listSurface(dir, manifest, CLUSTERS);
|
|
|
|
assert.ok(Array.isArray(result.enabled), 'enabled must be array');
|
|
assert.ok(Array.isArray(result.disabled), 'disabled must be array');
|
|
assert.ok(typeof result.tokenCost === 'number', 'tokenCost must be number');
|
|
assert.ok(result.tokenCost >= 0, 'tokenCost must be non-negative');
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('core profile: enabled has fewer skills than full; enabled + disabled = total stems', () => {
|
|
const dir = tmpDir('msd-surface-list-');
|
|
try {
|
|
fs.writeFileSync(path.join(dir, '.msd-source'), REAL_COMMANDS_DIR, 'utf8');
|
|
writeActiveProfile(dir, 'core');
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const coreList = listSurface(dir, manifest, CLUSTERS);
|
|
|
|
const totalStems = [...manifest.keys()].filter(k => !k.startsWith('_calls_agents_')).length;
|
|
assert.ok(
|
|
coreList.enabled.length < totalStems,
|
|
'core should enable fewer skills than total'
|
|
);
|
|
assert.ok(coreList.disabled.length > 0, 'core should have some disabled skills');
|
|
assert.ok(coreList.enabled.length + coreList.disabled.length === totalStems,
|
|
'enabled + disabled must equal total stems');
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('disabling utility cluster reduces enabled count', () => {
|
|
const dir = tmpDir('msd-surface-list-');
|
|
try {
|
|
fs.writeFileSync(path.join(dir, '.msd-source'), REAL_COMMANDS_DIR, 'utf8');
|
|
writeActiveProfile(dir, 'standard');
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
|
|
const beforeList = listSurface(dir, manifest, CLUSTERS);
|
|
|
|
writeSurface(dir, {
|
|
baseProfile: 'standard',
|
|
disabledClusters: ['utility'],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
const afterList = listSurface(dir, manifest, CLUSTERS);
|
|
|
|
assert.ok(afterList.enabled.length <= beforeList.enabled.length,
|
|
'disabling utility cluster should not increase enabled count');
|
|
assert.ok(afterList.tokenCost <= beforeList.tokenCost,
|
|
'disabling a cluster should not increase token cost');
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('tokenCost is sum of description char lengths ÷ 4 for enabled skills', () => {
|
|
const dir = tmpDir('msd-surface-list-');
|
|
try {
|
|
fs.writeFileSync(path.join(dir, '.msd-source'), REAL_COMMANDS_DIR, 'utf8');
|
|
writeActiveProfile(dir, 'core');
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const result = listSurface(dir, manifest, CLUSTERS);
|
|
|
|
let expected = 0;
|
|
for (const stem of result.enabled) {
|
|
const filePath = path.join(REAL_COMMANDS_DIR, `${stem}.md`);
|
|
if (!fs.existsSync(filePath)) continue;
|
|
const markdown = fs.readFileSync(filePath, 'utf8');
|
|
const description = readFrontmatterDescription(markdown);
|
|
if (description) expected += Math.ceil(description.length / 4);
|
|
}
|
|
|
|
assert.strictEqual(result.tokenCost, expected, 'tokenCost must equal sum of description lengths ÷ 4');
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('enabled and disabled arrays are sorted', () => {
|
|
const dir = tmpDir('msd-surface-list-');
|
|
try {
|
|
fs.writeFileSync(path.join(dir, '.msd-source'), REAL_COMMANDS_DIR, 'utf8');
|
|
writeActiveProfile(dir, 'standard');
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const result = listSurface(dir, manifest, CLUSTERS);
|
|
|
|
assert.deepStrictEqual(result.enabled, [...result.enabled].sort());
|
|
assert.deepStrictEqual(result.disabled, [...result.disabled].sort());
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── #1615: applySurface must rewrite commands kind (Windsurf workflows) ─────
|
|
// Adversarial review of PR #1622 found that applySurface only rewrites 'skills'
|
|
// kinds, skipping 'commands'. Windsurf's capability now stages workflow files
|
|
// as kind='commands'; without the rewrite, /msd-surface would write workflow
|
|
// bodies containing raw @~/.claude/... references that don't exist on a
|
|
// Windsurf install. The same gap affected any runtime with commands kinds.
|
|
describe('applySurface — commands kind path rewrite (#1615 adversarial review)', () => {
|
|
test('windsurf workflow bodies are rewritten to install target (no raw ~/.claude/)', (t) => {
|
|
const base = createTempDir('msd-surface-cmds-windsurf-');
|
|
t.after(() => cleanup(base));
|
|
const runtimeConfigDir = base;
|
|
|
|
// Stage the canonical command body the workflow delegates to.
|
|
const canonicalDir = path.join(runtimeConfigDir, 'msd-core', 'commands', 'msd');
|
|
fs.mkdirSync(canonicalDir, { recursive: true });
|
|
fs.writeFileSync(path.join(canonicalDir, 'help.md'),
|
|
'---\nname: help\ndescription: Show help\n---\n\nHelp body\n');
|
|
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const layout = resolveRuntimeArtifactLayout('windsurf', runtimeConfigDir, 'local');
|
|
|
|
// Sanity: layout must have a commands kind (workflows) — pre-condition
|
|
// introduced by PR #1622; if a future refactor removes it, this test
|
|
// would silently pass without exercising the rewrite path.
|
|
const commandsKind = layout.kinds.find((k) => k.kind === 'commands');
|
|
assert.ok(commandsKind, 'pre-condition: windsurf layout has a commands kind');
|
|
|
|
applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);
|
|
|
|
// Workflow files should be written to <configDir>/workflows/msd-*.md
|
|
const workflowsDir = path.join(runtimeConfigDir, 'workflows');
|
|
const workflowFiles = fs.existsSync(workflowsDir)
|
|
? fs.readdirSync(workflowsDir).filter((f) => f.startsWith('msd-') && f.endsWith('.md'))
|
|
: [];
|
|
assert.ok(workflowFiles.length > 0,
|
|
`expected at least one msd-*.md workflow under ${workflowsDir}; got [${workflowFiles.join(', ')}]`);
|
|
|
|
// Every workflow body must reference the install target, NOT the raw
|
|
// ~/.claude/ path. This is the regression: pre-fix, the commands kind
|
|
// was skipped and raw @~/.claude/... survived into the synced file.
|
|
for (const fileName of workflowFiles) {
|
|
const workflowPath = path.join(workflowsDir, fileName);
|
|
const content = fs.readFileSync(workflowPath, 'utf8');
|
|
assert.ok(
|
|
!content.includes('~/.claude/'),
|
|
`${fileName} must not contain raw ~/.claude/ after applySurface rewrite (got: ${content.slice(0, 200)})`,
|
|
);
|
|
assert.ok(
|
|
!content.includes('$HOME/.claude/'),
|
|
`${fileName} must not contain raw $HOME/.claude/ after applySurface rewrite`,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── #2911: skills-kind destination parity (installer vs surface-apply) ──────
|
|
//
|
|
// _copyStaged (src/install-engine.cts) resolves the write root as
|
|
// `kind.home ?? layout.configDir`. Before the #2911 fix, applySurface's
|
|
// _syncMsdDir call always resolved against `layout.configDir`, ignoring
|
|
// `kind.home`. For runtimes whose skills kind declares a `home` override
|
|
// (currently only codex: home='.agents' at GLOBAL scope — see
|
|
// capabilities/codex/capability.json), a fresh install landed skills under
|
|
// the override root while a surface re-stage created a SECOND tree under
|
|
// the runtime's own configDir.
|
|
//
|
|
// These tests exercise the REAL applySurface code path (not a hand-copy of
|
|
// its destination formula) so a future regression that re-diverges the two
|
|
// writers is caught by actual file placement, not by a self-consistent
|
|
// re-derivation of the (possibly still-buggy) formula.
|
|
describe('skills-kind destination parity: installer vs surface-apply (#2911)', () => {
|
|
const runtimeArtifactInstallPlan = require('../msd-core/bin/lib/runtime-artifact-install-plan.cjs');
|
|
const capabilityRegistry = require('../msd-core/bin/lib/capability-registry.cjs');
|
|
|
|
const RUNTIME_IDS = Object.keys(capabilityRegistry.runtimes);
|
|
const PARITY_MANIFEST = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
|
|
// os.homedir() on POSIX/Windows reads HOME/USERPROFILE from the environment
|
|
// (Node docs), so redirecting it here for the duration of a test is safe and
|
|
// avoids ever touching the real developer home directory even though the
|
|
// only current `home`-override runtime (codex) resolves via os.homedir().
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Runtimes whose registry entry declares a `home` override on any kind —
|
|
// stated explicitly per the #2911 acceptance criteria, not hidden. Recomputed
|
|
// from the registry (not hardcoded) so a newly-added override is picked up.
|
|
function runtimesWithHomeOverride() {
|
|
const found = [];
|
|
for (const runtime of RUNTIME_IDS) {
|
|
for (const scope of ['global', 'local']) {
|
|
let layout;
|
|
try {
|
|
layout = resolveRuntimeArtifactLayout(runtime, '/tmp/fake-config-dir-2911', scope);
|
|
} catch {
|
|
continue;
|
|
}
|
|
if (layout.kinds.some((k) => typeof k.home === 'string' && k.home !== '')) {
|
|
found.push(`${runtime}/${scope}`);
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
test('registry home-override discrimination report (#2911)', () => {
|
|
const overrides = runtimesWithHomeOverride();
|
|
// Stated per the brief: at time of writing only codex/global had a `home`
|
|
// override; #3738 added antigravity/global (skills AND agents →
|
|
// ~/.gemini/config, the dir AGY scans). This parity test discriminates on
|
|
// exactly these runtime/scope pairs. This assertion documents that fact and
|
|
// fails loudly if the set ever changes shape unexpectedly empty (a
|
|
// discrimination-less parity test would be silently vacuous).
|
|
assert.ok(overrides.length > 0, 'expected at least one runtime/scope with a home override (codex/global)');
|
|
assert.deepStrictEqual(overrides, ['antigravity/global', 'codex/global'], `home-override set changed — update this test's documentation. Found: ${overrides.join(', ')}`);
|
|
});
|
|
|
|
for (const scope of ['global', 'local']) {
|
|
test(`applySurface writes skills-kind output to the SAME destination the installer would use, for every runtime in the registry (${scope})`, (t) => {
|
|
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2911-fakehome-'));
|
|
t.after(() => cleanup(fakeHome));
|
|
|
|
const failures = [];
|
|
let discriminatingRuntimes = 0;
|
|
|
|
for (const runtime of RUNTIME_IDS) {
|
|
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), `msd-2911-parity-${runtime}-${scope}-`));
|
|
t.after(() => cleanup(configDir));
|
|
writePackageSourceMarkerFixture(configDir);
|
|
|
|
withFakeHome(fakeHome, () => {
|
|
let layout;
|
|
try {
|
|
layout = resolveRuntimeArtifactLayout(runtime, configDir, scope);
|
|
} catch {
|
|
return; // runtime/scope combination not supported
|
|
}
|
|
const skillsKind = layout.kinds.find((k) => k.kind === 'skills');
|
|
if (!skillsKind) return; // e.g. cline/kimi at local scope: no skills kind
|
|
|
|
if (typeof skillsKind.home === 'string' && skillsKind.home !== '') {
|
|
discriminatingRuntimes++;
|
|
}
|
|
|
|
writeActiveProfile(configDir, 'core');
|
|
writeSurface(configDir, {
|
|
baseProfile: 'core',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
|
|
applySurface(configDir, layout, PARITY_MANIFEST, CLUSTERS);
|
|
|
|
// The installer's REAL destination-selection formula (verbatim from
|
|
// createRuntimeArtifactInstallPlan / _copyStaged): honor kind.home as
|
|
// a FALLBACK-preferred override, else configDir.
|
|
const installerDest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(
|
|
skillsKind.home ?? layout.configDir,
|
|
skillsKind.destSubpath,
|
|
);
|
|
|
|
if (!fs.existsSync(installerDest) || fs.readdirSync(installerDest).length === 0) {
|
|
failures.push(
|
|
`${runtime}/${scope}: applySurface did NOT write skills-kind output to the installer's ` +
|
|
`destination "${installerDest}" — surface-apply and installer destination formulas have diverged.`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// If the runtime declares a home override, the OLD buggy root
|
|
// (configDir/destSubpath) must not have ALSO been populated —
|
|
// otherwise a re-stage creates a second, stale tree (#2911 symptom).
|
|
if (typeof skillsKind.home === 'string' && skillsKind.home !== '') {
|
|
const legacyDest = path.join(layout.configDir, skillsKind.destSubpath);
|
|
if (legacyDest !== installerDest && fs.existsSync(legacyDest) && fs.readdirSync(legacyDest).length > 0) {
|
|
failures.push(
|
|
`${runtime}/${scope}: applySurface ALSO wrote a second tree at the legacy location ` +
|
|
`"${legacyDest}" (kind.home override "${skillsKind.home}" was not honored consistently).`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
assert.deepStrictEqual(failures, [], `#2911 parity failures (${scope}):\n${failures.join('\n')}`);
|
|
// Not a hard requirement, but surfaces how many runtime/scope pairs this
|
|
// run actually discriminated on (i.e. exercised a non-trivial home
|
|
// override), matching the acceptance-criteria ask to state this plainly.
|
|
t.diagnostic(`${scope}: ${discriminatingRuntimes} runtime(s) discriminated on a home override`);
|
|
});
|
|
}
|
|
});
|
|
|
|
// ─── #2911: codex-specific regression (skills-kind home override) ───────────
|
|
describe('codex skills-kind destination: home override (#2911)', () => {
|
|
const runtimeArtifactInstallPlan = require('../msd-core/bin/lib/runtime-artifact-install-plan.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: applySurface stages skills under $HOME/.agents, NOT under $CODEX_HOME (#2911 AC)', (t) => {
|
|
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2911-codex-home-'));
|
|
t.after(() => cleanup(fakeHome));
|
|
const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2911-codex-home-dir-'));
|
|
t.after(() => cleanup(codexHome));
|
|
writePackageSourceMarkerFixture(codexHome);
|
|
|
|
withFakeHome(fakeHome, () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
writeActiveProfile(codexHome, 'core');
|
|
writeSurface(codexHome, {
|
|
baseProfile: 'core',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
const layout = resolveRuntimeArtifactLayout('codex', codexHome, 'global');
|
|
const skillsKind = layout.kinds.find((k) => k.kind === 'skills');
|
|
assert.ok(skillsKind, 'pre-condition: codex global layout has a skills kind');
|
|
assert.strictEqual(skillsKind.home, path.join(fakeHome, '.agents'), 'pre-condition: codex skills kind declares the $HOME/.agents override');
|
|
|
|
applySurface(codexHome, layout, manifest, CLUSTERS);
|
|
|
|
const expectedDest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(
|
|
path.join(fakeHome, '.agents'),
|
|
skillsKind.destSubpath,
|
|
);
|
|
assert.ok(
|
|
fs.existsSync(expectedDest) && fs.readdirSync(expectedDest).length > 0,
|
|
`#2911: codex global surface-apply must stage skills under $HOME/.agents (expected non-empty dir at ${expectedDest})`,
|
|
);
|
|
|
|
const wrongDest = path.join(codexHome, skillsKind.destSubpath);
|
|
assert.ok(
|
|
!fs.existsSync(wrongDest) || fs.readdirSync(wrongDest).length === 0,
|
|
`#2911: codex global surface-apply must NOT create a second skills tree under $CODEX_HOME (found populated dir at ${wrongDest})`,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('codex + local: no home override — surface-apply destination is unchanged (#2911 AC3)', (t) => {
|
|
const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2911-codex-local-'));
|
|
t.after(() => cleanup(codexHome));
|
|
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
writeActiveProfile(codexHome, 'core');
|
|
writeSurface(codexHome, {
|
|
baseProfile: 'core',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
const layout = resolveRuntimeArtifactLayout('codex', codexHome, 'local');
|
|
const skillsKind = layout.kinds.find((k) => k.kind === 'skills');
|
|
assert.ok(skillsKind, 'pre-condition: codex local layout has a skills kind');
|
|
assert.strictEqual(skillsKind.home, undefined, 'pre-condition (AC3): codex local scope declares NO home override');
|
|
|
|
applySurface(codexHome, layout, manifest, CLUSTERS);
|
|
|
|
const expectedDest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(codexHome, skillsKind.destSubpath);
|
|
assert.ok(
|
|
fs.existsSync(expectedDest) && fs.readdirSync(expectedDest).length > 0,
|
|
`#2911 AC3: codex local surface-apply must stage skills under $CODEX_HOME (expected non-empty dir at ${expectedDest})`,
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── installOpencodeFamilySkills destination parity (#2911 sibling coverage) ─
|
|
//
|
|
// installOpencodeFamilySkills (src/install-engine.cts) is a FOURTH destination-
|
|
// computation writer, alongside _copyStaged, the inline guard in
|
|
// installRuntimeArtifacts, and createRuntimeArtifactUninstallPlan — all three of
|
|
// which honor `skillsKindEntry.home ?? <install root>`. This writer originally did
|
|
// not, and would silently reproduce the #2911 duplicate-tree symptom the moment
|
|
// any combined-family runtime (opencode, kilo) gains a `home` override. Neither
|
|
// declares one today, so this test exercises the REAL production code path
|
|
// (installOpencodeFamilySkills, via the module-ref call convention documented at
|
|
// src/install-engine.cts:37-38) under a synthetic `home` override injected by
|
|
// monkeypatching resolveRuntimeArtifactLayout's shared module export — the same
|
|
// object install-engine.cjs calls through at runtime — rather than re-deriving
|
|
// the destination formula by hand. This proves discrimination even though no
|
|
// registry runtime exercises it yet, and will catch a future divergence the
|
|
// moment a combined-family runtime's descriptor grows a `home` override.
|
|
describe('installOpencodeFamilySkills destination parity (#2911 sibling coverage)', () => {
|
|
const installEngine = require('../msd-core/bin/lib/install-engine.cjs');
|
|
const runtimeArtifactLayoutModule = require('../msd-core/bin/lib/runtime-artifact-layout.cjs');
|
|
|
|
function stageRawCommands(runtime, configDir) {
|
|
const layout = resolveRuntimeArtifactLayout(runtime, configDir, 'global');
|
|
const commandsKind = layout.kinds.find((k) => k.kind === 'commands');
|
|
return commandsKind.stage(resolveProfile({ modes: ['core'], manifest: loadSkillsManifest(REAL_COMMANDS_DIR) }));
|
|
}
|
|
|
|
for (const runtime of ['opencode', 'kilo']) {
|
|
test(`${runtime}: installOpencodeFamilySkills honors a skills-kind home override instead of always resolving against configDir (#2911 sibling)`, (t) => {
|
|
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), `msd-2911-ocfs-${runtime}-`));
|
|
const fakeHomeOverride = fs.mkdtempSync(path.join(os.tmpdir(), `msd-2911-ocfs-home-${runtime}-`));
|
|
t.after(() => { cleanup(configDir); cleanup(fakeHomeOverride); });
|
|
writePackageSourceMarkerFixture(configDir);
|
|
// #3712 — this row drives a skills-kind `home` override on purpose, which is
|
|
// exactly what the test-home guard exists to police, so it has to declare the
|
|
// sandbox rather than rely on the destination happening to sit outside the
|
|
// real home. On POSIX it does (os.tmpdir() is /tmp or /var/folders); on
|
|
// Windows os.tmpdir() is under %USERPROFILE%, so without this the guard
|
|
// correctly refuses and the row fails on Windows only. HOME is the override
|
|
// itself, which is the home this call actually writes under.
|
|
sandboxHome(t, fakeHomeOverride);
|
|
|
|
const originalResolve = runtimeArtifactLayoutModule.resolveRuntimeArtifactLayout;
|
|
// Capture the real destSubpath before patching so the assertion below
|
|
// never hardcodes a literal path fragment.
|
|
const realLayout = originalResolve(runtime, configDir, 'global');
|
|
const skillsKindReal = realLayout.kinds.find((k) => k.kind === 'skills');
|
|
assert.ok(skillsKindReal, `pre-condition: ${runtime} global layout has a skills kind`);
|
|
|
|
runtimeArtifactLayoutModule.resolveRuntimeArtifactLayout = function (rt, targetDir, scope) {
|
|
const layout = originalResolve(rt, targetDir, scope);
|
|
if (rt === runtime) {
|
|
const patchedKinds = layout.kinds.map((k) => (k.kind === 'skills' ? { ...k, home: fakeHomeOverride } : k));
|
|
return { ...layout, kinds: patchedKinds };
|
|
}
|
|
return layout;
|
|
};
|
|
|
|
try {
|
|
const raw = stageRawCommands(runtime, configDir);
|
|
const count = installEngine.installOpencodeFamilySkills(runtime, configDir, raw, `${configDir}/`);
|
|
assert.ok(count >= 1, `${runtime}: installOpencodeFamilySkills should report installed skills`);
|
|
|
|
const overrideDest = path.join(fakeHomeOverride, skillsKindReal.destSubpath);
|
|
assert.ok(
|
|
fs.existsSync(overrideDest) && fs.readdirSync(overrideDest).length > 0,
|
|
`${runtime}: installOpencodeFamilySkills must write skills-kind output under the home override ` +
|
|
`"${overrideDest}" — it must not always resolve against configDir.`,
|
|
);
|
|
|
|
// If the home override is not honored, output lands under the legacy
|
|
// configDir/destSubpath location instead (the #2911 duplicate-tree symptom).
|
|
const legacyDest = path.join(configDir, skillsKindReal.destSubpath);
|
|
assert.ok(
|
|
!fs.existsSync(legacyDest) || fs.readdirSync(legacyDest).length === 0,
|
|
`${runtime}: installOpencodeFamilySkills must NOT ALSO write a second tree at the legacy location ` +
|
|
`"${legacyDest}" once a home override is declared.`,
|
|
);
|
|
} finally {
|
|
runtimeArtifactLayoutModule.resolveRuntimeArtifactLayout = originalResolve;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// ────────────────────────────────────────────────────────────────────────
|
|
// Folded from tests/issue-69-surface-keeps-nested.test.cjs — consolidation epic #1969 (H3 #3336)
|
|
// ────────────────────────────────────────────────────────────────────────
|
|
{
|
|
const { describe: __foldDescribe } = require('node:test');
|
|
__foldDescribe('folded:issue-69-surface-keeps-nested', () => {
|
|
|
|
// #69 regression, folded from issue-69-surface-keeps-nested.test.cjs:
|
|
// stageSkillsForRuntimeAsSkills gated nesting on `resolvedProfile.skills === '*'`
|
|
// (the sentinel). applySurface → resolveSurface materializes the full profile
|
|
// into a concrete Set<string>, so the sentinel check was never true on the
|
|
// surface path, causing applySurface to re-flatten a nested install. Fix
|
|
// (install-profiles.cts): gate nesting on full OR full-equivalent (all
|
|
// routerStems present in the concrete Set).
|
|
//
|
|
// #924: Claude was reverted to FLAT, so the claude case below asserts the
|
|
// flat layout is preserved (not re-nested) rather than a nested one.
|
|
describe('issue-69: applySurface preserves nested skill layout (no re-flatten)', () => {
|
|
test('cline global full: applySurface keeps 6 router dirs and nested msd-ns-manage/skills/help/SKILL.md', (t) => {
|
|
const installed = runMinimalInstall({ runtime: 'cline', scope: 'global' });
|
|
const dir = installed.configDir;
|
|
t.after(() => { try { cleanup(installed.root); } catch { /* best-effort */ } });
|
|
|
|
// Step 1: full install
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
|
|
const skillsDir = path.join(dir, 'skills');
|
|
|
|
// Sanity: install must produce nested layout (6 top-level router dirs)
|
|
const topLevelAfterInstall = fs.readdirSync(skillsDir).filter((n) => n.startsWith('msd-'));
|
|
assert.strictEqual(
|
|
topLevelAfterInstall.length,
|
|
6,
|
|
`Install must produce exactly 6 msd-* top-level dirs (routers). Got ${topLevelAfterInstall.length}: [${topLevelAfterInstall.join(', ')}]`,
|
|
);
|
|
assert.ok(
|
|
fs.existsSync(path.join(skillsDir, 'msd-ns-workflow', 'skills', 'plan-phase', 'SKILL.md')),
|
|
'After install: msd-ns-workflow/skills/plan-phase/SKILL.md must exist',
|
|
);
|
|
|
|
// Step 2: applySurface (full surface, no surface state file → resolves to full)
|
|
const layout = resolveRuntimeArtifactLayout('cline', dir, 'global');
|
|
applySurface(dir, layout, manifest);
|
|
|
|
// Step 3: assert nested layout is preserved after applySurface
|
|
const topLevelAfterSurface = fs.readdirSync(skillsDir).filter((n) => n.startsWith('msd-'));
|
|
assert.strictEqual(
|
|
topLevelAfterSurface.length,
|
|
6,
|
|
`After applySurface: expected exactly 6 msd-* top-level dirs (routers only). Got ${topLevelAfterSurface.length}: [${topLevelAfterSurface.join(', ')}]. ` +
|
|
'Re-flattening detected: applySurface must preserve nested layout (#69 regression).',
|
|
);
|
|
|
|
// The nested SKILL.md must still exist (not re-flattened to top-level concrete dir)
|
|
assert.ok(
|
|
fs.existsSync(path.join(skillsDir, 'msd-ns-workflow', 'skills', 'plan-phase', 'SKILL.md')),
|
|
'After applySurface: msd-ns-workflow/skills/plan-phase/SKILL.md must still exist (nested layout preserved)',
|
|
);
|
|
|
|
// The concrete skill must NOT have been promoted to a top-level flat dir
|
|
assert.ok(
|
|
!fs.existsSync(path.join(skillsDir, 'msd-plan-phase', 'SKILL.md')),
|
|
'After applySurface: msd-plan-phase/ must NOT exist at top level (#69 re-flatten regression guard)',
|
|
);
|
|
});
|
|
|
|
// #924 companion: Claude must use FLAT layout and applySurface must NOT re-nest it.
|
|
test('claude global full: install produces flat layout and applySurface preserves it (#924)', (t) => {
|
|
const installed = runMinimalInstall({ runtime: 'claude', scope: 'global' });
|
|
const dir = installed.configDir;
|
|
t.after(() => { try { cleanup(installed.root); } catch { /* best-effort */ } });
|
|
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
|
|
const skillsDir = path.join(dir, 'skills');
|
|
|
|
// Install must produce FLAT layout (>= 60 msd-* dirs)
|
|
const topLevelAfterInstall = fs.readdirSync(skillsDir).filter((n) => n.startsWith('msd-'));
|
|
assert.ok(
|
|
topLevelAfterInstall.length >= 60,
|
|
`Claude install must produce >= 60 msd-* top-level dirs (flat, #924). Got ${topLevelAfterInstall.length}.`,
|
|
);
|
|
|
|
// msd-plan-phase must be directly at top level
|
|
assert.ok(
|
|
fs.existsSync(path.join(skillsDir, 'msd-plan-phase', 'SKILL.md')),
|
|
'After claude install: msd-plan-phase/SKILL.md must be at top level (flat layout, #924)',
|
|
);
|
|
|
|
// No nested skills/ subdirs under msd-ns-* in Claude
|
|
assert.ok(
|
|
!fs.existsSync(path.join(skillsDir, 'msd-ns-workflow', 'skills')),
|
|
'After claude install: msd-ns-workflow/skills/ must NOT exist (flat layout, no nesting, #924)',
|
|
);
|
|
|
|
// applySurface must preserve flat layout
|
|
const layout = resolveRuntimeArtifactLayout('claude', dir, 'global');
|
|
applySurface(dir, layout, manifest);
|
|
|
|
const topLevelAfterSurface = fs.readdirSync(skillsDir).filter((n) => n.startsWith('msd-'));
|
|
assert.ok(
|
|
topLevelAfterSurface.length >= 60,
|
|
`After applySurface: claude must still have >= 60 msd-* dirs (flat preserved). Got ${topLevelAfterSurface.length}.`,
|
|
);
|
|
|
|
assert.ok(
|
|
fs.existsSync(path.join(skillsDir, 'msd-plan-phase', 'SKILL.md')),
|
|
'After applySurface: msd-plan-phase/SKILL.md must remain at top level (#924)',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// ─── #4211: kimi-agents materialization ─────────────────────────────────────
|
|
//
|
|
// Kimi's managed tree is `agents/msd.yaml` + `agents/msd.md` +
|
|
// `agents/subagents/msd-*.{yaml,md}` (runtime-artifact-layout.cts
|
|
// kimiAgentsKind), and install copies it recursively (_copyStaged in
|
|
// src/install-engine.cts). Surface apply fell through to the flat
|
|
// command/agent branch of _syncMsdDir, which reads only top-level `*.md`: it
|
|
// ignored the YAML half and the subagents/ subtree, and rewrote `msd.md` as
|
|
// `msdmsd.md` (the flat branch re-applies kind.prefix to a name that already
|
|
// carries it) — corrupting Kimi's installed artifacts while still exiting 0.
|
|
|
|
describe('#4211: applySurface materializes the kimi-agents kind like a fresh install', () => {
|
|
function kimiTree(dir, base = dir) {
|
|
if (!fs.existsSync(dir)) return [];
|
|
let out = [];
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) out = out.concat(kimiTree(full, base));
|
|
else out.push(path.relative(base, full).split(path.sep).join('/'));
|
|
}
|
|
return out.sort();
|
|
}
|
|
|
|
function installKimi(t) {
|
|
const installed = runMinimalInstall({ runtime: 'kimi', scope: 'global' });
|
|
t.after(() => { try { cleanup(installed.root); } catch { /* best-effort */ } });
|
|
return { configDir: installed.configDir, agentsDir: path.join(installed.configDir, 'agents') };
|
|
}
|
|
|
|
test('the materialized tree is identical to the installed one — no msdmsd.md, no dropped YAML', (t) => {
|
|
const { configDir, agentsDir } = installKimi(t);
|
|
|
|
const before = kimiTree(agentsDir);
|
|
assert.ok(before.includes('msd.yaml'), 'precondition: install writes agents/msd.yaml');
|
|
assert.ok(before.includes('msd.md'), 'precondition: install writes agents/msd.md');
|
|
assert.ok(before.some((f) => f.startsWith('subagents/msd-') && f.endsWith('.yaml')),
|
|
'precondition: install writes agents/subagents/msd-*.yaml');
|
|
const rootPromptBefore = fs.readFileSync(path.join(agentsDir, 'msd.md'), 'utf8');
|
|
|
|
const layout = resolveRuntimeArtifactLayout('kimi', configDir, 'global');
|
|
applySurface(configDir, layout, realManifest(), CLUSTERS);
|
|
|
|
const after = kimiTree(agentsDir);
|
|
assert.deepEqual(after, before,
|
|
'surface apply must produce the same managed artifact tree as the install it re-stages');
|
|
assert.ok(!after.includes('msdmsd.md'), 'the root prompt must not be re-prefixed into msdmsd.md');
|
|
assert.equal(fs.readFileSync(path.join(agentsDir, 'msd.md'), 'utf8'), rootPromptBefore,
|
|
'the root prompt content must survive re-materialization');
|
|
});
|
|
|
|
test('user-owned files under agents/ are preserved', (t) => {
|
|
const { configDir, agentsDir } = installKimi(t);
|
|
|
|
const userRoot = path.join(agentsDir, 'my-own-agent.yaml');
|
|
const userSub = path.join(agentsDir, 'subagents', 'my-own-subagent.yaml');
|
|
const userNote = path.join(agentsDir, 'subagents', 'notes.txt');
|
|
fs.writeFileSync(userRoot, 'name: mine\n');
|
|
fs.writeFileSync(userSub, 'name: mine-sub\n');
|
|
fs.writeFileSync(userNote, 'scratch\n');
|
|
|
|
const layout = resolveRuntimeArtifactLayout('kimi', configDir, 'global');
|
|
applySurface(configDir, layout, realManifest(), CLUSTERS);
|
|
|
|
for (const file of [userRoot, userSub, userNote]) {
|
|
assert.ok(fs.existsSync(file), `${path.basename(file)} is user-owned and must survive surface apply`);
|
|
}
|
|
});
|
|
|
|
test('a MSD subagent the surface no longer stages is pruned', (t) => {
|
|
const { configDir, agentsDir } = installKimi(t);
|
|
|
|
// Shaped exactly like a subagent an earlier version staged and this one
|
|
// does not — the case install's _removeMsdEntries prunes for this kind.
|
|
const retiredYaml = path.join(agentsDir, 'subagents', 'msd-retired-agent.yaml');
|
|
const retiredPrompt = path.join(agentsDir, 'subagents', 'msd-retired-agent.md');
|
|
fs.writeFileSync(retiredYaml, 'name: msd-retired-agent\n');
|
|
fs.writeFileSync(retiredPrompt, '# retired\n');
|
|
|
|
const layout = resolveRuntimeArtifactLayout('kimi', configDir, 'global');
|
|
applySurface(configDir, layout, realManifest(), CLUSTERS);
|
|
|
|
assert.equal(fs.existsSync(retiredYaml), false, 'a stale MSD subagent must be pruned');
|
|
assert.equal(fs.existsSync(retiredPrompt), false, 'a stale MSD subagent prompt must be pruned');
|
|
assert.ok(fs.existsSync(path.join(agentsDir, 'msd.yaml')), 'the live root agent must remain');
|
|
});
|
|
});
|