mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-17 01:31:26 +08:00
* 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>
92 lines
4.1 KiB
TypeScript
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);
|
|
});
|