import { afterEach, describe, expect, test } from "bun:test"; import fs from "node:fs"; import path from "node:path"; import { BROWSER_PROVIDER_CONTRACTS, renderBrowserProviderContract, } from "../scripts/gstack2/browser-provider-contract"; import { createReadinessServer } from "../runtime/browser-provider-smoke.mjs"; const ROOT = path.resolve(import.meta.dir, ".."); const TREES = ["plan", "design", "qa", "debug", "review", "ship"]; const fixtures: Array> = []; afterEach(async () => { await Promise.all(fixtures.splice(0).map((fixture) => fixture.stop())); }); describe("browser provider setup contract", () => { test("defines every verified host plus Gemini without inventing Kimi or Pi core automation", () => { expect(BROWSER_PROVIDER_CONTRACTS.map((provider) => provider.id)).toEqual([ "claude", "codex", "gemini", "cursor", "github-copilot", "openclaw", "kimi", "pi", ]); expect(BROWSER_PROVIDER_CONTRACTS.map((provider) => provider.kind)).toEqual([ "native-extension", "native-in-app", "native-agent", "native-mcp", "native-in-app", "native-plugin", "no-native-automation", "extension-only", ]); const rendered = renderBrowserProviderContract(); expect(rendered).toContain("Never install an extension"); expect(rendered).toContain("Tool names or metadata alone are insufficient evidence"); expect(rendered).toContain("Inherited `CODEX_*`"); expect(rendered).toContain("Never report `ready` merely because"); expect(rendered).toContain("Gemini CLI has an experimental bundled `browser_agent`"); expect(rendered).toContain("current VS Code can expose built-in browser agent tools"); expect(rendered).toContain("OpenClaw includes a browser plugin"); expect(rendered).toContain("Do not describe `kimi web` as browser automation"); expect(rendered).toContain("Pi intentionally has no core interactive browser tool"); expect(rendered).toContain("never silently add a browser MCP or alternate backend"); expect(rendered).toContain("GStack's existing local Chromium/Playwright implementation remains the only bundled browser backend"); expect(rendered).toContain("Do not run `./setup`"); }); test("generates the same provider contract and dependency-free probe for all six skills", () => { const expectedContract = `\n${renderBrowserProviderContract()}`; const expectedProbe = fs.readFileSync(path.join(ROOT, "runtime", "browser-provider-smoke.mjs")); for (const tree of TREES) { expect(fs.readFileSync(path.join(ROOT, "skills", tree, "references", "BROWSER-PROVIDERS.md"), "utf8")) .toBe(expectedContract); expect(fs.readFileSync(path.join(ROOT, "skills", tree, "references", "support", "browser-provider-smoke.mjs"))) .toEqual(expectedProbe); expect(fs.readFileSync(path.join(ROOT, "skills", tree, "references", "RUNTIME.md"), "utf8")) .toContain("Before interactive browser work, read `references/BROWSER-PROVIDERS.md` in full"); } }); test("readiness fixture proves navigation, protected interaction, and durable completion state", async () => { const token = "a".repeat(48); const fixture = createReadinessServer({ token }); fixtures.push(fixture); const started = await fixture.start(); expect(started.baseUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/); const page = await fetch(started.url); expect(page.status).toBe(200); const html = await page.text(); expect(html).toContain("GStack browser readiness"); expect(html).toContain('id="gstack-readiness-action"'); expect(html).toContain('id="gstack-readiness-status"'); const denied = await fetch(`${started.baseUrl}/proof?token=${"b".repeat(48)}`, { method: "POST" }); expect(denied.status).toBe(403); const before = await fetch(`${started.baseUrl}/status?token=${token}`).then((response) => response.json()); expect(before).toEqual({ ok: true, completed: false }); const proof = await fetch(`${started.baseUrl}/proof?token=${token}`, { method: "POST" }); expect(proof.status).toBe(200); expect(await proof.json()).toEqual({ ok: true, status: "READY" }); const after = await fetch(`${started.baseUrl}/status?token=${token}`).then((response) => response.json()); expect(after).toEqual({ ok: true, completed: true }); }); });