feat(doctor): surface skill directory health in diagnostics

This commit is contained in:
William Valentin
2026-02-12 17:05:04 -08:00
parent fc3d2ab4d8
commit 0a19f01639
3 changed files with 84 additions and 7 deletions
+50
View File
@@ -173,4 +173,54 @@ automation:
const telegramCheck = results.find(r => r.label.includes('Telegram'));
expect(telegramCheck?.status).toBe('skip');
});
it('reports WARN for skills when configured directories are missing', async () => {
mkdirSync(testDir, { recursive: true });
const configPath = join(testDir, 'skills-missing.yaml');
writeFileSync(configPath, `
telegram:
bot_token: "test-token"
allowed_chat_ids: [123]
models:
default:
provider: anthropic
model: claude-sonnet
skills:
bundled_dir: "${join(testDir, 'missing-bundled')}"
`);
const ctx: DoctorContext = { configPath, dataDir: testDir };
const results = await runChecks(ctx);
const skillsCheck = results.find(r => r.label.includes('Skills loaded'));
expect(skillsCheck?.status).toBe('warn');
expect(skillsCheck?.detail).toContain('missing dirs');
});
it('reports PASS for skills when configured directory exists', async () => {
const bundledDir = join(testDir, 'bundled');
const sampleSkillDir = join(bundledDir, 'sample');
mkdirSync(sampleSkillDir, { recursive: true });
writeFileSync(join(sampleSkillDir, 'SKILL.md'), '# Sample skill');
const configPath = join(testDir, 'skills-pass.yaml');
writeFileSync(configPath, `
telegram:
bot_token: "test-token"
allowed_chat_ids: [123]
models:
default:
provider: anthropic
model: claude-sonnet
skills:
bundled_dir: "${bundledDir}"
`);
const ctx: DoctorContext = { configPath, dataDir: testDir };
const results = await runChecks(ctx);
const skillsCheck = results.find(r => r.label.includes('Skills loaded'));
expect(skillsCheck?.status).toBe('pass');
expect(skillsCheck?.detail).toContain('1 skill(s), 1 available, 0 unavailable');
});
});
+22 -4
View File
@@ -189,13 +189,31 @@ const checkSkills: Check = async (ctx) => {
return { status: 'skip', label: 'Skills loaded', detail: '(config invalid)' };
}
try {
const skillDirs = {
bundled: ctx.config.skills.bundled_dir,
managed: ctx.config.skills.managed_dir,
workspace: ctx.config.skills.workspace_dir,
};
const missingDirs = Object.entries(skillDirs)
.filter(([, dir]) => Boolean(dir) && !existsSync(dir as string))
.map(([tier, dir]) => `${tier}:${dir as string}`);
const { loadAllSkills } = await import('../skills/index.js');
const skills = loadAllSkills({
bundledDir: ctx.config.skills.bundled_dir,
managedDir: ctx.config.skills.managed_dir,
workspaceDir: ctx.config.skills.workspace_dir,
bundledDir: skillDirs.bundled,
managedDir: skillDirs.managed,
workspaceDir: skillDirs.workspace,
});
return { status: 'pass', label: 'Skills loaded', detail: `(${skills.length} skill(s))` };
const available = skills.filter((skill) => skill.available).length;
const unavailable = skills.length - available;
const detailParts = [`${skills.length} skill(s), ${available} available, ${unavailable} unavailable`];
if (missingDirs.length > 0) {
detailParts.push(`missing dirs: ${missingDirs.join(', ')}`);
return { status: 'warn', label: 'Skills loaded', detail: detailParts.join(' — ') };
}
return { status: 'pass', label: 'Skills loaded', detail: detailParts.join(' — ') };
} catch (err) {
return { status: 'fail', label: 'Skills loaded', detail: err instanceof Error ? err.message : String(err) };
}