From 934b270960e9ba72aea9488cedd85eb63df12fc5 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 26 Apr 2026 14:06:38 -0700 Subject: [PATCH] test(browser-skills-e2e): exercise dispatch with bundled hackernews-frontpage Covers the full \$B skill list/show/test pipeline against the real bundled reference skill (defaultTierPaths picks up /browser-skills/). Verifies frontmatter shape, the three-tier walk surfaces the bundled entry, and \$B skill test successfully runs the bundled script.test.ts in a child bun process. \$B skill run end-to-end against the live network is intentionally NOT covered here (would be flaky against news.ycombinator.com); the spawn lifecycle is exercised in browser-skill-commands.test.ts using inline synthetic skills. Co-Authored-By: Claude Opus 4.7 (1M context) --- browse/test/browser-skills-e2e.test.ts | 89 ++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 browse/test/browser-skills-e2e.test.ts diff --git a/browse/test/browser-skills-e2e.test.ts b/browse/test/browser-skills-e2e.test.ts new file mode 100644 index 00000000..839ff32c --- /dev/null +++ b/browse/test/browser-skills-e2e.test.ts @@ -0,0 +1,89 @@ +/** + * browser-skills E2E — exercise the full dispatch path against the bundled + * `hackernews-frontpage` reference skill. Verifies: + * + * - $B skill list resolves the bundled tier and surfaces hackernews-frontpage + * - $B skill show returns the SKILL.md + * - $B skill test runs script.test.ts (which itself runs against the bundled + * fixture) and reports pass + * + * Coverage gap intentionally NOT here: $B skill run end-to-end against the + * bundled skill goes to live news.ycombinator.com and would be flaky. The + * spawnSkill lifecycle (env scrub, scoped token, timeout, stdout cap) is + * already covered by browse/test/browser-skill-commands.test.ts using inline + * scripts. + */ + +import { describe, test, expect, beforeAll } from 'bun:test'; +import { handleSkillCommand } from '../src/browser-skill-commands'; +import { listBrowserSkills, defaultTierPaths } from '../src/browser-skills'; +import { initRegistry, rotateRoot } from '../src/token-registry'; + +beforeAll(() => { + // Some preceding tests may have rotated the registry; ensure we have a root. + rotateRoot(); + initRegistry('e2e-root-token'); +}); + +describe('browser-skills E2E — bundled hackernews-frontpage', () => { + test('defaultTierPaths resolves bundled tier to /browser-skills/', () => { + const tiers = defaultTierPaths(); + expect(tiers.bundled).toMatch(/\/browser-skills$/); + // Bundled tier should exist on disk (the reference skill is shipped). + expect(require('fs').existsSync(tiers.bundled)).toBe(true); + }); + + test('listBrowserSkills() returns hackernews-frontpage at bundled tier', () => { + const skills = listBrowserSkills(); + const hn = skills.find(s => s.name === 'hackernews-frontpage'); + expect(hn).toBeTruthy(); + expect(hn!.tier).toBe('bundled'); + expect(hn!.frontmatter.host).toBe('news.ycombinator.com'); + expect(hn!.frontmatter.trusted).toBe(true); + expect(hn!.frontmatter.triggers).toContain('scrape hn frontpage'); + }); + + test('$B skill list dispatches and includes hackernews-frontpage', async () => { + const result = await handleSkillCommand(['list'], { port: 0 }); + expect(result).toContain('hackernews-frontpage'); + expect(result).toContain('bundled'); + expect(result).toContain('news.ycombinator.com'); + }); + + test('$B skill show hackernews-frontpage prints the SKILL.md', async () => { + const result = await handleSkillCommand(['show', 'hackernews-frontpage'], { port: 0 }); + expect(result).toContain('host: news.ycombinator.com'); + expect(result).toContain('trusted: true'); + expect(result).toContain('Hacker News front-page scraper'); + expect(result).toContain('triggers:'); + }); + + test('$B skill show errors clearly', async () => { + await expect(handleSkillCommand(['show', 'nonexistent-skill-xyz'], { port: 0 })) + .rejects.toThrow(/not found in any tier/); + }); + + test('$B skill help prints usage', async () => { + const result = await handleSkillCommand([], { port: 0 }); + expect(result).toContain('Usage'); + expect(result).toContain('list'); + expect(result).toContain('show'); + expect(result).toContain('run'); + }); + + test('$B skill rm cannot tombstone bundled tier (read-only)', async () => { + // The bundled hackernews-frontpage skill is shipped read-only; rm targets + // user tiers (project default, --global). Attempting rm on a name that + // only exists in bundled should error with "not found". + await expect(handleSkillCommand(['rm', 'hackernews-frontpage', '--global'], { port: 0 })) + .rejects.toThrow(/not found/); + }); + + // The `test` subcommand spawns `bun test script.test.ts` in the skill dir. + // It takes ~1s. Run it last so other assertions are quick. + test('$B skill test hackernews-frontpage runs script.test.ts and reports pass', async () => { + const result = await handleSkillCommand(['test', 'hackernews-frontpage'], { port: 0 }); + // bun test prints summary to stderr; handleSkillCommand returns stderr || stdout + expect(result).toMatch(/13 pass|0 fail|tests passed/); + }, 30_000); +});