UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

981 lines (846 loc) 29.9 kB
import { Alepha } from "alepha"; import { DateTimeProvider } from "alepha/datetime"; import { FileSystemProvider, MemoryFileSystemProvider, MemoryShellProvider, ShellProvider, } from "alepha/system"; import { describe, test } from "vitest"; import { CloudflareAdapter } from "../adapters/CloudflareAdapter.ts"; import type { PlatformContext } from "../adapters/PlatformAdapter.ts"; import { platformOptions } from "../atoms/platformOptions.ts"; import { CloudflareApi } from "../services/CloudflareApi.ts"; import { NamingService } from "../services/NamingService.ts"; /** * In-memory CloudflareApi for testing. * * Stores resources in maps and implements the same interface * without making any HTTP calls. */ class MemoryCloudflareApi extends CloudflareApi { public d1Databases: Array<{ uuid: string; name: string }> = []; public kvNamespaces: Array<{ id: string; title: string }> = []; public r2Buckets: Array<{ name: string; creation_date: string }> = []; public queues: Array<{ queue_id: string; queue_name: string }> = []; public hyperdriveConfigs: Array<{ id: string; name: string; origin: { host: string }; }> = []; public secrets: Map<string, Array<{ name: string; type: string }>> = new Map(); public override async resolveToken(): Promise<string> { return "test-token"; } public override async resolveAccountId(): Promise<string> { return "test-account-id"; } public override async listD1() { return this.d1Databases; } public override async createD1(name: string) { const db = { uuid: `d1-${name}-uuid`, name }; this.d1Databases.push(db); return db; } public override async deleteD1(databaseId: string) { this.d1Databases = this.d1Databases.filter((db) => db.uuid !== databaseId); } public override async listKV() { return this.kvNamespaces; } public override async createKV(title: string) { const ns = { id: `kv-${title}-id`, title }; this.kvNamespaces.push(ns); return ns; } public override async deleteKV(namespaceId: string) { this.kvNamespaces = this.kvNamespaces.filter((ns) => ns.id !== namespaceId); } public override async listR2() { return this.r2Buckets; } public override async createR2(name: string) { this.r2Buckets.push({ name, creation_date: new Date().toISOString() }); } public override async deleteR2(name: string) { this.r2Buckets = this.r2Buckets.filter((b) => b.name !== name); } public override async listQueues() { return this.queues; } public override async createQueue(name: string) { const queue = { queue_id: `q-${name}-id`, queue_name: name }; this.queues.push(queue); return queue; } public override async deleteQueue(queueId: string) { this.queues = this.queues.filter((q) => q.queue_id !== queueId); } public override async listQueueConsumers() { return []; } public override async deleteQueueConsumer() {} public override async listHyperdrive() { return this.hyperdriveConfigs; } public override async createHyperdrive(name: string) { const config = { id: `hd-${name}-id`, name, origin: { host: "localhost" } }; this.hyperdriveConfigs.push(config); return config; } public override async deleteHyperdrive(configId: string) { this.hyperdriveConfigs = this.hyperdriveConfigs.filter( (c) => c.id !== configId, ); } public override async listSecrets(scriptName: string) { return this.secrets.get(scriptName) ?? []; } public override async putSecret( scriptName: string, name: string, _value: string, ) { const existing = this.secrets.get(scriptName) ?? []; if (!existing.some((s) => s.name === name)) { existing.push({ name, type: "secret_text" }); } this.secrets.set(scriptName, existing); } // Full binding set (all types), used by the bulk-PATCH path. Existing // tests read `api.secrets` which we keep as a secret_text-only projection // of this map. public bindings: Map< string, Array<{ type: string; name: string; text?: string }> > = new Map(); public override async getWorkerSettings(scriptName: string) { return { bindings: this.bindings.get(scriptName) ?? [] }; } public override async patchWorkerBindings( scriptName: string, bindings: Array<{ type: string; name: string; text?: string }>, ) { this.bindings.set(scriptName, bindings); const secretView = bindings .filter((b) => b.type === "secret_text") .map((b) => ({ name: b.name, type: "secret_text" as const })); this.secrets.set(scriptName, secretView); } public override async listDeployments() { return []; } public override async listVersions() { return []; } public override async deleteWorker() {} } describe("CloudflareAdapter", () => { const createTestEnv = () => { // Ensure D1 path (not Hyperdrive) — isPostgres() checks process.env.DATABASE_URL delete process.env.DATABASE_URL; const alepha = Alepha.create() .with({ provide: FileSystemProvider, use: MemoryFileSystemProvider }) .with({ provide: ShellProvider, use: MemoryShellProvider }) .with({ provide: CloudflareApi, use: MemoryCloudflareApi }); const fs = alepha.inject(MemoryFileSystemProvider); const shell = alepha.inject(MemoryShellProvider); const dateTime = alepha.inject(DateTimeProvider); const adapter = alepha.inject(CloudflareAdapter); const naming = alepha.inject(NamingService); const api = alepha.inject(MemoryCloudflareApi); // Pre-seed package.json so ensureDependency finds wrangler already installed fs.files.set( "/project/package.json", Buffer.from( JSON.stringify({ name: "test", devDependencies: { wrangler: "^3.0.0" }, }), ), ); return { alepha, fs, shell, dateTime, adapter, naming, api }; }; const makeCtx = ( naming: NamingService, overrides: Partial<PlatformContext> = {}, ): PlatformContext => ({ project: "acme-portal", env: "production", envConfig: { adapter: "cloudflare" }, entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, root: "/project", naming: naming.forContext("acme-portal", "production"), ...overrides, }); describe("authenticate", () => { test("always validates token but skips account resolution when cache is fresh", async ({ expect, }) => { const { adapter, shell, dateTime, naming, api } = createTestEnv(); const ctx = makeCtx(naming); dateTime.pause(); shell.outputs.set( "wrangler auth token --json", JSON.stringify({ type: "oauth", token: "test-token" }), ); let resolveAccountCalls = 0; const originalResolve = api.resolveAccountId.bind(api); api.resolveAccountId = async () => { resolveAccountCalls++; return originalResolve(); }; const run = createMockRun(); await adapter.authenticate(ctx, run); expect(resolveAccountCalls).toBe(1); // Second call — token still validated, but account resolution skipped shell.calls.length = 0; resolveAccountCalls = 0; await adapter.authenticate(ctx, run); expect(shell.wasCalled("wrangler auth token --json")).toBe(true); expect(resolveAccountCalls).toBe(0); }); test("checks auth token when cache is stale", async ({ expect }) => { const { adapter, shell, dateTime, naming } = createTestEnv(); const ctx = makeCtx(naming); shell.outputs.set( "wrangler auth token --json", JSON.stringify({ type: "oauth", token: "test-token" }), ); dateTime.pause(); const run = createMockRun(); await adapter.authenticate(ctx, run); await dateTime.travel(5 * 60 * 60 * 1000); // 5 hours shell.calls.length = 0; await adapter.authenticate(ctx, run); expect(shell.wasCalled("wrangler auth token --json")).toBe(true); }); }); describe("provision", () => { test("creates D1 database via REST API when app has database", async ({ expect, }) => { const { adapter, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: true, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, }); const run = createMockRun(); await adapter.provision(ctx, run); expect(api.d1Databases).toHaveLength(1); expect(api.d1Databases[0].name).toBe("acme-portal-production"); }); test("skips D1 creation when database already exists", async ({ expect, }) => { const { adapter, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: true, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, }); // Pre-seed existing database api.d1Databases.push({ name: "acme-portal-production", uuid: "existing-uuid", }); const run = createMockRun(); await adapter.provision(ctx, run); // Should still be 1 (not 2) expect(api.d1Databases).toHaveLength(1); expect(api.d1Databases[0].uuid).toBe("existing-uuid"); }); test("creates R2 bucket via REST API when app has bucket", async ({ expect, }) => { const { adapter, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: true, hasKV: false, hasQueue: false, hasCron: false, }, }); const run = createMockRun(); await adapter.provision(ctx, run); expect(api.r2Buckets).toHaveLength(1); expect(api.r2Buckets[0].name).toBe("acme-portal-production"); }); test("creates KV namespace via REST API when app has KV", async ({ expect, }) => { const { adapter, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: false, hasKV: true, hasQueue: false, hasCron: false, }, }); const run = createMockRun(); await adapter.provision(ctx, run); expect(api.kvNamespaces).toHaveLength(1); expect(api.kvNamespaces[0].title).toBe("acme-portal-production"); }); test("creates queue via REST API when app has queue", async ({ expect, }) => { const { adapter, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: false, hasKV: false, hasQueue: true, hasCron: false, }, }); const run = createMockRun(); await adapter.provision(ctx, run); expect(api.queues).toHaveLength(1); expect(api.queues[0].queue_name).toBe("acme-portal-production"); }); }); describe("secrets", () => { test("pushes non-binding env vars via REST putSecret", async ({ expect, }) => { const { adapter, fs, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, }); await fs.writeFile( "/project/.env.production", [ "GOOGLE_API_KEY=sk-123", "APP_SECRET=my-secret", "DATABASE_URL=d1://mydb", "R2_BUCKET_NAME=my-bucket", "CLOUDFLARE_DOMAIN=example.com", "VITE_PUBLIC_KEY=public-abc", "NODE_ENV=production", "", ].join("\n"), ); const run = createMockRun(); await adapter.secrets(ctx, run); const pushed = api.secrets.get("acme-portal-production") ?? []; const names = pushed.map((s) => s.name).sort(); expect(names).toEqual(["APP_SECRET", "GOOGLE_API_KEY"]); }); test("with platform.secrets.keys, resolves the allowlist from process.env (no .env file) and ignores ambient vars", async ({ expect, }) => { const { adapter, alepha, naming, api } = createTestEnv(); // Declare an explicit allowlist — the CI shape: secrets arrive via the // job environment, there is no .env.production on the runner. alepha.set(platformOptions, { secrets: { keys: ["APP_SECRET", "GOOGLE_CLIENT_ID", "EMAIL_FROM"] }, environments: { production: { adapter: "cloudflare" } }, } as any); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, }); process.env.APP_SECRET = "from-env-secret"; process.env.GOOGLE_CLIENT_ID = "from-env-google"; // EMAIL_FROM intentionally unset → declared but unresolved, not pushed. // PATH-style ambient var that must never leak into worker secrets. process.env.AMBIENT_RUNNER_VAR = "leak-me-not"; try { const run = createMockRun(); await adapter.secrets(ctx, run); } finally { delete process.env.APP_SECRET; delete process.env.GOOGLE_CLIENT_ID; delete process.env.AMBIENT_RUNNER_VAR; } const pushed = api.secrets.get("acme-portal-production") ?? []; const names = pushed.map((s) => s.name).sort(); expect(names).toEqual(["APP_SECRET", "GOOGLE_CLIENT_ID"]); }); test("with platform.secrets.keys, the .env file overrides process.env per key", async ({ expect, }) => { const { adapter, alepha, fs, naming, api } = createTestEnv(); alepha.set(platformOptions, { secrets: { keys: ["APP_SECRET", "GOOGLE_CLIENT_ID"] }, environments: { production: { adapter: "cloudflare" } }, } as any); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, }); // APP_SECRET in the file → file wins. GOOGLE_CLIENT_ID only in env → env. await fs.writeFile( "/project/.env.production", ["APP_SECRET=from-file", ""].join("\n"), ); process.env.APP_SECRET = "from-env"; process.env.GOOGLE_CLIENT_ID = "env-google"; try { const run = createMockRun(); await adapter.secrets(ctx, run); } finally { delete process.env.APP_SECRET; delete process.env.GOOGLE_CLIENT_ID; } // Secret *values* land in the full binding set (api.bindings); the // api.secrets projection only keeps names. const bindings = api.bindings.get("acme-portal-production") ?? []; const byName = Object.fromEntries( bindings .filter((b) => b.type === "secret_text") .map((b) => [b.name, b.text]), ); expect(byName.APP_SECRET).toBe("from-file"); expect(byName.GOOGLE_CLIENT_ID).toBe("env-google"); }); test("uses dist/manifest.json `env` as the default allowlist, resolved from process.env", async ({ expect, }) => { const { adapter, fs, naming, api } = createTestEnv(); // No platform.secrets.keys and no .env file → the manifest's declared // env list is the allowlist. This is the CI shape. const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, }); await fs.writeFile( "/project/dist/manifest.json", JSON.stringify({ env: [ "APP_SECRET", "GOOGLE_CLIENT_ID", "CLOUDFLARE_ZONE", "LOG_LEVEL", ], }), ); process.env.APP_SECRET = "s1"; process.env.GOOGLE_CLIENT_ID = "g1"; process.env.CLOUDFLARE_ZONE = "example.com"; // declared but EXCLUDED // LOG_LEVEL is declared + ambient in the runner, but EXCLUDED (infra knob). try { const run = createMockRun(); await adapter.secrets(ctx, run); } finally { delete process.env.APP_SECRET; delete process.env.GOOGLE_CLIENT_ID; delete process.env.CLOUDFLARE_ZONE; } const pushed = api.secrets.get("acme-portal-production") ?? []; expect(pushed.map((s) => s.name).sort()).toEqual([ "APP_SECRET", "GOOGLE_CLIENT_ID", ]); }); test("pushes per-deploy keys from .env.<env>.local even when not in the manifest", async ({ expect, }) => { const { adapter, fs, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, }); // Manifest declares only APP_SECRET… await fs.writeFile( "/project/dist/manifest.json", JSON.stringify({ env: ["APP_SECRET"] }), ); // …but an orchestrator (Rocket) injected CLUB_CONFIG_JSON into the // per-deploy override file. It must still be pushed. await fs.writeFile( "/project/.env.production.local", ["APP_SECRET=s1", 'CLUB_CONFIG_JSON={"id":"b14"}', ""].join("\n"), ); const run = createMockRun(); await adapter.secrets(ctx, run); const pushed = api.secrets.get("acme-portal-production") ?? []; expect(pushed.map((s) => s.name).sort()).toEqual([ "APP_SECRET", "CLUB_CONFIG_JSON", ]); }); test("platform.secrets.keys overrides the manifest `env` allowlist", async ({ expect, }) => { const { adapter, alepha, fs, naming, api } = createTestEnv(); alepha.set(platformOptions, { secrets: { keys: ["APP_SECRET"] }, // narrow override environments: { production: { adapter: "cloudflare" } }, } as any); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, }); // Manifest lists more keys, but the explicit override wins. await fs.writeFile( "/project/dist/manifest.json", JSON.stringify({ env: ["APP_SECRET", "GOOGLE_CLIENT_ID"] }), ); process.env.APP_SECRET = "s1"; process.env.GOOGLE_CLIENT_ID = "g1"; try { const run = createMockRun(); await adapter.secrets(ctx, run); } finally { delete process.env.APP_SECRET; delete process.env.GOOGLE_CLIENT_ID; } const pushed = api.secrets.get("acme-portal-production") ?? []; expect(pushed.map((s) => s.name)).toEqual(["APP_SECRET"]); }); test("auto-derives PUBLIC_URL from the configured domain", async ({ expect, }) => { const { adapter, fs, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { envConfig: { adapter: "cloudflare", domain: "lore.alepha.dev" }, }); await fs.writeFile( "/project/.env.production", ["APP_SECRET=my-secret", ""].join("\n"), ); const run = createMockRun(); await adapter.secrets(ctx, run); const bindings = api.bindings.get("acme-portal-production") ?? []; const publicUrl = bindings.find((b) => b.name === "PUBLIC_URL"); expect(publicUrl?.text).toBe("https://lore.alepha.dev"); }); test("derives PUBLIC_URL for a tenant subdomain", async ({ expect }) => { const { adapter, fs, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { envConfig: { adapter: "cloudflare", domain: "alepha.dev" }, tenant: "acme", }); await fs.writeFile( "/project/.env.production", ["APP_SECRET=my-secret", ""].join("\n"), ); const run = createMockRun(); await adapter.secrets(ctx, run); const bindings = api.bindings.get("acme-portal-production") ?? []; const publicUrl = bindings.find((b) => b.name === "PUBLIC_URL"); expect(publicUrl?.text).toBe("https://acme.alepha.dev"); }); test("honors an explicit PUBLIC_URL over the derived one", async ({ expect, }) => { const { adapter, fs, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { envConfig: { adapter: "cloudflare", domain: "lore.alepha.dev" }, }); await fs.writeFile( "/project/.env.production", ["PUBLIC_URL=https://custom.example.com", ""].join("\n"), ); const run = createMockRun(); await adapter.secrets(ctx, run); const bindings = api.bindings.get("acme-portal-production") ?? []; const publicUrl = bindings.find((b) => b.name === "PUBLIC_URL"); expect(publicUrl?.text).toBe("https://custom.example.com"); }); test("skips when no env file exists", async ({ expect }) => { const { adapter, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, }); const run = createMockRun(); await adapter.secrets(ctx, run); expect(api.secrets.size).toBe(0); }); test("skips comments and empty lines", async ({ expect }) => { const { adapter, fs, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, }); await fs.writeFile( "/project/.env.production", ["# This is a comment", "", "ONLY_SECRET=value"].join("\n"), ); const run = createMockRun(); await adapter.secrets(ctx, run); const pushed = api.secrets.get("acme-portal-production") ?? []; expect(pushed.map((s) => s.name)).toEqual(["ONLY_SECRET"]); }); test("skips PATCH when ALEPHA_SECRETS_HASH binding matches", async ({ expect, }) => { const { adapter, fs, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, }); await fs.writeFile( "/project/.env.production", ["APP_SECRET=my-secret", "GOOGLE_API_KEY=sk-123"].join("\n"), ); const run = createMockRun(); // First push lands the hash binding on the worker. await adapter.secrets(ctx, run); const firstBindings = api.bindings.get("acme-portal-production") ?? []; const hashBinding = firstBindings.find( (b) => b.name === "ALEPHA_SECRETS_HASH", ); expect(hashBinding?.type).toBe("plain_text"); expect(hashBinding?.text).toMatch(/^[a-f0-9]{64}$/); // Mutate the bindings to a sentinel set we can watch for accidental // overwrites. If the second `secrets()` call decides to PATCH again, // the sentinel would be wiped. api.bindings.set("acme-portal-production", [ ...firstBindings, { type: "plain_text", name: "__SENTINEL__", text: "untouched" }, ]); await adapter.secrets(ctx, run); const afterBindings = api.bindings.get("acme-portal-production") ?? []; expect(afterBindings.find((b) => b.name === "__SENTINEL__")?.text).toBe( "untouched", ); }); test("PATCHes when secrets changed (hash mismatch)", async ({ expect }) => { const { adapter, fs, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: false, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, }); await fs.writeFile("/project/.env.production", "APP_SECRET=v1"); const run = createMockRun(); await adapter.secrets(ctx, run); const firstHash = api.bindings .get("acme-portal-production") ?.find((b) => b.name === "ALEPHA_SECRETS_HASH")?.text; // Same key, different value → hash should change → fresh PATCH. await fs.writeFile("/project/.env.production", "APP_SECRET=v2"); await adapter.secrets(ctx, run); const secondHash = api.bindings .get("acme-portal-production") ?.find((b) => b.name === "ALEPHA_SECRETS_HASH")?.text; expect(firstHash).toBeTruthy(); expect(secondHash).toBeTruthy(); expect(secondHash).not.toBe(firstHash); }); }); describe("inspect", () => { test("returns state of all expected resources via REST API", async ({ expect, }) => { const { adapter, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: true, hasBucket: true, hasKV: false, hasQueue: false, hasCron: false, }, }); // Pre-seed existing resources api.d1Databases.push({ name: "acme-portal-production", uuid: "db-uuid", }); api.r2Buckets.push({ name: "acme-portal-production", creation_date: "2025-01-01", }); const run = createMockRun(); const state = await adapter.inspect(ctx, run); expect(state.databases).toEqual([ { name: "acme-portal-production", exists: true, id: "db-uuid", }, ]); expect(state.buckets).toEqual([ { name: "acme-portal-production", exists: true, id: "2025-01-01", }, ]); }); }); describe("teardown", () => { test("deletes resources via REST API", async ({ expect }) => { const { adapter, naming, api } = createTestEnv(); const ctx = makeCtx(naming, { entry: { root: "/project", server: "src/main.ts" }, resources: { hasDatabase: true, hasBucket: true, hasKV: true, hasQueue: false, hasCron: false, }, }); // Pre-seed existing resources api.d1Databases.push({ name: "acme-portal-production", uuid: "db-uuid", }); api.r2Buckets.push({ name: "acme-portal-production", creation_date: "2025-01-01", }); api.kvNamespaces.push({ id: "kv-id", title: "acme-portal-production", }); const run = createMockRun(); await adapter.teardown(ctx, run); expect(api.d1Databases).toHaveLength(0); // expect(api.r2Buckets).toHaveLength(0); DISABLED FOR NOW expect(api.kvNamespaces).toHaveLength(0); }); }); describe("exportDb", () => { test("dumps remote D1 and imports it into a local SQLite snapshot", async ({ expect, }) => { const { adapter, naming } = createTestEnv(); const ctx = makeCtx(naming, { resources: { hasDatabase: true, hasBucket: false, hasKV: false, hasQueue: false, hasCron: false, }, }); // Record the raw shell strings exportDb issues via run(). const commands: string[] = []; const run: any = async (cmd: any) => { if (typeof cmd === "string") commands.push(cmd); }; run.end = () => {}; await adapter.exportDb(ctx, run, { output: "/tmp/snap.db", keepSql: true, }); expect( commands.some((c) => /^wrangler d1 export .+ --remote /.test(c)), ).toBe(true); expect(commands.some((c) => /sqlite3 '\/tmp\/snap\.db' < /.test(c))).toBe( true, ); }); test("refuses when no database is detected", async ({ expect }) => { const { adapter, naming } = createTestEnv(); const ctx = makeCtx(naming); // resources.hasDatabase defaults to false const run = createMockRun(); await expect(adapter.exportDb(ctx, run)).rejects.toThrow(/no database/i); }); }); }); /** * Create a mock RunnerMethod that just executes handlers directly. */ function createMockRun(): any { const run: any = async (task: any) => { if (Array.isArray(task)) { await Promise.all( task.map((t) => typeof t === "object" && t.handler ? t.handler() : Promise.resolve(), ), ); } if (typeof task === "object" && task.handler) { await task.handler(); } }; run.end = () => {}; return run; }