mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 04:10:47 +02:00
feat: add provider-aware browser QA setup
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.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
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 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user