Files
msd-core/tests/install-hermes-regressions.test.cjs
Tom Boucher f970c09bf3 refactor(3664): migrate bin/install.js install/uninstall to Runtime Artifact Layout Module (#3674)
Phase 2 of #3660 / ADR-3660. Routes both lifecycle verbs through the
Runtime Artifact Layout Module landed in Phase 1 (#3663):

- Add installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile)
  and uninstallRuntimeArtifacts(runtime, configDir, scope) as the public
  orchestrators. Both pre-prune stale gsd-* entries before staged copy;
  installRuntimeArtifacts brackets the prune+copy with preserveUserArtifacts /
  restoreUserArtifacts so user-owned content (e.g. gsd-dev-preferences) survives
  wipe-and-replace for claude/qwen/hermes runtimes.
- Add applyRuntimeContentRewritesInPlace as the per-runtime path/branding
  post-stage step (preserves byte-output equivalence with the legacy
  copyCommandsAs* pipeline, including Qwen/Hermes branding rewrites).
- Add _copyStaged, _removeGsdEntries kind-aware filesystem helpers.
- Add _runLegacyInstallMigrations, _runLegacyUninstallCleanup as thin
  dispatchers over existing ADR-0008 legacy migrations (Hermes flat->nested
  per #2841, dev-preferences-as-skill per #2973). For Hermes, also clean up
  the intermediate skills/gsd/gsd-*/ layout that pre-Phase-2 installs left
  on disk.
- Delete the 9 copyCommandsAs*Skills functions (Codex / Cursor / Windsurf /
  Trae / CodeBuddy / Copilot / Claude / Antigravity / Augment) and the
  _copyCommandsAsSkillsViaConverter helper. All test entry points migrated
  to call installRuntimeArtifacts directly through the unified seam.
- Collapse the 9-branch uninstall ladder to one uninstallRuntimeArtifacts
  call plus preserved non-layout side-effects (Codex TOML, Copilot
  instructions, hooks).
- Unify install dispatcher: a single _isSkillsRuntime gate routes all 11
  skills runtimes through installRuntimeArtifacts for both full and core/
  minimal profiles. Removes 11 per-runtime if-else branches (3 minimal-mode
  shim branches + 8 dead after-the-gate branches).

Net delta on bin/install.js: 11,495 -> 11,174 (-321 LOC).

New tests:
- tests/install-uninstall-layout-loop.test.cjs (34 tests) - per-runtime
  fixture assertions on install/uninstall/legacy-migration ordering.
- tests/install-hermes-regressions.test.cjs (6 tests) - covers the six
  defects surfaced by iterative review: Hermes upgrade leaves stale dirs,
  --hermes --profile=core fall-through, --qwen --profile=core fall-through,
  minimal-mode dev-preferences migration skipped (Hermes/Qwen/Claude-global),
  and ordering bug in _runLegacyInstallMigrations.

Existing tests (10,038 prior + 40 new) all green: 10,078/10,078 pass.

Refs #3664

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:00:59 -04:00

509 lines
21 KiB
JavaScript

'use strict';
/**
* Regression tests for two confirmed defects from commit 6c676dbc (#3664)
* and three legacy-migration gaps caught in cc2ecb97 (#3664 Phase 2):
*
* Defect #1 — Hermes upgrade leaves stale skills/gsd/gsd-<stem>/ dirs
* _runLegacyInstallMigrations only removed pre-#2841 flat skills/gsd-slash-star
* dirs; it did not remove the intermediate skills/gsd/gsd-slash-star layout that
* existed between #2841 and #3664.
*
* Defect #2 — `--hermes --profile=core` falls through to wrong path
* The dispatcher's minimal-mode block had no `isHermes && _isCoreProfileAlias`
* branch, so Hermes + core fell to the terminal `else` (Claude local path),
* writing commands/gsd/<stem>.md instead of skills/gsd/.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const { createTempDir, cleanup } = require('./helpers.cjs');
const {
loadSkillsManifest,
resolveProfile,
} = require('../get-shit-done/bin/lib/install-profiles.cjs');
// Load install exports via GSD_TEST_MODE to skip CLI main()
const savedTestMode = process.env.GSD_TEST_MODE;
process.env.GSD_TEST_MODE = '1';
let installExports;
try {
installExports = require('../bin/install.js');
} finally {
if (savedTestMode === undefined) delete process.env.GSD_TEST_MODE;
else process.env.GSD_TEST_MODE = savedTestMode;
}
const { installRuntimeArtifacts, uninstallRuntimeArtifacts } = installExports || {};
const INSTALL_SCRIPT = path.join(__dirname, '..', 'bin', 'install.js');
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
const MANIFEST = loadSkillsManifest(REAL_COMMANDS_DIR);
const RESOLVED_CORE = resolveProfile({ modes: ['core'], manifest: MANIFEST });
// ---------------------------------------------------------------------------
// Test A1 — Defect #1 regression: stale skills/gsd/gsd-*/ dirs are removed
// ---------------------------------------------------------------------------
describe('Defect #1 regression: _runLegacyInstallMigrations removes skills/gsd/gsd-*/ layout', () => {
test('installRuntimeArtifacts removes intermediate skills/gsd/gsd-*/ dirs and writes new bare-stem layout', (t) => {
const configDir = createTempDir('gsd-hermes-reg1-');
t.after(() => cleanup(configDir));
assert.strictEqual(
typeof installRuntimeArtifacts,
'function',
'installRuntimeArtifacts must be exported from bin/install.js',
);
// Pre-create the intermediate Hermes layout (between #2841 and #3664):
// skills/gsd/gsd-help/SKILL.md and skills/gsd/gsd-plan/SKILL.md
const nestedGsdDir = path.join(configDir, 'skills', 'gsd');
fs.mkdirSync(path.join(nestedGsdDir, 'gsd-help'), { recursive: true });
fs.writeFileSync(path.join(nestedGsdDir, 'gsd-help', 'SKILL.md'), '# legacy help\n');
fs.mkdirSync(path.join(nestedGsdDir, 'gsd-plan'), { recursive: true });
fs.writeFileSync(path.join(nestedGsdDir, 'gsd-plan', 'SKILL.md'), '# legacy plan\n');
// Create a sibling non-gsd dir inside skills/gsd/ that must survive
const userContentDir = path.join(nestedGsdDir, 'user-content');
fs.mkdirSync(userContentDir, { recursive: true });
fs.writeFileSync(path.join(userContentDir, 'SKILL.md'), '# user content\n');
// Run the unified install
installRuntimeArtifacts('hermes', configDir, 'global', RESOLVED_CORE);
// Stale intermediate dirs must be gone
assert.ok(
!fs.existsSync(path.join(nestedGsdDir, 'gsd-help')),
'skills/gsd/gsd-help/ must be removed by install migration (Defect #1)',
);
assert.ok(
!fs.existsSync(path.join(nestedGsdDir, 'gsd-plan')),
'skills/gsd/gsd-plan/ must be removed by install migration (Defect #1)',
);
// New bare-stem layout must exist
assert.ok(
fs.existsSync(path.join(nestedGsdDir, 'help', 'SKILL.md')),
'skills/gsd/help/SKILL.md must exist after install (new bare-stem layout)',
);
// User content in skills/gsd/ must be preserved (user-content has no gsd- prefix)
assert.ok(
fs.existsSync(path.join(userContentDir, 'SKILL.md')),
'skills/gsd/user-content/SKILL.md must be preserved (user content)',
);
});
});
// ---------------------------------------------------------------------------
// Test Q1 — Qwen variant of Defect #2: --qwen --profile=core uses skills/gsd-*/ path
// ---------------------------------------------------------------------------
describe('Qwen Defect #2 regression: --qwen --profile=core writes skills/gsd-*/, not commands/gsd/', () => {
test('spawn node bin/install.js --qwen --global --profile=core: skills/gsd-*/ written, no commands/gsd/', (t) => {
const root = createTempDir('gsd-qwen-reg2-');
t.after(() => cleanup(root));
const result = spawnSync(
process.execPath,
[INSTALL_SCRIPT, '--qwen', '--global', '--config-dir', root, '--profile=core'],
{
encoding: 'utf8',
env: { ...process.env, HOME: root, USERPROFILE: root },
},
);
assert.strictEqual(
result.status,
0,
`installer exited with status ${result.status}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
);
// skills/ must exist and contain at least one gsd-<stem>/ dir with SKILL.md
const qwenSkillsDir = path.join(root, 'skills');
assert.ok(
fs.existsSync(qwenSkillsDir),
`skills/ must exist after --qwen --profile=core install (got: ${result.stdout})`,
);
const skillDirs = fs.readdirSync(qwenSkillsDir, { withFileTypes: true })
.filter((e) => e.isDirectory() && e.name.startsWith('gsd-'));
assert.ok(
skillDirs.length >= 1,
`skills/ must contain at least one gsd-* skill dir, got: ${
fs.readdirSync(qwenSkillsDir, { withFileTypes: true }).map((e) => e.name).join(', ')
}`,
);
// Verify at least one gsd-* dir has SKILL.md
const hasSkillMd = skillDirs.some((e) =>
fs.existsSync(path.join(qwenSkillsDir, e.name, 'SKILL.md')),
);
assert.ok(hasSkillMd, 'at least one skills/gsd-*/SKILL.md must exist');
// commands/gsd/*.md must NOT exist (the regression path that Defect #2 causes for Qwen)
const commandsGsd = path.join(root, 'commands', 'gsd');
if (fs.existsSync(commandsGsd)) {
const mdFiles = fs.readdirSync(commandsGsd).filter((f) => f.endsWith('.md'));
assert.strictEqual(
mdFiles.length,
0,
`commands/gsd/ must not contain .md files after qwen install (Qwen Defect #2). Found: ${mdFiles.join(', ')}`,
);
}
});
});
// ---------------------------------------------------------------------------
// Test A2 — Defect #2 regression: --hermes --profile=core uses skills/gsd/ path
// ---------------------------------------------------------------------------
describe('Defect #2 regression: --hermes --profile=core writes skills/gsd/, not commands/gsd/', () => {
test('spawn node bin/install.js --hermes --global --profile=core: skills/gsd/ written, no commands/gsd/', (t) => {
const root = createTempDir('gsd-hermes-reg2-');
t.after(() => cleanup(root));
const result = spawnSync(
process.execPath,
[INSTALL_SCRIPT, '--hermes', '--global', '--config-dir', root, '--profile=core'],
{
encoding: 'utf8',
env: { ...process.env, HOME: root, USERPROFILE: root },
},
);
assert.strictEqual(
result.status,
0,
`installer exited with status ${result.status}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
);
// skills/gsd/ must exist and contain at least one <stem>/SKILL.md file
const hermesSkillsGsd = path.join(root, 'skills', 'gsd');
assert.ok(
fs.existsSync(hermesSkillsGsd),
`skills/gsd/ must exist after --hermes --profile=core install (got: ${result.stdout})`,
);
const skillDirs = fs.readdirSync(hermesSkillsGsd, { withFileTypes: true })
.filter((e) => e.isDirectory());
assert.ok(
skillDirs.length >= 1,
`skills/gsd/ must contain at least one skill dir, got: ${skillDirs.map((e) => e.name).join(', ')}`,
);
// Verify at least one skill dir has SKILL.md
const hasSkillMd = skillDirs.some((e) =>
fs.existsSync(path.join(hermesSkillsGsd, e.name, 'SKILL.md')),
);
assert.ok(hasSkillMd, 'at least one skills/gsd/<stem>/SKILL.md must exist');
// commands/gsd/*.md must NOT exist (the regression path that Defect #2 caused)
const commandsGsd = path.join(root, 'commands', 'gsd');
if (fs.existsSync(commandsGsd)) {
const mdFiles = fs.readdirSync(commandsGsd).filter((f) => f.endsWith('.md'));
assert.strictEqual(
mdFiles.length,
0,
`commands/gsd/ must not contain .md files after hermes install (Defect #2). Found: ${mdFiles.join(', ')}`,
);
}
});
});
// ---------------------------------------------------------------------------
// Test M1 — Hermes minimal-mode migrates dev-preferences (#2973)
// Hermes uses nested layout (skills/gsd/<stem>/SKILL.md, prefix=''),
// so dev-preferences lands at skills/gsd/dev-preferences/SKILL.md NOT
// skills/gsd-dev-preferences/SKILL.md (the flat layout for qwen/claude-global).
// ---------------------------------------------------------------------------
describe('M1: --hermes --global --profile=core migrates dev-preferences.md → skills/gsd/dev-preferences/SKILL.md', () => {
test('spawn node bin/install.js --hermes --global --profile=core: dev-preferences migrated, source gone', (t) => {
const root = createTempDir('gsd-hermes-m1-');
t.after(() => cleanup(root));
// Pre-create legacy commands/gsd/dev-preferences.md
const legacyDir = path.join(root, 'commands', 'gsd');
fs.mkdirSync(legacyDir, { recursive: true });
fs.writeFileSync(path.join(legacyDir, 'dev-preferences.md'), '# my hermes prefs\n');
const result = spawnSync(
process.execPath,
[INSTALL_SCRIPT, '--hermes', '--global', '--config-dir', root, '--profile=core'],
{
encoding: 'utf8',
env: { ...process.env, HOME: root, USERPROFILE: root },
},
);
assert.strictEqual(
result.status,
0,
`installer exited with status ${result.status}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
);
// Migration target must exist at the HERMES nested location (skills/gsd/dev-preferences/SKILL.md)
// NOT the flat location skills/gsd-dev-preferences/SKILL.md used by qwen/claude-global.
const skillFile = path.join(root, 'skills', 'gsd', 'dev-preferences', 'SKILL.md');
assert.ok(
fs.existsSync(skillFile),
`skills/gsd/dev-preferences/SKILL.md must exist after migration (M1: Hermes nested layout, not flat)`,
);
const content = fs.readFileSync(skillFile, 'utf8');
assert.strictEqual(
content,
'# my hermes prefs\n',
`skills/gsd/dev-preferences/SKILL.md content must equal original, got: ${JSON.stringify(content)}`,
);
// Legacy source must be gone
assert.ok(
!fs.existsSync(path.join(legacyDir, 'dev-preferences.md')),
'commands/gsd/dev-preferences.md must be removed after migration (M1)',
);
});
});
// ---------------------------------------------------------------------------
// Test M2 — Qwen minimal-mode migrates dev-preferences (#2973)
// ---------------------------------------------------------------------------
describe('M2: --qwen --global --profile=core migrates dev-preferences.md → skills/gsd-dev-preferences/SKILL.md', () => {
test('spawn node bin/install.js --qwen --global --profile=core: dev-preferences migrated, source gone', (t) => {
const root = createTempDir('gsd-qwen-m2-');
t.after(() => cleanup(root));
// Pre-create legacy commands/gsd/dev-preferences.md
const legacyDir = path.join(root, 'commands', 'gsd');
fs.mkdirSync(legacyDir, { recursive: true });
fs.writeFileSync(path.join(legacyDir, 'dev-preferences.md'), '# my qwen prefs\n');
const result = spawnSync(
process.execPath,
[INSTALL_SCRIPT, '--qwen', '--global', '--config-dir', root, '--profile=core'],
{
encoding: 'utf8',
env: { ...process.env, HOME: root, USERPROFILE: root },
},
);
assert.strictEqual(
result.status,
0,
`installer exited with status ${result.status}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
);
// Migration target must exist and contain the original content
const skillFile = path.join(root, 'skills', 'gsd-dev-preferences', 'SKILL.md');
assert.ok(
fs.existsSync(skillFile),
`skills/gsd-dev-preferences/SKILL.md must exist after migration (M2: _runLegacyInstallMigrations missing in qwen minimal branch)`,
);
const content = fs.readFileSync(skillFile, 'utf8');
assert.strictEqual(
content,
'# my qwen prefs\n',
`skills/gsd-dev-preferences/SKILL.md content must equal original, got: ${JSON.stringify(content)}`,
);
// Legacy source must be gone
assert.ok(
!fs.existsSync(path.join(legacyDir, 'dev-preferences.md')),
'commands/gsd/dev-preferences.md must be removed after migration (M2)',
);
});
});
// ---------------------------------------------------------------------------
// Test M3 — Claude global minimal-mode migrates dev-preferences (#2973)
// ---------------------------------------------------------------------------
describe('M3: --claude --global --profile=core migrates dev-preferences.md → skills/gsd-dev-preferences/SKILL.md', () => {
test('spawn node bin/install.js --claude --global --profile=core: dev-preferences migrated, source gone', (t) => {
const root = createTempDir('gsd-claude-m3-');
t.after(() => cleanup(root));
// Pre-create legacy commands/gsd/dev-preferences.md
const legacyDir = path.join(root, 'commands', 'gsd');
fs.mkdirSync(legacyDir, { recursive: true });
fs.writeFileSync(path.join(legacyDir, 'dev-preferences.md'), '# my claude prefs\n');
const result = spawnSync(
process.execPath,
[INSTALL_SCRIPT, '--claude', '--global', '--config-dir', root, '--profile=core'],
{
encoding: 'utf8',
env: { ...process.env, HOME: root, USERPROFILE: root },
},
);
assert.strictEqual(
result.status,
0,
`installer exited with status ${result.status}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
);
// Migration target must exist and contain the original content
const skillFile = path.join(root, 'skills', 'gsd-dev-preferences', 'SKILL.md');
assert.ok(
fs.existsSync(skillFile),
`skills/gsd-dev-preferences/SKILL.md must exist after migration (M3: _runLegacyInstallMigrations missing in claude-global-minimal branch)`,
);
const content = fs.readFileSync(skillFile, 'utf8');
assert.strictEqual(
content,
'# my claude prefs\n',
`skills/gsd-dev-preferences/SKILL.md content must equal original, got: ${JSON.stringify(content)}`,
);
// Legacy source must be gone
assert.ok(
!fs.existsSync(path.join(legacyDir, 'dev-preferences.md')),
'commands/gsd/dev-preferences.md must be removed after migration (M3)',
);
});
});
// ---------------------------------------------------------------------------
// Test U1 — Qwen uninstall preserves dev-preferences via migration to skill
// ---------------------------------------------------------------------------
describe('U1: uninstallRuntimeArtifacts qwen preserves dev-preferences.md via migration to skills/gsd-dev-preferences/SKILL.md', () => {
test('uninstallRuntimeArtifacts("qwen"): commands/gsd/ removed, dev-preferences migrated to skills skill', (t) => {
const configDir = createTempDir('gsd-qwen-uninstall-u1-');
t.after(() => cleanup(configDir));
assert.strictEqual(
typeof uninstallRuntimeArtifacts,
'function',
'uninstallRuntimeArtifacts must be exported from bin/install.js',
);
// Pre-create legacy commands/gsd/ with dev-preferences.md and a managed file
const legacyDir = path.join(configDir, 'commands', 'gsd');
fs.mkdirSync(legacyDir, { recursive: true });
fs.writeFileSync(path.join(legacyDir, 'dev-preferences.md'), '# my qwen prefs\n');
fs.writeFileSync(path.join(legacyDir, 'help.md'), '# help content\n');
uninstallRuntimeArtifacts('qwen', configDir, 'global');
// Legacy managed file must be gone (whole commands/gsd/ removed)
assert.ok(
!fs.existsSync(path.join(legacyDir, 'help.md')),
'commands/gsd/help.md must be removed by qwen uninstall (U1)',
);
// dev-preferences.md must NOT be left behind in the legacy dir
assert.ok(
!fs.existsSync(path.join(legacyDir, 'dev-preferences.md')),
'commands/gsd/dev-preferences.md must not exist after qwen uninstall (U1)',
);
// dev-preferences.md must be migrated to skills/gsd-dev-preferences/SKILL.md
const skillFile = path.join(configDir, 'skills', 'gsd-dev-preferences', 'SKILL.md');
assert.ok(
fs.existsSync(skillFile),
'skills/gsd-dev-preferences/SKILL.md must exist after qwen uninstall (U1: #2973 migration)',
);
const content = fs.readFileSync(skillFile, 'utf8');
assert.strictEqual(
content,
'# my qwen prefs\n',
`skills/gsd-dev-preferences/SKILL.md content must equal original, got: ${JSON.stringify(content)}`,
);
});
});
// ---------------------------------------------------------------------------
// Test U2 — Claude-global uninstall preserves dev-preferences via migration to skill
// ---------------------------------------------------------------------------
describe('U2: uninstallRuntimeArtifacts claude/global preserves dev-preferences.md via migration to skills/gsd-dev-preferences/SKILL.md', () => {
test('uninstallRuntimeArtifacts("claude", scope="global"): commands/gsd/ removed, dev-preferences migrated to skills skill', (t) => {
const configDir = createTempDir('gsd-claude-uninstall-u2-');
t.after(() => cleanup(configDir));
assert.strictEqual(
typeof uninstallRuntimeArtifacts,
'function',
'uninstallRuntimeArtifacts must be exported from bin/install.js',
);
// Pre-create legacy commands/gsd/ with dev-preferences.md
const legacyDir = path.join(configDir, 'commands', 'gsd');
fs.mkdirSync(legacyDir, { recursive: true });
fs.writeFileSync(path.join(legacyDir, 'dev-preferences.md'), '# my claude prefs\n');
uninstallRuntimeArtifacts('claude', configDir, 'global');
// dev-preferences.md must NOT be left in the legacy dir
assert.ok(
!fs.existsSync(path.join(legacyDir, 'dev-preferences.md')),
'commands/gsd/dev-preferences.md must not exist after claude-global uninstall (U2)',
);
// dev-preferences.md must be migrated to skills/gsd-dev-preferences/SKILL.md
const skillFile = path.join(configDir, 'skills', 'gsd-dev-preferences', 'SKILL.md');
assert.ok(
fs.existsSync(skillFile),
'skills/gsd-dev-preferences/SKILL.md must exist after claude-global uninstall (U2: #2973 migration)',
);
const content = fs.readFileSync(skillFile, 'utf8');
assert.strictEqual(
content,
'# my claude prefs\n',
`skills/gsd-dev-preferences/SKILL.md content must equal original, got: ${JSON.stringify(content)}`,
);
});
});
// ---------------------------------------------------------------------------
// Test U3 — Hermes uninstall migrates dev-preferences to NESTED location (#2973)
// Proves both Finding 1 (wrong target path) and Finding 2 (unconditional restore).
// ---------------------------------------------------------------------------
describe('U3: uninstallRuntimeArtifacts hermes migrates dev-preferences.md → skills/gsd/dev-preferences/SKILL.md', () => {
test('uninstallRuntimeArtifacts("hermes"): commands/gsd/ NOT recreated, dev-preferences at nested Hermes location', (t) => {
const configDir = createTempDir('gsd-hermes-uninstall-u3-');
t.after(() => cleanup(configDir));
assert.strictEqual(
typeof uninstallRuntimeArtifacts,
'function',
'uninstallRuntimeArtifacts must be exported from bin/install.js',
);
// Pre-create legacy commands/gsd/dev-preferences.md
const legacyDir = path.join(configDir, 'commands', 'gsd');
fs.mkdirSync(legacyDir, { recursive: true });
fs.writeFileSync(path.join(legacyDir, 'dev-preferences.md'), '# my hermes prefs (uninstall path)\n');
uninstallRuntimeArtifacts('hermes', configDir, 'global');
// commands/gsd/dev-preferences.md must be GONE — no recreation after rmSync
assert.ok(
!fs.existsSync(path.join(legacyDir, 'dev-preferences.md')),
'commands/gsd/dev-preferences.md must not exist after hermes uninstall (U3: Finding 2 — no unconditional restore)',
);
// dev-preferences must be at the HERMES nested location (skills/gsd/dev-preferences/SKILL.md)
// NOT the flat location skills/gsd-dev-preferences/SKILL.md (that is qwen/claude-global)
const skillFile = path.join(configDir, 'skills', 'gsd', 'dev-preferences', 'SKILL.md');
assert.ok(
fs.existsSync(skillFile),
'skills/gsd/dev-preferences/SKILL.md must exist after hermes uninstall (U3: Finding 1 — runtime-aware migration target)',
);
const content = fs.readFileSync(skillFile, 'utf8');
assert.strictEqual(
content,
'# my hermes prefs (uninstall path)\n',
`skills/gsd/dev-preferences/SKILL.md content must equal original, got: ${JSON.stringify(content)}`,
);
});
});