-
Notifications
You must be signed in to change notification settings - Fork 240
fix(skills): report YAML frontmatter parse errors and serialize SKILL.md safely #1321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
easonLiangWorldedtech
wants to merge
10
commits into
Zoo-Code-Org:main
Choose a base branch
from
easonLiangWorldedtech:fix/skill-frontmatter-yaml-859
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
e9eed2e
fix(skills): safely serialize skill frontmatter
taltas 55d95f5
Merge remote-tracking branch 'origin/main' into fix/issue-859-skill-yaml
taltas d4f2ca3
fix(skills): report YAML frontmatter parse errors and serialize SKILL…
easonliang28 872179d
Merge remote-tracking branch 'upstream/main' into fix/skill-frontmatt…
easonliang28 6c2e44e
fix(i18n): add skill diagnostics translations and cover updateSkillModes
easonliang28 928f3bf
fix(webview): default skillDiagnostics to empty array in SkillsSettings
easonliang28 987fa9a
test(e2e): smoke test skill diagnostics across the extension-host bou…
easonliang28 65a0ff3
test(api): cover the test-only getSkillsState accessor
easonliang28 ae9b80a
test(webview): cover the remaining skill diagnostics patch lines
easonliang28 5153db6
fix(skills): serialize discovery scans, scope the quote hint, and par…
easonliang28 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| import * as assert from "assert" | ||
| import * as fs from "fs/promises" | ||
| import * as path from "path" | ||
|
|
||
| import * as vscode from "vscode" | ||
|
|
||
| import { setDefaultSuiteTimeout } from "./test-utils" | ||
| import { waitFor } from "./utils" | ||
|
|
||
| const GOOD_SKILL = "e2e-skill-good" | ||
| const BAD_SKILL = "e2e-skill-bad" | ||
|
|
||
| // Issue #859 reproduction content: the description is a double-quoted YAML | ||
| // scalar whose inner double quotes are left unescaped, which makes the | ||
| // frontmatter unparseable. | ||
| const MALFORMED_SKILL_MD = `--- | ||
| name: ${BAD_SKILL} | ||
| description: "Use when implementing features. Triggers on: "TDD", "test-driven development" | ||
| --- | ||
|
|
||
| # E2E Skill Bad | ||
|
|
||
| Instructions here. | ||
| ` | ||
|
|
||
| const FIXED_SKILL_MD = `--- | ||
| name: ${BAD_SKILL} | ||
| description: 'Use when implementing features. Triggers on: "TDD", "test-driven development"' | ||
| --- | ||
|
|
||
| # E2E Skill Bad | ||
|
|
||
| Instructions here. | ||
| ` | ||
|
|
||
| const GOOD_SKILL_MD = `--- | ||
| name: ${GOOD_SKILL} | ||
| description: A healthy skill used by the skill diagnostics e2e smoke test. | ||
| --- | ||
|
|
||
| # E2E Skill Good | ||
|
|
||
| Instructions here. | ||
| ` | ||
|
|
||
| // Write a skill file atomically (write to a sidecar, then rename over the | ||
| // target) so the extension host's file watcher only ever observes complete | ||
| // content. An in-place fs.writeFile is visible mid-write, the watcher can | ||
| // fire for that moment, and - because discovery scans are serialized - a | ||
| // mid-write event could be the last one, leaving a stale scan result. | ||
| const writeSkillFileAtomic = async (finalPath: string, content: string): Promise<void> => { | ||
| const tmpPath = `${finalPath}.tmp` | ||
| await fs.writeFile(tmpPath, content, "utf8") | ||
| await fs.rename(tmpPath, finalPath) | ||
| } | ||
|
|
||
| suite("Roo Code Skill Diagnostics", function () { | ||
| setDefaultSuiteTimeout(this) | ||
|
|
||
| let skillsRoot: string | ||
|
|
||
| setup(async function () { | ||
| const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath | ||
| assert.ok(workspaceRoot, "e2e workspace folder must be open") | ||
| skillsRoot = path.join(workspaceRoot, ".roo", "skills") | ||
| }) | ||
|
|
||
| teardown(async function () { | ||
| // Remove only the skill directories this suite created so pre-existing | ||
| // or other suites' fixtures under .roo/skills are left intact. | ||
| await Promise.all( | ||
| [GOOD_SKILL, BAD_SKILL].map((name) => fs.rm(path.join(skillsRoot, name), { recursive: true, force: true })), | ||
| ) | ||
| }) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| test("should surface a malformed SKILL.md as a diagnostic without hiding healthy skills", async function () { | ||
| this.timeout(180_000) | ||
|
|
||
| // Arrange: one healthy skill and one malformed skill on real disk in the | ||
| // workspace's .roo/skills directory, written atomically so the watcher | ||
| // only observes complete files. | ||
| await fs.mkdir(path.join(skillsRoot, GOOD_SKILL), { recursive: true }) | ||
| await writeSkillFileAtomic(path.join(skillsRoot, GOOD_SKILL, "SKILL.md"), GOOD_SKILL_MD) | ||
| await fs.mkdir(path.join(skillsRoot, BAD_SKILL), { recursive: true }) | ||
| const badSkillMd = path.join(skillsRoot, BAD_SKILL, "SKILL.md") | ||
| await writeSkillFileAtomic(badSkillMd, MALFORMED_SKILL_MD) | ||
|
|
||
| // Act: the extension host's file watcher re-discovers skills; wait until | ||
| // the real SkillsManager reports the healthy skill and a diagnostic for | ||
| // the malformed one. | ||
| await waitFor( | ||
| async () => { | ||
| const state = globalThis.api.getSkillsState() | ||
| const goodVisible = state.skills.some((skill) => skill.name === GOOD_SKILL) | ||
| const badDiagnosed = state.skillDiagnostics.some((diagnostic) => diagnostic.path.includes(BAD_SKILL)) | ||
| return goodVisible && badDiagnosed | ||
| }, | ||
| { timeout: 60_000, interval: 500 }, | ||
| ) | ||
|
|
||
| // Assert: the malformed skill is skipped with a diagnostic pointing at it, | ||
| // while the healthy skill is unaffected. | ||
| const state = globalThis.api.getSkillsState() | ||
| const diagnostic = state.skillDiagnostics.find((d) => d.path.includes(BAD_SKILL)) | ||
| assert.ok(diagnostic, "malformed SKILL.md should produce a diagnostic") | ||
| assert.strictEqual(diagnostic.source, "project") | ||
| assert.ok(diagnostic.message.length > 0, "diagnostic should carry the parse error message") | ||
| assert.ok( | ||
| state.skills.some((skill) => skill.name === GOOD_SKILL), | ||
| "healthy skill should still be discovered", | ||
| ) | ||
| assert.ok( | ||
| !state.skills.some((skill) => skill.name === BAD_SKILL), | ||
| "malformed skill should be omitted from skills", | ||
| ) | ||
|
|
||
| // Act: repair the frontmatter in place (atomically); the watcher | ||
| // re-discovers and the diagnostic clears. | ||
| await writeSkillFileAtomic(badSkillMd, FIXED_SKILL_MD) | ||
|
|
||
| await waitFor( | ||
| async () => { | ||
| const next = globalThis.api.getSkillsState() | ||
| const cleared = !next.skillDiagnostics.some((d) => d.path.includes(BAD_SKILL)) | ||
| const loaded = next.skills.some((skill) => skill.name === BAD_SKILL) | ||
| return cleared && loaded | ||
| }, | ||
| { timeout: 60_000, interval: 500 }, | ||
| ) | ||
|
|
||
| const fixed = globalThis.api.getSkillsState() | ||
| assert.ok( | ||
| fixed.skills.some((skill) => skill.name === BAD_SKILL), | ||
| "fixed skill should load after repair", | ||
| ) | ||
| assert.ok(fixed.skills.find((skill) => skill.name === BAD_SKILL)?.description.includes("TDD") === true) | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.