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:
@@ -24,10 +24,7 @@ const { cleanup } = require('./helpers.cjs');
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const {
|
||||
getGlobalConfigDir,
|
||||
getGlobalSkillsBase,
|
||||
resolveAntigravityGlobalDir,
|
||||
resolveKimiGlobalDir,
|
||||
resolveKimiHooksTomlDir,
|
||||
resolveConfigHomeFromDescriptor,
|
||||
resolveSkillsBaseFromDescriptor,
|
||||
detectAntigravityDirAmbiguity,
|
||||
@@ -57,11 +54,9 @@ function withEnv(overrides, fn) {
|
||||
// All env vars for all runtimes — cleared in each test that calls getGlobalConfigDir directly
|
||||
const ALL_ENV_KEYS = [
|
||||
'CLAUDE_CONFIG_DIR', 'CURSOR_CONFIG_DIR', 'CODEX_HOME',
|
||||
'GROK_AGENTS_HOME', 'COPILOT_CONFIG_DIR', 'COPILOT_HOME', 'ANTIGRAVITY_CONFIG_DIR',
|
||||
'WINDSURF_CONFIG_DIR', 'AUGMENT_CONFIG_DIR', 'TRAE_CONFIG_DIR', 'QWEN_CONFIG_DIR',
|
||||
'HERMES_HOME', 'CODEBUDDY_CONFIG_DIR', 'CLINE_CONFIG_DIR', 'KIMI_CONFIG_DIR',
|
||||
'OPENCODE_CONFIG_DIR', 'OPENCODE_CONFIG', 'KILO_CONFIG_DIR', 'KILO_CONFIG',
|
||||
'XDG_CONFIG_HOME', 'PI_CODING_AGENT_DIR',
|
||||
'GROK_AGENTS_HOME', 'ANTIGRAVITY_CONFIG_DIR',
|
||||
'OPENCODE_CONFIG_DIR', 'OPENCODE_CONFIG',
|
||||
'XDG_CONFIG_HOME',
|
||||
];
|
||||
|
||||
function clearAllEnvKeys() {
|
||||
@@ -91,27 +86,14 @@ const GOLDEN_DEFAULTS = {
|
||||
cursor: path.join(HOME, '.cursor'),
|
||||
codex: path.join(HOME, '.codex'),
|
||||
grok: path.join(HOME, '.agents'),
|
||||
copilot: path.join(HOME, '.copilot'),
|
||||
antigravity: path.join(HOME, '.gemini', 'antigravity'), // probe-miss → first candidate
|
||||
windsurf: path.join(HOME, '.codeium', 'windsurf'),
|
||||
augment: path.join(HOME, '.augment'),
|
||||
trae: path.join(HOME, '.trae'),
|
||||
qwen: path.join(HOME, '.qwen'),
|
||||
hermes: path.join(HOME, '.hermes'),
|
||||
codebuddy: path.join(HOME, '.codebuddy'),
|
||||
cline: path.join(HOME, '.cline'),
|
||||
opencode: path.join(HOME, '.config', 'opencode'),
|
||||
kilo: path.join(HOME, '.config', 'kilo'),
|
||||
zcode: path.join(HOME, '.zcode'),
|
||||
pi: path.join(HOME, '.pi', 'agent'), // dot-home-nested, no probe (like windsurf)
|
||||
};
|
||||
|
||||
// ── GOLDEN DEFAULTS ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('descriptor-driven equivalence: defaults (no env vars, no probe hits)', () => {
|
||||
// kimi is excluded: its default depends on real filesystem probing (probe-hit/miss
|
||||
// vary by machine). kimi probe scenarios are covered in the generic-agents-root suite
|
||||
// with injected existsSync.
|
||||
// antigravity is excluded: it also depends on real fs probing (probe candidates
|
||||
// ~/.gemini/antigravity, ~/.gemini/antigravity-ide, ~/.gemini/antigravity-cli);
|
||||
// a machine that has antigravity-ide or antigravity-cli but not antigravity gets a
|
||||
@@ -148,18 +130,8 @@ describe('descriptor-driven equivalence: env-var overrides', () => {
|
||||
{ runtime: 'cursor', envKey: 'CURSOR_CONFIG_DIR', value: '/custom/cursor' },
|
||||
{ runtime: 'codex', envKey: 'CODEX_HOME', value: '/custom/codex' },
|
||||
{ runtime: 'grok', envKey: 'GROK_AGENTS_HOME', value: '/custom/grok' },
|
||||
{ runtime: 'augment', envKey: 'AUGMENT_CONFIG_DIR', value: '/custom/augment' },
|
||||
{ runtime: 'trae', envKey: 'TRAE_CONFIG_DIR', value: '/custom/trae' },
|
||||
{ runtime: 'qwen', envKey: 'QWEN_CONFIG_DIR', value: '/custom/qwen' },
|
||||
{ runtime: 'hermes', envKey: 'HERMES_HOME', value: '/custom/hermes' },
|
||||
{ runtime: 'codebuddy', envKey: 'CODEBUDDY_CONFIG_DIR', value: '/custom/codebuddy' },
|
||||
{ runtime: 'cline', envKey: 'CLINE_CONFIG_DIR', value: '/custom/cline' },
|
||||
{ runtime: 'windsurf', envKey: 'WINDSURF_CONFIG_DIR', value: '/custom/windsurf' },
|
||||
{ runtime: 'antigravity', envKey: 'ANTIGRAVITY_CONFIG_DIR', value: '/custom/antigravity' },
|
||||
{ runtime: 'kimi', envKey: 'KIMI_CONFIG_DIR', value: '/custom/kimi' },
|
||||
{ runtime: 'opencode', envKey: 'OPENCODE_CONFIG_DIR', value: '/custom/opencode' },
|
||||
{ runtime: 'kilo', envKey: 'KILO_CONFIG_DIR', value: '/custom/kilo' },
|
||||
{ runtime: 'pi', envKey: 'PI_CODING_AGENT_DIR', value: '/custom/pi-agent' },
|
||||
];
|
||||
|
||||
for (const { runtime, envKey, value } of cases) {
|
||||
@@ -173,28 +145,6 @@ describe('descriptor-driven equivalence: env-var overrides', () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// copilot: COPILOT_CONFIG_DIR takes precedence over COPILOT_HOME
|
||||
test('copilot: COPILOT_CONFIG_DIR override (first env wins)', () => {
|
||||
const saved = clearAllEnvKeys();
|
||||
process.env['COPILOT_CONFIG_DIR'] = '/custom/copilot-dir';
|
||||
process.env['COPILOT_HOME'] = '/should/not/win';
|
||||
try {
|
||||
assert.strictEqual(String(getGlobalConfigDir('copilot')).replace(/\\/g, '/'), '/custom/copilot-dir');
|
||||
} finally {
|
||||
restoreEnvKeys(saved);
|
||||
}
|
||||
});
|
||||
|
||||
test('copilot: COPILOT_HOME fallback when COPILOT_CONFIG_DIR absent', () => {
|
||||
const saved = clearAllEnvKeys();
|
||||
process.env['COPILOT_HOME'] = '/custom/copilot-home';
|
||||
try {
|
||||
assert.strictEqual(String(getGlobalConfigDir('copilot')).replace(/\\/g, '/'), '/custom/copilot-home');
|
||||
} finally {
|
||||
restoreEnvKeys(saved);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── GOLDEN TILDE EXPANSION ─────────────────────────────────────────────────────
|
||||
@@ -205,60 +155,6 @@ describe('descriptor-driven equivalence: tilde expansion in env overrides', () =
|
||||
assert.strictEqual(getGlobalConfigDir('claude'), path.join(HOME, 'foo'));
|
||||
});
|
||||
});
|
||||
|
||||
test('kimi: KIMI_CONFIG_DIR=~/kimi expands to homedir/kimi', () => {
|
||||
withEnv({ KIMI_CONFIG_DIR: '~/kimi' }, () => {
|
||||
assert.strictEqual(getGlobalConfigDir('kimi'), path.join(HOME, 'kimi'));
|
||||
});
|
||||
});
|
||||
|
||||
test('pi: PI_CODING_AGENT_DIR=~/pi-agent expands to homedir/pi-agent', () => {
|
||||
withEnv({ PI_CODING_AGENT_DIR: '~/pi-agent' }, () => {
|
||||
assert.strictEqual(getGlobalConfigDir('pi'), path.join(HOME, 'pi-agent'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── #3023: PI_CODING_AGENT_DIR override — pi's own upstream config-dir env var ──
|
||||
//
|
||||
// pi's real source (`packages/coding-agent/src/config.ts`) reads
|
||||
// `PI_CODING_AGENT_DIR` to override its GLOBAL agent dir outright — the whole
|
||||
// `~/.pi/agent` path, not just the `.pi` segment. That is exactly the
|
||||
// dot-home-nested env-override shape `resolveConfigHomeFromDescriptor` already
|
||||
// implements for antigravity/windsurf: `env[0]` set -> `expandTilde(value)`
|
||||
// returned directly, no join with parent/name. Adding the var to pi's
|
||||
// `capabilities/pi/capability.json` `configHome.env` was the whole fix; no new
|
||||
// resolver branch was needed.
|
||||
describe('#3023: pi PI_CODING_AGENT_DIR — dot-home-nested override semantics', () => {
|
||||
test('unset -> default ~/.pi/agent', () => {
|
||||
const result = resolveConfigHomeFromDescriptor(
|
||||
{ kind: 'dot-home-nested', name: 'agent', parent: '.pi', env: ['PI_CODING_AGENT_DIR'] },
|
||||
{ env: {}, home: '/home/u', existsSync: () => false },
|
||||
);
|
||||
assert.strictEqual(result, path.join('/home/u', '.pi', 'agent'));
|
||||
});
|
||||
|
||||
test('set -> full override wins outright (whole path, not joined with parent/name)', () => {
|
||||
const result = resolveConfigHomeFromDescriptor(
|
||||
{ kind: 'dot-home-nested', name: 'agent', parent: '.pi', env: ['PI_CODING_AGENT_DIR'] },
|
||||
{ env: { PI_CODING_AGENT_DIR: '/custom/pi-agent' }, home: '/home/u', existsSync: () => false },
|
||||
);
|
||||
assert.strictEqual(result, '/custom/pi-agent');
|
||||
});
|
||||
|
||||
// Tilde expansion against an INJECTED home (not the real os.homedir()) is
|
||||
// covered below in "expandTilde honors an injected opts.home" — the fix for
|
||||
// the bug where `expandTilde` ignored `resolveConfigHomeFromDescriptor`'s
|
||||
// `opts.home` and always resolved `~` against the real os.homedir(), shared
|
||||
// by every dot-home/dot-home-nested/xdg/generic-agents-root env override.
|
||||
|
||||
test('empty-string env value falls back to the default, never redirects to a bogus path', () => {
|
||||
const result = resolveConfigHomeFromDescriptor(
|
||||
{ kind: 'dot-home-nested', name: 'agent', parent: '.pi', env: ['PI_CODING_AGENT_DIR'] },
|
||||
{ env: { PI_CODING_AGENT_DIR: '' }, home: '/home/u', existsSync: () => false },
|
||||
);
|
||||
assert.strictEqual(result, path.join('/home/u', '.pi', 'agent'));
|
||||
});
|
||||
});
|
||||
|
||||
// ── expandTilde honors an injected opts.home (not just the real os.homedir()) ─
|
||||
@@ -283,15 +179,6 @@ describe('expandTilde honors an injected opts.home (regression)', () => {
|
||||
assert.notStrictEqual(result, path.join(HOME, 'custom'));
|
||||
});
|
||||
|
||||
test('pi (dot-home-nested): PI_CODING_AGENT_DIR=~/custom + injected home → resolves under injected home, not os.homedir()', () => {
|
||||
const result = resolveConfigHomeFromDescriptor(
|
||||
{ kind: 'dot-home-nested', name: 'agent', parent: '.pi', env: ['PI_CODING_AGENT_DIR'] },
|
||||
{ env: { PI_CODING_AGENT_DIR: '~/custom' }, home: INJECTED_HOME },
|
||||
);
|
||||
assert.strictEqual(result, path.join(INJECTED_HOME, 'custom'));
|
||||
assert.notStrictEqual(result, path.join(HOME, 'custom'));
|
||||
});
|
||||
|
||||
test('claude: absolute env override + injected home → unchanged (tilde expansion not triggered)', () => {
|
||||
const result = resolveConfigHomeFromDescriptor(
|
||||
{ kind: 'dot-home', name: '.claude', env: ['CLAUDE_CONFIG_DIR'] },
|
||||
@@ -300,14 +187,6 @@ describe('expandTilde honors an injected opts.home (regression)', () => {
|
||||
assert.strictEqual(result, '/absolute/custom');
|
||||
});
|
||||
|
||||
test('pi: absolute env override + injected home → unchanged (tilde expansion not triggered)', () => {
|
||||
const result = resolveConfigHomeFromDescriptor(
|
||||
{ kind: 'dot-home-nested', name: 'agent', parent: '.pi', env: ['PI_CODING_AGENT_DIR'] },
|
||||
{ env: { PI_CODING_AGENT_DIR: '/absolute/custom' }, home: INJECTED_HOME },
|
||||
);
|
||||
assert.strictEqual(result, '/absolute/custom');
|
||||
});
|
||||
|
||||
test('claude: no injected home → still resolves under the real os.homedir() (no behavior change for production callers)', () => {
|
||||
assert.strictEqual(
|
||||
resolveConfigHomeFromDescriptor(
|
||||
@@ -318,27 +197,12 @@ describe('expandTilde honors an injected opts.home (regression)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('pi: no injected home → still resolves under the real os.homedir() (no behavior change for production callers)', () => {
|
||||
assert.strictEqual(
|
||||
resolveConfigHomeFromDescriptor(
|
||||
{ kind: 'dot-home-nested', name: 'agent', parent: '.pi', env: ['PI_CODING_AGENT_DIR'] },
|
||||
{ env: { PI_CODING_AGENT_DIR: '~/custom' } },
|
||||
),
|
||||
path.join(HOME, 'custom'),
|
||||
);
|
||||
});
|
||||
|
||||
test('claude: no injected home, via getGlobalConfigDir (real end-to-end seam) → real os.homedir()', () => {
|
||||
withEnv({ CLAUDE_CONFIG_DIR: '~/custom' }, () => {
|
||||
assert.strictEqual(getGlobalConfigDir('claude'), path.join(HOME, 'custom'));
|
||||
});
|
||||
});
|
||||
|
||||
test('pi: no injected home, via getGlobalConfigDir (real end-to-end seam) → real os.homedir()', () => {
|
||||
withEnv({ PI_CODING_AGENT_DIR: '~/custom' }, () => {
|
||||
assert.strictEqual(getGlobalConfigDir('pi'), path.join(HOME, 'custom'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── #3023 review finding 1: whitespace-only env override must fall back ──────
|
||||
@@ -362,7 +226,6 @@ describe('expandTilde honors an injected opts.home (regression)', () => {
|
||||
// byte-identical". Only entirely-blank values are rejected.
|
||||
describe('#3023 review finding 1: whitespace-only env override falls back to default (regression)', () => {
|
||||
const CLAUDE_DESCRIPTOR = { kind: 'dot-home', name: '.claude', env: ['CLAUDE_CONFIG_DIR'] };
|
||||
const PI_DESCRIPTOR = { kind: 'dot-home-nested', name: 'agent', parent: '.pi', env: ['PI_CODING_AGENT_DIR'] };
|
||||
|
||||
describe('claude (dot-home)', () => {
|
||||
test('whitespace-only env value falls back to the descriptor default, never the literal whitespace string', () => {
|
||||
@@ -407,54 +270,6 @@ describe('#3023 review finding 1: whitespace-only env override falls back to def
|
||||
});
|
||||
});
|
||||
|
||||
describe('pi (dot-home-nested)', () => {
|
||||
test('whitespace-only env value falls back to the descriptor default, never the literal whitespace string', () => {
|
||||
const result = resolveConfigHomeFromDescriptor(PI_DESCRIPTOR, {
|
||||
env: { PI_CODING_AGENT_DIR: ' ' },
|
||||
home: '/home/u',
|
||||
existsSync: () => false,
|
||||
});
|
||||
assert.strictEqual(result, path.join('/home/u', '.pi', 'agent'));
|
||||
assert.notStrictEqual(result, ' ');
|
||||
});
|
||||
|
||||
test('empty-string env value falls back to the default (existing behavior preserved)', () => {
|
||||
const result = resolveConfigHomeFromDescriptor(PI_DESCRIPTOR, {
|
||||
env: { PI_CODING_AGENT_DIR: '' },
|
||||
home: '/home/u',
|
||||
existsSync: () => false,
|
||||
});
|
||||
assert.strictEqual(result, path.join('/home/u', '.pi', 'agent'));
|
||||
});
|
||||
|
||||
test('unset env value falls back to the default', () => {
|
||||
const result = resolveConfigHomeFromDescriptor(PI_DESCRIPTOR, {
|
||||
env: {},
|
||||
home: '/home/u',
|
||||
existsSync: () => false,
|
||||
});
|
||||
assert.strictEqual(result, path.join('/home/u', '.pi', 'agent'));
|
||||
});
|
||||
|
||||
test('env value with interior spaces resolves under the injected home, spaces intact (guard is not over-broad)', () => {
|
||||
const result = resolveConfigHomeFromDescriptor(PI_DESCRIPTOR, {
|
||||
env: { PI_CODING_AGENT_DIR: '~/My Agent Dir' },
|
||||
home: '/home/u',
|
||||
existsSync: () => false,
|
||||
});
|
||||
assert.strictEqual(result, path.join('/home/u', 'My Agent Dir'));
|
||||
});
|
||||
|
||||
test('normal absolute path env value is unchanged', () => {
|
||||
const result = resolveConfigHomeFromDescriptor(PI_DESCRIPTOR, {
|
||||
env: { PI_CODING_AGENT_DIR: '/custom/pi-agent' },
|
||||
home: '/home/u',
|
||||
existsSync: () => false,
|
||||
});
|
||||
assert.strictEqual(result, '/custom/pi-agent');
|
||||
});
|
||||
});
|
||||
|
||||
// Full branch coverage: the same whitespace-only guard applies to every
|
||||
// env-override consumption site in resolveConfigHomeFromDescriptor, not
|
||||
// just dot-home/dot-home-nested. Each of these fails before the fix and
|
||||
@@ -556,48 +371,6 @@ describe('descriptor-driven equivalence: xdg runtimes (opencode, kilo)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// kilo
|
||||
test('kilo: KILO_CONFIG (file-path) → dirname', () => {
|
||||
const saved = clearAllEnvKeys();
|
||||
process.env['KILO_CONFIG'] = '/home/u/cfg/kilo.json';
|
||||
try {
|
||||
assert.strictEqual(String(getGlobalConfigDir('kilo')).replace(/\\/g, '/'), '/home/u/cfg');
|
||||
} finally {
|
||||
restoreEnvKeys(saved);
|
||||
}
|
||||
});
|
||||
|
||||
test('kilo: KILO_CONFIG_DIR takes precedence over KILO_CONFIG', () => {
|
||||
const saved = clearAllEnvKeys();
|
||||
process.env['KILO_CONFIG_DIR'] = '/dir/wins';
|
||||
process.env['KILO_CONFIG'] = '/file/loses.json';
|
||||
try {
|
||||
assert.strictEqual(String(getGlobalConfigDir('kilo')).replace(/\\/g, '/'), '/dir/wins');
|
||||
} finally {
|
||||
restoreEnvKeys(saved);
|
||||
}
|
||||
});
|
||||
|
||||
test('kilo: KILO_CONFIG takes precedence over XDG_CONFIG_HOME', () => {
|
||||
const saved = clearAllEnvKeys();
|
||||
process.env['KILO_CONFIG'] = '/cfg/kilo.json';
|
||||
process.env['XDG_CONFIG_HOME'] = '/xdg/should/lose';
|
||||
try {
|
||||
assert.strictEqual(String(getGlobalConfigDir('kilo')).replace(/\\/g, '/'), '/cfg');
|
||||
} finally {
|
||||
restoreEnvKeys(saved);
|
||||
}
|
||||
});
|
||||
|
||||
test('kilo: XDG_CONFIG_HOME → ~/.config/kilo subdir', () => {
|
||||
const saved = clearAllEnvKeys();
|
||||
process.env['XDG_CONFIG_HOME'] = '/xdg';
|
||||
try {
|
||||
assert.strictEqual(getGlobalConfigDir('kilo'), path.join('/xdg', 'kilo'));
|
||||
} finally {
|
||||
restoreEnvKeys(saved);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── GOLDEN DOT-HOME-NESTED (antigravity probe) ────────────────────────────────
|
||||
@@ -800,18 +573,6 @@ describe('descriptor-driven equivalence: dot-home-nested antigravity probe hit/m
|
||||
assert.strictEqual(result, '/custom/ag');
|
||||
});
|
||||
|
||||
test('windsurf (no probe) → ~/.codeium/windsurf regardless of existsSync', () => {
|
||||
const result = resolveConfigHomeFromDescriptor(
|
||||
{
|
||||
kind: 'dot-home-nested',
|
||||
name: 'windsurf',
|
||||
parent: '.codeium',
|
||||
env: ['WINDSURF_CONFIG_DIR'],
|
||||
},
|
||||
{ env: {}, home: '/home/u', existsSync: () => true },
|
||||
);
|
||||
assert.strictEqual(result, path.join('/home/u', '.codeium', 'windsurf'));
|
||||
});
|
||||
});
|
||||
|
||||
// ── #213/#217 thread-4: existing-install ambiguity detector ───────────────────
|
||||
@@ -875,120 +636,9 @@ describe('detectAntigravityDirAmbiguity (migration/operator-guidance signal)', (
|
||||
});
|
||||
});
|
||||
|
||||
// ── GOLDEN GENERIC-AGENTS-ROOT (kimi probe) ───────────────────────────────────
|
||||
|
||||
describe('descriptor-driven equivalence: generic-agents-root kimi probe hit/miss', () => {
|
||||
test('kimi probe-miss → recommended root ~/.config/agents', () => {
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-equiv-kimi-miss-'));
|
||||
try {
|
||||
const result = resolveConfigHomeFromDescriptor(
|
||||
{
|
||||
kind: 'generic-agents-root',
|
||||
name: 'agents',
|
||||
env: ['KIMI_CONFIG_DIR'],
|
||||
probe: ['~/.config/agents', '~/.agents'],
|
||||
probeExists: 'skills',
|
||||
},
|
||||
{ env: {}, home: tmpHome, existsSync: () => false },
|
||||
);
|
||||
assert.strictEqual(result, path.join(tmpHome, '.config', 'agents'));
|
||||
} finally {
|
||||
cleanup(tmpHome);
|
||||
}
|
||||
});
|
||||
|
||||
test('kimi probe-hit on recommended root ~/.config/agents/skills', () => {
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-equiv-kimi-recommended-'));
|
||||
try {
|
||||
const recommended = path.join(tmpHome, '.config', 'agents');
|
||||
const result = resolveConfigHomeFromDescriptor(
|
||||
{
|
||||
kind: 'generic-agents-root',
|
||||
name: 'agents',
|
||||
env: ['KIMI_CONFIG_DIR'],
|
||||
probe: ['~/.config/agents', '~/.agents'],
|
||||
probeExists: 'skills',
|
||||
},
|
||||
{
|
||||
env: {},
|
||||
home: tmpHome,
|
||||
existsSync: (p) => p === path.join(recommended, 'skills'),
|
||||
},
|
||||
);
|
||||
assert.strictEqual(result, recommended);
|
||||
} finally {
|
||||
cleanup(tmpHome);
|
||||
}
|
||||
});
|
||||
|
||||
test('kimi probe-hit on fallback ~/.agents/skills (recommended does not exist)', () => {
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-equiv-kimi-fallback-'));
|
||||
try {
|
||||
const fallback = path.join(tmpHome, '.agents');
|
||||
const result = resolveConfigHomeFromDescriptor(
|
||||
{
|
||||
kind: 'generic-agents-root',
|
||||
name: 'agents',
|
||||
env: ['KIMI_CONFIG_DIR'],
|
||||
probe: ['~/.config/agents', '~/.agents'],
|
||||
probeExists: 'skills',
|
||||
},
|
||||
{
|
||||
env: {},
|
||||
home: tmpHome,
|
||||
existsSync: (p) => p === path.join(fallback, 'skills'),
|
||||
},
|
||||
);
|
||||
assert.strictEqual(result, fallback);
|
||||
} finally {
|
||||
cleanup(tmpHome);
|
||||
}
|
||||
});
|
||||
|
||||
test('kimi: KIMI_CONFIG_DIR env override wins over any probe', () => {
|
||||
const result = resolveConfigHomeFromDescriptor(
|
||||
{
|
||||
kind: 'generic-agents-root',
|
||||
name: 'agents',
|
||||
env: ['KIMI_CONFIG_DIR'],
|
||||
probe: ['~/.config/agents', '~/.agents'],
|
||||
probeExists: 'skills',
|
||||
},
|
||||
{ env: { KIMI_CONFIG_DIR: '/custom/kimi' }, home: '/home/u', existsSync: () => true },
|
||||
);
|
||||
assert.strictEqual(result, '/custom/kimi');
|
||||
});
|
||||
|
||||
// Verify resolveKimiGlobalDir wrapper delegates correctly
|
||||
test('resolveKimiGlobalDir wrapper: probe-miss → recommended root', () => {
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-equiv-rkgd-miss-'));
|
||||
try {
|
||||
assert.strictEqual(
|
||||
resolveKimiGlobalDir({ env: {}, home: tmpHome, existsSync: () => false }),
|
||||
path.join(tmpHome, '.config', 'agents'),
|
||||
);
|
||||
} finally {
|
||||
cleanup(tmpHome);
|
||||
}
|
||||
});
|
||||
|
||||
test('resolveKimiGlobalDir wrapper: fallback probe-hit', () => {
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-equiv-rkgd-hit-'));
|
||||
try {
|
||||
const fallback = path.join(tmpHome, '.agents');
|
||||
assert.strictEqual(
|
||||
resolveKimiGlobalDir({
|
||||
env: {},
|
||||
home: tmpHome,
|
||||
existsSync: (p) => p === path.join(fallback, 'skills'),
|
||||
}),
|
||||
fallback,
|
||||
);
|
||||
} finally {
|
||||
cleanup(tmpHome);
|
||||
}
|
||||
});
|
||||
// ── resolveAntigravityGlobalDir wrapper ───────────────────────────────────────
|
||||
|
||||
describe('descriptor-driven equivalence: resolveAntigravityGlobalDir wrapper', () => {
|
||||
// Verify resolveAntigravityGlobalDir wrapper delegates correctly
|
||||
test('resolveAntigravityGlobalDir wrapper: probe-miss → ~/.gemini/antigravity', () => {
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-equiv-ragd-miss-'));
|
||||
@@ -1089,23 +739,6 @@ describe('descriptor-driven equivalence: unknown runtime fallback', () => {
|
||||
});
|
||||
|
||||
describe('descriptor-driven global skills base', () => {
|
||||
test('hermes skills base is derived from descriptor artifact layout', () => {
|
||||
const saved = clearAllEnvKeys();
|
||||
try {
|
||||
assert.strictEqual(getGlobalSkillsBase('hermes'), path.join(HOME, '.hermes', 'skills', 'msd'));
|
||||
} finally {
|
||||
restoreEnvKeys(saved);
|
||||
}
|
||||
});
|
||||
|
||||
test('kilo skills base is derived from configHome.skillsHome descriptor', () => {
|
||||
const saved = clearAllEnvKeys();
|
||||
try {
|
||||
assert.strictEqual(getGlobalSkillsBase('kilo'), path.join(HOME, '.kilo', 'skills'));
|
||||
} finally {
|
||||
restoreEnvKeys(saved);
|
||||
}
|
||||
});
|
||||
|
||||
test('synthetic runtime skillsHome descriptor resolves without a runtime-name branch', () => {
|
||||
const base = resolveSkillsBaseFromDescriptor(
|
||||
@@ -1215,17 +848,8 @@ describe('bug #3126: runtime-homes getGlobalConfigDir — defaults', () => {
|
||||
['claude', path.join(os.homedir(), '.claude')],
|
||||
['cursor', path.join(os.homedir(), '.cursor')],
|
||||
['codex', path.join(os.homedir(), '.codex')],
|
||||
['copilot', path.join(os.homedir(), '.copilot')],
|
||||
['antigravity', path.join(os.homedir(), '.gemini', 'antigravity')],
|
||||
['windsurf', path.join(os.homedir(), '.codeium', 'windsurf')],
|
||||
['augment', path.join(os.homedir(), '.augment')],
|
||||
['trae', path.join(os.homedir(), '.trae')],
|
||||
['qwen', path.join(os.homedir(), '.qwen')],
|
||||
['hermes', path.join(os.homedir(), '.hermes')],
|
||||
['codebuddy', path.join(os.homedir(), '.codebuddy')],
|
||||
['cline', path.join(os.homedir(), '.cline')],
|
||||
['opencode', path.join(os.homedir(), '.config', 'opencode')],
|
||||
['kilo', path.join(os.homedir(), '.config', 'kilo')],
|
||||
];
|
||||
for (const [runtime, expected] of defaults) {
|
||||
test(`${runtime} default configDir`, () => {
|
||||
@@ -1285,15 +909,6 @@ describe('bug #3126: runtime-homes env-var overrides', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
test('kilo uses XDG_CONFIG_HOME when KILO_CONFIG_DIR absent', () => {
|
||||
withEnv('KILO_CONFIG_DIR', undefined, () => {
|
||||
withEnv('KILO_CONFIG', undefined, () => {
|
||||
withEnv('XDG_CONFIG_HOME', '/xdg', () => {
|
||||
assert.strictEqual(getGlobalConfigDir('kilo'), path.join('/xdg', 'kilo'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('antigravity detects 2.x IDE dir when legacy dir is absent', () => {
|
||||
const home = require('node:fs').mkdtempSync(path.join(os.tmpdir(), 'msd-antigravity-home-'));
|
||||
@@ -1328,22 +943,6 @@ describe('bug #3126: runtime-homes getGlobalSkillsBase', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
test('hermes: skills at <configDir>/skills/msd (nested layout #2841)', () => {
|
||||
withEnv('HERMES_HOME', undefined, () => {
|
||||
assert.strictEqual(
|
||||
getGlobalSkillsBase('hermes'),
|
||||
path.join(os.homedir(), '.hermes', 'skills', 'msd'),
|
||||
);
|
||||
});
|
||||
});
|
||||
test('cline: returns ~/.cline/skills (skills-capable since v3.48.0 — #782)', () => {
|
||||
withEnv('CLINE_CONFIG_DIR', undefined, () => {
|
||||
assert.strictEqual(
|
||||
getGlobalSkillsBase('cline'),
|
||||
path.join(os.homedir(), '.cline', 'skills'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('bug #3126: runtime-homes getGlobalSkillDir', () => {
|
||||
@@ -1355,22 +954,6 @@ describe('bug #3126: runtime-homes getGlobalSkillDir', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
test('hermes: <configDir>/skills/msd/<skillName>', () => {
|
||||
withEnv('HERMES_HOME', undefined, () => {
|
||||
assert.strictEqual(
|
||||
getGlobalSkillDir('hermes', 'msd-executor'),
|
||||
path.join(os.homedir(), '.hermes', 'skills', 'msd', 'msd-executor'),
|
||||
);
|
||||
});
|
||||
});
|
||||
test('cline: returns ~/.cline/skills/msd-executor (skills-capable since v3.48.0 — #782)', () => {
|
||||
withEnv('CLINE_CONFIG_DIR', undefined, () => {
|
||||
assert.strictEqual(
|
||||
getGlobalSkillDir('cline', 'msd-executor'),
|
||||
path.join(os.homedir(), '.cline', 'skills', 'msd-executor'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGlobalConfigDir — explicitDir override and opencode/kilo file-path precedence', () => {
|
||||
@@ -1434,47 +1017,6 @@ describe('getGlobalConfigDir — explicitDir override and opencode/kilo file-pat
|
||||
});
|
||||
});
|
||||
|
||||
// ── kilo: KILO_CONFIG file-path step ─────────────────────────────────────
|
||||
test('kilo: KILO_CONFIG → path.dirname(expandTilde(value))', () => {
|
||||
withEnv('KILO_CONFIG_DIR', undefined, () => {
|
||||
withEnv('XDG_CONFIG_HOME', undefined, () => {
|
||||
withEnv('KILO_CONFIG', '/home/u/cfg/kilo.json', () => {
|
||||
assert.strictEqual(String(getGlobalConfigDir('kilo')).replace(/\\/g, '/'), '/home/u/cfg');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('kilo: KILO_CONFIG_DIR takes precedence over KILO_CONFIG', () => {
|
||||
withEnv('KILO_CONFIG_DIR', '/dir/wins', () => {
|
||||
withEnv('KILO_CONFIG', '/file/loses.json', () => {
|
||||
assert.strictEqual(String(getGlobalConfigDir('kilo')).replace(/\\/g, '/'), '/dir/wins');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('kilo: KILO_CONFIG takes precedence over XDG_CONFIG_HOME', () => {
|
||||
withEnv('KILO_CONFIG_DIR', undefined, () => {
|
||||
withEnv('KILO_CONFIG', '/cfg/kilo.json', () => {
|
||||
withEnv('XDG_CONFIG_HOME', '/xdg/should/lose', () => {
|
||||
assert.strictEqual(String(getGlobalConfigDir('kilo')).replace(/\\/g, '/'), '/cfg');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('kilo: default ~/.config/kilo when no env vars set', () => {
|
||||
withEnv('KILO_CONFIG_DIR', undefined, () => {
|
||||
withEnv('KILO_CONFIG', undefined, () => {
|
||||
withEnv('XDG_CONFIG_HOME', undefined, () => {
|
||||
assert.strictEqual(
|
||||
getGlobalConfigDir('kilo'),
|
||||
path.join(os.homedir(), '.config', 'kilo'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('bug #3126: buildAgentSkillsBlock resolves the agent-skills path per runtime (not hardcoded .claude)', () => {
|
||||
@@ -1570,93 +1112,3 @@ describe('bug #3126: buildAgentSkillsBlock resolves the agent-skills path per ru
|
||||
}
|
||||
|
||||
// ── resolveKimiHooksTomlDir: per-runtime hooks root (#2755) ──────────────────
|
||||
|
||||
describe('resolveKimiHooksTomlDir — per-runtime hooks root (#2755)', () => {
|
||||
// Pure path computation; the fixture home never needs to exist on disk.
|
||||
const FIXTURE_HOME = path.join(os.tmpdir(), 'msd-2755-home-fixture');
|
||||
const at = (...seg) => path.join(FIXTURE_HOME, ...seg);
|
||||
|
||||
test('a bare call does not throw', () => {
|
||||
assert.doesNotThrow(() => resolveKimiHooksTomlDir());
|
||||
});
|
||||
|
||||
test('an omitted runtime still resolves Kimi CLI\'s ~/.kimi (back-compat)', () => {
|
||||
// The function is exported; callers and tests outside this diff pass no
|
||||
// runtime, and their destination must not move.
|
||||
assert.equal(resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: {} }), at('.kimi'));
|
||||
});
|
||||
|
||||
test('runtime "kimi" resolves ~/.kimi', () => {
|
||||
assert.equal(
|
||||
resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: {}, runtime: 'kimi' }),
|
||||
at('.kimi'),
|
||||
);
|
||||
});
|
||||
|
||||
test('runtime "kimi-code" resolves ~/.kimi-code', () => {
|
||||
assert.equal(
|
||||
resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: {}, runtime: 'kimi-code' }),
|
||||
at('.kimi-code'),
|
||||
);
|
||||
});
|
||||
|
||||
test('KIMI_SHARE_DIR overrides the kimi root', () => {
|
||||
assert.equal(
|
||||
String(resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: { KIMI_SHARE_DIR: '/share' }, runtime: 'kimi' })).replace(/\\/g, '/'),
|
||||
'/share',
|
||||
);
|
||||
});
|
||||
|
||||
test('KIMI_CODE_HOME overrides the kimi-code root', () => {
|
||||
assert.equal(
|
||||
String(resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: { KIMI_CODE_HOME: '/kcode' }, runtime: 'kimi-code' })).replace(/\\/g, '/'),
|
||||
'/kcode',
|
||||
);
|
||||
});
|
||||
|
||||
test('KIMI_SHARE_DIR does not hijack the kimi-code root', () => {
|
||||
// KIMI_SHARE_DIR is Kimi CLI's own upstream var. Before #2755 it silently
|
||||
// redirected kimi-code too — that accident was the issue's suggested
|
||||
// workaround, and the fix must make it inert.
|
||||
assert.equal(
|
||||
resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: { KIMI_SHARE_DIR: '/share' }, runtime: 'kimi-code' }),
|
||||
at('.kimi-code'),
|
||||
);
|
||||
});
|
||||
|
||||
test('KIMI_CODE_HOME does not hijack the kimi root', () => {
|
||||
assert.equal(
|
||||
resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: { KIMI_CODE_HOME: '/kcode' }, runtime: 'kimi' }),
|
||||
at('.kimi'),
|
||||
);
|
||||
});
|
||||
|
||||
test('an unknown runtime falls back to ~/.kimi', () => {
|
||||
assert.equal(
|
||||
resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: {}, runtime: 'not-a-kimi' }),
|
||||
at('.kimi'),
|
||||
);
|
||||
});
|
||||
|
||||
test('every kimi-hooks-toml runtime resolves a distinct hooks root', () => {
|
||||
// Divergence guard (CLAUDE.md → Generative Fix Divergence). The resolver
|
||||
// hardcodes the two Kimi roots while the capability registry independently
|
||||
// decides which runtimes use the kimi-hooks-toml surface. If a third one is
|
||||
// ever added without teaching the resolver its root, it silently inherits
|
||||
// ~/.kimi — which IS the #2755 defect, re-created. This fails the moment
|
||||
// those two surfaces drift apart.
|
||||
const capsDir = path.join(ROOT, 'capabilities');
|
||||
const ids = fs.readdirSync(capsDir).filter((id) => {
|
||||
const file = path.join(capsDir, id, 'capability.json');
|
||||
if (!fs.existsSync(file)) return false;
|
||||
return JSON.parse(fs.readFileSync(file, 'utf8'))?.runtime?.hooksSurface === 'kimi-hooks-toml';
|
||||
});
|
||||
|
||||
assert.deepEqual(ids.sort(), ['kimi', 'kimi-code'],
|
||||
'the kimi-hooks-toml runtime set changed — teach resolveKimiHooksTomlDir the new root, then update this list');
|
||||
|
||||
const roots = ids.map((id) => resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: {}, runtime: id }));
|
||||
assert.equal(new Set(roots).size, roots.length,
|
||||
`each kimi-hooks-toml runtime must resolve its own root; got ${JSON.stringify(roots)}`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user