Merge branch 'main' into feature/support-agents-directory-skills

This commit is contained in:
LaZzyMan 2026-03-18 19:12:00 +08:00
commit 6f362a89ae
304 changed files with 17148 additions and 9229 deletions

View file

@ -621,6 +621,118 @@ Skill 3 content`);
expect(baseDirs).toContain(path.join('/home/user', '.codex', 'skills'));
expect(baseDirs).toContain(path.join('/home/user', '.claude', 'skills'));
});
it('should return bundled-level base dir', () => {
const baseDir = manager.getSkillsBaseDir('bundled');
expect(baseDir).toMatch(/skills[/\\]bundled$/);
});
it('should throw for extension level', () => {
expect(() => manager.getSkillsBaseDir('extension')).toThrow(
'Extension skills do not have a base directory',
);
});
});
describe('bundled skills', () => {
const bundledDirSegment = path.join('skills', 'bundled');
const projectDirSegment = path.join('.qwen', 'skills');
const userDirSegment = path.join('.qwen', 'skills');
const projectPrefix = path.join('/test/project');
const userPrefix = path.join('/home/user');
const reviewDirEntry = {
name: 'review',
isDirectory: () => true,
isFile: () => false,
isSymbolicLink: () => false,
};
const emptyDir = [] as unknown as Awaited<ReturnType<typeof fs.readdir>>;
function mockReaddirForLevels(levels: Set<string>) {
vi.mocked(fs.readdir).mockImplementation((dirPath) => {
const pathStr = String(dirPath);
const isBundled =
pathStr.endsWith(bundledDirSegment) && !pathStr.includes('.qwen');
const isProject =
pathStr.includes(projectDirSegment) &&
pathStr.startsWith(projectPrefix);
const isUser =
pathStr.includes(userDirSegment) && pathStr.startsWith(userPrefix);
if (
(levels.has('bundled') && isBundled) ||
(levels.has('project') && isProject) ||
(levels.has('user') && isUser)
) {
return Promise.resolve([reviewDirEntry] as unknown as Awaited<
ReturnType<typeof fs.readdir>
>);
}
return Promise.resolve(emptyDir);
});
}
function setupReviewSkillMocks() {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue(`---
name: review
description: Review code changes
---
Review content`);
mockParseYaml.mockReturnValue({
name: 'review',
description: 'Review code changes',
});
}
it('should load bundled skills in listSkills', async () => {
mockReaddirForLevels(new Set(['bundled']));
setupReviewSkillMocks();
const skills = await manager.listSkills({ force: true });
expect(skills.some((s) => s.name === 'review')).toBe(true);
const reviewSkill = skills.find((s) => s.name === 'review');
expect(reviewSkill!.level).toBe('bundled');
});
it('should prioritize project-level over bundled skills with same name', async () => {
mockReaddirForLevels(new Set(['project', 'bundled']));
setupReviewSkillMocks();
const skills = await manager.listSkills({ force: true });
const reviewSkills = skills.filter((s) => s.name === 'review');
expect(reviewSkills).toHaveLength(1);
expect(reviewSkills[0].level).toBe('project');
});
it('should prioritize user-level over bundled skills with same name', async () => {
mockReaddirForLevels(new Set(['user', 'bundled']));
setupReviewSkillMocks();
const skills = await manager.listSkills({ force: true });
const reviewSkills = skills.filter((s) => s.name === 'review');
expect(reviewSkills).toHaveLength(1);
expect(reviewSkills[0].level).toBe('user');
});
it('should fall back to bundled level in loadSkill', async () => {
// Project, user, extension all empty; bundled has the skill
mockReaddirForLevels(new Set(['bundled']));
setupReviewSkillMocks();
const skill = await manager.loadSkill('review');
expect(skill).toBeDefined();
expect(skill!.name).toBe('review');
expect(skill!.level).toBe('bundled');
});
});
describe('change listeners', () => {