import { expect, test } from "@playwright/test"; /** * Stage-5 smoke spec — exercises every wired adapter from a real HTTP * call against a `createTestHost(...)`-backed server. The companion * `src/server.ts` boots the Bun.serve listener; this spec hits the * endpoints and asserts on the observable behaviour. */ test.describe("createTestHost — Stage-5 smoke", () => { test("1. zero-config plugin install lands on host.plugins", async ({ request, }) => { const res = await request.get("/api/plugins"); expect(res.status()).toBe(200); const body = (await res.json()) as { plugins: string[] }; expect(body.plugins).toContain("smoke-fixture"); }); test("2. fixture-plugin route resolves via the in-memory router", async ({ request, }) => { const res = await request.get("/api/principal"); expect(res.status()).toBe(200); const body = (await res.json()) as { id: string | null; scopes: string[] | null; }; // Even with no tenant header, principal still resolves through the // seeded static-principal resolver. expect(body.id).toBe("smoke-principal"); }); test("3. seed.principal reaches ctx.principal", async ({ request }) => { const res = await request.get("/api/principal"); const body = (await res.json()) as { id: string | null; scopes: string[] | null; }; expect(body.id).toBe("smoke-principal"); expect(body.scopes).toEqual(["smoke:read", "smoke:write"]); }); test("4. seed.tenants + X-Tenant-Id reaches ctx.tenant", async ({ request, }) => { const res = await request.get("/api/tenant", { headers: { "X-Tenant-Id": "smoke-tenant" }, }); expect(res.status()).toBe(200); const body = (await res.json()) as { id: string | null; slug: string | null; }; expect(body.id).toBe("smoke-tenant"); expect(body.slug).toBe("smoke"); }); test("5. seed.secrets exposes ctx.secrets.require", async ({ request, }) => { const res = await request.get("/api/secret"); expect(res.status()).toBe(200); const body = (await res.json()) as { name: string; length: number }; expect(body.name).toBe("SMOKE_TOKEN"); expect(body.length).toBe("shh-very-secret-smoke".length); }); test("6. ctx.db round-trips a real bun:sqlite write/read", async ({ request, }) => { const res = await request.get("/api/db", { headers: { "X-Tenant-Id": "smoke-tenant" }, }); expect(res.status()).toBe(200); const body = (await res.json()) as { ok: boolean; count: number; first: string | null; }; expect(body.ok).toBe(true); expect(body.count).toBeGreaterThanOrEqual(1); expect(body.first).toBe("hello"); }); test("7. handler emits an event observable via the in-memory bus", async ({ request, }) => { // Fire-and-forget — the in-memory bus captures the publish in // history, so a subsequent endpoint (or `waitForEvent` in a // shared-process consumer) can resolve against it. const res = await request.get("/api/events/emit"); expect(res.status()).toBe(200); const body = (await res.json()) as { ok: boolean; topic: string }; expect(body.ok).toBe(true); expect(body.topic).toBe("smoke.events.fired"); }); test("8. host serves /internal/livez during the smoke", async ({ request, }) => { const res = await request.get("/internal/livez"); expect(res.status()).toBe(200); const body = await res.text(); expect(body).toBe("ok"); }); });