Files
claudecodeui/server/src/modules/llm/tests/llm-unifier.skills.test.ts

208 lines
8.1 KiB
TypeScript

import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { llmSkillsService } from '@/modules/llm/services/skills.service.js';
const patchHomeDir = (nextHomeDir: string) => {
const original = os.homedir;
(os as any).homedir = () => nextHomeDir;
return () => {
(os as any).homedir = original;
};
};
const createSkill = async (
rootSkillsDirectory: string,
directoryName: string,
metadata: {
name: string;
description: string;
},
) => {
const skillDirectory = path.join(rootSkillsDirectory, directoryName);
await fs.mkdir(skillDirectory, { recursive: true });
await fs.writeFile(
path.join(skillDirectory, 'SKILL.md'),
`---\nname: ${metadata.name}\ndescription: ${metadata.description}\n---\n\n# ${metadata.name}\n`,
'utf8',
);
};
/**
* This test covers Claude skills fetching from user/project/plugin locations and plugin namespace invocation.
*/
test('llmSkillsService lists claude user/project/plugin skills with proper invocation names', { concurrency: false }, async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-claude-'));
const workspacePath = path.join(tempRoot, 'workspace');
const pluginInstallPath = path.join(tempRoot, 'plugin-install');
await fs.mkdir(workspacePath, { recursive: true });
const restoreHomeDir = patchHomeDir(tempRoot);
try {
await createSkill(path.join(tempRoot, '.claude', 'skills'), 'user-helper', {
name: 'user-helper',
description: 'User skill description',
});
await createSkill(path.join(workspacePath, '.claude', 'skills'), 'project-helper', {
name: 'project-helper',
description: 'Project skill description',
});
await createSkill(path.join(pluginInstallPath, 'skills'), 'plugin-helper', {
name: 'plugin-helper',
description: 'Plugin skill description',
});
await fs.mkdir(path.join(tempRoot, '.claude', 'plugins'), { recursive: true });
await fs.writeFile(
path.join(tempRoot, '.claude', 'settings.json'),
JSON.stringify({
enabledPlugins: {
'example-skills@anthropic-agent-skills': true,
},
}),
'utf8',
);
await fs.writeFile(
path.join(tempRoot, '.claude', 'plugins', 'installed_plugins.json'),
JSON.stringify({
version: 2,
plugins: {
'example-skills@anthropic-agent-skills': [
{
installPath: pluginInstallPath,
},
],
},
}),
'utf8',
);
const skills = await llmSkillsService.listProviderSkills('claude', { workspacePath });
assert.ok(skills.some((skill) => skill.scope === 'user' && skill.invocation === '/user-helper'));
assert.ok(skills.some((skill) => skill.scope === 'project' && skill.invocation === '/project-helper'));
assert.ok(skills.some((skill) => skill.scope === 'plugin' && skill.invocation === '/example-skills:plugin-helper'));
} finally {
restoreHomeDir();
await fs.rm(tempRoot, { recursive: true, force: true });
}
});
/**
* This test covers Codex skills discovery across repo/user/system locations and `$` invocation prefix.
*/
test('llmSkillsService lists codex skills from repo/user/system locations with dollar invocation', { concurrency: false }, async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-codex-'));
const repoRoot = path.join(tempRoot, 'repo');
const workspacePath = path.join(repoRoot, 'packages', 'app');
await fs.mkdir(workspacePath, { recursive: true });
await fs.mkdir(path.join(repoRoot, '.git'), { recursive: true });
const restoreHomeDir = patchHomeDir(tempRoot);
try {
await createSkill(path.join(workspacePath, '.agents', 'skills'), 'cwd-skill', {
name: 'cwd-skill',
description: 'cwd skill',
});
await createSkill(path.join(workspacePath, '..', '.agents', 'skills'), 'parent-skill', {
name: 'parent-skill',
description: 'parent skill',
});
await createSkill(path.join(repoRoot, '.agents', 'skills'), 'repo-root-skill', {
name: 'repo-root-skill',
description: 'repo root skill',
});
await createSkill(path.join(tempRoot, '.agents', 'skills'), 'user-skill', {
name: 'user-skill',
description: 'user skill',
});
await createSkill(path.join(tempRoot, '.codex', 'skills', '.system'), 'system-skill', {
name: 'system-skill',
description: 'system skill',
});
const skills = await llmSkillsService.listProviderSkills('codex', { workspacePath });
assert.ok(skills.some((skill) => skill.name === 'cwd-skill' && skill.invocation === '$cwd-skill'));
assert.ok(skills.some((skill) => skill.name === 'parent-skill' && skill.invocation === '$parent-skill'));
assert.ok(skills.some((skill) => skill.name === 'repo-root-skill' && skill.invocation === '$repo-root-skill'));
assert.ok(skills.some((skill) => skill.name === 'user-skill' && skill.invocation === '$user-skill'));
assert.ok(skills.some((skill) => skill.name === 'system-skill' && skill.invocation === '$system-skill'));
} finally {
restoreHomeDir();
await fs.rm(tempRoot, { recursive: true, force: true });
}
});
/**
* This test covers Gemini skill fetch locations and slash-based invocation format.
*/
test('llmSkillsService lists gemini skills from documented directories', { concurrency: false }, async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-gemini-'));
const workspacePath = path.join(tempRoot, 'workspace');
await fs.mkdir(workspacePath, { recursive: true });
const restoreHomeDir = patchHomeDir(tempRoot);
try {
await createSkill(path.join(tempRoot, '.gemini', 'skills'), 'home-gemini', {
name: 'home-gemini',
description: 'home gemini skill',
});
await createSkill(path.join(tempRoot, '.agents', 'skills'), 'home-agents', {
name: 'home-agents',
description: 'home agents skill',
});
await createSkill(path.join(workspacePath, '.gemini', 'skills'), 'project-gemini', {
name: 'project-gemini',
description: 'project gemini skill',
});
await createSkill(path.join(workspacePath, '.agents', 'skills'), 'project-agents', {
name: 'project-agents',
description: 'project agents skill',
});
const skills = await llmSkillsService.listProviderSkills('gemini', { workspacePath });
assert.ok(skills.some((skill) => skill.invocation === '/home-gemini'));
assert.ok(skills.some((skill) => skill.invocation === '/home-agents'));
assert.ok(skills.some((skill) => skill.invocation === '/project-gemini'));
assert.ok(skills.some((skill) => skill.invocation === '/project-agents'));
} finally {
restoreHomeDir();
await fs.rm(tempRoot, { recursive: true, force: true });
}
});
/**
* This test covers Cursor skill fetch locations and slash-based invocation format.
*/
test('llmSkillsService lists cursor skills from documented directories', { concurrency: false }, async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-cursor-'));
const workspacePath = path.join(tempRoot, 'workspace');
await fs.mkdir(workspacePath, { recursive: true });
const restoreHomeDir = patchHomeDir(tempRoot);
try {
await createSkill(path.join(workspacePath, '.agents', 'skills'), 'project-agents', {
name: 'project-agents',
description: 'project agents skill',
});
await createSkill(path.join(workspacePath, '.cursor', 'skills'), 'project-cursor', {
name: 'project-cursor',
description: 'project cursor skill',
});
await createSkill(path.join(tempRoot, '.cursor', 'skills'), 'user-cursor', {
name: 'user-cursor',
description: 'user cursor skill',
});
const skills = await llmSkillsService.listProviderSkills('cursor', { workspacePath });
assert.ok(skills.some((skill) => skill.invocation === '/project-agents'));
assert.ok(skills.some((skill) => skill.invocation === '/project-cursor'));
assert.ok(skills.some((skill) => skill.invocation === '/user-cursor'));
} finally {
restoreHomeDir();
await fs.rm(tempRoot, { recursive: true, force: true });
}
});