Files
clawX/tests/unit/skill-quick-access.test.ts

249 lines
8.0 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { collectQuickAccessSkills, filterEnabledQuickAccessSkills, type QuickAccessSkill } from '@electron/utils/skill-quick-access';
const testRoot = join(tmpdir(), 'clawx-tests', 'skill-quick-access');
function writeSkill(baseDir: string, skillName: string, content: string): void {
const skillDir = join(baseDir, skillName);
mkdirSync(skillDir, { recursive: true });
writeFileSync(join(skillDir, 'SKILL.md'), content, 'utf8');
}
describe('collectQuickAccessSkills', () => {
beforeEach(() => {
rmSync(testRoot, { recursive: true, force: true });
mkdirSync(testRoot, { recursive: true });
});
afterEach(() => {
rmSync(testRoot, { recursive: true, force: true });
});
it('prioritizes workspace over openclaw over agents duplicates', async () => {
const workspaceDir = join(testRoot, 'workspace');
const openClawDir = join(testRoot, 'openclaw');
const personalAgentsDir = join(testRoot, 'personal-agents');
writeSkill(
join(workspaceDir, 'skill'),
'create-skill',
"---\ndescription: Workspace version wins.\n---\n# Workspace Skill\n",
);
writeSkill(
join(openClawDir, 'skills'),
'create-skill',
"---\ndescription: OpenClaw fallback.\n---\n# OpenClaw Skill\n",
);
writeSkill(
join(personalAgentsDir, '.agents', 'skills'),
'create-skill',
"---\ndescription: Agents fallback.\n---\n# Agents Skill\n",
);
writeSkill(
join(openClawDir, 'skills'),
'summarize',
"---\ndescription: Summarize files and URLs.\n---\n# Summarize\n",
);
const skills = await collectQuickAccessSkills({
agentsRoots: [join(personalAgentsDir, '.agents', 'skills')],
legacyRoots: [],
openClawRoots: [join(openClawDir, 'skills')],
workspace: workspaceDir,
openClawDir,
});
expect(skills.map((skill) => `${skill.source}:${skill.name}`)).toEqual([
'workspace:create-skill',
'openclaw:summarize',
]);
expect(skills[0]).toMatchObject({
name: 'create-skill',
source: 'workspace',
description: 'Workspace version wins.',
});
});
it('supports plural skills directories and falls back to body text descriptions', async () => {
const workspaceDir = join(testRoot, 'workspace');
const openClawDir = join(testRoot, 'openclaw');
writeSkill(
join(workspaceDir, 'skills'),
'docs-search',
"# Docs Search\n\nSearch project docs and summarize the answer.\n",
);
const skills = await collectQuickAccessSkills({
agentsRoots: [],
legacyRoots: [],
openClawRoots: [join(openClawDir, 'skills')],
workspace: workspaceDir,
openClawDir,
});
expect(skills).toHaveLength(1);
expect(skills[0]).toMatchObject({
name: 'docs-search',
source: 'workspace',
description: 'Search project docs and summarize the answer.',
});
});
it('loads project and personal .agents skill directories under the agents source', async () => {
const workspaceDir = join(testRoot, 'workspace');
const personalAgentsDir = join(testRoot, 'personal-agents');
writeSkill(
join(workspaceDir, '.agents', 'skills'),
'project-skill',
"---\ndescription: Project level .agents skill.\n---\n# Project Skill\n",
);
writeSkill(
join(personalAgentsDir, '.agents', 'skills'),
'personal-skill',
"---\ndescription: Personal .agents skill.\n---\n# Personal Skill\n",
);
const skills = await collectQuickAccessSkills({
agentsRoots: [
join(workspaceDir, '.agents', 'skills'),
join(personalAgentsDir, '.agents', 'skills'),
],
legacyRoots: [],
openClawRoots: [join(testRoot, 'openclaw', 'skills')],
workspace: workspaceDir,
openClawDir: join(testRoot, 'openclaw'),
});
expect(skills.map((skill) => `${skill.source}:${skill.name}`)).toEqual([
'agents:personal-skill',
'agents:project-skill',
]);
expect(skills.find((skill) => skill.name === 'personal-skill')).toMatchObject({
source: 'agents',
sourceLabel: '.agents',
});
expect(skills.find((skill) => skill.name === 'project-skill')).toMatchObject({
source: 'agents',
sourceLabel: '.agents',
});
});
it('prefers project .agents skills over personal .agents duplicates', async () => {
const workspaceDir = join(testRoot, 'workspace');
const personalAgentsDir = join(testRoot, 'personal-agents');
writeSkill(
join(workspaceDir, '.agents', 'skills'),
'shared-skill',
"---\ndescription: Project .agents wins.\n---\n# Shared Skill\n",
);
writeSkill(
join(personalAgentsDir, '.agents', 'skills'),
'shared-skill',
"---\ndescription: Personal .agents fallback.\n---\n# Shared Skill\n",
);
const skills = await collectQuickAccessSkills({
agentsRoots: [
join(workspaceDir, '.agents', 'skills'),
join(personalAgentsDir, '.agents', 'skills'),
],
legacyRoots: [],
openClawRoots: [join(testRoot, 'openclaw', 'skills')],
workspace: workspaceDir,
openClawDir: join(testRoot, 'openclaw'),
});
expect(skills).toHaveLength(1);
expect(skills[0]).toMatchObject({
name: 'shared-skill',
source: 'agents',
sourceLabel: '.agents',
description: 'Project .agents wins.',
});
});
it('loads legacy openclaw and extension skill roots at the lowest priority', async () => {
const workspaceDir = join(testRoot, 'workspace');
const openClawDir = join(testRoot, 'openclaw');
const agentsDir = join(testRoot, 'agents-home');
const extensionDir = join(testRoot, 'extensions');
writeSkill(
join(openClawDir, 'skills'),
'apple-notes',
"---\ndescription: Legacy OpenClaw built-in skill.\n---\n# Apple Notes\n",
);
writeSkill(
join(extensionDir, 'wecom', 'skills'),
'wecom-meeting-manage',
"---\ndescription: Extension-provided skill.\n---\n# WeCom Meeting Manage\n",
);
const skills = await collectQuickAccessSkills({
agentsRoots: [join(agentsDir, '.agents', 'skills')],
legacyRoots: [
join(openClawDir, 'skills'),
join(extensionDir, 'wecom', 'skills'),
],
openClawRoots: [],
workspace: workspaceDir,
});
expect(skills.map((skill) => `${skill.source}:${skill.name}`)).toEqual([
'legacy:apple-notes',
'legacy:wecom-meeting-manage',
]);
});
it('filters out disabled skills from runtime/config state', () => {
const skills: QuickAccessSkill[] = [
{
name: 'apple-notes',
description: 'Legacy OpenClaw built-in skill.',
source: 'legacy',
sourceLabel: 'Legacy',
manifestPath: '/tmp/openclaw/skills/apple-notes/SKILL.md',
baseDir: '/tmp/openclaw/skills/apple-notes',
},
{
name: 'wecom-meeting-manage',
description: 'Extension skill.',
source: 'legacy',
sourceLabel: 'Legacy',
manifestPath: '/tmp/extensions/wecom/skills/wecom-meeting-manage/SKILL.md',
baseDir: '/tmp/extensions/wecom/skills/wecom-meeting-manage',
},
{
name: 'workspace-skill',
description: 'Workspace skill.',
source: 'workspace',
sourceLabel: 'Workspace',
manifestPath: '/tmp/workspace/skills/workspace-skill/SKILL.md',
baseDir: '/tmp/workspace/skills/workspace-skill',
},
];
const filtered = filterEnabledQuickAccessSkills(
skills,
[
{ skillKey: 'apple-notes', disabled: false, baseDir: '/tmp/openclaw/skills/apple-notes' },
{ skillKey: 'wecom-meeting-manage', disabled: true, baseDir: '/tmp/extensions/wecom/skills/wecom-meeting-manage' },
],
{
'workspace-skill': { enabled: true },
},
);
expect(filtered.map((skill) => skill.name)).toEqual([
'apple-notes',
'workspace-skill',
]);
});
});