@@ -28,9 +28,9 @@ describe('Video provider client', () => {
2828 isValid : true ,
2929 resolvedIP : '203.0.113.1' ,
3030 } )
31- inputValidationMockFns . mockSecureFetchWithPinnedIP . mockResolvedValue (
32- new Response ( Buffer . from ( 'video' ) )
33- )
31+ inputValidationMockFns . mockSecureFetchWithPinnedIP
32+ . mockReset ( )
33+ . mockResolvedValue ( new Response ( Buffer . from ( 'video' ) ) )
3434 } )
3535
3636 afterEach ( ( ) => {
@@ -204,23 +204,6 @@ describe('Video provider client', () => {
204204 } ,
205205 } ,
206206 } ,
207- {
208- provider : 'veo' as const ,
209- model : 'gemini-omni-1.1-flash' ,
210- submit : '/v1beta/interactions' ,
211- poll : '/v1beta/interactions/job' ,
212- payload : { model : 'gemini-omni-1.1-flash' , input : 'A cinematic sunrise' } ,
213- created : { id : 'job' , status : 'in_progress' } ,
214- completed : {
215- status : 'completed' ,
216- steps : [
217- {
218- type : 'model_output' ,
219- content : [ { type : 'video' , mime_type : 'video/mp4' , data : 'dmlkZW8=' } ] ,
220- } ,
221- ] ,
222- } ,
223- } ,
224207 ] ) ( 'executes $model through its native submit, poll and result protocol' , async ( testCase ) => {
225208 let submissions = 0
226209 vi . stubGlobal ( 'fetch' , async ( url : string | URL , init ?: RequestInit ) => {
@@ -251,6 +234,156 @@ describe('Video provider client', () => {
251234 expect ( submissions ) . toBe ( 1 )
252235 } )
253236
237+ describe ( 'Gemini URI delivery' , ( ) => {
238+ const input = { provider : 'veo' as const , apiKey : 'key' , prompt : 'A cinematic sunrise' }
239+ const videoUri =
240+ 'https://generativelanguage.googleapis.com/v1beta/files/video-file:download?alt=media'
241+ const completed = ( uri = videoUri ) => ( {
242+ id : 'interaction-1' ,
243+ status : 'completed' ,
244+ steps : [ { type : 'model_output' , content : [ { type : 'video' , uri } ] } ] ,
245+ } )
246+
247+ it ( 'downloads a normal-sized video after Files becomes ACTIVE without reading inline polling data' , async ( ) => {
248+ const video = Buffer . alloc ( 2 * 1024 * 1024 )
249+ let filePolls = 0
250+ let submissions = 0
251+ vi . stubGlobal ( 'fetch' , async ( url : string | URL , init ?: RequestInit ) => {
252+ const path = new URL ( url ) . pathname
253+ if ( path === '/v1beta/interactions' && init ?. method === 'POST' ) {
254+ submissions ++
255+ const body = JSON . parse ( String ( init . body ) )
256+ return jsonResponse (
257+ body . background ? { id : 'interaction-1' , status : 'in_progress' } : completed ( )
258+ )
259+ }
260+ if ( path === '/v1beta/interactions/interaction-1' )
261+ return jsonResponse ( {
262+ ...completed ( ) ,
263+ steps : [
264+ {
265+ type : 'model_output' ,
266+ content : [ { type : 'video' , data : video . toString ( 'base64' ) } ] ,
267+ } ,
268+ ] ,
269+ } )
270+ if ( path === '/v1beta/files/video-file' ) {
271+ filePolls ++
272+ expect ( new Headers ( init ?. headers ) . get ( 'x-goog-api-key' ) ) . toBe ( 'key' )
273+ return jsonResponse ( { state : filePolls === 1 ? 'PROCESSING' : 'ACTIVE' } )
274+ }
275+ return new Response ( null , { status : 404 } )
276+ } )
277+ inputValidationMockFns . mockSecureFetchWithPinnedIP . mockImplementationOnce (
278+ async ( _url , _ip , options ) => {
279+ expect ( new Headers ( options . headers ) . get ( 'x-goog-api-key' ) ) . toBe ( 'key' )
280+ return filePolls >= 2
281+ ? new Response ( video )
282+ : new Response ( 'File is not ACTIVE' , { status : 409 } )
283+ }
284+ )
285+ const result = generateVideo ( input , { requestId : 'google-uri' } )
286+ const completion = result . then ( ( output ) => {
287+ expect ( output . buffer . equals ( video ) ) . toBe ( true )
288+ expect ( output . jobId ) . toBe ( 'interaction-1' )
289+ } )
290+ await Promise . all ( [
291+ completion ,
292+ vi . advanceTimersByTimeAsync ( 0 ) . then ( ( ) => vi . advanceTimersByTimeAsync ( 5000 ) ) ,
293+ ] )
294+ expect ( submissions ) . toBe ( 1 )
295+ } )
296+
297+ it . each ( [
298+ { state : 'FAILED' , error : / G o o g l e v i d e o p r o c e s s i n g f a i l e d / } ,
299+ { state : 'PROCESSING' , error : / G o o g l e v i d e o p r o c e s s i n g t i m e d o u t / } ,
300+ ] ) ( 'does not download a file that remains $state' , async ( { state, error } ) => {
301+ vi . stubGlobal (
302+ 'fetch' ,
303+ vi
304+ . fn ( )
305+ . mockResolvedValueOnce ( jsonResponse ( completed ( ) ) )
306+ . mockImplementation ( ( ) => Promise . resolve ( jsonResponse ( { state } ) ) )
307+ )
308+ const rejection = expect (
309+ generateVideo ( input , { requestId : 'google-processing' } )
310+ ) . rejects . toThrow ( error )
311+ await Promise . all ( [ rejection , vi . advanceTimersByTimeAsync ( 5000 ) ] )
312+ expect ( inputValidationMockFns . mockSecureFetchWithPinnedIP ) . not . toHaveBeenCalled ( )
313+ } )
314+
315+ it ( 'stops during Files processing without downloading or polling again' , async ( ) => {
316+ const controller = new AbortController ( )
317+ const fetchMock = vi
318+ . fn ( )
319+ . mockResolvedValueOnce ( jsonResponse ( completed ( ) ) )
320+ . mockResolvedValueOnce ( jsonResponse ( { state : 'PROCESSING' } ) )
321+ vi . stubGlobal ( 'fetch' , fetchMock )
322+ const result = generateVideo ( input , {
323+ requestId : 'google-stopped' ,
324+ signal : controller . signal ,
325+ } )
326+ const rejection = expect ( result ) . rejects . toMatchObject ( { name : 'AbortError' } )
327+ await Promise . all ( [
328+ rejection ,
329+ vi . advanceTimersByTimeAsync ( 0 ) . then ( ( ) => {
330+ controller . abort ( new DOMException ( 'Stopped' , 'AbortError' ) )
331+ } ) ,
332+ ] )
333+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 2 )
334+ expect ( inputValidationMockFns . mockSecureFetchWithPinnedIP ) . not . toHaveBeenCalled ( )
335+ } )
336+
337+ it . each ( [
338+ 'https://foreign.example/v1beta/files/video-file:download?alt=media' ,
339+ 'https://generativelanguage.googleapis.com/v1beta/models/model:download?alt=media' ,
340+ 'https://generativelanguage.googleapis.com/v1beta/files/nested%2Ffile:download?alt=media' ,
341+ ] ) ( 'rejects a non-Files download URI before sending credentials: %s' , async ( uri ) => {
342+ const fetchMock = vi . fn ( ) . mockResolvedValueOnce ( jsonResponse ( completed ( uri ) ) )
343+ vi . stubGlobal ( 'fetch' , fetchMock )
344+ await expect ( generateVideo ( input , { requestId : 'google-invalid-uri' } ) ) . rejects . toThrow (
345+ / I n v a l i d G o o g l e v i d e o f i l e U R I /
346+ )
347+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 )
348+ expect ( inputValidationMockFns . mockSecureFetchWithPinnedIP ) . not . toHaveBeenCalled ( )
349+ } )
350+
351+ it ( 'keeps Files metadata within the ordinary JSON response limit' , async ( ) => {
352+ vi . stubGlobal (
353+ 'fetch' ,
354+ vi
355+ . fn ( )
356+ . mockResolvedValueOnce ( jsonResponse ( completed ( ) ) )
357+ . mockResolvedValueOnce (
358+ jsonResponse (
359+ { state : 'ACTIVE' } ,
360+ { headers : { 'Content-Length' : String ( 2 * 1024 * 1024 + 1 ) } }
361+ )
362+ )
363+ )
364+ await expect (
365+ generateVideo ( input , { requestId : 'google-metadata-limit' } )
366+ ) . rejects . toMatchObject ( {
367+ name : 'PayloadSizeLimitError' ,
368+ } )
369+ expect ( inputValidationMockFns . mockSecureFetchWithPinnedIP ) . not . toHaveBeenCalled ( )
370+ } )
371+
372+ it ( 'does not poll a full interaction when synchronous generation does not complete' , async ( ) => {
373+ const fetchMock = vi
374+ . fn ( )
375+ . mockImplementation ( ( ) =>
376+ Promise . resolve ( jsonResponse ( { id : 'interaction-1' , status : 'in_progress' } ) )
377+ )
378+ vi . stubGlobal ( 'fetch' , fetchMock )
379+ const rejection = expect (
380+ generateVideo ( input , { requestId : 'google-incomplete' } )
381+ ) . rejects . toThrow ( / G o o g l e s y n c h r o n o u s v i d e o g e n e r a t i o n d i d n o t c o m p l e t e / )
382+ await Promise . all ( [ rejection , vi . advanceTimersByTimeAsync ( 5000 ) ] )
383+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 )
384+ } )
385+ } )
386+
254387 it ( 'reports a failed MiniMax job immediately instead of polling until timeout' , async ( ) => {
255388 const fetchMock = vi
256389 . fn ( )
0 commit comments