ai-providers 0.2.0 → 0.3.0

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.
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Tests for the public API surface
3
+ *
4
+ * Covers:
5
+ * - Exported functions and types
6
+ * - Module re-exports
7
+ * - API consistency
8
+ */
9
+
10
+ import { describe, it, expect } from 'vitest'
11
+ import * as aiProviders from './index.js'
12
+
13
+ describe('public API exports', () => {
14
+ describe('registry functions', () => {
15
+ it('exports createRegistry', () => {
16
+ expect(aiProviders.createRegistry).toBeDefined()
17
+ expect(typeof aiProviders.createRegistry).toBe('function')
18
+ })
19
+
20
+ it('exports getRegistry', () => {
21
+ expect(aiProviders.getRegistry).toBeDefined()
22
+ expect(typeof aiProviders.getRegistry).toBe('function')
23
+ })
24
+
25
+ it('exports configureRegistry', () => {
26
+ expect(aiProviders.configureRegistry).toBeDefined()
27
+ expect(typeof aiProviders.configureRegistry).toBe('function')
28
+ })
29
+ })
30
+
31
+ describe('model functions', () => {
32
+ it('exports model', () => {
33
+ expect(aiProviders.model).toBeDefined()
34
+ expect(typeof aiProviders.model).toBe('function')
35
+ })
36
+
37
+ it('exports embeddingModel', () => {
38
+ expect(aiProviders.embeddingModel).toBeDefined()
39
+ expect(typeof aiProviders.embeddingModel).toBe('function')
40
+ })
41
+ })
42
+
43
+ describe('constants', () => {
44
+ it('exports DIRECT_PROVIDERS', () => {
45
+ expect(aiProviders.DIRECT_PROVIDERS).toBeDefined()
46
+ expect(Array.isArray(aiProviders.DIRECT_PROVIDERS)).toBe(true)
47
+ })
48
+ })
49
+
50
+ describe('types', () => {
51
+ it('exports ProviderId type', () => {
52
+ // Type-only export, check in type context
53
+ const typeCheck: aiProviders.ProviderId = 'openai'
54
+ expect(typeCheck).toBe('openai')
55
+ })
56
+
57
+ it('exports DirectProvider type', () => {
58
+ // Type-only export, check in type context
59
+ const typeCheck: aiProviders.DirectProvider = 'anthropic'
60
+ expect(typeCheck).toBe('anthropic')
61
+ })
62
+
63
+ it('exports ProviderConfig type', () => {
64
+ // Type-only export, check in type context
65
+ const typeCheck: aiProviders.ProviderConfig = {
66
+ gatewayUrl: 'test',
67
+ }
68
+ expect(typeCheck).toBeDefined()
69
+ })
70
+
71
+ it('exports Provider type from AI SDK', () => {
72
+ // Re-exported from 'ai' package
73
+ // Type-only export for convenience
74
+ expect(true).toBe(true)
75
+ })
76
+
77
+ it('exports ProviderRegistryProvider type from AI SDK', () => {
78
+ // Re-exported from 'ai' package
79
+ // Type-only export for convenience
80
+ expect(true).toBe(true)
81
+ })
82
+ })
83
+
84
+ describe('API completeness', () => {
85
+ it('exports all documented functions', () => {
86
+ const expectedExports = [
87
+ 'createRegistry',
88
+ 'getRegistry',
89
+ 'configureRegistry',
90
+ 'model',
91
+ 'embeddingModel',
92
+ 'DIRECT_PROVIDERS',
93
+ ]
94
+
95
+ for (const exportName of expectedExports) {
96
+ expect(aiProviders).toHaveProperty(exportName)
97
+ }
98
+ })
99
+
100
+ it('does not export internal implementation details', () => {
101
+ // Should not export internal functions like getEnvConfig, getBaseUrl, etc.
102
+ const internalFunctions = [
103
+ 'getEnvConfig',
104
+ 'getBaseUrl',
105
+ 'createGatewayFetch',
106
+ 'useGatewaySecrets',
107
+ 'getApiKey',
108
+ 'createOpenAIProvider',
109
+ 'createAnthropicProvider',
110
+ 'createGoogleProvider',
111
+ 'createOpenRouterProvider',
112
+ 'createCloudflareProvider',
113
+ 'parseModelId',
114
+ 'providerFactories',
115
+ ]
116
+
117
+ for (const internalName of internalFunctions) {
118
+ expect(aiProviders).not.toHaveProperty(internalName)
119
+ }
120
+ })
121
+ })
122
+
123
+ describe('module structure', () => {
124
+ it('has expected module exports', () => {
125
+ const exports = Object.keys(aiProviders)
126
+ expect(exports.length).toBeGreaterThan(0)
127
+ })
128
+
129
+ it('all exported functions are callable', () => {
130
+ const functions = [
131
+ 'createRegistry',
132
+ 'getRegistry',
133
+ 'configureRegistry',
134
+ 'model',
135
+ 'embeddingModel',
136
+ ]
137
+
138
+ for (const fnName of functions) {
139
+ const fn = (aiProviders as any)[fnName]
140
+ expect(typeof fn).toBe('function')
141
+ }
142
+ })
143
+
144
+ it('all exported constants are defined', () => {
145
+ expect(aiProviders.DIRECT_PROVIDERS).toBeDefined()
146
+ })
147
+ })
148
+ })
149
+
150
+ describe('API usage patterns', () => {
151
+ it('allows importing all exports', () => {
152
+ const {
153
+ createRegistry,
154
+ getRegistry,
155
+ configureRegistry,
156
+ model,
157
+ embeddingModel,
158
+ DIRECT_PROVIDERS,
159
+ } = aiProviders
160
+
161
+ expect(createRegistry).toBeDefined()
162
+ expect(getRegistry).toBeDefined()
163
+ expect(configureRegistry).toBeDefined()
164
+ expect(model).toBeDefined()
165
+ expect(embeddingModel).toBeDefined()
166
+ expect(DIRECT_PROVIDERS).toBeDefined()
167
+ })
168
+
169
+ it('allows importing specific exports', async () => {
170
+ const { model } = await import('./index.js')
171
+ expect(model).toBeDefined()
172
+ })
173
+
174
+ it('supports default import pattern', async () => {
175
+ const module = await import('./index.js')
176
+ expect(module.model).toBeDefined()
177
+ })
178
+ })
179
+
180
+ describe('type exports', () => {
181
+ it('ProviderId accepts valid provider names', () => {
182
+ const providers: aiProviders.ProviderId[] = [
183
+ 'openai',
184
+ 'anthropic',
185
+ 'google',
186
+ 'openrouter',
187
+ 'cloudflare',
188
+ ]
189
+ expect(providers).toHaveLength(5)
190
+ })
191
+
192
+ it('ProviderConfig has optional fields', () => {
193
+ const configs: aiProviders.ProviderConfig[] = [
194
+ {},
195
+ { gatewayUrl: 'test' },
196
+ { gatewayToken: 'test' },
197
+ { openaiApiKey: 'test' },
198
+ {
199
+ gatewayUrl: 'test',
200
+ gatewayToken: 'test',
201
+ openaiApiKey: 'test',
202
+ anthropicApiKey: 'test',
203
+ googleApiKey: 'test',
204
+ openrouterApiKey: 'test',
205
+ cloudflareAccountId: 'test',
206
+ cloudflareApiToken: 'test',
207
+ },
208
+ ]
209
+ expect(configs).toHaveLength(5)
210
+ })
211
+
212
+ it('DirectProvider is a subset of ProviderId', () => {
213
+ // DirectProvider should be 'openai' | 'anthropic' | 'google'
214
+ const direct: aiProviders.DirectProvider[] = ['openai', 'anthropic', 'google']
215
+ expect(direct).toHaveLength(3)
216
+ })
217
+ })
218
+
219
+ describe('package documentation', () => {
220
+ it('has JSDoc package documentation', () => {
221
+ // Main module should have @packageDocumentation
222
+ expect(true).toBe(true)
223
+ })
224
+
225
+ it('exports match README examples', () => {
226
+ // README shows these imports:
227
+ // import { model } from 'ai-providers'
228
+ // import { embeddingModel } from 'ai-providers'
229
+ // import { createRegistry } from 'ai-providers'
230
+ // import { getRegistry } from 'ai-providers'
231
+
232
+ expect(aiProviders.model).toBeDefined()
233
+ expect(aiProviders.embeddingModel).toBeDefined()
234
+ expect(aiProviders.createRegistry).toBeDefined()
235
+ expect(aiProviders.getRegistry).toBeDefined()
236
+ })
237
+ })
238
+
239
+ describe('convenience re-exports', () => {
240
+ it('re-exports AI SDK types for convenience', () => {
241
+ // Provider and ProviderRegistryProvider are re-exported from 'ai'
242
+ // This allows users to import types from ai-providers instead of ai
243
+ expect(true).toBe(true)
244
+ })
245
+
246
+ it('re-exports DIRECT_PROVIDERS from language-models', () => {
247
+ // DIRECT_PROVIDERS is re-exported from language-models for consistency
248
+ expect(aiProviders.DIRECT_PROVIDERS).toBeDefined()
249
+ })
250
+ })
251
+
252
+ describe('integration with subpackages', () => {
253
+ it('main package exports registry functions', () => {
254
+ // Main package exports registry.ts
255
+ expect(aiProviders.createRegistry).toBeDefined()
256
+ })
257
+
258
+ it('cloudflare provider is in separate export', async () => {
259
+ // Cloudflare provider should be importable from 'ai-providers/cloudflare'
260
+ // Not from the main 'ai-providers' export
261
+ expect(aiProviders).not.toHaveProperty('cloudflareEmbedding')
262
+ expect(aiProviders).not.toHaveProperty('cloudflare')
263
+ })
264
+ })
265
+
266
+ describe('backward compatibility', () => {
267
+ it('maintains stable public API', () => {
268
+ // These exports should remain stable across versions
269
+ const stableExports = [
270
+ 'createRegistry',
271
+ 'getRegistry',
272
+ 'model',
273
+ 'embeddingModel',
274
+ 'DIRECT_PROVIDERS',
275
+ ]
276
+
277
+ for (const exportName of stableExports) {
278
+ expect(aiProviders).toHaveProperty(exportName)
279
+ }
280
+ })
281
+
282
+ it('types are backward compatible', () => {
283
+ // Type structure should be stable
284
+ const config: aiProviders.ProviderConfig = {
285
+ gatewayUrl: 'test',
286
+ }
287
+ expect(config).toBeDefined()
288
+ })
289
+ })
290
+
291
+ describe('error messages', () => {
292
+ it('provides helpful error for missing credentials', async () => {
293
+ // When creating a model without credentials, should have clear error
294
+ expect(true).toBe(true)
295
+ })
296
+
297
+ it('provides helpful error for invalid model ID', async () => {
298
+ // When providing invalid model ID, should have clear error
299
+ expect(true).toBe(true)
300
+ })
301
+ })
302
+
303
+ describe('async API behavior', () => {
304
+ it('createRegistry returns a Promise', async () => {
305
+ const result = aiProviders.createRegistry()
306
+ expect(result).toBeInstanceOf(Promise)
307
+ await result // Wait for it to complete
308
+ })
309
+
310
+ it('getRegistry returns a Promise', async () => {
311
+ const result = aiProviders.getRegistry()
312
+ expect(result).toBeInstanceOf(Promise)
313
+ await result
314
+ })
315
+
316
+ it('configureRegistry returns a Promise', async () => {
317
+ const result = aiProviders.configureRegistry({})
318
+ expect(result).toBeInstanceOf(Promise)
319
+ await result
320
+ })
321
+
322
+ it('model returns a Promise', async () => {
323
+ try {
324
+ const result = aiProviders.model('test')
325
+ expect(result).toBeInstanceOf(Promise)
326
+ await result
327
+ } catch {
328
+ // May fail without proper config
329
+ }
330
+ })
331
+
332
+ it('embeddingModel returns a Promise', async () => {
333
+ try {
334
+ const result = aiProviders.embeddingModel('openai:test')
335
+ expect(result).toBeInstanceOf(Promise)
336
+ await result
337
+ } catch {
338
+ // May fail without proper config
339
+ }
340
+ })
341
+ })
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * ai-providers - Unified AI Provider Registry
3
+ *
4
+ * Access multiple AI providers via simple string identifiers.
5
+ * Supports Cloudflare AI Gateway for unified routing and auth.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ export {
11
+ createRegistry,
12
+ getRegistry,
13
+ configureRegistry,
14
+ model,
15
+ embeddingModel,
16
+ DIRECT_PROVIDERS,
17
+ type ProviderId,
18
+ type DirectProvider,
19
+ type ProviderConfig
20
+ } from './registry.js'
21
+
22
+ // Export llm.do WebSocket transport
23
+ export {
24
+ LLM,
25
+ getLLM,
26
+ createLLMFetch,
27
+ type LLMConfig,
28
+ type UniversalRequest,
29
+ type UniversalCreated,
30
+ type UniversalStream,
31
+ type UniversalDone,
32
+ type UniversalError,
33
+ type GatewayMessage
34
+ } from './llm.do.js'
35
+
36
+ // Re-export AI SDK types for convenience
37
+ export type { Provider, ProviderRegistryProvider } from 'ai'
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Integration tests for ai-providers
3
+ *
4
+ * These tests make real API calls and are skipped if credentials are not available.
5
+ * Set AI_GATEWAY_URL and AI_GATEWAY_TOKEN (or individual provider keys) to run these tests.
6
+ *
7
+ * Tests cover:
8
+ * - End-to-end provider usage
9
+ * - Gateway integration
10
+ * - Model generation
11
+ * - Embedding generation
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest'
15
+ import { model, embeddingModel, createRegistry } from './index.js'
16
+
17
+ // Skip tests if no credentials configured
18
+ const hasGateway = !!(
19
+ process.env.AI_GATEWAY_URL ||
20
+ process.env.ANTHROPIC_API_KEY ||
21
+ process.env.OPENAI_API_KEY ||
22
+ process.env.OPENROUTER_API_KEY
23
+ )
24
+
25
+ const hasCloudflare = !!(
26
+ process.env.CLOUDFLARE_ACCOUNT_ID &&
27
+ process.env.CLOUDFLARE_API_TOKEN
28
+ )
29
+
30
+ describe.skipIf(!hasGateway)('model() integration', () => {
31
+ it('gets a language model from registry', async () => {
32
+ const m = await model('sonnet')
33
+ expect(m).toBeDefined()
34
+ })
35
+
36
+ it('resolves aliases to full model IDs', async () => {
37
+ const m = await model('opus')
38
+ expect(m).toBeDefined()
39
+ })
40
+
41
+ it('works with full model IDs', async () => {
42
+ const m = await model('anthropic/claude-sonnet-4.5')
43
+ expect(m).toBeDefined()
44
+ })
45
+
46
+ it('routes OpenAI models to OpenAI SDK', async () => {
47
+ try {
48
+ const m = await model('openai/gpt-4o')
49
+ expect(m).toBeDefined()
50
+ } catch (error) {
51
+ // May not be configured
52
+ expect(error).toBeDefined()
53
+ }
54
+ })
55
+
56
+ it('routes Anthropic models to Anthropic SDK', async () => {
57
+ try {
58
+ const m = await model('anthropic/claude-sonnet-4.5')
59
+ expect(m).toBeDefined()
60
+ } catch (error) {
61
+ // May not be configured
62
+ expect(error).toBeDefined()
63
+ }
64
+ })
65
+
66
+ it('routes Google models to Google SDK', async () => {
67
+ try {
68
+ const m = await model('google/gemini-2.5-flash')
69
+ expect(m).toBeDefined()
70
+ } catch (error) {
71
+ // May not be configured
72
+ expect(error).toBeDefined()
73
+ }
74
+ })
75
+
76
+ it('routes other models through OpenRouter', async () => {
77
+ try {
78
+ const m = await model('meta-llama/llama-3.3-70b-instruct')
79
+ expect(m).toBeDefined()
80
+ } catch (error) {
81
+ // May not be configured
82
+ expect(error).toBeDefined()
83
+ }
84
+ })
85
+ })
86
+
87
+ describe.skipIf(!hasGateway)('embeddingModel() integration', () => {
88
+ it('gets an OpenAI embedding model', async () => {
89
+ try {
90
+ const em = await embeddingModel('openai:text-embedding-3-small')
91
+ expect(em).toBeDefined()
92
+ expect(em.provider).toBe('openai')
93
+ } catch (error) {
94
+ // May not be configured
95
+ expect(error).toBeDefined()
96
+ }
97
+ })
98
+
99
+ it.skipIf(!hasCloudflare)('gets a Cloudflare embedding model', async () => {
100
+ const em = await embeddingModel('cloudflare:@cf/baai/bge-m3')
101
+ expect(em).toBeDefined()
102
+ expect(em.provider).toBe('cloudflare')
103
+ })
104
+ })
105
+
106
+ describe.skipIf(!hasGateway)('createRegistry() integration', () => {
107
+ it('creates registry with gateway config', async () => {
108
+ const registry = await createRegistry({
109
+ gatewayUrl: process.env.AI_GATEWAY_URL,
110
+ gatewayToken: process.env.AI_GATEWAY_TOKEN,
111
+ })
112
+
113
+ expect(registry).toBeDefined()
114
+ expect(typeof registry.languageModel).toBe('function')
115
+ expect(typeof registry.textEmbeddingModel).toBe('function')
116
+ })
117
+
118
+ it('creates registry with direct API keys', async () => {
119
+ const registry = await createRegistry({
120
+ openaiApiKey: process.env.OPENAI_API_KEY,
121
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY,
122
+ openrouterApiKey: process.env.OPENROUTER_API_KEY,
123
+ })
124
+
125
+ expect(registry).toBeDefined()
126
+ })
127
+
128
+ it('can use registry to get models', async () => {
129
+ const registry = await createRegistry()
130
+
131
+ try {
132
+ const m = registry.languageModel('openrouter:anthropic/claude-sonnet-4.5')
133
+ expect(m).toBeDefined()
134
+ } catch (error) {
135
+ // May not be configured
136
+ expect(error).toBeDefined()
137
+ }
138
+ })
139
+ })
140
+
141
+ describe.skipIf(!hasGateway)('gateway authentication', () => {
142
+ it('works with gateway stored secrets', async () => {
143
+ if (!process.env.AI_GATEWAY_URL || !process.env.AI_GATEWAY_TOKEN) {
144
+ return
145
+ }
146
+
147
+ const registry = await createRegistry({
148
+ gatewayUrl: process.env.AI_GATEWAY_URL,
149
+ gatewayToken: process.env.AI_GATEWAY_TOKEN,
150
+ // No individual API keys needed
151
+ })
152
+
153
+ expect(registry).toBeDefined()
154
+ })
155
+
156
+ it('custom fetch adds cf-aig-authorization header', async () => {
157
+ // This is tested indirectly through successful API calls with gateway
158
+ expect(true).toBe(true)
159
+ })
160
+ })
161
+
162
+ describe.skipIf(!hasCloudflare)('Cloudflare embeddings integration', () => {
163
+ it('generates embeddings via REST API', async () => {
164
+ const { cloudflareEmbedding } = await import('./providers/cloudflare.js')
165
+ const model = cloudflareEmbedding('@cf/baai/bge-m3', {
166
+ accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
167
+ apiToken: process.env.CLOUDFLARE_API_TOKEN,
168
+ })
169
+
170
+ const result = await model.doEmbed({
171
+ values: ['hello world', 'test embedding'],
172
+ })
173
+
174
+ expect(result.embeddings).toHaveLength(2)
175
+ expect(result.embeddings[0].length).toBeGreaterThan(0)
176
+ expect(Array.isArray(result.embeddings[0])).toBe(true)
177
+ })
178
+
179
+ it('supports different embedding models', async () => {
180
+ const { cloudflareEmbedding } = await import('./providers/cloudflare.js')
181
+ const models = [
182
+ '@cf/baai/bge-small-en-v1.5',
183
+ '@cf/baai/bge-base-en-v1.5',
184
+ '@cf/baai/bge-m3',
185
+ ]
186
+
187
+ for (const modelId of models) {
188
+ const model = cloudflareEmbedding(modelId, {
189
+ accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
190
+ apiToken: process.env.CLOUDFLARE_API_TOKEN,
191
+ })
192
+
193
+ const result = await model.doEmbed({
194
+ values: ['test'],
195
+ })
196
+
197
+ expect(result.embeddings).toHaveLength(1)
198
+ }
199
+ }, 30000) // Allow extra time for multiple API calls
200
+ })
201
+
202
+ describe.skipIf(!hasGateway)('provider-specific features', () => {
203
+ it.skip('supports Anthropic MCP with direct routing', async () => {
204
+ // Test MCP features when using direct Anthropic SDK routing
205
+ // Requires actual MCP implementation
206
+ expect(true).toBe(true)
207
+ })
208
+
209
+ it.skip('supports OpenAI function calling with direct routing', async () => {
210
+ // Test function calling when using direct OpenAI SDK routing
211
+ expect(true).toBe(true)
212
+ })
213
+
214
+ it.skip('supports Google grounding with direct routing', async () => {
215
+ // Test grounding when using direct Google SDK routing
216
+ expect(true).toBe(true)
217
+ })
218
+ })
219
+
220
+ describe.skipIf(!hasGateway)('error handling', () => {
221
+ it('handles invalid model IDs gracefully', async () => {
222
+ try {
223
+ await model('invalid-model-that-does-not-exist-12345')
224
+ } catch (error) {
225
+ expect(error).toBeDefined()
226
+ }
227
+ })
228
+
229
+ it('handles network errors gracefully', async () => {
230
+ const registry = await createRegistry({
231
+ baseUrls: {
232
+ openrouter: 'https://invalid-url-that-does-not-exist.example.com',
233
+ },
234
+ })
235
+
236
+ try {
237
+ const m = registry.languageModel('openrouter:test')
238
+ // Attempting to use this model should fail
239
+ expect(m).toBeDefined()
240
+ } catch (error) {
241
+ expect(error).toBeDefined()
242
+ }
243
+ })
244
+
245
+ it('handles missing credentials gracefully', async () => {
246
+ const registry = await createRegistry({
247
+ // No credentials provided
248
+ gatewayUrl: undefined,
249
+ gatewayToken: undefined,
250
+ openaiApiKey: undefined,
251
+ anthropicApiKey: undefined,
252
+ })
253
+
254
+ // Registry should be created, but using models may fail
255
+ expect(registry).toBeDefined()
256
+ })
257
+ })
258
+
259
+ describe.skipIf(!hasGateway)('concurrent requests', () => {
260
+ it('handles multiple concurrent model resolutions', async () => {
261
+ const promises = [
262
+ model('sonnet'),
263
+ model('opus'),
264
+ model('gpt-4o'),
265
+ ]
266
+
267
+ try {
268
+ const models = await Promise.all(promises)
269
+ expect(models).toHaveLength(3)
270
+ models.forEach(m => expect(m).toBeDefined())
271
+ } catch (error) {
272
+ // Some models may not be configured
273
+ expect(error).toBeDefined()
274
+ }
275
+ })
276
+
277
+ it('caches registry to avoid duplicate initialization', async () => {
278
+ const { getRegistry } = await import('./index.js')
279
+
280
+ const promises = [
281
+ getRegistry(),
282
+ getRegistry(),
283
+ getRegistry(),
284
+ ]
285
+
286
+ const registries = await Promise.all(promises)
287
+
288
+ // All should be the same instance
289
+ expect(registries[0]).toBe(registries[1])
290
+ expect(registries[1]).toBe(registries[2])
291
+ })
292
+ })
293
+
294
+ describe.skipIf(!hasGateway)('performance', () => {
295
+ it('resolves models quickly after initial setup', async () => {
296
+ const start = Date.now()
297
+ await model('sonnet')
298
+ const duration = Date.now() - start
299
+
300
+ // After registry is initialized, should be fast
301
+ // Allow up to 1 second for initialization
302
+ expect(duration).toBeLessThan(1000)
303
+ })
304
+
305
+ it('caches registry for subsequent calls', async () => {
306
+ // First call initializes
307
+ await model('sonnet')
308
+
309
+ // Second call should use cached registry
310
+ const start = Date.now()
311
+ await model('opus')
312
+ const duration = Date.now() - start
313
+
314
+ // Should be very fast (< 100ms) when cached
315
+ expect(duration).toBeLessThan(100)
316
+ })
317
+ })