diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 742cbd6e56b..fcc192412f2 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -638,6 +638,7 @@ jobs: src/main/windows/windows-host-job.win32.test.ts src/main/cli/wsl-cli-powershell-boundary.test.ts src/main/orca-profiles/profile-index-store.test.ts + config/scripts/generate-bundled-skill-guides.test.mjs src/main/runtime/repo-worktree-admin-fingerprint.test.ts src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts src/shared/secure-file-fsync-flags.test.ts diff --git a/config/scripts/generate-bundled-skill-guides.mjs b/config/scripts/generate-bundled-skill-guides.mjs index f78ce1402b0..bc44f5e72d6 100644 --- a/config/scripts/generate-bundled-skill-guides.mjs +++ b/config/scripts/generate-bundled-skill-guides.mjs @@ -169,6 +169,12 @@ async function assertStubSourcesMatchTopics(repoRoot) { } } +// Why: path.relative yields `\` on Windows, but these paths are asserted in tests and pasted into commands. +// split(sep) rather than replaceAll('\\', '/') so a POSIX filename containing a backslash survives intact. +function toPosixRelativePath(repoRoot, filePath, pathModule = path) { + return pathModule.relative(repoRoot, filePath).split(pathModule.sep).join('/') +} + async function buildArtifacts(repoRoot = REPO_ROOT) { const guideRoot = path.join(repoRoot, 'skill-guides') const sourceFiles = (await readdir(guideRoot, { withFileTypes: true })) @@ -193,7 +199,7 @@ async function buildArtifacts(repoRoot = REPO_ROOT) { // Why: Git may render text with native EOLs despite repository policy; the // embedded guide and generated projection must have one platform-neutral identity. const markdown = normalizeMarkdown(await readFile(sourcePath, 'utf8')) - const frontmatter = parseFrontmatter(markdown, path.relative(repoRoot, sourcePath)) + const frontmatter = parseFrontmatter(markdown, toPosixRelativePath(repoRoot, sourcePath)) if (frontmatter.name !== name) { throw new Error(`Guide source ${name}.md declares mismatched name ${frontmatter.name}`) } @@ -243,7 +249,7 @@ async function verifyArtifacts(artifacts, repoRoot = REPO_ROOT) { if (stale.length > 0) { throw new Error( `Generated bundled skill guides are stale:\n${stale - .map((filePath) => path.relative(repoRoot, filePath).split(path.sep).join('/')) + .map((filePath) => toPosixRelativePath(repoRoot, filePath)) .join('\n')}\nRun node config/scripts/generate-bundled-skill-guides.mjs --write.` ) } @@ -272,6 +278,7 @@ export { normalizeMarkdown, parseFrontmatter, serializeEmbeddedModule, + toPosixRelativePath, verifyArtifacts, writeArtifacts } diff --git a/config/scripts/generate-bundled-skill-guides.test.mjs b/config/scripts/generate-bundled-skill-guides.test.mjs index e756157595b..e8e456f9934 100644 --- a/config/scripts/generate-bundled-skill-guides.test.mjs +++ b/config/scripts/generate-bundled-skill-guides.test.mjs @@ -14,6 +14,7 @@ import { frontmatterBlock, normalizeMarkdown, parseFrontmatter, + toPosixRelativePath, verifyArtifacts, writeArtifacts } from './generate-bundled-skill-guides.mjs' @@ -232,6 +233,20 @@ describe('bundled skill guide generator', () => { await expect(verifyArtifacts(artifacts, root)).rejects.toThrow('skills/computer-use/SKILL.md') }) + // Why: the stale-artifact assertions above only exercise the Windows separator on Windows, + // and this file is not in pr.yml's Windows test allowlist — inject path.win32 so CI covers it. + it('formats contributor-facing paths with forward slashes on every platform', () => { + expect( + toPosixRelativePath('C:\\repo', 'C:\\repo\\src\\cli\\bundled-skill-guides.ts', path.win32) + ).toBe('src/cli/bundled-skill-guides.ts') + expect( + toPosixRelativePath('C:\\repo', 'C:\\repo\\skills\\computer-use\\SKILL.md', path.win32) + ).toBe('skills/computer-use/SKILL.md') + expect(toPosixRelativePath('/repo', '/repo/skills/computer-use/SKILL.md', path.posix)).toBe( + 'skills/computer-use/SKILL.md' + ) + }) + it('rejects mismatched source names and ambiguous aliases', async () => { const root = await createFixture() await writeFile(