refactor(core): harden skill frontmatter parsing (#16705)

This commit is contained in:
N. Taylor Mullen
2026-01-14 18:44:08 -08:00
committed by GitHub
parent 4848f42486
commit d0bbc7fa59
2 changed files with 71 additions and 5 deletions

View File

@@ -194,4 +194,64 @@ Do something.
expect(skills[0].description).toContain('Expertise in reviewing code');
expect(skills[0].description).toContain('check');
});
it('should handle empty name or description', async () => {
const skillDir = path.join(testRootDir, 'empty-skill');
await fs.mkdir(skillDir, { recursive: true });
const skillFile = path.join(skillDir, 'SKILL.md');
await fs.writeFile(
skillFile,
`---
name:
description:
---
`,
);
const skills = await loadSkillsFromDir(testRootDir);
expect(skills).toHaveLength(1);
expect(skills[0].name).toBe('');
expect(skills[0].description).toBe('');
});
it('should handle indented name and description fields', async () => {
const skillDir = path.join(testRootDir, 'indented-fields');
await fs.mkdir(skillDir, { recursive: true });
const skillFile = path.join(skillDir, 'SKILL.md');
await fs.writeFile(
skillFile,
`---
name: indented-name
description: indented-desc
---
`,
);
const skills = await loadSkillsFromDir(testRootDir);
expect(skills).toHaveLength(1);
expect(skills[0].name).toBe('indented-name');
expect(skills[0].description).toBe('indented-desc');
});
it('should handle missing space after colon', async () => {
const skillDir = path.join(testRootDir, 'no-space');
await fs.mkdir(skillDir, { recursive: true });
const skillFile = path.join(skillDir, 'SKILL.md');
await fs.writeFile(
skillFile,
`---
name:no-space-name
description:no-space-desc
---
`,
);
const skills = await loadSkillsFromDir(testRootDir);
expect(skills).toHaveLength(1);
expect(skills[0].name).toBe('no-space-name');
expect(skills[0].description).toBe('no-space-desc');
});
});

View File

@@ -71,16 +71,22 @@ function parseSimpleFrontmatter(
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('name:')) {
name = line.substring(5).trim();
// Match "name:" at the start of the line (optional whitespace)
const nameMatch = line.match(/^\s*name:\s*(.*)$/);
if (nameMatch) {
name = nameMatch[1].trim();
continue;
}
if (line.startsWith('description:')) {
const descLines = [line.substring(12).trim()];
// Match "description:" at the start of the line (optional whitespace)
const descMatch = line.match(/^\s*description:\s*(.*)$/);
if (descMatch) {
const descLines = [descMatch[1].trim()];
// Check for multi-line description (indented continuation lines)
while (i + 1 < lines.length) {
const nextLine = lines[i + 1];
// If next line is indented, it's a continuation of the description
if (nextLine.match(/^[ \t]+\S/)) {
descLines.push(nextLine.trim());
i++;
@@ -169,7 +175,7 @@ export async function loadSkillFromFile(
name: frontmatter.name,
description: frontmatter.description,
location: filePath,
body: match[2].trim(),
body: match[2]?.trim() ?? '',
};
} catch (error) {
debugLogger.log(`Error parsing skill file ${filePath}:`, error);