test(02-02): add failing Kimi global skill install tests

- Expect Kimi global layout to stage skills/gsd-*/SKILL.md
- Assert --kimi --global writes gsd-new-project SKILL.md
- Preserve explicit --kimi --local no-op guard
This commit is contained in:
Viktorplus
2026-06-06 13:59:55 +02:00
parent ca68213664
commit da1b6111e7
3 changed files with 64 additions and 10 deletions

View File

@@ -94,11 +94,15 @@ describe('Kimi runtime homes', () => {
});
describe('Kimi runtime artifact layout', () => {
test('known runtime with empty Phase 1 layout placeholder', () => {
test('global layout stages Kimi skills while local layout remains guarded', () => {
const globalLayout = resolveRuntimeArtifactLayout('kimi', '/tmp/kimi-config', 'global');
assert.strictEqual(globalLayout.runtime, 'kimi');
assert.strictEqual(globalLayout.configDir, '/tmp/kimi-config');
assert.deepStrictEqual(globalLayout.kinds, []);
assert.strictEqual(globalLayout.kinds.length, 1);
assert.strictEqual(globalLayout.kinds[0].kind, 'skills');
assert.strictEqual(globalLayout.kinds[0].destSubpath, 'skills');
assert.strictEqual(globalLayout.kinds[0].prefix, 'gsd-');
assert.strictEqual(typeof globalLayout.kinds[0].stage, 'function');
const localLayout = resolveRuntimeArtifactLayout('kimi', '/tmp/kimi-config', 'local');
assert.strictEqual(localLayout.runtime, 'kimi');
@@ -139,7 +143,7 @@ describe('Kimi local install guard', () => {
}
});
test('--kimi --global exits successfully without writing unconverted Kimi artifacts', () => {
test('--kimi --global writes converted Kimi skills only', () => {
const tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-project-'));
const tmpConfig = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-config-'));
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-home-'));
@@ -157,14 +161,20 @@ describe('Kimi local install guard', () => {
assert.strictEqual(
result.status,
0,
`expected --kimi --global skeleton guard to no-op successfully\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
`expected --kimi --global to install Kimi skills successfully\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
);
const combined = `${result.stdout}\n${result.stderr}`;
assert.match(combined, /Kimi global install/i);
assert.match(combined, /deferred/i);
assert.match(combined, /Installing for .*Kimi/i);
assert.match(combined, /Installed \d+ skills to skills\//i);
assert.ok(!fs.existsSync(path.join(tmpConfig, 'skills')), 'must not write unconverted Kimi skills');
assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents')), 'must not write unconverted Kimi agents');
const skillFile = path.join(tmpConfig, 'skills', 'gsd-new-project', 'SKILL.md');
assert.ok(fs.existsSync(skillFile), 'must write gsd-new-project/SKILL.md');
const skillContent = fs.readFileSync(skillFile, 'utf8');
assert.match(skillContent, /^name: gsd-new-project$/m);
assert.match(skillContent, /\/skill:gsd-new-project/);
assert.doesNotMatch(skillContent, /kimi_cli\.tools|system_prompt_path|^version: 1$/m);
assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents')), 'must not write Kimi agents');
assert.ok(!fs.existsSync(path.join(tmpConfig, 'gsd-core')), 'must not write workflow payloads as Kimi artifacts');
assert.ok(!fs.existsSync(path.join(tmpConfig, 'hooks')), 'must not write hooks under the Kimi root');
} finally {

View File

@@ -49,13 +49,13 @@ const RESOLVED_CORE = resolveProfile({ modes: ['core'], manifest: MANIFEST });
const SKILLS_RUNTIMES_LAYOUT = [
'claude', 'cursor', 'codex', 'copilot', 'antigravity',
'windsurf', 'augment', 'trae', 'qwen', 'codebuddy',
'windsurf', 'augment', 'trae', 'qwen', 'kimi', 'codebuddy',
];
const ALL_RUNTIMES_LAYOUT = [
'claude', 'cursor', 'gemini', 'codex', 'copilot', 'antigravity',
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy',
'cline', 'opencode', 'kilo',
'cline', 'kimi', 'opencode', 'kilo',
];
function countPrefixedEntries(destDir, prefix) {
@@ -94,6 +94,15 @@ describe('installRuntimeArtifacts — skills runtimes write gsd-prefixed skill d
`${runtime}: ${skillsKind.prefix}help/SKILL.md must exist`
);
if (runtime === 'kimi') {
const newProjectSkill = path.join(destDir, 'gsd-new-project', 'SKILL.md');
assert.ok(fs.existsSync(newProjectSkill), 'kimi: gsd-new-project/SKILL.md must exist');
const content = fs.readFileSync(newProjectSkill, 'utf8');
assert.match(content, /^name: gsd-new-project$/m);
assert.match(content, /\/skill:gsd-new-project/);
assert.doesNotMatch(content, /kimi_cli\.tools|system_prompt_path|^version: 1$/m);
}
if (RESOLVED_CORE.skills !== '*') {
const prefixedCount = countPrefixedEntries(destDir, skillsKind.prefix || 'gsd-');
assert.strictEqual(prefixedCount, RESOLVED_CORE.skills.size,

View File

@@ -175,6 +175,24 @@ describe('resolveRuntimeArtifactLayout — qwen', () => {
});
});
describe('resolveRuntimeArtifactLayout — kimi', () => {
test('returns global skills layout and guarded empty local layout for kimi', () => {
const globalLayout = resolveRuntimeArtifactLayout('kimi', FAKE_DIR, 'global');
assert.strictEqual(globalLayout.runtime, 'kimi');
assert.strictEqual(globalLayout.configDir, FAKE_DIR);
assert.strictEqual(globalLayout.kinds.length, 1);
assert.strictEqual(globalLayout.kinds[0].kind, 'skills');
assert.strictEqual(globalLayout.kinds[0].destSubpath, 'skills');
assert.strictEqual(globalLayout.kinds[0].prefix, 'gsd-');
assert.strictEqual(typeof globalLayout.kinds[0].stage, 'function');
const localLayout = resolveRuntimeArtifactLayout('kimi', FAKE_DIR, 'local');
assert.strictEqual(localLayout.runtime, 'kimi');
assert.strictEqual(localLayout.configDir, FAKE_DIR);
assert.deepStrictEqual(localLayout.kinds, []);
});
});
describe('resolveRuntimeArtifactLayout — hermes', () => {
test('returns correct layout for hermes', () => {
const layout = resolveRuntimeArtifactLayout('hermes', FAKE_DIR);
@@ -379,6 +397,23 @@ describe('stage — skills kind (claude global)', () => {
});
});
describe('stage — skills kind (kimi global)', () => {
test('stage returns Kimi SKILL.md dirs with /skill:gsd-* invocations', () => {
const layout = resolveRuntimeArtifactLayout('kimi', FAKE_STAGE_DIR, 'global');
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
assert.ok(skillsKind, 'should have a skills kind');
const stagedDir = skillsKind.stage(PROFILE_CORE);
assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist');
const skillMd = path.join(stagedDir, 'gsd-new-project', 'SKILL.md');
assert.ok(fs.existsSync(skillMd), 'gsd-new-project/SKILL.md must exist');
const content = fs.readFileSync(skillMd, 'utf8');
assert.match(content, /^name: gsd-new-project$/m);
assert.match(content, /\/skill:gsd-new-project/);
assert.doesNotMatch(content, /kimi_cli\.tools|system_prompt_path|^version: 1$/m);
});
});
describe('stage — opencode commands kind', () => {
test('opencode stage returns directory with .md files for selected skills', () => {
const layout = resolveRuntimeArtifactLayout('opencode', FAKE_STAGE_DIR);