refactor: hard-fork GSD -> MSD (Make Software Done)
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.
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
process.env.MSD_TEST_MODE = '1';
|
||||
|
||||
const { describe, test, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
@@ -41,7 +41,7 @@ const RUNTIME_INSTALL_CONTRACTS = {
|
||||
codex: { surface: 'flat-skills', settings: false, hooksPackageJson: true, codexConfig: true },
|
||||
copilot: { surface: 'flat-skills', settings: false, hooksPackageJson: false, copilotInstructions: true },
|
||||
cursor: { surface: 'flat-skills', settings: false, hooksPackageJson: true },
|
||||
gemini: { surface: 'commands-gsd', settings: true, hooksPackageJson: true },
|
||||
gemini: { surface: 'commands-msd', settings: true, hooksPackageJson: true },
|
||||
hermes: { surface: 'hermes-skills', settings: true, hooksPackageJson: true },
|
||||
kimi: { surface: 'kimi-skills-agents', settings: false, hooksPackageJson: false },
|
||||
// #2454: Kimi Code (Node CLI) has NO custom named subagents (per official
|
||||
@@ -53,17 +53,17 @@ const RUNTIME_INSTALL_CONTRACTS = {
|
||||
// (#1821 excluded Kilo on the false premise that it had no plugin surface.)
|
||||
kilo: { surface: 'flat-command', settings: false, hooksPackageJson: true },
|
||||
// #2329: OpenCode discovers commands from the PLURAL `commands/` dir — the
|
||||
// singular `command/` (still correct for Kilo) made all /gsd-* commands
|
||||
// singular `command/` (still correct for Kilo) made all /msd-* commands
|
||||
// invisible to OpenCode. commandDirName overrides the flat-command default.
|
||||
opencode: { surface: 'flat-command', settings: true, hooksPackageJson: true, commandDirName: 'commands' },
|
||||
// #2102 Stage 1/2: pi is a PLUGIN-ONLY install (hostBehaviors.pluginOnlyInstall)
|
||||
// for commands/agents/skills — NO commands/, agents/, or skills/ dir. pi's
|
||||
// /gsd command is registered programmatically by the native extension
|
||||
// (extensions/gsd.cjs) and dispatches via a bounded subprocess to
|
||||
// gsd-tools.cjs; it has no host-read markdown surface. Stage 2 (adversarial-
|
||||
// /msd command is registered programmatically by the native extension
|
||||
// (extensions/msd.cjs) and dispatches via a bounded subprocess to
|
||||
// msd-tools.cjs; it has no host-read markdown surface. Stage 2 (adversarial-
|
||||
// review fix): pi's native extension DOES spawn the shared hooks/*.js bundle
|
||||
// as bounded subprocesses (session_start/before_agent_start/session_before_
|
||||
// compact bridges) and its /gsd tokenizer requires hooks/lib/git-cmd.js, so
|
||||
// compact bridges) and its /msd tokenizer requires hooks/lib/git-cmd.js, so
|
||||
// `hostBehaviors.skipSharedHooksInstall` was removed — pi now receives
|
||||
// hooks/ + hooks/lib/ + the {"type":"commonjs"} package.json marker, exactly
|
||||
// like OpenCode and (since #2305) Kilo — architecturally identical:
|
||||
@@ -90,7 +90,7 @@ function writeFile(root, relPath, content) {
|
||||
|
||||
function writeManifest(root, files) {
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'gsd-file-manifest.json'),
|
||||
path.join(root, 'msd-file-manifest.json'),
|
||||
JSON.stringify({
|
||||
version: '1.49.0',
|
||||
timestamp: '2026-05-10T00:00:00.000Z',
|
||||
@@ -199,7 +199,7 @@ function stripAnsi(value) {
|
||||
function runInstallerCli(runtime, targetDir, options = {}) {
|
||||
const { minimal = true } = options;
|
||||
const env = { ...process.env };
|
||||
delete env.GSD_TEST_MODE;
|
||||
delete env.MSD_TEST_MODE;
|
||||
env.HOME = path.join(path.dirname(targetDir), 'home');
|
||||
env.USERPROFILE = env.HOME;
|
||||
|
||||
@@ -226,10 +226,10 @@ function listDirNames(root, relPath) {
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).map((entry) => entry.name);
|
||||
}
|
||||
|
||||
function assertHasGsdDirectory(root, relPath) {
|
||||
function assertHasMsdDirectory(root, relPath) {
|
||||
assert.ok(
|
||||
listDirNames(root, relPath).some((name) => name.startsWith('gsd-')),
|
||||
`${relPath} should contain generated GSD entries`
|
||||
listDirNames(root, relPath).some((name) => name.startsWith('msd-')),
|
||||
`${relPath} should contain generated MSD entries`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,38 +237,38 @@ function assertFreshInstallContract(runtime, targetDir) {
|
||||
const contract = RUNTIME_INSTALL_CONTRACTS[runtime];
|
||||
assert.ok(contract, `missing runtime install contract for ${runtime}`);
|
||||
// #3023: the shared hooks bundle's staged directory name is per-runtime
|
||||
// (hostBehaviors.sharedHooksDirName; pi renames it to `gsd-hooks/` to avoid
|
||||
// (hostBehaviors.sharedHooksDirName; pi renames it to `msd-hooks/` to avoid
|
||||
// pi's own host-reserved `hooks/`) — resolve it the same way the installer
|
||||
// does rather than hardcoding 'hooks', which is only the default.
|
||||
const hooksDirName = installModule.resolveSharedHooksDirName(runtime);
|
||||
|
||||
if (contract.workflowPayload !== false) {
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(targetDir, 'gsd-core', 'VERSION'), 'utf8'),
|
||||
fs.readFileSync(path.join(targetDir, 'msd-core', 'VERSION'), 'utf8'),
|
||||
pkg.version,
|
||||
`${runtime} should install the package VERSION`
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(targetDir, 'gsd-core', 'bin', 'gsd-tools.cjs')),
|
||||
`${runtime} should install the GSD tool payload`
|
||||
fs.existsSync(path.join(targetDir, 'msd-core', 'bin', 'msd-tools.cjs')),
|
||||
`${runtime} should install the MSD tool payload`
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(targetDir, 'gsd-file-manifest.json')),
|
||||
fs.existsSync(path.join(targetDir, 'msd-file-manifest.json')),
|
||||
`${runtime} should write the install manifest`
|
||||
);
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(targetDir, 'gsd-file-manifest.json'), 'utf8'));
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(targetDir, 'msd-file-manifest.json'), 'utf8'));
|
||||
assert.equal(manifest.version, pkg.version, `${runtime} manifest should record the package version`);
|
||||
assert.equal(manifest.mode, 'full', `${runtime} manifest should record a full install`);
|
||||
assert.ok(
|
||||
manifest.files['gsd-core/VERSION'],
|
||||
manifest.files['msd-core/VERSION'],
|
||||
`${runtime} manifest should track the installed VERSION file`
|
||||
);
|
||||
} else {
|
||||
assert.equal(
|
||||
fs.existsSync(path.join(targetDir, 'gsd-core')),
|
||||
fs.existsSync(path.join(targetDir, 'msd-core')),
|
||||
false,
|
||||
`${runtime} should not install the GSD workflow payload`
|
||||
`${runtime} should not install the MSD workflow payload`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -279,59 +279,59 @@ function assertFreshInstallContract(runtime, targetDir) {
|
||||
// runInstallerCli sandboxes HOME to <dirname(targetDir)>/home, so assert
|
||||
// the skill dir under that sandboxed home instead of under targetDir.
|
||||
const codexSandboxHome = path.join(path.dirname(targetDir), 'home');
|
||||
assertHasGsdDirectory(path.join(codexSandboxHome, '.agents'), 'skills');
|
||||
assertHasMsdDirectory(path.join(codexSandboxHome, '.agents'), 'skills');
|
||||
} else if (runtime === 'antigravity') {
|
||||
// #3738: Antigravity's machine-local discovery scans ~/.gemini/config, so
|
||||
// global skills install under the sandboxed home's .gemini/config root
|
||||
// (kind `home` override), NOT `<config-dir>/skills`. Same sandboxed-HOME
|
||||
// reasoning as the codex branch above.
|
||||
const agySandboxHome = path.join(path.dirname(targetDir), 'home');
|
||||
assertHasGsdDirectory(path.join(agySandboxHome, '.gemini', 'config'), 'skills');
|
||||
assertHasGsdDirectory(path.join(agySandboxHome, '.gemini', 'config'), 'agents');
|
||||
assertHasMsdDirectory(path.join(agySandboxHome, '.gemini', 'config'), 'skills');
|
||||
assertHasMsdDirectory(path.join(agySandboxHome, '.gemini', 'config'), 'agents');
|
||||
} else {
|
||||
// Pre-#3562: codex was special-cased to expect zero gsd-* skill dirs
|
||||
// Pre-#3562: codex was special-cased to expect zero msd-* skill dirs
|
||||
// (assumption: Codex auto-discovers from workflows). That assumption
|
||||
// does not hold for Codex CLI 0.130.0 — fresh installs now materialize
|
||||
// the same flat-skills surface as the other runtimes.
|
||||
assertHasGsdDirectory(targetDir, 'skills');
|
||||
assertHasMsdDirectory(targetDir, 'skills');
|
||||
}
|
||||
} else if (contract.surface === 'hermes-skills') {
|
||||
// Hermes layout uses prefix: '' — skill dirs have bare stem names (no gsd- prefix).
|
||||
// Hermes layout uses prefix: '' — skill dirs have bare stem names (no msd- prefix).
|
||||
// Assert that the category dir contains at least one skill dir with SKILL.md.
|
||||
const hermesGsdDir = path.join(targetDir, 'skills', 'gsd');
|
||||
const hermesSkillCount = fs.existsSync(hermesGsdDir)
|
||||
? fs.readdirSync(hermesGsdDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && fs.existsSync(path.join(hermesGsdDir, e.name, 'SKILL.md')))
|
||||
const hermesMsdDir = path.join(targetDir, 'skills', 'msd');
|
||||
const hermesSkillCount = fs.existsSync(hermesMsdDir)
|
||||
? fs.readdirSync(hermesMsdDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && fs.existsSync(path.join(hermesMsdDir, e.name, 'SKILL.md')))
|
||||
.length
|
||||
: 0;
|
||||
assert.ok(hermesSkillCount > 0, `skills/gsd should contain generated GSD entries (got ${hermesSkillCount})`);
|
||||
assert.ok(hermesSkillCount > 0, `skills/msd should contain generated MSD entries (got ${hermesSkillCount})`);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(targetDir, 'skills', 'gsd', 'DESCRIPTION.md')),
|
||||
'Hermes should install the nested GSD category description'
|
||||
fs.existsSync(path.join(targetDir, 'skills', 'msd', 'DESCRIPTION.md')),
|
||||
'Hermes should install the nested MSD category description'
|
||||
);
|
||||
} else if (contract.surface === 'flat-command') {
|
||||
// #2329: dir name is per-runtime (opencode='commands', kilo stays 'command').
|
||||
const commandDirName = contract.commandDirName || 'command';
|
||||
assert.ok(
|
||||
listDirNames(targetDir, commandDirName).some((name) => name.startsWith('gsd-') && name.endsWith('.md')),
|
||||
listDirNames(targetDir, commandDirName).some((name) => name.startsWith('msd-') && name.endsWith('.md')),
|
||||
`${runtime} should install flattened command markdown files in ${commandDirName}/`
|
||||
);
|
||||
} else if (contract.surface === 'plugin-only') {
|
||||
// #2102 Stage 1/2: pi — PLUGIN-ONLY install for commands/agents/skills
|
||||
// (hostBehaviors.pluginOnlyInstall). pi's /gsd command is registered
|
||||
// (hostBehaviors.pluginOnlyInstall). pi's /msd command is registered
|
||||
// programmatically by the native extension and dispatches via a bounded
|
||||
// subprocess to gsd-tools.cjs — pi has no host-read markdown surface, so
|
||||
// subprocess to msd-tools.cjs — pi has no host-read markdown surface, so
|
||||
// NO commands/, agents/, or skills/ dir is written. The extension DOES
|
||||
// spawn the shared hooks bundle as bounded subprocesses (Stage 2
|
||||
// adversarial-review fix — hooksSurface:'none' no longer implies
|
||||
// skipSharedHooksInstall for pi, mirroring OpenCode), so the bundle + the
|
||||
// git-cmd.js tokenizer helper ARE part of the artifact surface now. #3023:
|
||||
// that bundle is staged under `gsd-hooks/` for pi (hooksDirName above), not
|
||||
// that bundle is staged under `msd-hooks/` for pi (hooksDirName above), not
|
||||
// the generic `hooks/` — pi reserves `hooks/` for its own deprecated
|
||||
// extension location.
|
||||
// #2470: the dest filename comes from pi's descriptor, and must satisfy
|
||||
// pi's isExtensionFile() auto-discovery filter (.ts/.js only) — otherwise
|
||||
// the file installs but pi never loads it and /gsd never registers.
|
||||
// the file installs but pi never loads it and /msd never registers.
|
||||
const piNativePlugin = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'pi', 'capability.json'), 'utf8')
|
||||
).runtime.hostBehaviors.nativePlugin;
|
||||
@@ -344,12 +344,12 @@ function assertFreshInstallContract(runtime, targetDir) {
|
||||
`${runtime} should install the native extension file at ${piNativePlugin.dir}/${piNativePlugin.file}`
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(targetDir, hooksDirName, 'gsd-ensure-canonical-path.js')),
|
||||
fs.existsSync(path.join(targetDir, hooksDirName, 'msd-ensure-canonical-path.js')),
|
||||
`${runtime} should install the shared ${hooksDirName}/ bundle (spawned by the native extension's event bridges)`
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(targetDir, hooksDirName, 'lib', 'git-cmd.js')),
|
||||
`${runtime} should install ${hooksDirName}/lib/git-cmd.js (the /gsd command tokenizer)`
|
||||
`${runtime} should install ${hooksDirName}/lib/git-cmd.js (the /msd command tokenizer)`
|
||||
);
|
||||
assert.equal(
|
||||
fs.existsSync(path.join(targetDir, 'commands')),
|
||||
@@ -366,31 +366,31 @@ function assertFreshInstallContract(runtime, targetDir) {
|
||||
false,
|
||||
`${runtime} should NOT install a skills/ dir (plugin-only)`
|
||||
);
|
||||
} else if (contract.surface === 'commands-gsd') {
|
||||
} else if (contract.surface === 'commands-msd') {
|
||||
assert.ok(
|
||||
listDirNames(targetDir, path.join('commands', 'gsd')).length > 0,
|
||||
`${runtime} should install commands/gsd entries`
|
||||
listDirNames(targetDir, path.join('commands', 'msd')).length > 0,
|
||||
`${runtime} should install commands/msd entries`
|
||||
);
|
||||
} else if (contract.surface === 'kimi-skills-agents') {
|
||||
assertHasGsdDirectory(targetDir, 'skills');
|
||||
assertHasMsdDirectory(targetDir, 'skills');
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(targetDir, 'agents', 'gsd.yaml')),
|
||||
fs.existsSync(path.join(targetDir, 'agents', 'msd.yaml')),
|
||||
'Kimi should install the root agent YAML'
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(targetDir, 'agents', 'gsd.md')),
|
||||
fs.existsSync(path.join(targetDir, 'agents', 'msd.md')),
|
||||
'Kimi should install the root agent prompt'
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(targetDir, 'agents', 'subagents', 'gsd-executor.yaml')),
|
||||
'Kimi should install GSD subagent YAML'
|
||||
fs.existsSync(path.join(targetDir, 'agents', 'subagents', 'msd-executor.yaml')),
|
||||
'Kimi should install MSD subagent YAML'
|
||||
);
|
||||
} else if (contract.surface === 'clinerules') {
|
||||
// #787: Cline now uses the .clinerules/ directory form (rules at gsd.md).
|
||||
// #787: Cline now uses the .clinerules/ directory form (rules at msd.md).
|
||||
assert.match(
|
||||
fs.readFileSync(path.join(targetDir, '.clinerules', 'gsd.md'), 'utf8'),
|
||||
/GSD workflows live in `gsd-core\/workflows\/`/,
|
||||
'Cline should install .clinerules/gsd.md guidance'
|
||||
fs.readFileSync(path.join(targetDir, '.clinerules', 'msd.md'), 'utf8'),
|
||||
/MSD workflows live in `msd-core\/workflows\/`/,
|
||||
'Cline should install .clinerules/msd.md guidance'
|
||||
);
|
||||
} else if (contract.surface === 'global-artifacts-noop') {
|
||||
assert.equal(
|
||||
@@ -411,12 +411,12 @@ function assertFreshInstallContract(runtime, targetDir) {
|
||||
// .gemini/config root (see the flat-skills branch above), not targetDir.
|
||||
const agySandboxHomeForAgents = path.join(path.dirname(targetDir), 'home');
|
||||
assert.ok(
|
||||
listDirNames(path.join(agySandboxHomeForAgents, '.gemini', 'config'), 'agents').some((name) => name.startsWith('gsd-')),
|
||||
listDirNames(path.join(agySandboxHomeForAgents, '.gemini', 'config'), 'agents').some((name) => name.startsWith('msd-')),
|
||||
`${runtime} full install should install agents`
|
||||
);
|
||||
} else {
|
||||
assert.ok(
|
||||
listDirNames(targetDir, 'agents').some((name) => name.startsWith('gsd-')),
|
||||
listDirNames(targetDir, 'agents').some((name) => name.startsWith('msd-')),
|
||||
`${runtime} full install should install agents`
|
||||
);
|
||||
}
|
||||
@@ -427,8 +427,8 @@ function assertFreshInstallContract(runtime, targetDir) {
|
||||
contract.settings,
|
||||
`${runtime} settings.json presence should match the runtime contract`
|
||||
);
|
||||
// #2544: the CommonJS marker lives in the shared hooks dir (the dir GSD
|
||||
// fills with its own .js scripts — `gsd-hooks/` for pi, `hooks/` for every
|
||||
// #2544: the CommonJS marker lives in the shared hooks dir (the dir MSD
|
||||
// fills with its own .js scripts — `msd-hooks/` for pi, `hooks/` for every
|
||||
// other runtime, #3023), never at the config root — that file is user-owned
|
||||
// territory on OpenCode/Kilo and was being clobbered on every install.
|
||||
assert.equal(
|
||||
@@ -439,21 +439,21 @@ function assertFreshInstallContract(runtime, targetDir) {
|
||||
assert.equal(
|
||||
fs.existsSync(path.join(targetDir, 'package.json')),
|
||||
false,
|
||||
`${runtime} must not receive a GSD package.json at the config root (#2544)`
|
||||
`${runtime} must not receive a MSD package.json at the config root (#2544)`
|
||||
);
|
||||
|
||||
if (contract.codexConfig) {
|
||||
assert.match(
|
||||
fs.readFileSync(path.join(targetDir, 'config.toml'), 'utf8'),
|
||||
/GSD Agent Configuration/,
|
||||
'Codex should install config.toml with the GSD marker'
|
||||
/MSD Agent Configuration/,
|
||||
'Codex should install config.toml with the MSD marker'
|
||||
);
|
||||
}
|
||||
|
||||
if (contract.copilotInstructions) {
|
||||
assert.match(
|
||||
fs.readFileSync(path.join(targetDir, 'copilot-instructions.md'), 'utf8'),
|
||||
/GSD Configuration/,
|
||||
/MSD Configuration/,
|
||||
'Copilot should install managed copilot instructions'
|
||||
);
|
||||
}
|
||||
@@ -464,7 +464,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
let codexHome;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpRoot = createTempDir('gsd-install-migrations-');
|
||||
tmpRoot = createTempDir('msd-install-migrations-');
|
||||
codexHome = path.join(tmpRoot, '.codex');
|
||||
fs.mkdirSync(codexHome, { recursive: true });
|
||||
});
|
||||
@@ -494,7 +494,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
});
|
||||
|
||||
test('blocks install before materialization when baseline needs explicit user choice', () => {
|
||||
writeFile(codexHome, 'hooks/gsd-retired-hook.txt', 'old gsd hook\n');
|
||||
writeFile(codexHome, 'hooks/msd-retired-hook.txt', 'old msd hook\n');
|
||||
|
||||
assert.throws(
|
||||
() => captureConsole(() =>
|
||||
@@ -503,13 +503,13 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
/installer migration blocked/
|
||||
);
|
||||
|
||||
assert.equal(fs.readFileSync(path.join(codexHome, 'hooks/gsd-retired-hook.txt'), 'utf8'), 'old gsd hook\n');
|
||||
assert.equal(fs.readFileSync(path.join(codexHome, 'hooks/msd-retired-hook.txt'), 'utf8'), 'old msd hook\n');
|
||||
assert.equal(fs.existsSync(path.join(codexHome, 'skills')), false);
|
||||
// #2088: with HOME sandboxed to codexHome via withCodexEnv, Codex's
|
||||
// canonical skill root ($HOME/.agents/skills) resolves under codexHome
|
||||
// too — assert nothing was materialized there when the install is blocked.
|
||||
assert.equal(fs.existsSync(path.join(codexHome, '.agents', 'skills')), false);
|
||||
assert.equal(fs.existsSync(path.join(codexHome, 'gsd-core', 'VERSION')), false);
|
||||
assert.equal(fs.existsSync(path.join(codexHome, 'msd-core', 'VERSION')), false);
|
||||
});
|
||||
|
||||
test('rolls back applied migrations when package materialization fails for non-Codex installs', () => {
|
||||
@@ -523,7 +523,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
assert.throws(
|
||||
() => captureConsole(() =>
|
||||
withEnv('CLAUDE_CONFIG_DIR', claudeHome, () =>
|
||||
withWriteFailure(path.join(claudeHome, 'gsd-core', 'VERSION'), () => install(true, 'claude'))
|
||||
withWriteFailure(path.join(claudeHome, 'msd-core', 'VERSION'), () => install(true, 'claude'))
|
||||
)
|
||||
),
|
||||
/injected write failure for VERSION/
|
||||
@@ -533,7 +533,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
fs.readFileSync(path.join(claudeHome, 'hooks/statusline.js'), 'utf8'),
|
||||
'legacy managed hook\n'
|
||||
);
|
||||
assert.equal(fs.existsSync(path.join(claudeHome, 'gsd-install-state.json')), false);
|
||||
assert.equal(fs.existsSync(path.join(claudeHome, 'msd-install-state.json')), false);
|
||||
});
|
||||
|
||||
test('rolls back applied migrations when multi-runtime finalization fails', () => {
|
||||
@@ -561,7 +561,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
fs.readFileSync(path.join(claudeHome, 'hooks/statusline.js'), 'utf8'),
|
||||
'legacy managed hook\n'
|
||||
);
|
||||
assert.equal(fs.existsSync(path.join(claudeHome, 'gsd-install-state.json')), false);
|
||||
assert.equal(fs.existsSync(path.join(claudeHome, 'msd-install-state.json')), false);
|
||||
});
|
||||
|
||||
test('rolls back completed runtime migrations when a later runtime install fails', () => {
|
||||
@@ -581,7 +581,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
() => captureConsole(() =>
|
||||
withEnv('CLAUDE_CONFIG_DIR', claudeHome, () =>
|
||||
withCodexEnv(codexHome, () =>
|
||||
withWriteFailure(path.join(codexHome, 'gsd-core', 'VERSION'), () =>
|
||||
withWriteFailure(path.join(codexHome, 'msd-core', 'VERSION'), () =>
|
||||
installModule.installAllRuntimes(['claude', 'codex'], true, false)
|
||||
)
|
||||
)
|
||||
@@ -594,12 +594,12 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
fs.readFileSync(path.join(claudeHome, 'hooks/statusline.js'), 'utf8'),
|
||||
'legacy managed hook\n'
|
||||
);
|
||||
assert.equal(fs.existsSync(path.join(claudeHome, 'gsd-install-state.json')), false);
|
||||
assert.equal(fs.existsSync(path.join(claudeHome, 'msd-install-state.json')), false);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(codexHome, 'hooks/statusline.js'), 'utf8'),
|
||||
'legacy managed hook\n'
|
||||
);
|
||||
assert.equal(fs.existsSync(path.join(codexHome, 'gsd-install-state.json')), false);
|
||||
assert.equal(fs.existsSync(path.join(codexHome, 'msd-install-state.json')), false);
|
||||
});
|
||||
|
||||
for (const runtime of SUPPORTED_RUNTIMES) {
|
||||
@@ -626,7 +626,7 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
assert.match(output, /Done!/);
|
||||
assert.equal(fs.existsSync(path.join(targetDir, 'hooks/statusline.js')), false);
|
||||
|
||||
const installState = JSON.parse(fs.readFileSync(path.join(targetDir, 'gsd-install-state.json'), 'utf8'));
|
||||
const installState = JSON.parse(fs.readFileSync(path.join(targetDir, 'msd-install-state.json'), 'utf8'));
|
||||
assert.ok(
|
||||
installState.appliedMigrations.some((entry) => entry.id === '2026-05-11-legacy-orphan-files'),
|
||||
`${runtime} should track the applied cleanup migration in install state`
|
||||
@@ -649,30 +649,30 @@ describe('installer migration install integration', { concurrency: false }, () =
|
||||
assert.match(output, /Installer migrations/);
|
||||
assert.match(output, /removed\s+hooks\/statusline\.js/);
|
||||
assert.equal(fs.existsSync(path.join(targetDir, 'hooks/statusline.js')), false);
|
||||
const installState = JSON.parse(fs.readFileSync(path.join(targetDir, 'gsd-install-state.json'), 'utf8'));
|
||||
const installState = JSON.parse(fs.readFileSync(path.join(targetDir, 'msd-install-state.json'), 'utf8'));
|
||||
assert.ok(
|
||||
installState.appliedMigrations.some((entry) => entry.id === '2026-05-11-legacy-orphan-files'),
|
||||
'successful install should write install state for the applied cleanup migration'
|
||||
);
|
||||
});
|
||||
|
||||
test(`blocks ambiguous GSD-looking user-choice artifacts for ${runtime}`, () => {
|
||||
test(`blocks ambiguous MSD-looking user-choice artifacts for ${runtime}`, () => {
|
||||
const targetDir = path.join(tmpRoot, `.${runtime}-blocked`);
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
writeFile(targetDir, 'gsd-core/gsd-retired-tool.cjs', 'old ambiguous artifact\n');
|
||||
writeFile(targetDir, 'msd-core/msd-retired-tool.cjs', 'old ambiguous artifact\n');
|
||||
|
||||
const result = runInstallerCli(runtime, targetDir);
|
||||
|
||||
assert.notEqual(result.exitCode, 0, 'install should fail before materialization');
|
||||
const output = stripAnsi(`${result.stdout}\n${result.stderr}`);
|
||||
assert.match(output, /Installer migrations/);
|
||||
assert.match(output, /blocked\s+gsd-core\/gsd-retired-tool\.cjs/);
|
||||
assert.match(output, /blocked\s+msd-core\/msd-retired-tool\.cjs/);
|
||||
assert.match(output, /installer migration blocked/);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(targetDir, 'gsd-core/gsd-retired-tool.cjs'), 'utf8'),
|
||||
fs.readFileSync(path.join(targetDir, 'msd-core/msd-retired-tool.cjs'), 'utf8'),
|
||||
'old ambiguous artifact\n'
|
||||
);
|
||||
assert.equal(fs.existsSync(path.join(targetDir, 'gsd-core', 'VERSION')), false);
|
||||
assert.equal(fs.existsSync(path.join(targetDir, 'msd-core', 'VERSION')), false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user