Files
msd-core/tests/runtime-name-policy.test.cjs
Tom Boucher 8f2ebbe9bf feat(#1928): remove sunset Gemini CLI runtime, redirect to Antigravity (#1996)
* feat(#1928): remove sunset gemini cli runtime, redirect to antigravity

Google sunset Gemini CLI on 2026-06-18; Antigravity CLI is its official successor (already a first-class GSD runtime). Remove the gemini runtime from the enum (16->15), aliases, labels, config-home fragment, install path, converters (convertClaudeToGemini{Markdown,Toml,Agent}, convertSlashCommandsToGeminiMentions), capability descriptor, gemini-extension.json, RULESET.GEMINI.*, and the interactive menu (renumbered, no gap).

--gemini now prints an explicit deprecation notice citing the 2026-06-18 sunset and redirects to --antigravity (no silent alias, per the issue's Hyrum's-Law rejection). Antigravity is preserved throughout: its GEMINI.md contextFileName, .gemini/antigravity config home, the shared convertGeminiToolName/claudeToGeminiTools tool vocabulary, and the 'gemini' hookEvents dialect it declares. GEMINI.md retargeted as Antigravity's context file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#1928): backfill changeset PR number (#1996)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#1928): drop Gemini CLI from issue templates (review nit)

Removes the sunset Gemini CLI runtime from the two GitHub issue-template
runtime lists that the removal PR missed, per @davesienkowski's review nit:
- feature_request.yml: 'Applicable runtimes' checkbox (a user could otherwise
  request a feature for a runtime GSD no longer supports)
- bug_report.yml: 'Runtime' dropdown + the stale ~/.gemini/settings.json
  retrieval-help line

Leaves the post-removal templates fully consistent with the Antigravity redirect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 13:32:51 -04:00

243 lines
10 KiB
JavaScript

'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const fs = require('node:fs');
const ROOT = path.join(__dirname, '..');
const {
canonicalizeRuntimeName,
resolveRuntimeNameFromCandidates,
getProjectInstructionFile,
} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-name-policy.cjs'));
describe('runtime-name-policy canonical runtime ids', () => {
test('canonicalizes Kimi without adding extra aliases', () => {
assert.strictEqual(canonicalizeRuntimeName('kimi'), 'kimi');
assert.strictEqual(canonicalizeRuntimeName(' KIMI '), 'kimi');
assert.strictEqual(resolveRuntimeNameFromCandidates('', null, 'kimi'), 'kimi');
assert.strictEqual(canonicalizeRuntimeName('kimi-cli'), null);
});
test('canonicalizes devin-desktop to windsurf (#792)', () => {
assert.strictEqual(canonicalizeRuntimeName('devin-desktop'), 'windsurf');
assert.strictEqual(canonicalizeRuntimeName('DEVIN-DESKTOP'), 'windsurf');
assert.strictEqual(resolveRuntimeNameFromCandidates('devin-desktop'), 'windsurf');
});
});
describe('runtime-name-policy windsurf alias parity — manifest vs FALLBACK_ALIASES (#792)', () => {
// DEFECT.GENERATIVE-FIX: manifest and FALLBACK_ALIASES are manually mirrored;
// this test fails if they diverge for the windsurf key.
const manifestPath = path.join(ROOT, 'gsd-core', 'bin', 'shared', 'runtime-aliases.manifest.json');
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
test('manifest windsurf array includes devin-desktop', () => {
assert.ok(
Array.isArray(manifest.windsurf) && manifest.windsurf.includes('devin-desktop'),
`runtime-aliases.manifest.json windsurf array must include 'devin-desktop'; got: ${JSON.stringify(manifest.windsurf)}`,
);
});
test('FALLBACK_ALIASES windsurf includes devin-desktop (via canonicalization round-trip)', () => {
// The built module merges manifest over FALLBACK_ALIASES; if the manifest is present
// this verifies the combined set. The manifest test above separately guards the manifest.
// Here we verify the live canonicalizer sees devin-desktop -> windsurf.
assert.strictEqual(
canonicalizeRuntimeName('devin-desktop'),
'windsurf',
'devin-desktop must resolve to windsurf via alias lookup',
);
});
test('manifest and FALLBACK_ALIASES windsurf alias sets are identical', () => {
// Read FALLBACK_ALIASES from source to detect manual drift before a build.
const srcPath = path.join(ROOT, 'src', 'runtime-name-policy.cts');
// allow-test-rule: runtime-contract-is-the-product — FALLBACK_ALIASES source text IS the
// product contract for runtimes that can't load the manifest at runtime; verifying
// both surfaces contain the same windsurf aliases catches manual-mirror drift.
const src = fs.readFileSync(srcPath, 'utf8');
const match = src.match(/windsurf:\s*\[([^\]]+)\]/);
assert.ok(match, 'FALLBACK_ALIASES windsurf row must exist in src/runtime-name-policy.cts');
const srcAliases = match[1]
.split(',')
.map(s => s.trim().replace(/^['"]|['"]$/g, ''))
.filter(Boolean);
const manifestAliases = [...manifest.windsurf].sort();
assert.deepStrictEqual(
[...srcAliases].sort(),
manifestAliases,
`FALLBACK_ALIASES windsurf=${JSON.stringify(srcAliases.sort())} must match manifest windsurf=${JSON.stringify(manifestAliases)}`,
);
});
});
describe('runtime-name-policy getProjectInstructionFile (#1529)', () => {
test('claude maps to .claude/CLAUDE.md (kept-as-is boundary case)', () => {
assert.strictEqual(getProjectInstructionFile('claude'), '.claude/CLAUDE.md');
});
test('codex maps to AGENTS.md', () => {
assert.strictEqual(getProjectInstructionFile('codex'), 'AGENTS.md');
});
test('opencode maps to AGENTS.md (the #1529 bug surface)', () => {
assert.strictEqual(getProjectInstructionFile('opencode'), 'AGENTS.md');
});
test('kilo maps to AGENTS.md', () => {
assert.strictEqual(getProjectInstructionFile('kilo'), 'AGENTS.md');
});
test('kimi maps to AGENTS.md', () => {
assert.strictEqual(getProjectInstructionFile('kimi'), 'AGENTS.md');
});
test('copilot maps to .github/copilot-instructions.md (GitHub docs read path)', () => {
assert.strictEqual(getProjectInstructionFile('copilot'), '.github/copilot-instructions.md');
});
test('gemini is no longer a known runtime — falls back to AGENTS.md (#1928: Gemini CLI runtime removed)', () => {
assert.strictEqual(getProjectInstructionFile('gemini'), 'AGENTS.md');
});
test('antigravity maps to GEMINI.md', () => {
assert.strictEqual(getProjectInstructionFile('antigravity'), 'GEMINI.md');
});
test('unknown runtime maps to AGENTS.md (safe cross-agent default, boundary case)', () => {
assert.strictEqual(getProjectInstructionFile('future-runtime-xyz'), 'AGENTS.md');
assert.strictEqual(getProjectInstructionFile(''), 'AGENTS.md');
assert.strictEqual(getProjectInstructionFile(null), 'AGENTS.md');
assert.strictEqual(getProjectInstructionFile(undefined), 'AGENTS.md');
});
test('aliases normalize via canonicalizeRuntimeName before mapping', () => {
// codex-cli is an alias for codex; it must resolve to the codex mapping.
assert.strictEqual(getProjectInstructionFile('codex-cli'), 'AGENTS.md');
// opencode-cli is an alias for opencode.
assert.strictEqual(getProjectInstructionFile('opencode-cli'), 'AGENTS.md');
// gemini-cli was an alias for gemini; the gemini runtime was removed
// (#1928) so it is now an unrecognized runtime -> safe AGENTS.md default.
assert.strictEqual(getProjectInstructionFile('gemini-cli'), 'AGENTS.md');
// github-copilot is an alias for copilot.
assert.strictEqual(getProjectInstructionFile('github-copilot'), '.github/copilot-instructions.md');
});
});
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-783-kilo-global-skills-base.test.cjs — consolidation epic #1969 (B3 #1972)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-783-kilo-global-skills-base (consolidation epic #1969 B3 #1972)", () => {
'use strict';
// Regression guard for bug #783.
//
// getGlobalSkillsBase('kilo') was returning ~/.config/kilo/skills (the XDG
// config dir) instead of ~/.kilo/skills — where Kilo Code actually discovers
// global skills per its docs:
// https://kilo.ai/docs/customize/skills
// "Global skills are located in the `.kilo` directory within your Home
// directory: ~/.kilo/skills/"
//
// The fix adds a special case in getGlobalSkillsBase() that resolves kilo's
// skills dir from HOME (not from the XDG config dir). The config dir at
// ~/.config/kilo is still CORRECT for commands (command/) and must stay
// unchanged — this test verifies both roles are separate.
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const os = require('node:os');
const ROOT = path.join(__dirname, '..');
const {
getGlobalConfigDir,
getGlobalSkillsBase,
} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-homes.cjs'));
// Helper: temporarily override env vars for a test, restoring them afterwards.
function withEnv(overrides, fn) {
const saved = {};
for (const [key, value] of Object.entries(overrides)) {
saved[key] = process.env[key];
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
try {
return fn();
} finally {
for (const [key] of Object.entries(overrides)) {
if (saved[key] === undefined) delete process.env[key];
else process.env[key] = saved[key];
}
}
}
// Clear all kilo-relevant env vars so tests are hermetic.
const kiloEnvClears = {
KILO_CONFIG_DIR: undefined,
XDG_CONFIG_HOME: undefined,
};
describe('bug #783: kilo global skills dir is ~/.kilo/skills, not ~/.config/kilo/skills', () => {
test('getGlobalSkillsBase("kilo") resolves to ~/.kilo/skills', () => {
withEnv(kiloEnvClears, () => {
assert.strictEqual(
getGlobalSkillsBase('kilo'),
path.join(os.homedir(), '.kilo', 'skills'),
);
});
});
test('getGlobalConfigDir("kilo") still resolves to ~/.config/kilo (config dir unchanged)', () => {
withEnv(kiloEnvClears, () => {
assert.strictEqual(
getGlobalConfigDir('kilo'),
path.join(os.homedir(), '.config', 'kilo'),
);
});
});
test('kilo skills dir and config dir are decoupled (not equal, not nested)', () => {
withEnv(kiloEnvClears, () => {
const skillsBase = getGlobalSkillsBase('kilo');
const configDir = getGlobalConfigDir('kilo');
assert.notStrictEqual(skillsBase, configDir, 'skills dir must differ from config dir');
assert.ok(
!skillsBase.startsWith(configDir + path.sep),
`skills dir (${skillsBase}) must not be nested under config dir (${configDir})`,
);
assert.ok(
!configDir.startsWith(skillsBase + path.sep),
`config dir (${configDir}) must not be nested under skills dir (${skillsBase})`,
);
});
});
test('getGlobalSkillsBase("kilo") is NOT affected by KILO_CONFIG_DIR override', () => {
// Skills always live in ~/.kilo/skills regardless of XDG/config-dir overrides.
withEnv({ KILO_CONFIG_DIR: '/tmp/custom-kilo-config', XDG_CONFIG_HOME: undefined }, () => {
assert.strictEqual(
getGlobalSkillsBase('kilo'),
path.join(os.homedir(), '.kilo', 'skills'),
);
});
});
test('getGlobalSkillsBase("kilo") is NOT affected by XDG_CONFIG_HOME override', () => {
withEnv({ KILO_CONFIG_DIR: undefined, XDG_CONFIG_HOME: '/tmp/custom-xdg' }, () => {
assert.strictEqual(
getGlobalSkillsBase('kilo'),
path.join(os.homedir(), '.kilo', 'skills'),
);
});
});
});
});
}