From d0297b80a051541df6c9c5619c4db296c17a3fb8 Mon Sep 17 00:00:00 2001 From: Galymzhan Date: Thu, 20 Aug 2026 11:51:11 +0200 Subject: [PATCH] test(playground): smoke-test the playground's own server plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The playground's server plugins had no tests at all. `tests/` holds Playwright specs that intercept `/api` at the browser boundary (`page.route` + `fulfill`), so the Express server never runs in them — route handlers, `execute()` interceptors, and `executeStream` are all unexercised. These five tests cover that side using the harness from #540: real HTTP, faked data plane, no workspace, no credentials, no network. Adds a `dev-playground` vitest project so `pnpm test` (and CI's Unit Tests job) picks them up: 4517 -> 4522. The `tests/**` exclusion is load-bearing — without it vitest's default `**/*.spec.ts` glob collects the Playwright specs and they fail on import with "Playwright Test did not expect test.describe() to be called here". Two things worth knowing, both found by writing this: - `telemetry-example-plugin` really calls `fetch("https://example.com")` in its external-api span, so a naive test needs the internet. The suite stubs non-loopback fetches and passes loopback through, since that is how the harness reaches its own server. Verified hermetic under a socket guard that throws on any non-loopback connect. - `expectStream` buffers a source to completion and throws on timeout rather than returning partial events, so it cannot assert on a long-lived stream. The reconnect stream is five messages three seconds apart, so the SSE test reads one payload and hangs up instead of costing ~12s. Scoped to smoke coverage deliberately. Not included: reconnection replay (`executeStream`'s ring buffer and `Last-Event-ID` handling is the real prize and deserves its own tests), and a `typecheck` script for the app — the existing tsconfig reports 2128 errors from the client's unset `--jsx`, and a server-scoped one still has 13 pre-existing errors that CI has never seen because `pnpm -r typecheck` skips this workspace. Signed-off-by: Galymzhan --- apps/dev-playground/server/smoke.test.ts | 121 +++++++++++++++++++++++ vitest.config.ts | 13 +++ 2 files changed, 134 insertions(+) create mode 100644 apps/dev-playground/server/smoke.test.ts diff --git a/apps/dev-playground/server/smoke.test.ts b/apps/dev-playground/server/smoke.test.ts new file mode 100644 index 000000000..b1915f95c --- /dev/null +++ b/apps/dev-playground/server/smoke.test.ts @@ -0,0 +1,121 @@ +import { createTestApp } from "@databricks/appkit/testing"; +import { describe, expect, test, vi } from "vitest"; + +import { lakebaseExamples } from "./lakebase-examples-plugin"; +import { reconnect } from "./reconnect-plugin"; +import { telemetryExamples } from "./telemetry-example-plugin"; + +/** + * Smoke tests for the playground's own server plugins. + * + * `tests/` holds Playwright specs that fake `/api` responses at the browser + * boundary (`page.route` + `fulfill`), so the Express server never runs there. + * These cover the other side: the plugins boot and answer over real HTTP with + * the Databricks data plane faked by the harness — no workspace, no + * credentials, no network. + */ + +/** + * Read one SSE payload, then hang up. + * + * `expectStream` buffers a source to completion, and the reconnect stream is + * five messages three seconds apart — so asserting through it would cost ~12s + * for a smoke test. + */ +async function firstSSEPayload(res: Response): Promise { + if (!res.body) throw new Error("expected a streaming body, got none"); + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffered = ""; + try { + while (!buffered.includes("\n\n")) { + const { value, done } = await reader.read(); + if (done) break; + buffered += decoder.decode(value, { stream: true }); + } + const data = buffered.split("\n").find((line) => line.startsWith("data:")); + return data ? JSON.parse(data.slice("data:".length).trim()) : undefined; + } finally { + await reader.cancel(); + } +} + +describe("dev-playground server plugins", () => { + test("all three boot together and register under their manifest names", async () => { + await using app = await createTestApp({ + plugins: [reconnect(), telemetryExamples(), lakebaseExamples()], + }); + + expect(app.plugins.reconnect).toBeDefined(); + expect(app.plugins["telemetry-examples"]).toBeDefined(); + expect(app.plugins["lakebase-examples"]).toBeDefined(); + }); + + test("GET /api/reconnect answers", async () => { + await using app = await createTestApp({ plugins: [reconnect()] }); + + const res = await app.get("/api/reconnect"); + + expect(res.status).toBe(200); + await expect(res.json()).resolves.toEqual({ message: "Reconnected" }); + }); + + test("the reconnect stream opens as SSE and emits its first message", async () => { + await using app = await createTestApp({ plugins: [reconnect()] }); + + const res = await app.get("/api/reconnect/stream?sessionId=smoke"); + + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toContain("text/event-stream"); + await expect(firstSSEPayload(res)).resolves.toMatchObject({ + type: "message", + count: 1, + total: 5, + content: "Message 1 of 5", + }); + }); + + test("POST /api/telemetry-examples/combined threads the userId through every span", async () => { + // This route really calls `fetch("https://example.com")` (its + // external-api span). Left alone the suite would need the internet, so + // non-loopback requests are stubbed — loopback must pass through, because + // that is how the harness reaches its own server. + const realFetch = globalThis.fetch; + vi.stubGlobal("fetch", (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input instanceof Request ? input.url : input); + return /127\.0\.0\.1|localhost/.test(url) + ? realFetch(input, init) + : Promise.resolve(new Response("stubbed", { status: 200 })); + }); + + try { + await using app = await createTestApp({ plugins: [telemetryExamples()] }); + + const res = await app.post("/api/telemetry-examples/combined", { + body: { userId: "smoke-user" }, + }); + + expect(res.status).toBe(200); + // A 200 means the whole nested-span body ran against the real + // TelemetryProvider: tracer, meter, and logger. + await expect(res.json()).resolves.toMatchObject({ + success: true, + result: { userId: "smoke-user" }, + }); + } finally { + vi.unstubAllGlobals(); + } + }); + + test("lakebase-examples degrades to no routes when Lakebase is unconfigured", async () => { + // Its setup() and injectRoutes() both bail on missing PGHOST/LAKEBASE_ENDPOINT. + // The app must still boot; the routes must simply be absent. + await using app = await createTestApp({ + plugins: [lakebaseExamples()], + env: { PGHOST: "", LAKEBASE_ENDPOINT: "" }, + }); + + expect(app.plugins["lakebase-examples"]).toBeDefined(); + expect((await app.get("/api/lakebase-examples/raw")).status).toBe(404); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index d466c3a51..38f0c3e06 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -59,6 +59,19 @@ export default defineConfig({ environment: "node", }, }, + { + plugins: [tsconfigPaths()], + test: { + name: "dev-playground", + root: "./apps/dev-playground", + environment: "node", + // tests/ holds Playwright specs. Vitest's default `**/*.spec.ts` + // glob would collect them and they fail on import with "Playwright + // Test did not expect test.describe() to be called here". They run + // via `pnpm test:integration`. + exclude: ["**/node_modules/**", "**/dist/**", "tests/**"], + }, + }, ], }, });