Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions apps/dev-playground/server/smoke.test.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> {
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);
});
});
239 changes: 239 additions & 0 deletions apps/dev-playground/server/testing-kit.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { ApiError, files, genie } from "@databricks/appkit";
import {
createApiError,
createMockRequest,
createMockResponse,
createMockWorkspaceClient,
createTestApp,
createTestPlugin,
createTestPluginContext,
expectStream,
getMock,
resetTestCache,
useServiceContextMock,
useTestApp,
useTestCache,
withEnv,
} from "@databricks/appkit/testing";
import { describe, expect, test, vi } from "vitest";

import { reconnect } from "./reconnect-plugin";

/**
* One worked example per `@databricks/appkit/testing` helper. `smoke.test.ts`
* covers that the plugins boot and answer; this covers the kit itself.
*/

// Dummy values: the strict validator only checks the vars are present.
const FILES_ENV = {
DATABRICKS_VOLUME_FILES: "/Volumes/main/default/vol",
DATABRICKS_VOLUME_REPORTS: "/Volumes/main/default/vol",
};
const filesPlugin = () =>
files({ volumes: { reports: { policy: files.policy.allowAll() } } });

describe("getMock — assert the workspace-client call a route made", () => {
test("GET /api/files/reports/metadata calls files.getMetadata", async () => {
await using app = await createTestApp({
plugins: [filesPlugin()],
env: FILES_ENV,
responses: {
"files.getMetadata": {
"content-length": "42",
"content-type": "text/plain",
},
},
});

const res = await app.get("/api/files/reports/metadata?path=report.csv");

expect(res.status).toBe(200);
expect(getMock(app.client, "files.getMetadata")).toHaveBeenCalled();
});
});

describe("createMockWorkspaceClient — build the fake client yourself", () => {
test("declared paths answer; undeclared ones resolve undefined", async () => {
const client = createMockWorkspaceClient({
responses: { "jobs.getRun": { state: "TERMINATED" } },
});

await expect(client.jobs.getRun({ run_id: 1 })).resolves.toEqual({
state: "TERMINATED",
});
expect(getMock(client, "jobs.getRun")).toHaveBeenCalledWith({ run_id: 1 });
await expect(
client.genie.getMessage({ id: "m-1" }),
).resolves.toBeUndefined();
});
});

describe("createApiError — simulate a typed workspace error", () => {
test("produces a genuine ApiError instance", () => {
const err = createApiError({
statusCode: 404,
message: "No such directory",
errorCode: "NOT_FOUND",
});

expect(err).toBeInstanceOf(ApiError);
expect(err.statusCode).toBe(404);
});

test("seeded as a rejection, a route surfaces the failure instead of crashing", async () => {
await using app = await createTestApp({
plugins: [filesPlugin()],
env: FILES_ENV,
responses: {
"files.getMetadata": () => {
throw createApiError({
statusCode: 404,
message: "No such file",
errorCode: "NOT_FOUND",
});
},
},
});

const res = await app.get("/api/files/reports/metadata?path=report.csv");

expect(res.ok).toBe(false);
});
});

describe("useTestApp — a fresh app per test, closed for you", () => {
const app = useTestApp({ plugins: [reconnect()] });

test("the first test gets its own app", async () => {
expect((await app.current.get("/api/reconnect")).status).toBe(200);
});

test("the second gets a fresh one — no close() to forget", async () => {
await expect(
app.current.get("/api/reconnect").then((r) => r.json()),
).resolves.toEqual({ message: "Reconnected" });
});
});

describe("withEnv — set env for a block, restored after", () => {
test("the variable is present inside the block and gone after", async () => {
expect(process.env.DEMO_FLAG).toBeUndefined();

await withEnv({ DEMO_FLAG: "on" }, async () => {
expect(process.env.DEMO_FLAG).toBe("on");
});

expect(process.env.DEMO_FLAG).toBeUndefined();
});
});

describe("useTestCache — assert caching against the real CacheManager", () => {
const cache = useTestCache();

test("generateKey is stable for identical inputs and scoped per user", () => {
const parts = ["analytics:query", "top_users"];
expect(cache.current.generateKey(parts, "")).toBe(
cache.current.generateKey(parts, ""),
);
expect(cache.current.generateKey(parts, "alice")).not.toBe(
cache.current.generateKey(parts, "bob"),
);
});

test("a second identical call is a hit; resetTestCache forces a miss", async () => {
const work = vi.fn(async () => "value");

await cache.current.getOrExecute(["report"], work, "");
await cache.current.getOrExecute(["report"], work, "");
expect(work).toHaveBeenCalledTimes(1); // second served from cache

await resetTestCache();
await cache.current.getOrExecute(["report"], work, "");
expect(work).toHaveBeenCalledTimes(2); // cleared, so recomputed
});
});

describe("createTestPlugin — instantiate a factory the way production does", () => {
test("applies the config merge and manifest name", () => {
const plugin = createTestPlugin(genie, { spaces: { demo: "space-test" } });

expect(plugin.name).toBe("genie");
});
});

describe("createTestPluginContext — unit-test wiring with no boot, no socket", () => {
test("dispatches a cross-plugin tool call on-behalf-of the user", async () => {
// No playground plugin dispatches tools hermetically (agents needs a live
// model), so drive the context directly with a faked analytics tool.
const mock = createTestPluginContext({
analytics: { top_users: (args) => [{ user: "alice", args }] },
});

const req = createMockRequest({ obo: { userId: "analyst@example.com" } });
const result = await mock.ctx.executeTool(req, "analytics", "top_users", {
limit: 5,
});

expect(result).toEqual([{ user: "alice", args: { limit: 5 } }]);
expect(mock.toolCalls[0]).toMatchObject({
plugin: "analytics",
tool: "top_users",
asUser: true,
userId: "analyst@example.com",
});
});

test("a token-less request rejects on the on-behalf-of path", async () => {
const mock = createTestPluginContext({
analytics: { top_users: () => [] },
});

await expect(
mock.ctx.executeTool(createMockRequest(), "analytics", "top_users", {}),
).rejects.toThrow();
});
});

describe("expectStream — assert the ordered events a stream emits", () => {
// reconnect's real stream is long-lived, so smoke.test.ts reads it by hand;
// expectStream buffers to completion, so demo it on a bounded source.
async function* runReport() {
yield { type: "warehouse_status", state: "RUNNING" };
yield { type: "row", n: 1 };
yield { type: "result", rows: 1 };
}

test("toEmit matches an in-order subsequence", async () => {
await expectStream(runReport()).toEmit("warehouse_status", "result");
});

test("toEmitExactly matches the full shape, in order", async () => {
await expectStream(runReport()).toEmitExactly(
"warehouse_status",
"row",
"result",
);
});

test("reads an SSE handler's writes back through createMockResponse", async () => {
const res = createMockResponse();
res.setHeader("content-type", "text/event-stream");
res.write(`event: status\ndata: ${JSON.stringify({ s: "go" })}\n\n`);
res.write(`event: result\ndata: ${JSON.stringify({ n: 1 })}\n\n`);
res.end();

await expectStream(res).toEmit("status", "result");
});
});

describe("useServiceContextMock — spy the data-plane singleton in one line", () => {
// Spies the ServiceContext singleton directly — the seam beneath the client
// createTestApp injects.
const ctx = useServiceContextMock();

test("installs live spies over the service context", () => {
expect(ctx.current.getSpy).toBeDefined();
expect(ctx.current.createUserContextSpy).toBeDefined();
expect(ctx.current.createUserContextSpy).not.toHaveBeenCalled();
});
});
13 changes: 13 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/**"],
},
},
],
},
});