lopata 0.7.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard/{chunk-pqnphvm2.css → chunk-csyd2tq2.css} +27 -0
- package/dist/dashboard/{chunk-5nxa3jfc.js → chunk-yxzrcvyh.js} +364 -3
- package/dist/dashboard/index.html +1 -1
- package/package.json +5 -3
- package/src/api/handlers/generations.ts +19 -5
- package/src/api/types.ts +14 -0
- package/src/bindings/cache.ts +14 -8
- package/src/bindings/durable-object.ts +80 -21
- package/src/bindings/kv.ts +12 -8
- package/src/bindings/queue.ts +22 -12
- package/src/bindings/workflow.ts +332 -25
- package/src/env.ts +3 -2
- package/src/file-watcher.ts +59 -32
- package/src/generation-manager.ts +6 -1
- package/src/generation.ts +15 -3
- package/src/plugin.ts +2 -90
- package/src/setup-globals.ts +23 -21
- package/src/testing/clock.ts +26 -0
- package/src/testing/durable-object.ts +325 -0
- package/src/testing/env-builder.ts +126 -0
- package/src/testing/fetch-mock.ts +145 -0
- package/src/testing/index.ts +288 -0
- package/src/testing/setup.ts +68 -0
- package/src/testing/types.ts +68 -0
- package/src/testing/workflow.ts +323 -0
- package/src/tracing/store.ts +6 -0
- package/src/tracing/types.ts +1 -0
- package/src/virtual-modules.ts +99 -0
- package/src/vite-plugin/config-plugin.ts +2 -0
- package/src/vite-plugin/dev-server-plugin.ts +159 -56
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { randomUUIDv7 } from 'bun'
|
|
2
|
+
import { rmSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { SqliteCacheStorage } from '../bindings/cache'
|
|
5
|
+
import type { DurableObjectNamespaceImpl } from '../bindings/durable-object'
|
|
6
|
+
import { ForwardableEmailMessage } from '../bindings/email'
|
|
7
|
+
import { createScheduledController } from '../bindings/scheduled'
|
|
8
|
+
import type { SqliteWorkflowBinding } from '../bindings/workflow'
|
|
9
|
+
import { setGlobalEnv } from '../env'
|
|
10
|
+
import { ExecutionContext, runWithExecutionContext } from '../execution-context'
|
|
11
|
+
import { TestClock } from './clock'
|
|
12
|
+
import { TestDurableObjectNamespace } from './durable-object'
|
|
13
|
+
import { buildTestEnv, configToBindings } from './env-builder'
|
|
14
|
+
import { FetchMock, runWithFetchMock } from './fetch-mock'
|
|
15
|
+
import { setupTestEnv, testCachesRef } from './setup'
|
|
16
|
+
import type { TestEnv, TestEnvOptions, WorkerHandlers, WorkerModule } from './types'
|
|
17
|
+
import { TestWorkflowBinding } from './workflow'
|
|
18
|
+
|
|
19
|
+
export { TestClock } from './clock'
|
|
20
|
+
export type { Clock } from './clock'
|
|
21
|
+
export type { TestDurableObjectHandle, TestDurableObjectNamespace, TestDurableObjectStorage, TestWebSocket } from './durable-object'
|
|
22
|
+
export { FetchMock } from './fetch-mock'
|
|
23
|
+
export type { FetchCall } from './fetch-mock'
|
|
24
|
+
export type { BindingSpec, TestEnv, TestEnvOptions, WorkerHandlers, WorkerModule } from './types'
|
|
25
|
+
export type { TestWorkflowBinding, TestWorkflowInstance, TestWorkflowRun } from './workflow'
|
|
26
|
+
|
|
27
|
+
export async function createTestEnv<Env = Record<string, unknown>>(options: TestEnvOptions = {}): Promise<TestEnv<Env>> {
|
|
28
|
+
// Ensure virtual modules + globals are registered (no-op if preload already ran)
|
|
29
|
+
setupTestEnv()
|
|
30
|
+
|
|
31
|
+
// Resolve clock
|
|
32
|
+
let clock: TestClock | null = null
|
|
33
|
+
if (options.clock === true) {
|
|
34
|
+
clock = new TestClock()
|
|
35
|
+
} else if (options.clock instanceof TestClock) {
|
|
36
|
+
clock = options.clock
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create fetch mock (always available, defaults to passthrough)
|
|
40
|
+
const fetchMock = new FetchMock()
|
|
41
|
+
|
|
42
|
+
let mergedBindings = options.bindings
|
|
43
|
+
let mergedVars = options.vars
|
|
44
|
+
|
|
45
|
+
// Load from wrangler config if specified — translate to BindingSpec
|
|
46
|
+
if (options.wrangler) {
|
|
47
|
+
const { loadConfig } = await import('../config')
|
|
48
|
+
const config = await loadConfig(resolve(options.wrangler))
|
|
49
|
+
const { bindings: configBindings, vars: configVars } = configToBindings(config)
|
|
50
|
+
// Merge: explicit options.bindings override wrangler-derived bindings
|
|
51
|
+
mergedBindings = { ...configBindings, ...options.bindings }
|
|
52
|
+
// Merge: explicit options.vars override wrangler vars
|
|
53
|
+
mergedVars = { ...configVars, ...options.vars }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { db, env, registry, tmpDirs } = buildTestEnv(mergedBindings, mergedVars, clock ?? undefined)
|
|
57
|
+
|
|
58
|
+
// Wire in-memory caches for this test env
|
|
59
|
+
testCachesRef.current = new SqliteCacheStorage(db, undefined, clock ?? undefined)
|
|
60
|
+
|
|
61
|
+
// Resolve worker module
|
|
62
|
+
let workerModule: Record<string, unknown>
|
|
63
|
+
let defaultExport: unknown
|
|
64
|
+
let classBasedExport = false
|
|
65
|
+
|
|
66
|
+
if (typeof options.worker === 'string') {
|
|
67
|
+
workerModule = await import(resolve(options.worker))
|
|
68
|
+
defaultExport = workerModule.default
|
|
69
|
+
if (typeof defaultExport === 'function' && defaultExport.prototype) {
|
|
70
|
+
classBasedExport = typeof defaultExport.prototype.fetch === 'function'
|
|
71
|
+
}
|
|
72
|
+
} else if (options.worker && 'default' in options.worker) {
|
|
73
|
+
// WorkerModule — has a `default` export (class or object) + named exports
|
|
74
|
+
const mod = options.worker as WorkerModule
|
|
75
|
+
defaultExport = mod.default
|
|
76
|
+
workerModule = { ...mod }
|
|
77
|
+
if (typeof defaultExport === 'function' && defaultExport.prototype) {
|
|
78
|
+
classBasedExport = typeof defaultExport.prototype.fetch === 'function'
|
|
79
|
+
}
|
|
80
|
+
} else if (options.worker) {
|
|
81
|
+
// Inline handlers object — also expose extra properties (e.g. DO/Workflow classes)
|
|
82
|
+
// as top-level module exports so wireClassRefs can find them
|
|
83
|
+
defaultExport = options.worker
|
|
84
|
+
workerModule = { default: defaultExport, ...options.worker }
|
|
85
|
+
} else {
|
|
86
|
+
defaultExport = {}
|
|
87
|
+
workerModule = { default: defaultExport }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Wire DO/Workflow classes
|
|
91
|
+
for (const entry of registry.durableObjects) {
|
|
92
|
+
const cls = workerModule[entry.className]
|
|
93
|
+
if (!cls) throw new Error(`Durable Object class "${entry.className}" not exported from worker module`)
|
|
94
|
+
entry.namespace._setClass(cls as any, env)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const entry of registry.workflows) {
|
|
98
|
+
const cls = workerModule[entry.className]
|
|
99
|
+
if (!cls) throw new Error(`Workflow class "${entry.className}" not exported from worker module`)
|
|
100
|
+
entry.binding._setClass(cls as any, env)
|
|
101
|
+
entry.binding.resumeInterrupted()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Wire service bindings — self-referencing
|
|
105
|
+
for (const entry of registry.serviceBindings) {
|
|
106
|
+
const wire = entry.proxy._wire as ((resolver: () => { workerModule: Record<string, unknown>; env: Record<string, unknown> }) => void) | undefined
|
|
107
|
+
if (wire) {
|
|
108
|
+
wire(() => ({ workerModule, env }))
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Set globalEnv so `import { env } from 'cloudflare:workers'` works
|
|
113
|
+
setGlobalEnv(env)
|
|
114
|
+
|
|
115
|
+
// --- Handler dispatch helpers ---
|
|
116
|
+
|
|
117
|
+
function getHandler(name: string): ((...args: unknown[]) => Promise<unknown>) | undefined {
|
|
118
|
+
if (classBasedExport) {
|
|
119
|
+
if (typeof (defaultExport as any).prototype[name] === 'function') {
|
|
120
|
+
return (...args: unknown[]) => {
|
|
121
|
+
const ctx = new ExecutionContext()
|
|
122
|
+
const instance = new (defaultExport as new(ctx: ExecutionContext, env: unknown) => Record<string, unknown>)(ctx, env)
|
|
123
|
+
return (instance[name] as (...a: unknown[]) => Promise<unknown>)(...args)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return undefined
|
|
127
|
+
}
|
|
128
|
+
const method = (defaultExport as Record<string, unknown>)?.[name]
|
|
129
|
+
return typeof method === 'function' ? method.bind(defaultExport) : undefined
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function fetchHandler(input: string | Request, init?: RequestInit): Promise<Response> {
|
|
133
|
+
let request: Request
|
|
134
|
+
if (typeof input === 'string') {
|
|
135
|
+
const url = input.startsWith('/') ? `http://localhost${input}` : input
|
|
136
|
+
request = new Request(url, init)
|
|
137
|
+
} else {
|
|
138
|
+
request = init ? new Request(input, init) : input
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const ctx = new ExecutionContext()
|
|
142
|
+
return runWithExecutionContext(ctx, () =>
|
|
143
|
+
runWithFetchMock(fetchMock, async () => {
|
|
144
|
+
let response: Response
|
|
145
|
+
if (classBasedExport) {
|
|
146
|
+
const instance = new (defaultExport as new(ctx: ExecutionContext, env: unknown) => Record<string, unknown>)(ctx, env)
|
|
147
|
+
response = await (instance.fetch as (r: Request) => Promise<Response>)(request)
|
|
148
|
+
} else {
|
|
149
|
+
const handler = (defaultExport as Record<string, unknown>)?.fetch
|
|
150
|
+
if (typeof handler !== 'function') {
|
|
151
|
+
throw new Error('No fetch handler found')
|
|
152
|
+
}
|
|
153
|
+
response = await (handler as (r: Request, e: unknown, c: ExecutionContext) => Promise<Response>)(request, env, ctx)
|
|
154
|
+
}
|
|
155
|
+
await ctx._awaitAll()
|
|
156
|
+
return response
|
|
157
|
+
}))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function queueHandler(queueName: string, messages: { body: unknown; contentType?: string }[]): Promise<void> {
|
|
161
|
+
const handler = getHandler('queue')
|
|
162
|
+
if (!handler) throw new Error('No queue handler found')
|
|
163
|
+
|
|
164
|
+
const builtMessages = messages.map((msg, i) => ({
|
|
165
|
+
id: randomUUIDv7(),
|
|
166
|
+
timestamp: new Date(),
|
|
167
|
+
body: msg.body,
|
|
168
|
+
attempts: 1,
|
|
169
|
+
ack() {},
|
|
170
|
+
retry(_options?: { delaySeconds?: number }) {},
|
|
171
|
+
}))
|
|
172
|
+
|
|
173
|
+
const batch = {
|
|
174
|
+
queue: queueName,
|
|
175
|
+
messages: builtMessages,
|
|
176
|
+
ackAll() {},
|
|
177
|
+
retryAll(_options?: { delaySeconds?: number }) {},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const ctx = new ExecutionContext()
|
|
181
|
+
await runWithExecutionContext(ctx, () =>
|
|
182
|
+
runWithFetchMock(fetchMock, async () => {
|
|
183
|
+
await handler(batch, env, ctx)
|
|
184
|
+
await ctx._awaitAll()
|
|
185
|
+
}))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function scheduledHandler(opts?: { cron?: string; scheduledTime?: number }): Promise<void> {
|
|
189
|
+
const handler = getHandler('scheduled')
|
|
190
|
+
if (!handler) throw new Error('No scheduled handler found')
|
|
191
|
+
|
|
192
|
+
const controller = createScheduledController(opts?.cron ?? '* * * * *', opts?.scheduledTime ?? Date.now())
|
|
193
|
+
const ctx = new ExecutionContext()
|
|
194
|
+
await runWithExecutionContext(ctx, () =>
|
|
195
|
+
runWithFetchMock(fetchMock, async () => {
|
|
196
|
+
await handler(controller, env, ctx)
|
|
197
|
+
await ctx._awaitAll()
|
|
198
|
+
}))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function emailHandler(opts: { from: string; to: string; raw: Uint8Array | string }): Promise<void> {
|
|
202
|
+
const handler = getHandler('email')
|
|
203
|
+
if (!handler) throw new Error('No email handler found')
|
|
204
|
+
|
|
205
|
+
const rawBytes = typeof opts.raw === 'string' ? new TextEncoder().encode(opts.raw) : opts.raw
|
|
206
|
+
const messageId = randomUUIDv7()
|
|
207
|
+
db.run(
|
|
208
|
+
"INSERT INTO email_messages (id, binding, from_addr, to_addr, raw, raw_size, status, created_at) VALUES (?, ?, ?, ?, ?, ?, 'received', ?)",
|
|
209
|
+
[messageId, '_incoming', opts.from, opts.to, rawBytes, rawBytes.byteLength, Date.now()],
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
const message = new ForwardableEmailMessage(db, messageId, opts.from, opts.to, rawBytes)
|
|
213
|
+
const ctx = new ExecutionContext()
|
|
214
|
+
await runWithExecutionContext(ctx, () =>
|
|
215
|
+
runWithFetchMock(fetchMock, async () => {
|
|
216
|
+
await handler(message, env, ctx)
|
|
217
|
+
await ctx._awaitAll()
|
|
218
|
+
}))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- Test helper factories ---
|
|
222
|
+
|
|
223
|
+
const testWorkflows: TestWorkflowBinding[] = []
|
|
224
|
+
const testDOs: TestDurableObjectNamespace[] = []
|
|
225
|
+
|
|
226
|
+
function workflowHelper(bindingName: string): TestWorkflowBinding {
|
|
227
|
+
const entry = registry.workflows.find(e => e.bindingName === bindingName)
|
|
228
|
+
if (!entry) throw new Error(`Workflow binding "${bindingName}" not found. Available: ${registry.workflows.map(e => e.bindingName).join(', ')}`)
|
|
229
|
+
const tw = new TestWorkflowBinding(entry.binding as SqliteWorkflowBinding, db)
|
|
230
|
+
testWorkflows.push(tw)
|
|
231
|
+
return tw
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function durableObjectHelper(bindingName: string): TestDurableObjectNamespace {
|
|
235
|
+
const entry = registry.durableObjects.find(e => e.bindingName === bindingName)
|
|
236
|
+
if (!entry) {
|
|
237
|
+
throw new Error(`Durable Object binding "${bindingName}" not found. Available: ${registry.durableObjects.map(e => e.bindingName).join(', ')}`)
|
|
238
|
+
}
|
|
239
|
+
const td = new TestDurableObjectNamespace(entry.namespace as DurableObjectNamespaceImpl)
|
|
240
|
+
testDOs.push(td)
|
|
241
|
+
return td
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function advanceTime(ms: number): Promise<void> {
|
|
245
|
+
if (!clock) throw new Error('advanceTime requires clock: true in createTestEnv options')
|
|
246
|
+
clock.advance(ms)
|
|
247
|
+
// Fire ready DO alarms
|
|
248
|
+
for (const entry of registry.durableObjects) {
|
|
249
|
+
await Promise.all(entry.namespace._fireReadyAlarms())
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function dispose(): void {
|
|
254
|
+
for (const tw of testWorkflows) tw.dispose()
|
|
255
|
+
for (const td of testDOs) td.dispose()
|
|
256
|
+
for (const entry of registry.durableObjects) {
|
|
257
|
+
entry.namespace.destroy()
|
|
258
|
+
}
|
|
259
|
+
for (const entry of registry.workflows) {
|
|
260
|
+
entry.binding.abortRunning()
|
|
261
|
+
}
|
|
262
|
+
db.close()
|
|
263
|
+
for (const dir of tmpDirs) {
|
|
264
|
+
try {
|
|
265
|
+
rmSync(dir, { recursive: true, force: true })
|
|
266
|
+
} catch {}
|
|
267
|
+
}
|
|
268
|
+
// Clean up global state
|
|
269
|
+
setGlobalEnv({})
|
|
270
|
+
testCachesRef.current = null
|
|
271
|
+
fetchMock.reset()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
env: env as Env,
|
|
276
|
+
db,
|
|
277
|
+
fetch: fetchHandler,
|
|
278
|
+
queue: queueHandler,
|
|
279
|
+
scheduled: scheduledHandler,
|
|
280
|
+
email: emailHandler,
|
|
281
|
+
workflow: workflowHelper as TestEnv<Env>['workflow'],
|
|
282
|
+
durableObject: durableObjectHelper as TestEnv<Env>['durableObject'],
|
|
283
|
+
clock,
|
|
284
|
+
fetchMock,
|
|
285
|
+
advanceTime,
|
|
286
|
+
dispose,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { plugin } from 'bun'
|
|
2
|
+
import type { SqliteCacheStorage } from '../bindings/cache'
|
|
3
|
+
import { setupCloudflareGlobals } from '../setup-globals'
|
|
4
|
+
import { registerVirtualModules } from '../virtual-modules'
|
|
5
|
+
import { getActiveFetchMock } from './fetch-mock'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Mutable ref for per-test caches instance.
|
|
9
|
+
* Set by `createTestEnv()`, cleared by `dispose()`.
|
|
10
|
+
* The `caches` global getter reads from this ref.
|
|
11
|
+
*/
|
|
12
|
+
export const testCachesRef: { current: SqliteCacheStorage | null } = { current: null }
|
|
13
|
+
|
|
14
|
+
let initialized = false
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sets up the test environment:
|
|
18
|
+
* - Registers cloudflare:* virtual modules via Bun.plugin
|
|
19
|
+
* - Sets up global Cloudflare APIs (HTMLRewriter, WebSocketPair, crypto, etc.)
|
|
20
|
+
* - Overrides `caches` global to use in-memory storage from testCachesRef
|
|
21
|
+
* - Overrides `globalThis.fetch` to support ALS-scoped fetch mocking
|
|
22
|
+
*
|
|
23
|
+
* Idempotent — safe to call multiple times.
|
|
24
|
+
*/
|
|
25
|
+
export function setupTestEnv() {
|
|
26
|
+
if (initialized) return
|
|
27
|
+
initialized = true
|
|
28
|
+
|
|
29
|
+
setupCloudflareGlobals()
|
|
30
|
+
|
|
31
|
+
// Override caches to use the per-test in-memory ref instead of filesystem
|
|
32
|
+
Object.defineProperty(globalThis, 'caches', {
|
|
33
|
+
get: () => {
|
|
34
|
+
if (!testCachesRef.current) {
|
|
35
|
+
throw new Error('caches is not available — call createTestEnv() first')
|
|
36
|
+
}
|
|
37
|
+
return testCachesRef.current
|
|
38
|
+
},
|
|
39
|
+
configurable: true,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Override globalThis.fetch to support ALS-scoped fetch mocking
|
|
43
|
+
const _originalFetch = globalThis.fetch
|
|
44
|
+
const mockedFetch = async (input: string | Request | URL, init?: RequestInit) => {
|
|
45
|
+
const mock = getActiveFetchMock()
|
|
46
|
+
if (!mock) return _originalFetch(input, init)
|
|
47
|
+
|
|
48
|
+
const request = new Request(input as any, init)
|
|
49
|
+
const result = await mock._handle(request)
|
|
50
|
+
if (result) return result.response
|
|
51
|
+
|
|
52
|
+
// Passthrough — call original fetch and record it
|
|
53
|
+
const response = await _originalFetch(input, init)
|
|
54
|
+
mock._recordPassthrough(new Request(input as any, init), response)
|
|
55
|
+
return response
|
|
56
|
+
}
|
|
57
|
+
globalThis.fetch = mockedFetch as typeof globalThis.fetch
|
|
58
|
+
|
|
59
|
+
plugin({
|
|
60
|
+
name: 'cloudflare-workers-test-shim',
|
|
61
|
+
setup(build) {
|
|
62
|
+
registerVirtualModules(build)
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Auto-run on import (preload behavior)
|
|
68
|
+
setupTestEnv()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite'
|
|
2
|
+
import type { TestClock } from './clock'
|
|
3
|
+
import type { TestDurableObjectNamespace } from './durable-object'
|
|
4
|
+
import type { FetchMock } from './fetch-mock'
|
|
5
|
+
import type { TestWorkflowBinding } from './workflow'
|
|
6
|
+
|
|
7
|
+
export interface WorkerHandlers {
|
|
8
|
+
fetch?(request: Request, env: Record<string, unknown>, ctx: unknown): Promise<Response> | Response
|
|
9
|
+
queue?(batch: unknown, env: Record<string, unknown>, ctx: unknown): Promise<void> | void
|
|
10
|
+
scheduled?(controller: unknown, env: Record<string, unknown>, ctx: unknown): Promise<void> | void
|
|
11
|
+
email?(message: unknown, env: Record<string, unknown>, ctx: unknown): Promise<void> | void
|
|
12
|
+
[key: string]: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Worker module with a default export (class or object) plus named exports (DO/Workflow classes) */
|
|
16
|
+
export interface WorkerModule {
|
|
17
|
+
default: (new(ctx: unknown, env: unknown) => Record<string, unknown>) | WorkerHandlers
|
|
18
|
+
[key: string]: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TestEnvOptions {
|
|
22
|
+
/** Worker: file path (string), inline handlers object, or module with default + named exports */
|
|
23
|
+
worker?: string | WorkerHandlers | WorkerModule
|
|
24
|
+
/** Binding declarations — keys become binding names in env */
|
|
25
|
+
bindings?: Record<string, BindingSpec>
|
|
26
|
+
/** Path to wrangler.toml/.json/.jsonc to load bindings from */
|
|
27
|
+
wrangler?: string
|
|
28
|
+
/** Plain string variables to add to env */
|
|
29
|
+
vars?: Record<string, string>
|
|
30
|
+
/** Enable test clock for time control. Pass true to create a new TestClock, or pass a TestClock instance. */
|
|
31
|
+
clock?: boolean | TestClock
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type BindingSpec =
|
|
35
|
+
| 'kv'
|
|
36
|
+
| 'r2'
|
|
37
|
+
| 'd1'
|
|
38
|
+
| 'queue'
|
|
39
|
+
| { type: 'durable-object'; className: string }
|
|
40
|
+
| { type: 'workflow'; className: string }
|
|
41
|
+
| { type: 'service'; service: string; entrypoint?: string }
|
|
42
|
+
|
|
43
|
+
export interface TestEnv<Env = Record<string, unknown>> {
|
|
44
|
+
/** The built env object with all bindings */
|
|
45
|
+
env: Env
|
|
46
|
+
/** The shared in-memory database used by bindings */
|
|
47
|
+
db: Database
|
|
48
|
+
/** Dispatch a fetch request to the worker */
|
|
49
|
+
fetch(input: string | Request, init?: RequestInit): Promise<Response>
|
|
50
|
+
/** Dispatch a queue batch to the worker's queue handler */
|
|
51
|
+
queue(queueName: string, messages: { body: unknown; contentType?: string }[]): Promise<void>
|
|
52
|
+
/** Dispatch a scheduled event to the worker */
|
|
53
|
+
scheduled(options?: { cron?: string; scheduledTime?: number }): Promise<void>
|
|
54
|
+
/** Dispatch an email event to the worker */
|
|
55
|
+
email(options: { from: string; to: string; raw: Uint8Array | string }): Promise<void>
|
|
56
|
+
/** Get a test-friendly workflow binding wrapper */
|
|
57
|
+
workflow(bindingName: string & keyof Env): TestWorkflowBinding
|
|
58
|
+
/** Get a test-friendly durable object namespace wrapper */
|
|
59
|
+
durableObject(bindingName: string & keyof Env): TestDurableObjectNamespace
|
|
60
|
+
/** Test clock for time control (null if not enabled) */
|
|
61
|
+
clock: TestClock | null
|
|
62
|
+
/** Fetch mock for intercepting outgoing HTTP requests */
|
|
63
|
+
fetchMock: FetchMock
|
|
64
|
+
/** Advance time and fire ready DO alarms */
|
|
65
|
+
advanceTime(ms: number): Promise<void>
|
|
66
|
+
/** Cleanup: close DB, remove temp dirs, destroy DO namespaces */
|
|
67
|
+
dispose(): void
|
|
68
|
+
}
|