import { randomUUID } from "node:crypto";import { existsSync, mkdirSync, writeFileSync } from "node:fs";import { resolve } from "node:path";import { fileURLToPath } from "node:url"; import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest";import { unstable_dev, type Unstable_DevWorker } from "wrangler"; // ---------------------------------------------------------------------------// End-to-end test for the Cloudflare host: boots the REAL worker on workerd via// Miniflare (wrangler `unstable_dev`) with a local D1 + R2, dev-auth on. This is// the only test that exercises the CF-specific stack together — D1 schema// bring-up, the R2 large-value offload, QuickJS-WASM execution, and the MCP// envelope — through the actual HTTP surface.// --------------------------------------------------------------------------- const dir = fileURLToPath(new URL(".", import.meta.url));const runId = randomUUID().slice(0, 8); // Inline spec (no network); registers one tool, exercising the D1 write path.const SPEC = JSON.stringify({ openapi: "3.0.0", info: { title: "Test", version: "1.0.0" }, servers: [{ url: "https://example.com" }], paths: { "/ping": { get: { operationId: "ping", responses: { "200": { description: "ok" } } }, }, },}); describe("cloudflare host e2e (workerd/miniflare)", () => { let worker: Unstable_DevWorker; beforeAll(async () => { // CI runs from a fresh checkout with no `vite build`, so `./dist` (the SPA // assets dir wrangler.jsonc points `assets.directory` at) is absent and // `unstable_dev`'s assets validation aborts boot. This e2e drives the // API/MCP surface (all `run_worker_first` paths), not the SPA, so a minimal // placeholder index.html satisfies the validation without a real build. const distIndex = resolve(dir, "../dist/index.html"); if (!existsSync(distIndex)) { mkdirSync(resolve(dir, "../dist"), { recursive: true }); writeFileSync(distIndex, "<!doctype html><title>executor</title>"); } worker = await unstable_dev(resolve(dir, "worker.ts"), { config: resolve(dir, "../wrangler.jsonc"), ip: "127.0.0.1", local: true, experimental: { disableExperimentalWarning: true }, vars: { EXECUTOR_SECRET_KEY: "test-secret-key-0123456789abcdef", ENABLE_DEV_AUTH: "true", }, }); }, 120_000); afterAll(async () => { await worker?.stop(); }); it("executes TypeScript via /api/executions (QuickJS on workerd)", async () => { const res = await worker.fetch("/api/executions", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ code: "export default 6 * 7" }), }); expect(res.status).toBe(200); const body = (await res.json()) as { status: string; text: string; isError: boolean; }; expect(body.status).toBe("completed"); expect(body.isError).toBe(false); expect(body.text).toBe("42"); }, 60_000); it("adds a LARGE OpenAPI source — exercises R2 offload (>800KB blob) + createMany batching (>100 tools)", async () => { // Synthesize a spec big enough to (a) push the stored config blob past the // ~800KB R2-offload threshold and (b) derive >100 tools (past D1's 100 // bound-param createMany limit) — the real-worker regression for two of the // three D1 fixes. const paths: Record<string, unknown> = {}; for (let i = 0; i < 250; i++) { paths[`/op${i}`] = { get: { operationId: `op${i}`, summary: `operation ${i}`, description: "d".repeat(4000), // padding -> ~1MB total spec responses: { "200": { description: "ok" } }, }, }; } const largeSpec = JSON.stringify({ openapi: "3.0.0", info: { title: "Large", version: "1.0.0" }, servers: [{ url: "https://example.com" }], paths, }); expect(largeSpec.length).toBeGreaterThan(900_000); const slug = `largeapi-${runId}`; const add = await worker.fetch("/api/openapi/specs", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ spec: { kind: "blob", value: largeSpec }, slug, description: "Large API", baseUrl: "https://example.com", }), }); expect(add.status).toBe(200); const added = (await add.json()) as { toolCount: number }; expect(added.toolCount).toBe(250); // Reads back through the R2 rehydration path (the >800KB blob lives in R2). const got = await worker.fetch(`/api/openapi/integrations/${slug}`); expect(got.status).toBe(200); const integration = (await got.json()) as { slug: string } | null; expect(integration?.slug).toBe(slug); }, 90_000); it("adds an OpenAPI source and reads it back (D1 write + read path)", async () => { const slug = `testapi-${runId}`; const add = await worker.fetch("/api/openapi/specs", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ spec: { kind: "blob", value: SPEC }, slug, description: "Test API", baseUrl: "https://example.com", }), }); expect(add.status).toBe(200); const added = (await add.json()) as { toolCount: number; slug: string }; expect(added.toolCount).toBeGreaterThan(0); const got = await worker.fetch(`/api/openapi/integrations/${slug}`); expect(got.status).toBe(200); const integration = (await got.json()) as { slug: string } | null; expect(integration?.slug).toBe(slug); }, 60_000); it("gates the API when dev-auth is on but treats the request as the dev admin", async () => { // dev-auth means the request is the fixed dev admin; a gated route resolves // to the principal. There is no scope stack in v2 — account/me is the // identity-backed read that the API gate protects. const res = await worker.fetch("/api/account/me"); expect(res.status).toBe(200); const me = (await res.json()) as { user: { id: string }; organization: { id: string }; }; expect(me.user.id).toBe("dev"); }); it("lists tools on a follow-up request after a fresh initialize (DO session survives across requests)", async () => { // The production regression: `initialize` creates the session, then a // SEPARATE `tools/list` request must find it. With the old in-process store a // second Worker isolate never saw the session and this returned "Not // connected"; the MCP-session Durable Object (id == session id) routes the // follow-up back to the same isolate, so the tool list comes through. const accept = "application/json, text/event-stream"; const rpc = (sessionId: string | null, body: unknown) => worker.fetch("/mcp", { method: "POST", headers: { "content-type": "application/json", accept, ...(sessionId ? { "mcp-session-id": sessionId } : {}), }, body: JSON.stringify(body), }); const init = await rpc(null, { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "test", version: "1" }, }, }); expect(init.status).toBe(200); const sessionId = init.headers.get("mcp-session-id"); expect(sessionId).toBeTruthy(); await rpc(sessionId, { jsonrpc: "2.0", method: "notifications/initialized", }); const list = await rpc(sessionId, { jsonrpc: "2.0", id: 2, method: "tools/list", }); expect(list.status).toBe(200); const listed = (await list.json()) as { result?: { tools?: ReadonlyArray<{ name: string }> }; }; const toolNames = listed.result?.tools?.map((t) => t.name) ?? []; expect(toolNames).toContain("execute"); }, 60_000); it("invokes the execute tool over MCP (initialize → tools/call → QuickJS)", async () => { const accept = "application/json, text/event-stream"; const rpc = (sessionId: string | null, body: unknown) => worker.fetch("/mcp", { method: "POST", headers: { "content-type": "application/json", accept, ...(sessionId ? { "mcp-session-id": sessionId } : {}), }, body: JSON.stringify(body), }); const init = await rpc(null, { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "test", version: "1" }, }, }); expect(init.status).toBe(200); const sessionId = init.headers.get("mcp-session-id"); expect(sessionId).toBeTruthy(); await rpc(sessionId, { jsonrpc: "2.0", method: "notifications/initialized", }); const call = await rpc(sessionId, { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "execute", arguments: { code: "export default 6 * 7" } }, }); expect(call.status).toBe(200); const result = (await call.json()) as { result?: { structuredContent?: { result?: number } }; }; expect(result.result?.structuredContent?.result).toBe(42); }, 60_000);});
import { randomUUID } from "node:crypto";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest";
import { unstable_dev, type Unstable_DevWorker } from "wrangler";
// ---------------------------------------------------------------------------
// End-to-end test for the Cloudflare host: boots the REAL worker on workerd via
// Miniflare (wrangler `unstable_dev`) with a local D1 + R2, dev-auth on. This is
// the only test that exercises the CF-specific stack together — D1 schema
// bring-up, the R2 large-value offload, QuickJS-WASM execution, and the MCP
// envelope — through the actual HTTP surface.
// ---------------------------------------------------------------------------
const dir = fileURLToPath(new URL(".", import.meta.url));
const runId = randomUUID().slice(0, 8);
// Inline spec (no network); registers one tool, exercising the D1 write path.
const SPEC = JSON.stringify({
openapi: "3.0.0",
info: { title: "Test", version: "1.0.0" },
servers: [{ url: "https://example.com" }],
paths: {
"/ping": {
get: { operationId: "ping", responses: { "200": { description: "ok" } } },
},
},
});
describe("cloudflare host e2e (workerd/miniflare)", () => {
let worker: Unstable_DevWorker;
beforeAll(async () => {
// CI runs from a fresh checkout with no `vite build`, so `./dist` (the SPA
// assets dir wrangler.jsonc points `assets.directory` at) is absent and
// `unstable_dev`'s assets validation aborts boot. This e2e drives the
// API/MCP surface (all `run_worker_first` paths), not the SPA, so a minimal
// placeholder index.html satisfies the validation without a real build.
const distIndex = resolve(dir, "../dist/index.html");
if (!existsSync(distIndex)) {
mkdirSync(resolve(dir, "../dist"), { recursive: true });
writeFileSync(distIndex, "<!doctype html><title>executor</title>");
}
worker = await unstable_dev(resolve(dir, "worker.ts"), {
config: resolve(dir, "../wrangler.jsonc"),
ip: "127.0.0.1",
local: true,
experimental: { disableExperimentalWarning: true },
vars: {
EXECUTOR_SECRET_KEY: "test-secret-key-0123456789abcdef",
ENABLE_DEV_AUTH: "true",
},
});
}, 120_000);
afterAll(async () => {
await worker?.stop();
});
it("executes TypeScript via /api/executions (QuickJS on workerd)", async () => {
const res = await worker.fetch("/api/executions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ code: "export default 6 * 7" }),
});
expect(res.status).toBe(200);
const body = (await res.json()) as {
status: string;
text: string;
isError: boolean;
};
expect(body.status).toBe("completed");
expect(body.isError).toBe(false);
expect(body.text).toBe("42");
}, 60_000);
it("adds a LARGE OpenAPI source — exercises R2 offload (>800KB blob) + createMany batching (>100 tools)", async () => {
// Synthesize a spec big enough to (a) push the stored config blob past the
// ~800KB R2-offload threshold and (b) derive >100 tools (past D1's 100
// bound-param createMany limit) — the real-worker regression for two of the
// three D1 fixes.
const paths: Record<string, unknown> = {};
for (let i = 0; i < 250; i++) {
paths[`/op${i}`] = {
get: {
operationId: `op${i}`,
summary: `operation ${i}`,
description: "d".repeat(4000), // padding -> ~1MB total spec
responses: { "200": { description: "ok" } },
},
};
}
const largeSpec = JSON.stringify({
openapi: "3.0.0",
info: { title: "Large", version: "1.0.0" },
servers: [{ url: "https://example.com" }],
paths,
});
expect(largeSpec.length).toBeGreaterThan(900_000);
const slug = `largeapi-${runId}`;
const add = await worker.fetch("/api/openapi/specs", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
spec: { kind: "blob", value: largeSpec },
slug,
description: "Large API",
baseUrl: "https://example.com",
}),
});
expect(add.status).toBe(200);
const added = (await add.json()) as { toolCount: number };
expect(added.toolCount).toBe(250);
// Reads back through the R2 rehydration path (the >800KB blob lives in R2).
const got = await worker.fetch(`/api/openapi/integrations/${slug}`);
expect(got.status).toBe(200);
const integration = (await got.json()) as { slug: string } | null;
expect(integration?.slug).toBe(slug);
}, 90_000);
it("adds an OpenAPI source and reads it back (D1 write + read path)", async () => {
const slug = `testapi-${runId}`;
const add = await worker.fetch("/api/openapi/specs", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
spec: { kind: "blob", value: SPEC },
slug,
description: "Test API",
baseUrl: "https://example.com",
}),
});
expect(add.status).toBe(200);
const added = (await add.json()) as { toolCount: number; slug: string };
expect(added.toolCount).toBeGreaterThan(0);
const got = await worker.fetch(`/api/openapi/integrations/${slug}`);
expect(got.status).toBe(200);
const integration = (await got.json()) as { slug: string } | null;
expect(integration?.slug).toBe(slug);
}, 60_000);
it("gates the API when dev-auth is on but treats the request as the dev admin", async () => {
// dev-auth means the request is the fixed dev admin; a gated route resolves
// to the principal. There is no scope stack in v2 — account/me is the
// identity-backed read that the API gate protects.
const res = await worker.fetch("/api/account/me");
expect(res.status).toBe(200);
const me = (await res.json()) as {
user: { id: string };
organization: { id: string };
};
expect(me.user.id).toBe("dev");
});
it("lists tools on a follow-up request after a fresh initialize (DO session survives across requests)", async () => {
// The production regression: `initialize` creates the session, then a
// SEPARATE `tools/list` request must find it. With the old in-process store a
// second Worker isolate never saw the session and this returned "Not
// connected"; the MCP-session Durable Object (id == session id) routes the
// follow-up back to the same isolate, so the tool list comes through.
const accept = "application/json, text/event-stream";
const rpc = (sessionId: string | null, body: unknown) =>
worker.fetch("/mcp", {
method: "POST",
headers: {
"content-type": "application/json",
accept,
...(sessionId ? { "mcp-session-id": sessionId } : {}),
},
body: JSON.stringify(body),
});
const init = await rpc(null, {
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "test", version: "1" },
},
});
expect(init.status).toBe(200);
const sessionId = init.headers.get("mcp-session-id");
expect(sessionId).toBeTruthy();
await rpc(sessionId, {
jsonrpc: "2.0",
method: "notifications/initialized",
});
const list = await rpc(sessionId, {
jsonrpc: "2.0",
id: 2,
method: "tools/list",
});
expect(list.status).toBe(200);
const listed = (await list.json()) as {
result?: { tools?: ReadonlyArray<{ name: string }> };
};
const toolNames = listed.result?.tools?.map((t) => t.name) ?? [];
expect(toolNames).toContain("execute");
}, 60_000);
it("invokes the execute tool over MCP (initialize → tools/call → QuickJS)", async () => {
const accept = "application/json, text/event-stream";
const rpc = (sessionId: string | null, body: unknown) =>
worker.fetch("/mcp", {
method: "POST",
headers: {
"content-type": "application/json",
accept,
...(sessionId ? { "mcp-session-id": sessionId } : {}),
},
body: JSON.stringify(body),
});
const init = await rpc(null, {
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "test", version: "1" },
},
});
expect(init.status).toBe(200);
const sessionId = init.headers.get("mcp-session-id");
expect(sessionId).toBeTruthy();
await rpc(sessionId, {
jsonrpc: "2.0",
method: "notifications/initialized",
});
const call = await rpc(sessionId, {
jsonrpc: "2.0",
id: 2,
method: "tools/call",
params: { name: "execute", arguments: { code: "export default 6 * 7" } },
});
expect(call.status).toBe(200);
const result = (await call.json()) as {
result?: { structuredContent?: { result?: number } };
};
expect(result.result?.structuredContent?.result).toBe(42);
}, 60_000);
});