From 225305416e9fef551af0a2144dd594847581e6b7 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 26 Apr 2026 05:07:25 -0700 Subject: [PATCH] test(skill-validation): cover bundled browser-skills Adds 7 assertions per bundled skill at /browser-skills//: - SKILL.md exists - frontmatter parses with required fields (name/host/triggers/args) - script.ts exists - _lib/browse-client.ts exists and matches the canonical SDK byte-for-byte - script.test.ts exists - script.ts imports browse from ./_lib/browse-client The byte-identical SDK check enforces the version-pinning contract: when the canonical SDK at browse/src/browse-client.ts changes, every bundled skill's _lib/ copy must be re-synced or this test fails. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/skill-validation.test.ts | 80 +++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/test/skill-validation.test.ts b/test/skill-validation.test.ts index 5bedeb24..6c5d3306 100644 --- a/test/skill-validation.test.ts +++ b/test/skill-validation.test.ts @@ -1669,3 +1669,83 @@ describe('sidebar agent (#584)', () => { expect(content).not.toContain("proc.stderr.on('data', () => {})"); }); }); + +// ─── Browser-skills validation ────────────────────────────────── +// +// Browser-skills are bundled in /browser-skills//. Each +// must have a SKILL.md whose frontmatter satisfies the contract enforced by +// browse/src/browser-skills.ts:parseSkillFile (host required, args + triggers +// parseable as the right shape). This test catches malformed bundled skills +// at CI time, before they ship. + +describe('Bundled browser-skills frontmatter contract', () => { + const browserSkillsRoot = path.join(ROOT, 'browser-skills'); + + function listBundledSkillDirs(): string[] { + if (!fs.existsSync(browserSkillsRoot)) return []; + return fs.readdirSync(browserSkillsRoot) + .filter(name => !name.startsWith('.')) + .map(name => path.join(browserSkillsRoot, name)) + .filter(dir => { + try { return fs.statSync(dir).isDirectory(); } catch { return false; } + }); + } + + test('each bundled skill has a SKILL.md', () => { + for (const dir of listBundledSkillDirs()) { + const skillFile = path.join(dir, 'SKILL.md'); + expect(fs.existsSync(skillFile)).toBe(true); + } + }); + + test('each bundled skill SKILL.md frontmatter parses with required fields', async () => { + const { parseSkillFile } = await import('../browse/src/browser-skills'); + for (const dir of listBundledSkillDirs()) { + const name = path.basename(dir); + const content = fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf-8'); + // parseSkillFile throws on missing required fields; we just want to + // make sure none of our shipped skills tripwire it. + const { frontmatter } = parseSkillFile(content, { skillName: name }); + expect(frontmatter.name).toBe(name); + expect(typeof frontmatter.host).toBe('string'); + expect(frontmatter.host.length).toBeGreaterThan(0); + expect(Array.isArray(frontmatter.triggers)).toBe(true); + expect(Array.isArray(frontmatter.args)).toBe(true); + } + }); + + test('each bundled skill has a script.ts', () => { + for (const dir of listBundledSkillDirs()) { + expect(fs.existsSync(path.join(dir, 'script.ts'))).toBe(true); + } + }); + + test('each bundled skill ships a sibling SDK at _lib/browse-client.ts', () => { + for (const dir of listBundledSkillDirs()) { + expect(fs.existsSync(path.join(dir, '_lib', 'browse-client.ts'))).toBe(true); + } + }); + + test('each bundled skill has a script.test.ts', () => { + for (const dir of listBundledSkillDirs()) { + expect(fs.existsSync(path.join(dir, 'script.test.ts'))).toBe(true); + } + }); + + test("each bundled skill's _lib/browse-client.ts matches the canonical SDK", () => { + // If the canonical SDK changes, the bundled copy must be updated. This + // test enforces that — the _lib copy should be byte-identical. + const canonical = fs.readFileSync(path.join(ROOT, 'browse', 'src', 'browse-client.ts'), 'utf-8'); + for (const dir of listBundledSkillDirs()) { + const sibling = fs.readFileSync(path.join(dir, '_lib', 'browse-client.ts'), 'utf-8'); + expect(sibling).toBe(canonical); + } + }); + + test('script.ts imports browse from ./_lib/browse-client', () => { + for (const dir of listBundledSkillDirs()) { + const content = fs.readFileSync(path.join(dir, 'script.ts'), 'utf-8'); + expect(content).toMatch(/from\s+['"]\.\/_lib\/browse-client['"]/); + } + }); +});