@outputai/llm 0.11.1-next.da26845.0 → 0.12.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 (47) hide show
  1. package/package.json +7 -3
  2. package/src/agent.js +7 -2
  3. package/src/generate.js +1 -0
  4. package/src/index.d.ts +14 -6
  5. package/src/utils/wrap.js +35 -2
  6. package/src/agent.spec.js +0 -595
  7. package/src/ai_provider.spec.js +0 -217
  8. package/src/ai_sdk_options.spec.js +0 -390
  9. package/src/fixtures/image_response_v7_google_vertex.js +0 -72
  10. package/src/fixtures/image_response_v7_openai.js +0 -77
  11. package/src/fixtures/models_api_light.json +0 -675
  12. package/src/fixtures/models_api_v7_subset.json +0 -259
  13. package/src/fixtures/stream_response_v7_anthropic.js +0 -389
  14. package/src/fixtures/stream_response_v7_google_vertex.js +0 -462
  15. package/src/fixtures/stream_response_v7_openai.js +0 -353
  16. package/src/fixtures/stream_response_v7_perplexity.js +0 -671
  17. package/src/fixtures/text_response_v7_anthropic.js +0 -359
  18. package/src/fixtures/text_response_v7_anthropic_cache.js +0 -719
  19. package/src/fixtures/text_response_v7_google_vertex.js +0 -435
  20. package/src/fixtures/text_response_v7_openai.js +0 -347
  21. package/src/fixtures/text_response_v7_perplexity.js +0 -764
  22. package/src/generate.spec.js +0 -525
  23. package/src/prompt/content.spec.js +0 -208
  24. package/src/prompt/interpolations.spec.js +0 -109
  25. package/src/prompt/loader.full.spec.js +0 -309
  26. package/src/prompt/loader.spec.js +0 -258
  27. package/src/prompt/markup/attributes.spec.js +0 -132
  28. package/src/prompt/markup/nodes.spec.js +0 -333
  29. package/src/prompt/markup/tokenizer.spec.js +0 -149
  30. package/src/prompt/markup/tokens.spec.js +0 -250
  31. package/src/prompt/validations.spec.js +0 -812
  32. package/src/utils/cost.full.spec.js +0 -272
  33. package/src/utils/cost.spec.js +0 -290
  34. package/src/utils/error_handler.spec.js +0 -230
  35. package/src/utils/file.spec.js +0 -89
  36. package/src/utils/image.spec.js +0 -20
  37. package/src/utils/legacy_cost_attribute.spec.js +0 -278
  38. package/src/utils/models.spec.js +0 -119
  39. package/src/utils/models_pricing.spec.js +0 -162
  40. package/src/utils/skills.spec.js +0 -168
  41. package/src/utils/sources.spec.js +0 -122
  42. package/src/utils/stream.spec.js +0 -55
  43. package/src/utils/tools.spec.js +0 -167
  44. package/src/utils/usage.full.spec.js +0 -215
  45. package/src/utils/usage.spec.js +0 -246
  46. package/src/utils/wrap.spec.js +0 -467
  47. package/src/validations.spec.js +0 -631
@@ -1,525 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
-
3
- const aiFns = vi.hoisted( () => ( {
4
- generateText: vi.fn(),
5
- streamText: vi.fn(),
6
- generateImage: vi.fn()
7
- } ) );
8
-
9
- const validations = vi.hoisted( () => ( {
10
- parseGenerateTextArgs: vi.fn(),
11
- parseGenerateTextWithStreamingArgs: vi.fn(),
12
- parseStreamTextArgs: vi.fn(),
13
- parseGenerateImageArgs: vi.fn()
14
- } ) );
15
-
16
- const toPromptFileArgs = ( { prompt, ...rest } ) => ( {
17
- promptFile: prompt,
18
- ...rest
19
- } );
20
-
21
- const promptMocks = vi.hoisted( () => ( {
22
- loadPrompt: vi.fn()
23
- } ) );
24
-
25
- const skillMocks = vi.hoisted( () => ( {
26
- loadSkills: vi.fn()
27
- } ) );
28
-
29
- const optionMocks = vi.hoisted( () => ( {
30
- loadAiSdkTextOptions: vi.fn(),
31
- loadAiSdkImageOptions: vi.fn()
32
- } ) );
33
-
34
- const wrapMocks = vi.hoisted( () => ( {
35
- wrapGeneration: vi.fn(),
36
- wrapStream: vi.fn(),
37
- streamHooks: { onEndHook: vi.fn(), onErrorHook: vi.fn() }
38
- } ) );
39
-
40
- const streamMocks = vi.hoisted( () => ( {
41
- drainStream: vi.fn()
42
- } ) );
43
-
44
- vi.mock( 'ai', () => aiFns );
45
-
46
- vi.mock( './validations.js', () => validations );
47
-
48
- vi.mock( './prompt/loader.js', () => ( {
49
- loadPrompt: ( ...args ) => promptMocks.loadPrompt( ...args )
50
- } ) );
51
-
52
- vi.mock( './utils/skills.js', () => ( {
53
- loadSkills: ( ...args ) => skillMocks.loadSkills( ...args )
54
- } ) );
55
-
56
- vi.mock( './ai_sdk_options.js', () => ( {
57
- loadAiSdkTextOptions: ( ...args ) => optionMocks.loadAiSdkTextOptions( ...args ),
58
- loadAiSdkImageOptions: ( ...args ) => optionMocks.loadAiSdkImageOptions( ...args )
59
- } ) );
60
-
61
- vi.mock( './utils/wrap.js', () => ( {
62
- wrapGeneration: ( ...args ) => wrapMocks.wrapGeneration( ...args ),
63
- wrapStream: ( ...args ) => wrapMocks.wrapStream( ...args )
64
- } ) );
65
-
66
- vi.mock( './utils/stream.js', () => ( {
67
- drainStream: ( ...args ) => streamMocks.drainStream( ...args )
68
- } ) );
69
-
70
- const importSut = async () => import( './generate.js' );
71
-
72
- const loadedPrompt = {
73
- name: 'test@v1',
74
- config: { provider: 'openai', model: 'test-model' },
75
- messages: [ { role: 'user', content: 'Hello' } ]
76
- };
77
-
78
- const loadedSkills = [ { name: 'writer', description: 'Writes', instructions: 'Do it.' } ];
79
-
80
- const textOptions = {
81
- model: 'MODEL',
82
- messages: loadedPrompt.messages,
83
- providerOptions: { test: true }
84
- };
85
-
86
- const textResponse = {
87
- text: 'TEXT',
88
- usage: { inputTokens: 1, outputTokens: 2 },
89
- finishReason: 'stop'
90
- };
91
-
92
- const streamResult = {
93
- textStream: 'TEXT_STREAM',
94
- stream: 'STREAM'
95
- };
96
-
97
- const imageOptions = {
98
- model: 'IMAGE_MODEL',
99
- prompt: {
100
- text: 'Generate an image'
101
- },
102
- providerOptions: { openai: { quality: 'high' } }
103
- };
104
-
105
- const imageResponse = {
106
- images: [ { mediaType: 'image/png', base64: 'aW1hZ2U=' } ],
107
- usage: { inputTokens: 1, outputTokens: 2 }
108
- };
109
-
110
- describe( 'generate', () => {
111
- beforeEach( () => {
112
- aiFns.generateText.mockReset().mockResolvedValue( textResponse );
113
- aiFns.streamText.mockReset().mockReturnValue( streamResult );
114
- aiFns.generateImage.mockReset().mockResolvedValue( imageResponse );
115
-
116
- validations.parseGenerateTextArgs.mockReset().mockImplementation( toPromptFileArgs );
117
- validations.parseGenerateTextWithStreamingArgs.mockReset().mockImplementation( toPromptFileArgs );
118
- validations.parseStreamTextArgs.mockReset().mockImplementation( toPromptFileArgs );
119
- validations.parseGenerateImageArgs.mockReset().mockImplementation( toPromptFileArgs );
120
-
121
- promptMocks.loadPrompt.mockReset().mockReturnValue( loadedPrompt );
122
- skillMocks.loadSkills.mockReset().mockReturnValue( loadedSkills );
123
-
124
- optionMocks.loadAiSdkTextOptions.mockReset().mockReturnValue( textOptions );
125
- optionMocks.loadAiSdkImageOptions.mockReset().mockReturnValue( imageOptions );
126
-
127
- wrapMocks.wrapGeneration.mockReset().mockImplementation( async ( { fn } ) => fn() );
128
- wrapMocks.streamHooks = {
129
- onEndHook: vi.fn( async ( response, callback ) => callback?.( response ) ),
130
- onErrorHook: vi.fn( ( event, callback ) => callback?.( event.error ) )
131
- };
132
- wrapMocks.wrapStream.mockReset().mockImplementation( ( { fn } ) => fn( wrapMocks.streamHooks ) );
133
-
134
- streamMocks.drainStream.mockReset().mockResolvedValue( undefined );
135
- } );
136
-
137
- afterEach( async () => {
138
- await vi.resetModules();
139
- } );
140
-
141
- describe( 'generateText', () => {
142
- it( 'parses args, loads prompt skills, wraps generation, and calls AI SDK', async () => {
143
- const { generateText } = await importSut();
144
- const variables = { topic: 'testing' };
145
- const tools = { userTool: true };
146
- const output = { type: 'object' };
147
- const toolChoice = 'required';
148
- const stopWhen = { type: 'custom-stop' };
149
- const abortSignal = new AbortController().signal;
150
-
151
- const result = await generateText( {
152
- prompt: 'test@v1',
153
- variables,
154
- promptDir: '/prompts',
155
- tools,
156
- output,
157
- toolChoice,
158
- stopWhen,
159
- abortSignal
160
- } );
161
-
162
- expect( validations.parseGenerateTextArgs ).toHaveBeenCalledWith( {
163
- prompt: 'test@v1',
164
- variables,
165
- promptDir: '/prompts',
166
- tools,
167
- output,
168
- toolChoice,
169
- stopWhen,
170
- abortSignal
171
- } );
172
- expect( promptMocks.loadPrompt ).toHaveBeenCalledWith( 'test@v1', variables, '/prompts' );
173
- expect( skillMocks.loadSkills ).toHaveBeenCalledWith( loadedPrompt );
174
- expect( wrapMocks.wrapGeneration ).toHaveBeenCalledWith( {
175
- name: 'generateText',
176
- prompt: loadedPrompt,
177
- fn: expect.any( Function )
178
- } );
179
- expect( optionMocks.loadAiSdkTextOptions ).toHaveBeenCalledWith( {
180
- prompt: loadedPrompt,
181
- skills: loadedSkills,
182
- tools,
183
- output,
184
- toolChoice,
185
- stopWhen,
186
- abortSignal
187
- } );
188
- expect( aiFns.generateText ).toHaveBeenCalledWith( textOptions );
189
- expect( result ).toBe( textResponse );
190
- } );
191
-
192
- it( 'uses a prompt object without loading a prompt file', async () => {
193
- validations.parseGenerateTextArgs.mockReturnValueOnce( { promptObject: loadedPrompt } );
194
- const { generateText } = await importSut();
195
-
196
- await generateText( { prompt: loadedPrompt } );
197
-
198
- expect( promptMocks.loadPrompt ).not.toHaveBeenCalled();
199
- expect( skillMocks.loadSkills ).toHaveBeenCalledWith( loadedPrompt );
200
- expect( optionMocks.loadAiSdkTextOptions ).toHaveBeenCalledWith( {
201
- prompt: loadedPrompt,
202
- skills: loadedSkills
203
- } );
204
- } );
205
-
206
- it( 'propagates parse errors before wrapping or calling AI SDK', async () => {
207
- const validationError = new Error( 'Invalid args' );
208
- validations.parseGenerateTextArgs.mockImplementationOnce( () => {
209
- throw validationError;
210
- } );
211
- const { generateText } = await importSut();
212
-
213
- await expect( generateText( { prompt: '' } ) ).rejects.toThrow( validationError );
214
- expect( promptMocks.loadPrompt ).not.toHaveBeenCalled();
215
- expect( wrapMocks.wrapGeneration ).not.toHaveBeenCalled();
216
- expect( aiFns.generateText ).not.toHaveBeenCalled();
217
- } );
218
- } );
219
-
220
- describe( 'generateTextWithStreaming', () => {
221
- it( 'consumes the stream inside wrapGeneration and returns the completed response', async () => {
222
- const { generateTextWithStreaming } = await importSut();
223
- const variables = { topic: 'testing' };
224
- const output = { summary: 'Structured result' };
225
- const chunk = { type: 'text-delta', text: 'TEXT' };
226
- const onChunk = vi.fn();
227
- const stream = { output: Promise.resolve( output ) };
228
- const response = { ...textResponse };
229
- aiFns.streamText.mockImplementationOnce( options => {
230
- options.onChunk( { chunk } );
231
- options.onEnd( response );
232
- return stream;
233
- } );
234
-
235
- const result = await generateTextWithStreaming( {
236
- prompt: 'test@v1',
237
- variables,
238
- promptDir: '/prompts',
239
- onChunk
240
- } );
241
-
242
- expect( validations.parseGenerateTextWithStreamingArgs ).toHaveBeenCalledWith( {
243
- prompt: 'test@v1',
244
- variables,
245
- promptDir: '/prompts',
246
- onChunk
247
- } );
248
- expect( wrapMocks.wrapGeneration ).toHaveBeenCalledWith( {
249
- name: 'generateTextWithStreaming',
250
- prompt: loadedPrompt,
251
- fn: expect.any( Function )
252
- } );
253
- expect( optionMocks.loadAiSdkTextOptions ).toHaveBeenCalledWith( {
254
- prompt: loadedPrompt,
255
- skills: loadedSkills
256
- } );
257
- expect( aiFns.streamText ).toHaveBeenCalledWith( {
258
- ...textOptions,
259
- onChunk,
260
- onEnd: expect.any( Function ),
261
- onError: expect.any( Function )
262
- } );
263
- expect( streamMocks.drainStream ).toHaveBeenCalledWith( stream, undefined );
264
- expect( onChunk ).toHaveBeenCalledWith( { chunk } );
265
- expect( result ).toEqual( { ...textResponse, output } );
266
- } );
267
-
268
- it( 'uses a prompt object without loading a prompt file', async () => {
269
- const stream = { output: Promise.resolve( undefined ) };
270
- validations.parseGenerateTextWithStreamingArgs.mockReturnValueOnce( { promptObject: loadedPrompt } );
271
- aiFns.streamText.mockImplementationOnce( options => {
272
- options.onEnd( { ...textResponse } );
273
- return stream;
274
- } );
275
- const { generateTextWithStreaming } = await importSut();
276
-
277
- await generateTextWithStreaming( { prompt: loadedPrompt } );
278
-
279
- expect( promptMocks.loadPrompt ).not.toHaveBeenCalled();
280
- expect( skillMocks.loadSkills ).toHaveBeenCalledWith( loadedPrompt );
281
- expect( optionMocks.loadAiSdkTextOptions ).toHaveBeenCalledWith( {
282
- prompt: loadedPrompt,
283
- skills: loadedSkills
284
- } );
285
- } );
286
-
287
- it( 'omits onChunk when the caller does not provide it', async () => {
288
- const { generateTextWithStreaming } = await importSut();
289
- const stream = { output: Promise.resolve( undefined ) };
290
- aiFns.streamText.mockImplementationOnce( options => {
291
- options.onEnd( { ...textResponse } );
292
- return stream;
293
- } );
294
-
295
- await generateTextWithStreaming( { prompt: 'test@v1' } );
296
- const callOptions = aiFns.streamText.mock.calls[0][0];
297
-
298
- expect( callOptions ).not.toHaveProperty( 'onChunk' );
299
- } );
300
-
301
- it( 'rejects with the abort reason', async () => {
302
- const abortController = new AbortController();
303
- const abortReason = new Error( 'Cancelled by caller' );
304
- const stream = { output: Promise.resolve( undefined ) };
305
- abortController.abort( abortReason );
306
- streamMocks.drainStream.mockRejectedValueOnce( abortReason );
307
- aiFns.streamText.mockReturnValueOnce( stream );
308
- const { generateTextWithStreaming } = await importSut();
309
-
310
- await expect( generateTextWithStreaming( {
311
- prompt: 'test@v1',
312
- abortSignal: abortController.signal
313
- } ) ).rejects.toBe( abortReason );
314
-
315
- expect( optionMocks.loadAiSdkTextOptions ).toHaveBeenCalledWith( {
316
- prompt: loadedPrompt,
317
- skills: loadedSkills,
318
- abortSignal: abortController.signal
319
- } );
320
- expect( streamMocks.drainStream ).toHaveBeenCalledWith( stream, abortController.signal );
321
- } );
322
- } );
323
-
324
- describe( 'streamText', () => {
325
- it( 'parses args, loads prompt skills, wraps the stream, and calls AI SDK', async () => {
326
- const { streamText } = await importSut();
327
- const variables = { topic: 'testing' };
328
- const onEnd = vi.fn();
329
- const onChunk = vi.fn();
330
- const tools = { userTool: true };
331
-
332
- const result = streamText( {
333
- prompt: 'test@v1',
334
- variables,
335
- promptDir: '/prompts',
336
- onEnd,
337
- onChunk,
338
- tools
339
- } );
340
-
341
- expect( validations.parseStreamTextArgs ).toHaveBeenCalledWith( {
342
- prompt: 'test@v1',
343
- variables,
344
- promptDir: '/prompts',
345
- onEnd,
346
- onChunk,
347
- tools
348
- } );
349
- expect( promptMocks.loadPrompt ).toHaveBeenCalledWith( 'test@v1', variables, '/prompts' );
350
- expect( skillMocks.loadSkills ).toHaveBeenCalledWith( loadedPrompt );
351
- expect( wrapMocks.wrapStream ).toHaveBeenCalledWith( {
352
- name: 'streamText',
353
- prompt: loadedPrompt,
354
- fn: expect.any( Function )
355
- } );
356
- expect( optionMocks.loadAiSdkTextOptions ).toHaveBeenCalledWith( {
357
- prompt: loadedPrompt,
358
- skills: loadedSkills,
359
- tools
360
- } );
361
- expect( aiFns.streamText ).toHaveBeenCalledWith( {
362
- ...textOptions,
363
- onChunk,
364
- onEnd: expect.any( Function ),
365
- onError: expect.any( Function )
366
- } );
367
- const callOptions = aiFns.streamText.mock.calls[0][0];
368
- await callOptions.onEnd( textResponse );
369
- expect( wrapMocks.streamHooks.onEndHook ).toHaveBeenCalledWith( textResponse, onEnd );
370
- expect( onEnd ).toHaveBeenCalledWith( textResponse );
371
- expect( result ).toBe( streamResult );
372
- } );
373
-
374
- it( 'uses a prompt object without loading a prompt file', async () => {
375
- validations.parseStreamTextArgs.mockReturnValueOnce( { promptObject: loadedPrompt } );
376
- const { streamText } = await importSut();
377
-
378
- streamText( { prompt: loadedPrompt } );
379
-
380
- expect( promptMocks.loadPrompt ).not.toHaveBeenCalled();
381
- expect( skillMocks.loadSkills ).toHaveBeenCalledWith( loadedPrompt );
382
- expect( optionMocks.loadAiSdkTextOptions ).toHaveBeenCalledWith( {
383
- prompt: loadedPrompt,
384
- skills: loadedSkills
385
- } );
386
- } );
387
-
388
- it( 'omits onChunk when the caller does not provide it', async () => {
389
- const { streamText } = await importSut();
390
-
391
- streamText( { prompt: 'test@v1' } );
392
- const callOptions = aiFns.streamText.mock.calls[0][0];
393
-
394
- expect( callOptions ).not.toHaveProperty( 'onChunk' );
395
- } );
396
-
397
- it( 'lets caller stopWhen reach the options loader', async () => {
398
- const { streamText } = await importSut();
399
- const stopWhen = { type: 'custom-stop' };
400
-
401
- streamText( { prompt: 'test@v1', stopWhen } );
402
-
403
- expect( optionMocks.loadAiSdkTextOptions ).toHaveBeenCalledWith( {
404
- prompt: loadedPrompt,
405
- skills: loadedSkills,
406
- stopWhen
407
- } );
408
- expect( aiFns.streamText ).toHaveBeenCalledWith( expect.objectContaining( textOptions ) );
409
- } );
410
-
411
- it( 'forwards stream onError through wrapStream with the mapped event payload', async () => {
412
- const { streamText } = await importSut();
413
- const onError = vi.fn();
414
- const error = new Error( 'Stream failed' );
415
- const mappedError = new Error( 'Mapped stream failed' );
416
-
417
- streamText( { prompt: 'test@v1', onError } );
418
- const callOptions = aiFns.streamText.mock.calls[0][0];
419
- callOptions.onError( { error, extra: true } );
420
-
421
- expect( wrapMocks.streamHooks.onErrorHook ).toHaveBeenCalledWith(
422
- { error, extra: true },
423
- expect.any( Function )
424
- );
425
- wrapMocks.streamHooks.onErrorHook.mock.calls[0][1]( mappedError );
426
- expect( onError ).toHaveBeenCalledWith( { error: mappedError, extra: true } );
427
- } );
428
-
429
- it( 'propagates validation errors before loading or wrapping', async () => {
430
- const validationError = new Error( 'Invalid args' );
431
- validations.parseStreamTextArgs.mockImplementationOnce( () => {
432
- throw validationError;
433
- } );
434
- const { streamText } = await importSut();
435
-
436
- expect( () => streamText( { prompt: '' } ) ).toThrow( validationError );
437
- expect( promptMocks.loadPrompt ).not.toHaveBeenCalled();
438
- expect( skillMocks.loadSkills ).not.toHaveBeenCalled();
439
- expect( wrapMocks.wrapStream ).not.toHaveBeenCalled();
440
- expect( aiFns.streamText ).not.toHaveBeenCalled();
441
- } );
442
- } );
443
-
444
- describe( 'generateImage', () => {
445
- it( 'parses args, loads prompt, wraps generation, and calls AI SDK', async () => {
446
- const { generateImage } = await importSut();
447
- const variables = { scene: 'race cars' };
448
- const images = [ Buffer.from( 'image-bytes' ) ];
449
- const mask = Buffer.from( 'mask-bytes' );
450
- const abortSignal = AbortSignal.abort();
451
-
452
- const result = await generateImage( {
453
- prompt: 'image@v1',
454
- variables,
455
- promptDir: '/prompts',
456
- images,
457
- mask,
458
- abortSignal
459
- } );
460
-
461
- expect( validations.parseGenerateImageArgs ).toHaveBeenCalledWith( {
462
- prompt: 'image@v1',
463
- variables,
464
- promptDir: '/prompts',
465
- images,
466
- mask,
467
- abortSignal
468
- } );
469
- expect( promptMocks.loadPrompt ).toHaveBeenCalledWith( 'image@v1', variables, '/prompts' );
470
- expect( skillMocks.loadSkills ).not.toHaveBeenCalled();
471
- expect( wrapMocks.wrapGeneration ).toHaveBeenCalledWith( {
472
- name: 'generateImage',
473
- prompt: loadedPrompt,
474
- fn: expect.any( Function )
475
- } );
476
- expect( optionMocks.loadAiSdkImageOptions ).toHaveBeenCalledWith( {
477
- prompt: loadedPrompt,
478
- images,
479
- mask,
480
- abortSignal
481
- } );
482
- expect( aiFns.generateImage ).toHaveBeenCalledWith( imageOptions );
483
- expect( result ).toBe( imageResponse );
484
- } );
485
-
486
- it( 'uses a prompt object without loading a prompt file', async () => {
487
- validations.parseGenerateImageArgs.mockReturnValueOnce( { promptObject: loadedPrompt } );
488
- const { generateImage } = await importSut();
489
-
490
- await generateImage( { prompt: loadedPrompt } );
491
-
492
- expect( promptMocks.loadPrompt ).not.toHaveBeenCalled();
493
- expect( optionMocks.loadAiSdkImageOptions ).toHaveBeenCalledWith( {
494
- prompt: loadedPrompt
495
- } );
496
- } );
497
-
498
- it( 'supports text-to-image calls without images or mask', async () => {
499
- const { generateImage } = await importSut();
500
-
501
- await generateImage( { prompt: 'image@v1' } );
502
-
503
- expect( validations.parseGenerateImageArgs ).toHaveBeenCalledWith( {
504
- prompt: 'image@v1'
505
- } );
506
- expect( optionMocks.loadAiSdkImageOptions ).toHaveBeenCalledWith( {
507
- prompt: loadedPrompt
508
- } );
509
- expect( aiFns.generateImage ).toHaveBeenCalledWith( imageOptions );
510
- } );
511
-
512
- it( 'propagates parse errors before loading or wrapping', async () => {
513
- const validationError = new Error( 'Invalid image args' );
514
- validations.parseGenerateImageArgs.mockImplementationOnce( () => {
515
- throw validationError;
516
- } );
517
- const { generateImage } = await importSut();
518
-
519
- await expect( generateImage( { prompt: '' } ) ).rejects.toThrow( validationError );
520
- expect( promptMocks.loadPrompt ).not.toHaveBeenCalled();
521
- expect( wrapMocks.wrapGeneration ).not.toHaveBeenCalled();
522
- expect( aiFns.generateImage ).not.toHaveBeenCalled();
523
- } );
524
- } );
525
- } );