refactor: drop 12 runtimes, keep Claude, Codex, OpenCode, Cursor, ZCode, Antigravity
Removes kilo, kimi, kimi-code, copilot, windsurf, augment, trae, qwen, hermes, cline, codebuddy and pi end to end: capability descriptors, installer branches and converters (bin/install.js 14.9k -> 11.2k lines), TypeScript converters, hook surfaces and runtime homes, review lanes qwen/kimi-code, the two pi migrations, Kimi payload normalization in the hook guards, dead hostBehaviors vocabulary, launcher home probes, fixtures, runtime-specific tests and the prose that presented them as supported. Installer output for the six kept runtimes is byte-identical to before the prune. The Kimi tool-vocabulary tests in workflow-guard, read-guard and read-injection-scanner are left in place pending a decision.
This commit is contained in:
@@ -59,14 +59,14 @@ test('finishInstall rejects an invalid configured entrypoint before Done output'
|
||||
process.env.USERPROFILE = root;
|
||||
const restoreConfigLocationEnv = helpers.scrubConfigLocationEnv();
|
||||
try {
|
||||
assert.throws(() => finishInstall(null, null, null, false, 'cline', false, root, {
|
||||
configuredEntrypoints: [{ runtime: 'cline', configPath: path.join(root, 'config'), scriptPath: path.join(root, 'missing.js') }],
|
||||
assert.throws(() => finishInstall(null, null, null, false, 'cursor', false, root, {
|
||||
configuredEntrypoints: [{ runtime: 'cursor', configPath: path.join(root, 'config'), scriptPath: path.join(root, 'missing.js') }],
|
||||
}), /Configured entrypoint validation failed/);
|
||||
// #4249 review, Major: the message must name the actual consequence for
|
||||
// the failing runtime, not just that something failed — Cline has no
|
||||
// the failing runtime, not just that something failed — Cursor has no
|
||||
// revert path, so its entry must be flagged "NOT reverted".
|
||||
assert.throws(() => finishInstall(null, null, null, false, 'cline', false, root, {
|
||||
configuredEntrypoints: [{ runtime: 'cline', configPath: path.join(root, 'config'), scriptPath: path.join(root, 'missing.js') }],
|
||||
assert.throws(() => finishInstall(null, null, null, false, 'cursor', false, root, {
|
||||
configuredEntrypoints: [{ runtime: 'cursor', configPath: path.join(root, 'config'), scriptPath: path.join(root, 'missing.js') }],
|
||||
}), /NOT reverted/);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
@@ -357,35 +357,6 @@ test('runtime config writers expose the exact configured entrypoints they emit',
|
||||
);
|
||||
assert.ok(cursor.configuredEntrypoints.every(entry => entry.configPath === cursor.hooksJsonPath));
|
||||
|
||||
const windsurfRoot = path.join(root, 'windsurf');
|
||||
const windsurf = hooksSurface.writeWindsurfHooksJson(windsurfRoot, sourceRoot);
|
||||
assert.deepEqual(
|
||||
windsurf.configuredEntrypoints.map(entry => path.basename(entry.scriptPath)).sort(),
|
||||
['msd-windsurf-pre-command.js', 'msd-windsurf-pre-write.js'],
|
||||
);
|
||||
|
||||
const kimiRoot = path.join(root, 'kimi');
|
||||
fs.cpSync(path.join(sourceRoot, 'hooks'), path.join(kimiRoot, 'hooks'), { recursive: true });
|
||||
const kimiConfig = path.join(root, 'kimi-config.toml');
|
||||
const kimi = hooksSurface.writeKimiHooksToml(kimiConfig, kimiRoot, {
|
||||
hookOpts: { runtime: 'kimi' },
|
||||
});
|
||||
assert.equal(kimi.configuredEntrypoints.length, kimi.entryCount);
|
||||
assert.ok(kimi.configuredEntrypoints.every(entry => entry.configPath === kimiConfig));
|
||||
|
||||
const clineRoot = path.join(root, 'cline');
|
||||
const cline = hooksSurface.writeClineArtifacts(clineRoot, false);
|
||||
assert.deepEqual(
|
||||
cline.configuredEntrypoints.map(entry => path.basename(entry.scriptPath)),
|
||||
['PreToolUse'],
|
||||
);
|
||||
// #4249 (CodeRabbit): Cline's `#!/usr/bin/env node` hook is a hybrid —
|
||||
// self-executable (needs its own execute bit checked) AND PATH-dependent
|
||||
// on `node` (needs an interpreter candidate resolved), unlike MSD's other
|
||||
// JS hooks which bake an absolute node path specifically to avoid this.
|
||||
assert.equal(cline.configuredEntrypoints[0].selfExecutable, true);
|
||||
assert.deepEqual(cline.configuredEntrypoints[0].interpreterCandidates, ['node']);
|
||||
|
||||
const portable = [];
|
||||
assert.ok(hooksSurface.buildHookCommand(root, 'msd-context-monitor.js', {
|
||||
runtime: 'claude',
|
||||
@@ -431,6 +402,22 @@ test('a minimal Codex rollback restores skills from the alternate skills home (#
|
||||
});
|
||||
});
|
||||
|
||||
// Drive a REAL aggregate failure through installAllRuntimes: delegate to the real
|
||||
// validator, then append an invalid entry for the named runtime. Every real
|
||||
// entrypoint is a path the installer just wrote, so there is no PATH trick left
|
||||
// that fails one runtime's entry on its own.
|
||||
function forceEntrypointFailureFor(runtime) {
|
||||
const real = hooksSurface.validateConfiguredEntrypoints;
|
||||
hooksSurface.validateConfiguredEntrypoints = (entries, deps) => {
|
||||
const result = real(entries, deps);
|
||||
const invalid = [...(result.invalid || []), {
|
||||
runtime, configPath: '/nonexistent/config', role: 'script', path: '/nonexistent/hook.js', reason: 'missing',
|
||||
}];
|
||||
return { ok: false, invalid };
|
||||
};
|
||||
return () => { hooksSurface.validateConfiguredEntrypoints = real; };
|
||||
}
|
||||
|
||||
test('an aggregate entrypoint validation failure rolls the Codex install back (#4249)', (t) => {
|
||||
withSandboxedHome(t, 'configured-entrypoint-aggregate-', () => {
|
||||
const first = install(true, 'codex');
|
||||
@@ -441,21 +428,17 @@ test('an aggregate entrypoint validation failure rolls the Codex install back (#
|
||||
const priorBytes = '# bytes only this test wrote\n';
|
||||
fs.writeFileSync(configPath, priorBytes);
|
||||
|
||||
// Cline's `#!/usr/bin/env node` hook records interpreterCandidates: ['node'],
|
||||
// resolved off PATH. Emptying PATH makes exactly that entry fail, which is
|
||||
// the only way to drive a REAL aggregate failure through installAllRuntimes:
|
||||
// every other entrypoint is a path the installer just wrote. Codex installs
|
||||
// first, so the reversed rollback loop reaches its restoreCodexSnapshot.
|
||||
const savedPath = process.env.PATH;
|
||||
process.env.PATH = '';
|
||||
// Codex installs first, so the reversed rollback loop reaches its
|
||||
// restoreCodexSnapshot.
|
||||
const restore = forceEntrypointFailureFor('cursor');
|
||||
try {
|
||||
assert.throws(
|
||||
() => installAllRuntimes(['codex', 'cline'], true, false),
|
||||
() => installAllRuntimes(['codex', 'cursor'], true, false),
|
||||
/Configured entrypoint validation failed/,
|
||||
'an unresolvable interpreter must fail the aggregate gate',
|
||||
'an invalid entrypoint must fail the aggregate gate',
|
||||
);
|
||||
} finally {
|
||||
process.env.PATH = savedPath;
|
||||
restore();
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
@@ -466,38 +449,32 @@ test('an aggregate entrypoint validation failure rolls the Codex install back (#
|
||||
});
|
||||
});
|
||||
|
||||
test('an aggregate entrypoint validation failure leaves Cline\'s own config file on disk, unreverted (#4249 review, Major)', (t) => {
|
||||
test('an aggregate entrypoint validation failure leaves Cursor\'s own config file on disk, unreverted (#4249 review, Major)', (t) => {
|
||||
withSandboxedHome(t, 'configured-entrypoint-unreverted-', () => {
|
||||
// Cline is one of the four runtimes (Cursor/Windsurf/Kimi/Cline) that
|
||||
// write their config file inside install(), ahead of the validation
|
||||
// gate, with no snapshot/restore path — see the rollback-matrix
|
||||
// paragraph in the update-msd how-to. Unlike the Codex companion test above,
|
||||
// this asserts the POSITIVE case that same paragraph discloses in
|
||||
// prose but no prior test proved: the file the failing runtime itself
|
||||
// just wrote is still there afterward, broken and unreverted.
|
||||
const clineFirst = install(true, 'cline');
|
||||
const clineHookPath = path.join(clineFirst.configDir, '.clinerules', 'hooks', 'PreToolUse');
|
||||
assert.equal(fs.existsSync(clineHookPath), true, 'precondition: Cline hook must exist before the failing install');
|
||||
// Cursor writes its config file inside install(), ahead of the validation
|
||||
// gate, with no snapshot/restore path — see the rollback-matrix paragraph
|
||||
// in the update-msd how-to. This asserts the POSITIVE case that paragraph
|
||||
// discloses in prose: the file the failing runtime itself just wrote is
|
||||
// still there afterward, broken and unreverted.
|
||||
const cursorFirst = install(true, 'cursor');
|
||||
const cursorHooksJson = path.join(cursorFirst.configDir, 'hooks.json');
|
||||
assert.equal(fs.existsSync(cursorHooksJson), true, 'precondition: Cursor hooks.json must exist before the failing install');
|
||||
|
||||
// Same technique as the Codex rollback test: an emptied PATH makes
|
||||
// Cline's own `#!/usr/bin/env node` hook fail interpreter resolution,
|
||||
// driving a REAL aggregate failure through installAllRuntimes.
|
||||
const savedPath = process.env.PATH;
|
||||
process.env.PATH = '';
|
||||
const restore = forceEntrypointFailureFor('cursor');
|
||||
try {
|
||||
assert.throws(
|
||||
() => installAllRuntimes(['codex', 'cline'], true, false),
|
||||
/Configured entrypoint validation failed/,
|
||||
'an unresolvable interpreter must fail the aggregate gate',
|
||||
() => installAllRuntimes(['codex', 'cursor'], true, false),
|
||||
/Configured entrypoint validation failed[\s\S]*cursor[\s\S]*NOT reverted/,
|
||||
'an invalid entrypoint must fail the aggregate gate and flag Cursor as NOT reverted',
|
||||
);
|
||||
} finally {
|
||||
process.env.PATH = savedPath;
|
||||
restore();
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
fs.existsSync(clineHookPath),
|
||||
fs.existsSync(cursorHooksJson),
|
||||
true,
|
||||
'Cline has no snapshot/restore path — its already-written hook file must remain on disk, not be deleted',
|
||||
'Cursor has no snapshot/restore path — its already-written hooks.json must remain on disk, not be deleted',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -511,20 +488,18 @@ test('a NON-entrypoint finalize failure leaves an already-successful Codex insta
|
||||
const priorBytes = '# bytes only this test wrote\n';
|
||||
fs.writeFileSync(configPath, priorBytes);
|
||||
|
||||
// Kilo is the smallest real non-Codex runtime whose finishInstall still
|
||||
// writes: plan.finishPermissionWriter === 'kilo' makes it call
|
||||
// configureKiloPermissions unconditionally (that writer, unlike OpenCode's,
|
||||
// is NOT MSD_TEST_MODE-gated), ending in an unguarded
|
||||
// fs.writeFileSync(<configDir>/kilo.json). Cline can't stand in here — its
|
||||
// plan is writesSharedSettings:false + finishPermissionWriter:null, so its
|
||||
// finishInstall performs no write at all and has no non-entrypoint failure
|
||||
// path to force. EACCES on the Kilo write is injected by monkeypatching
|
||||
// Antigravity is the smallest real non-Codex runtime whose finishInstall
|
||||
// still writes: plan.finishPermissionWriter === 'antigravity' makes it call
|
||||
// configureAntigravityMcpConfig unconditionally (that writer, unlike
|
||||
// OpenCode's, is NOT MSD_TEST_MODE-gated), ending in an unguarded
|
||||
// fs.writeFileSync(<configDir>/mcp_config.json). EACCES on that write is
|
||||
// injected by monkeypatching
|
||||
// node:fs and restoring it in a finally — never chmod 0o000, which root
|
||||
// bypasses under Docker/CI and would give this test zero real coverage.
|
||||
const realWriteFileSync = fs.writeFileSync;
|
||||
fs.writeFileSync = function poisonedWriteFileSync(target, ...args) {
|
||||
const targetStr = typeof target === 'string' ? target : String(target);
|
||||
if (/[\\/]kilo\.jsonc?$/.test(targetStr)) {
|
||||
if (/[\\/]mcp_config\.json$/.test(targetStr)) {
|
||||
const injected = new Error(`EACCES: permission denied, open '${targetStr}'`);
|
||||
injected.code = 'EACCES';
|
||||
throw injected;
|
||||
@@ -533,16 +508,16 @@ test('a NON-entrypoint finalize failure leaves an already-successful Codex insta
|
||||
};
|
||||
try {
|
||||
assert.throws(
|
||||
() => installAllRuntimes(['codex', 'kilo'], true, false),
|
||||
() => installAllRuntimes(['codex', 'antigravity'], true, false),
|
||||
/EACCES: permission denied/,
|
||||
'a failed Kilo permission write must abort the aggregate install',
|
||||
'a failed Antigravity MCP config write must abort the aggregate install',
|
||||
);
|
||||
} finally {
|
||||
fs.writeFileSync = realWriteFileSync;
|
||||
}
|
||||
|
||||
// Codex installs first and finishInstall prints its "Done!" summary before
|
||||
// Kilo's throws, so the reversed rollback loop does reach Codex's entry —
|
||||
// Antigravity's throws, so the reversed rollback loop does reach Codex's entry —
|
||||
// but with a non-entrypoint error it must take the installer-migrations-only
|
||||
// closure, not restoreCodexSnapshot. A configured-entrypoint validation
|
||||
// failure is the only trigger docs/how-to/update-msd.md documents for
|
||||
|
||||
Reference in New Issue
Block a user