mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 12:20:48 +02:00
Detect host-native browser tools before offering the isolated local Chromium fallback, add a common readiness fixture, harden managed browser startup, and verify standards installs expose one canonical QA skill.
95 lines
4.4 KiB
TypeScript
95 lines
4.4 KiB
TypeScript
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<ReturnType<typeof createReadinessServer>> = [];
|
|
|
|
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 = `<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->\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 });
|
|
});
|
|
});
|