Files
gstack/browse/test/browser-skills-e2e.test.ts
Garry Tan 25cf5edf21 v1.39.0.0 feat: buildFetchHandler factory unblocks gbrowser submodule consumption (#1511)
* feat: buildFetchHandler factory unblocks gbrowser submodule consumption

Add buildFetchHandler(cfg: ServerConfig): ServerHandle in browse/src/server.ts.
Refactor start() to delegate handler construction to the factory and read env
once via resolveConfigFromEnv(). Wire the beforeRoute hook (runs after the
tunnel surface filter, before per-route dispatch).

Auth is now cfg-driven end-to-end. Module-level AUTH_TOKEN const +
initRegistry(AUTH_TOKEN) boot call, validateAuth, and shutdown are deleted;
factory closure owns them. start() threads cfg.authToken into launchHeaded,
the state-file write, and the factory.

initRegistry is idempotent for same-token re-init; throws clearly for
different-token re-init. __resetRegistry() test helper added (mirrors
__resetConnectRateLimit). Existing tests that did rotateRoot() ->
initRegistry('fixed-token') swap to __resetRegistry() to avoid the new guard.

14 factory contract tests added covering ServerHandle shape, auth wiring,
validation throws, hook semantics across both surfaces, and registry
idempotency.

Source-pattern tests in dual-listener.test.ts and server-auth.test.ts
updated for the new identifiers (handle.fetchLocal/fetchTunnel, authToken,
shutdownFn).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: bump version and changelog (v1.39.0.0)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 21:55:29 -07:00

92 lines
4.1 KiB
TypeScript

/**
* 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, __resetRegistry } from '../src/token-registry';
beforeAll(() => {
// __resetRegistry zeroes rootToken so the new initRegistry mismatch guard
// doesn't fire. rotateRoot would leave a UUID in rootToken and the next
// initRegistry call would throw.
__resetRegistry();
initRegistry('e2e-root-token');
});
describe('browser-skills E2E — bundled hackernews-frontpage', () => {
test('defaultTierPaths resolves bundled tier to <repo>/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 <missing> 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);
});