ai-props 2.1.3 → 2.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.
Files changed (73) hide show
  1. package/.dev.vars +2 -0
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +2 -0
  4. package/package.json +39 -13
  5. package/src/ai.ts +12 -31
  6. package/src/cascade.ts +795 -0
  7. package/src/client.ts +440 -0
  8. package/src/durable-cascade.ts +743 -0
  9. package/src/event-bridge.ts +478 -0
  10. package/src/generate.ts +14 -12
  11. package/src/hoc.ts +15 -19
  12. package/src/hono-jsx.ts +675 -0
  13. package/src/index.ts +30 -0
  14. package/src/mdx-types.ts +169 -0
  15. package/src/mdx-utils.ts +437 -0
  16. package/src/mdx.ts +1008 -0
  17. package/src/rpc.ts +614 -0
  18. package/src/streaming.ts +618 -0
  19. package/src/validate.ts +15 -29
  20. package/src/worker.ts +547 -0
  21. package/test/cascade.test.ts +338 -0
  22. package/test/durable-cascade.test.ts +319 -0
  23. package/test/event-bridge.test.ts +351 -0
  24. package/test/generate.test.ts +6 -16
  25. package/test/mdx.test.ts +817 -0
  26. package/test/worker/capnweb-rpc.test.ts +1084 -0
  27. package/test/worker/full-flow.integration.test.ts +1463 -0
  28. package/test/worker/hono-jsx.test.ts +1258 -0
  29. package/test/worker/mdx-parsing.test.ts +1148 -0
  30. package/test/worker/setup.ts +56 -0
  31. package/test/worker.test.ts +595 -0
  32. package/tsconfig.json +2 -1
  33. package/vitest.config.js +6 -0
  34. package/vitest.config.ts +15 -1
  35. package/vitest.workers.config.ts +58 -0
  36. package/wrangler.jsonc +27 -0
  37. package/.turbo/turbo-build.log +0 -4
  38. package/LICENSE +0 -21
  39. package/dist/ai.d.ts +0 -125
  40. package/dist/ai.d.ts.map +0 -1
  41. package/dist/ai.js +0 -199
  42. package/dist/ai.js.map +0 -1
  43. package/dist/cache.d.ts +0 -66
  44. package/dist/cache.d.ts.map +0 -1
  45. package/dist/cache.js +0 -183
  46. package/dist/cache.js.map +0 -1
  47. package/dist/generate.d.ts +0 -69
  48. package/dist/generate.d.ts.map +0 -1
  49. package/dist/generate.js +0 -221
  50. package/dist/generate.js.map +0 -1
  51. package/dist/hoc.d.ts +0 -164
  52. package/dist/hoc.d.ts.map +0 -1
  53. package/dist/hoc.js +0 -236
  54. package/dist/hoc.js.map +0 -1
  55. package/dist/index.d.ts +0 -15
  56. package/dist/index.d.ts.map +0 -1
  57. package/dist/index.js +0 -21
  58. package/dist/index.js.map +0 -1
  59. package/dist/types.d.ts +0 -152
  60. package/dist/types.d.ts.map +0 -1
  61. package/dist/types.js +0 -7
  62. package/dist/types.js.map +0 -1
  63. package/dist/validate.d.ts +0 -58
  64. package/dist/validate.d.ts.map +0 -1
  65. package/dist/validate.js +0 -253
  66. package/dist/validate.js.map +0 -1
  67. package/src/ai.js +0 -198
  68. package/src/cache.js +0 -182
  69. package/src/generate.js +0 -220
  70. package/src/hoc.js +0 -235
  71. package/src/index.js +0 -20
  72. package/src/types.js +0 -6
  73. package/src/validate.js +0 -252
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Test setup for worker tests
3
+ *
4
+ * Configures the AI providers registry with gateway credentials
5
+ * from environment bindings before tests run.
6
+ */
7
+
8
+ import { env } from 'cloudflare:test'
9
+ import { beforeAll } from 'vitest'
10
+
11
+ // Populate process.env from worker bindings
12
+ // This is needed because ai-providers uses process.env which isn't
13
+ // automatically populated from worker bindings in miniflare
14
+ const typedEnv = env as Record<string, string>
15
+ const envKeys = ['AI_GATEWAY_URL', 'AI_GATEWAY_TOKEN', 'AWS_BEARER_TOKEN_BEDROCK']
16
+
17
+ // Try multiple methods to set process.env
18
+ for (const key of envKeys) {
19
+ if (typedEnv[key]) {
20
+ try {
21
+ // Method 1: Direct assignment (may be blocked by readonly)
22
+ // @ts-expect-error - process.env may be readonly
23
+ process.env[key] = typedEnv[key]
24
+ } catch {
25
+ // Method 2: Use Object.defineProperty
26
+ try {
27
+ Object.defineProperty(process.env, key, {
28
+ value: typedEnv[key],
29
+ writable: true,
30
+ configurable: true,
31
+ enumerable: true,
32
+ })
33
+ } catch {
34
+ // If both methods fail, log a warning
35
+ console.warn(`Could not set process.env.${key}`)
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ // Configure the AI providers registry with gateway credentials
42
+ // This must be done AFTER setting process.env but BEFORE any AI calls
43
+ beforeAll(async () => {
44
+ if (typedEnv.AI_GATEWAY_URL && typedEnv.AI_GATEWAY_TOKEN) {
45
+ try {
46
+ // Dynamically import ai-providers and configure it
47
+ const aiProviders = await import('ai-providers')
48
+ await aiProviders.configureRegistry({
49
+ gatewayUrl: typedEnv.AI_GATEWAY_URL,
50
+ gatewayToken: typedEnv.AI_GATEWAY_TOKEN,
51
+ })
52
+ } catch (error) {
53
+ console.error('[setup] Failed to configure AI providers registry:', error)
54
+ }
55
+ }
56
+ })
@@ -0,0 +1,595 @@
1
+ /**
2
+ * Worker Export Tests for ai-props
3
+ *
4
+ * Tests for the PropsService WorkerEntrypoint and PropsServiceCore RpcTarget.
5
+ * Validates AI props generation, caching, configuration, and validation methods.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
9
+
10
+ // Mock cloudflare:workers module (not available outside Cloudflare Workers runtime)
11
+ vi.mock('cloudflare:workers', () => {
12
+ class RpcTarget {}
13
+ class WorkerEntrypoint<T = unknown> {}
14
+ return { RpcTarget, WorkerEntrypoint }
15
+ })
16
+
17
+ // Mock the ai-functions generateObject before importing worker
18
+ vi.mock('ai-functions', () => ({
19
+ generateObject: vi.fn().mockImplementation(async ({ schema }) => {
20
+ // Generate mock data based on schema
21
+ const mockData: Record<string, unknown> = {}
22
+ for (const [key, value] of Object.entries(schema)) {
23
+ if (typeof value === 'string') {
24
+ if (value.includes('(number)')) {
25
+ mockData[key] = 42
26
+ } else if (value.includes('(boolean)')) {
27
+ mockData[key] = true
28
+ } else {
29
+ mockData[key] = `generated-${key}`
30
+ }
31
+ } else if (Array.isArray(value)) {
32
+ mockData[key] = ['item1', 'item2']
33
+ } else if (typeof value === 'object') {
34
+ mockData[key] = { nested: 'value' }
35
+ }
36
+ }
37
+ return { object: mockData }
38
+ }),
39
+ schema: vi.fn((s) => s),
40
+ }))
41
+
42
+ import { PropsService, PropsServiceCore } from '../src/worker.js'
43
+ import { resetConfig, clearCache } from '../src/index.js'
44
+
45
+ describe('PropsServiceCore (RpcTarget)', () => {
46
+ let service: PropsServiceCore
47
+
48
+ beforeEach(() => {
49
+ service = new PropsServiceCore()
50
+ resetConfig()
51
+ clearCache()
52
+ vi.clearAllMocks()
53
+ })
54
+
55
+ describe('constructor', () => {
56
+ it('creates a new PropsServiceCore instance', () => {
57
+ expect(service).toBeInstanceOf(PropsServiceCore)
58
+ })
59
+
60
+ it('extends RpcTarget for RPC communication', () => {
61
+ expect(service.constructor.name).toBe('PropsServiceCore')
62
+ })
63
+ })
64
+
65
+ describe('generation operations', () => {
66
+ describe('generate()', () => {
67
+ it('generates props from schema', async () => {
68
+ const result = await service.generate({
69
+ schema: {
70
+ title: 'Page title',
71
+ description: 'Page description',
72
+ },
73
+ })
74
+
75
+ expect(result.props).toBeDefined()
76
+ expect(result.props.title).toBe('generated-title')
77
+ expect(result.props.description).toBe('generated-description')
78
+ })
79
+
80
+ it('returns cached flag', async () => {
81
+ const result = await service.generate({
82
+ schema: { name: 'User name' },
83
+ })
84
+
85
+ expect(result.cached).toBe(false)
86
+ })
87
+
88
+ it('returns metadata with model info', async () => {
89
+ const result = await service.generate({
90
+ schema: { name: 'User name' },
91
+ })
92
+
93
+ expect(result.metadata).toBeDefined()
94
+ expect(result.metadata?.model).toBe('anthropic/claude-sonnet-4.5')
95
+ })
96
+
97
+ it('uses custom model when specified', async () => {
98
+ const result = await service.generate({
99
+ schema: { name: 'User name' },
100
+ model: 'gpt-4',
101
+ })
102
+
103
+ expect(result.metadata?.model).toBe('gpt-4')
104
+ })
105
+
106
+ it('caches results for subsequent calls', async () => {
107
+ const schema = { name: 'User name' }
108
+
109
+ // First call
110
+ await service.generate({ schema })
111
+
112
+ // Second call should be cached
113
+ const result = await service.generate({ schema })
114
+
115
+ expect(result.cached).toBe(true)
116
+ })
117
+ })
118
+
119
+ describe('getSync()', () => {
120
+ it('returns cached props synchronously', async () => {
121
+ const schema = { name: 'User name' }
122
+
123
+ // Pre-populate cache
124
+ await service.generate({ schema })
125
+
126
+ // Get synchronously
127
+ const props = service.getSync(schema)
128
+
129
+ expect(props.name).toBe('generated-name')
130
+ })
131
+
132
+ it('throws when not cached', () => {
133
+ expect(() => {
134
+ service.getSync({ name: 'User name' })
135
+ }).toThrow('Props not in cache')
136
+ })
137
+ })
138
+
139
+ describe('prefetch()', () => {
140
+ it('prefetches multiple schemas', async () => {
141
+ await service.prefetch([
142
+ { schema: { name: 'User name' } },
143
+ { schema: { title: 'Page title' } },
144
+ ])
145
+
146
+ // Both should be cached now
147
+ const name = service.getSync({ name: 'User name' })
148
+ const title = service.getSync({ title: 'Page title' })
149
+
150
+ expect(name.name).toBe('generated-name')
151
+ expect(title.title).toBe('generated-title')
152
+ })
153
+ })
154
+
155
+ describe('generateMany()', () => {
156
+ it('generates multiple prop sets in parallel', async () => {
157
+ const results = await service.generateMany([
158
+ { schema: { name: 'User name' } },
159
+ { schema: { title: 'Page title' } },
160
+ ])
161
+
162
+ expect(results).toHaveLength(2)
163
+ expect(results[0]?.props.name).toBe('generated-name')
164
+ expect(results[1]?.props.title).toBe('generated-title')
165
+ })
166
+ })
167
+
168
+ describe('mergeWithGenerated()', () => {
169
+ it('generates only missing props', async () => {
170
+ const result = await service.mergeWithGenerated(
171
+ { name: 'User name', email: 'Email address' },
172
+ { name: 'John Doe' }
173
+ )
174
+
175
+ expect(result.name).toBe('John Doe') // Preserved
176
+ expect(result.email).toBe('generated-email') // Generated
177
+ })
178
+
179
+ it('returns as-is when all props provided', async () => {
180
+ const result = await service.mergeWithGenerated(
181
+ { name: 'User name', email: 'Email address' },
182
+ { name: 'John Doe', email: 'john@example.com' }
183
+ )
184
+
185
+ expect(result.name).toBe('John Doe')
186
+ expect(result.email).toBe('john@example.com')
187
+ })
188
+ })
189
+ })
190
+
191
+ describe('configuration operations', () => {
192
+ describe('configure()', () => {
193
+ it('sets model configuration', () => {
194
+ service.configure({ model: 'gpt-4' })
195
+ expect(service.getConfig().model).toBe('gpt-4')
196
+ })
197
+
198
+ it('sets cache configuration', () => {
199
+ service.configure({ cache: false })
200
+ expect(service.getConfig().cache).toBe(false)
201
+ })
202
+
203
+ it('merges with existing config', () => {
204
+ service.configure({ model: 'gpt-4' })
205
+ service.configure({ cache: false })
206
+
207
+ const config = service.getConfig()
208
+ expect(config.model).toBe('gpt-4')
209
+ expect(config.cache).toBe(false)
210
+ })
211
+ })
212
+
213
+ describe('getConfig()', () => {
214
+ it('returns default config', () => {
215
+ const config = service.getConfig()
216
+
217
+ expect(config.model).toBe('anthropic/claude-sonnet-4.5')
218
+ expect(config.cache).toBe(true)
219
+ expect(config.cacheTTL).toBe(5 * 60 * 1000)
220
+ })
221
+ })
222
+
223
+ describe('resetConfig()', () => {
224
+ it('resets to default values', () => {
225
+ service.configure({ model: 'gpt-4', cache: false })
226
+ service.resetConfig()
227
+
228
+ const config = service.getConfig()
229
+ expect(config.model).toBe('anthropic/claude-sonnet-4.5')
230
+ expect(config.cache).toBe(true)
231
+ })
232
+ })
233
+ })
234
+
235
+ describe('cache operations', () => {
236
+ describe('setCached() and getCached()', () => {
237
+ it('stores and retrieves props by key', () => {
238
+ service.setCached('test-key', { name: 'Test' })
239
+
240
+ const entry = service.getCached('test-key')
241
+
242
+ expect(entry).toBeDefined()
243
+ expect(entry?.props).toEqual({ name: 'Test' })
244
+ })
245
+
246
+ it('returns undefined for non-existent key', () => {
247
+ const entry = service.getCached('non-existent')
248
+
249
+ expect(entry).toBeUndefined()
250
+ })
251
+ })
252
+
253
+ describe('deleteCached()', () => {
254
+ it('removes cached entry', () => {
255
+ service.setCached('delete-key', { name: 'Test' })
256
+
257
+ const result = service.deleteCached('delete-key')
258
+
259
+ expect(result).toBe(true)
260
+ expect(service.getCached('delete-key')).toBeUndefined()
261
+ })
262
+
263
+ it('returns false for non-existent key', () => {
264
+ const result = service.deleteCached('non-existent')
265
+
266
+ expect(result).toBe(false)
267
+ })
268
+ })
269
+
270
+ describe('clearCache()', () => {
271
+ it('removes all cached entries', () => {
272
+ service.setCached('key1', { a: 1 })
273
+ service.setCached('key2', { b: 2 })
274
+
275
+ service.clearCache()
276
+
277
+ expect(service.getCacheSize()).toBe(0)
278
+ })
279
+ })
280
+
281
+ describe('getCacheSize()', () => {
282
+ it('returns number of cached entries', () => {
283
+ service.setCached('key1', { a: 1 })
284
+ service.setCached('key2', { b: 2 })
285
+
286
+ expect(service.getCacheSize()).toBe(2)
287
+ })
288
+ })
289
+
290
+ describe('createCacheKey()', () => {
291
+ it('creates deterministic key from schema', () => {
292
+ const key1 = service.createCacheKey({ name: 'User name' })
293
+ const key2 = service.createCacheKey({ name: 'User name' })
294
+
295
+ expect(key1).toBe(key2)
296
+ })
297
+
298
+ it('creates different keys for different schemas', () => {
299
+ const key1 = service.createCacheKey({ name: 'User name' })
300
+ const key2 = service.createCacheKey({ title: 'Page title' })
301
+
302
+ expect(key1).not.toBe(key2)
303
+ })
304
+
305
+ it('includes context in key', () => {
306
+ const key1 = service.createCacheKey({ name: 'User name' }, { id: '1' })
307
+ const key2 = service.createCacheKey({ name: 'User name' }, { id: '2' })
308
+
309
+ expect(key1).not.toBe(key2)
310
+ })
311
+ })
312
+
313
+ describe('configureCache()', () => {
314
+ it('sets cache TTL', () => {
315
+ service.configureCache(10000)
316
+
317
+ // This should create a new cache with the specified TTL
318
+ expect(service.getCacheSize()).toBe(0)
319
+ })
320
+ })
321
+ })
322
+
323
+ describe('validation operations', () => {
324
+ describe('validate()', () => {
325
+ it('validates props against schema', () => {
326
+ const result = service.validate(
327
+ { name: 'John', age: 25 },
328
+ { name: 'User name', age: 'Age (number)' }
329
+ )
330
+
331
+ expect(result.valid).toBe(true)
332
+ expect(result.errors).toHaveLength(0)
333
+ })
334
+
335
+ it('returns errors for invalid props', () => {
336
+ const result = service.validate(
337
+ { name: 'John', age: 'not a number' },
338
+ { name: 'User name', age: 'Age (number)' }
339
+ )
340
+
341
+ expect(result.valid).toBe(false)
342
+ expect(result.errors.length).toBeGreaterThan(0)
343
+ })
344
+ })
345
+
346
+ describe('hasRequired()', () => {
347
+ it('returns true when all required props present', () => {
348
+ const result = service.hasRequired({ name: 'John', email: 'john@example.com' }, [
349
+ 'name',
350
+ 'email',
351
+ ])
352
+
353
+ expect(result).toBe(true)
354
+ })
355
+
356
+ it('returns false when required props missing', () => {
357
+ const result = service.hasRequired({ name: 'John' }, ['name', 'email'])
358
+
359
+ expect(result).toBe(false)
360
+ })
361
+ })
362
+
363
+ describe('getMissing()', () => {
364
+ it('returns list of missing props', () => {
365
+ const missing = service.getMissing(
366
+ { name: 'John' },
367
+ { name: 'User name', email: 'Email address', age: 'Age' }
368
+ )
369
+
370
+ expect(missing).toContain('email')
371
+ expect(missing).toContain('age')
372
+ expect(missing).not.toContain('name')
373
+ })
374
+ })
375
+
376
+ describe('isComplete()', () => {
377
+ it('returns true when all schema props present', () => {
378
+ const result = service.isComplete(
379
+ { name: 'John', email: 'john@example.com' },
380
+ { name: 'User name', email: 'Email address' }
381
+ )
382
+
383
+ expect(result).toBe(true)
384
+ })
385
+
386
+ it('returns false when props missing', () => {
387
+ const result = service.isComplete(
388
+ { name: 'John' },
389
+ { name: 'User name', email: 'Email address' }
390
+ )
391
+
392
+ expect(result).toBe(false)
393
+ })
394
+ })
395
+
396
+ describe('sanitize()', () => {
397
+ it('removes extra keys not in schema', () => {
398
+ const sanitized = service.sanitize(
399
+ { name: 'John', extra: 'value', unknown: 123 },
400
+ { name: 'User name' }
401
+ )
402
+
403
+ expect(sanitized).toEqual({ name: 'John' })
404
+ expect(sanitized).not.toHaveProperty('extra')
405
+ expect(sanitized).not.toHaveProperty('unknown')
406
+ })
407
+ })
408
+
409
+ describe('mergeDefaults()', () => {
410
+ it('merges props with defaults', () => {
411
+ const result = service.mergeDefaults(
412
+ { name: 'John' },
413
+ { name: 'Default', email: 'default@example.com' },
414
+ { name: 'User name', email: 'Email address' }
415
+ )
416
+
417
+ expect(result.name).toBe('John')
418
+ expect(result.email).toBe('default@example.com')
419
+ })
420
+
421
+ it('preserves provided props over defaults', () => {
422
+ const result = service.mergeDefaults(
423
+ { name: 'John', email: 'john@example.com' },
424
+ { name: 'Default', email: 'default@example.com' },
425
+ { name: 'User name', email: 'Email address' }
426
+ )
427
+
428
+ expect(result.name).toBe('John')
429
+ expect(result.email).toBe('john@example.com')
430
+ })
431
+ })
432
+ })
433
+ })
434
+
435
+ describe('PropsService (WorkerEntrypoint)', () => {
436
+ describe('class definition', () => {
437
+ it('exports PropsService class', async () => {
438
+ const { default: PropsServiceClass } = await import('../src/worker.js')
439
+ expect(PropsServiceClass).toBeDefined()
440
+ expect(typeof PropsServiceClass).toBe('function')
441
+ })
442
+
443
+ it('PropsService has connect method in prototype', () => {
444
+ expect(typeof PropsService.prototype.connect).toBe('function')
445
+ })
446
+
447
+ it('is named PropsService', () => {
448
+ expect(PropsService.name).toBe('PropsService')
449
+ })
450
+ })
451
+
452
+ describe('connect()', () => {
453
+ // Note: WorkerEntrypoint classes cannot be instantiated directly in tests.
454
+ // They require the Cloudflare Workers runtime context.
455
+ // We verify the connect method behavior by testing that:
456
+ // 1. The method exists on the prototype
457
+ // 2. The return type (PropsServiceCore) is properly constructable and functional
458
+
459
+ it('returns a PropsServiceCore instance', () => {
460
+ // Since we can't instantiate PropsService directly (requires Workers runtime),
461
+ // we verify that PropsServiceCore (the return type of connect()) works correctly
462
+ const core = new PropsServiceCore()
463
+ expect(core).toBeInstanceOf(PropsServiceCore)
464
+ })
465
+
466
+ it('returns RpcTarget for RPC communication', () => {
467
+ // Test that PropsServiceCore (what connect() returns) has all required methods
468
+ const core = new PropsServiceCore()
469
+
470
+ expect(core).toBeDefined()
471
+ expect(typeof core.generate).toBe('function')
472
+ expect(typeof core.getSync).toBe('function')
473
+ expect(typeof core.prefetch).toBe('function')
474
+ expect(typeof core.generateMany).toBe('function')
475
+ expect(typeof core.mergeWithGenerated).toBe('function')
476
+ expect(typeof core.configure).toBe('function')
477
+ expect(typeof core.getConfig).toBe('function')
478
+ expect(typeof core.resetConfig).toBe('function')
479
+ expect(typeof core.getCached).toBe('function')
480
+ expect(typeof core.setCached).toBe('function')
481
+ expect(typeof core.deleteCached).toBe('function')
482
+ expect(typeof core.clearCache).toBe('function')
483
+ expect(typeof core.getCacheSize).toBe('function')
484
+ expect(typeof core.createCacheKey).toBe('function')
485
+ expect(typeof core.configureCache).toBe('function')
486
+ expect(typeof core.validate).toBe('function')
487
+ expect(typeof core.hasRequired).toBe('function')
488
+ expect(typeof core.getMissing).toBe('function')
489
+ expect(typeof core.isComplete).toBe('function')
490
+ expect(typeof core.sanitize).toBe('function')
491
+ expect(typeof core.mergeDefaults).toBe('function')
492
+ })
493
+
494
+ it('creates independent service instances', () => {
495
+ // Each PropsServiceCore instance should be independent
496
+ const core1 = new PropsServiceCore()
497
+ const core2 = new PropsServiceCore()
498
+
499
+ // Configure core1
500
+ core1.configure({ model: 'gpt-4' })
501
+
502
+ // Each instance should be independent
503
+ expect(core1).not.toBe(core2)
504
+ // Note: config is global, so this will affect both
505
+ // In production, each worker instance would have its own state
506
+ })
507
+ })
508
+ })
509
+
510
+ describe('Integration: Real Props Generation', () => {
511
+ let service: PropsServiceCore
512
+
513
+ beforeEach(() => {
514
+ service = new PropsServiceCore()
515
+ resetConfig()
516
+ clearCache()
517
+ vi.clearAllMocks()
518
+ })
519
+
520
+ it('generates props and validates them', async () => {
521
+ const schema = {
522
+ title: 'Page title',
523
+ description: 'Page description',
524
+ published: 'Is published (boolean)',
525
+ }
526
+
527
+ // Generate props
528
+ const result = await service.generate({ schema })
529
+
530
+ expect(result.props).toBeDefined()
531
+
532
+ // Validate generated props
533
+ const validation = service.validate(result.props, schema)
534
+
535
+ expect(validation.valid).toBe(true)
536
+ })
537
+
538
+ it('generates partial props and merges with provided', async () => {
539
+ const schema = {
540
+ name: 'User name',
541
+ email: 'Email address',
542
+ bio: 'User bio',
543
+ }
544
+
545
+ // Generate only missing props
546
+ const result = await service.mergeWithGenerated(schema, {
547
+ name: 'John Doe',
548
+ email: 'john@example.com',
549
+ })
550
+
551
+ expect(result.name).toBe('John Doe')
552
+ expect(result.email).toBe('john@example.com')
553
+ expect(result.bio).toBe('generated-bio')
554
+ })
555
+
556
+ it('prefetches and retrieves props synchronously', async () => {
557
+ const schemas = [
558
+ { schema: { productName: 'Product name' } },
559
+ { schema: { categoryName: 'Category name' } },
560
+ ]
561
+
562
+ // Prefetch all
563
+ await service.prefetch(schemas)
564
+
565
+ // Retrieve synchronously
566
+ const product = service.getSync({ productName: 'Product name' })
567
+ const category = service.getSync({ categoryName: 'Category name' })
568
+
569
+ expect(product.productName).toBe('generated-productName')
570
+ expect(category.categoryName).toBe('generated-categoryName')
571
+ })
572
+
573
+ it('sanitizes and merges props with defaults', async () => {
574
+ const schema = {
575
+ name: 'User name',
576
+ role: 'User role',
577
+ }
578
+
579
+ const defaults = {
580
+ name: 'Anonymous',
581
+ role: 'viewer',
582
+ }
583
+
584
+ // Sanitize user input (remove extra fields)
585
+ const userInput = { name: 'John', extra: 'ignored', role: 'admin' }
586
+ const sanitized = service.sanitize(userInput, schema)
587
+
588
+ // Merge with defaults
589
+ const result = service.mergeDefaults(sanitized, defaults, schema)
590
+
591
+ expect(result.name).toBe('John')
592
+ expect(result.role).toBe('admin')
593
+ expect(result).not.toHaveProperty('extra')
594
+ })
595
+ })
package/tsconfig.json CHANGED
@@ -2,7 +2,8 @@
2
2
  "extends": "../../tsconfig.base.json",
3
3
  "compilerOptions": {
4
4
  "rootDir": "src",
5
- "outDir": "dist"
5
+ "outDir": "dist",
6
+ "types": ["@cloudflare/workers-types", "node"]
6
7
  },
7
8
  "include": ["src/**/*"],
8
9
  "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
package/vitest.config.js CHANGED
@@ -1,6 +1,12 @@
1
1
  import { defineConfig } from 'vitest/config';
2
2
  export default defineConfig({
3
3
  test: {
4
+ // CRITICAL: Limit concurrency to prevent resource exhaustion
5
+ maxConcurrency: 1,
6
+ maxWorkers: 1,
7
+ minWorkers: 1,
8
+ fileParallelism: false,
9
+
4
10
  globals: true,
5
11
  environment: 'node',
6
12
  include: ['test/**/*.test.ts'],
package/vitest.config.ts CHANGED
@@ -2,14 +2,28 @@ import { defineConfig } from 'vitest/config'
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
5
+ // CRITICAL: Limit concurrency to prevent resource exhaustion
6
+ maxConcurrency: 1,
7
+ maxWorkers: 1,
8
+ minWorkers: 1,
9
+ fileParallelism: false,
10
+
5
11
  globals: true,
6
12
  environment: 'node',
7
13
  include: ['test/**/*.test.ts'],
14
+ // Exclude worker tests that require cloudflare:test module
15
+ exclude: ['test/worker/**/*.test.ts', '**/node_modules/**'],
8
16
  coverage: {
9
17
  provider: 'v8',
10
18
  reporter: ['text', 'json', 'html'],
11
19
  include: ['src/**/*.ts'],
12
- exclude: ['src/**/*.d.ts'],
20
+ exclude: ['src/**/*.d.ts', '**/*.test.ts', '**/__tests__/**'],
21
+ thresholds: {
22
+ statements: 65,
23
+ branches: 60,
24
+ functions: 60,
25
+ lines: 65,
26
+ },
13
27
  },
14
28
  },
15
29
  })