import * as helpers from '@utils/helpers'; import { mockDeep } from 'jest-mock-extended'; import type { IExecuteFunctions, IBinaryData, INode } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow'; import * as audio from './actions/audio'; import * as file from './actions/file'; import * as image from './actions/image'; import * as text from './actions/text'; import * as video from './actions/video'; import * as utils from './helpers/utils'; import * as transport from './transport'; describe('GoogleGemini Node', () => { const executeFunctionsMock = mockDeep(); const apiRequestMock = jest.spyOn(transport, 'apiRequest'); const getConnectedToolsMock = jest.spyOn(helpers, 'getConnectedTools'); const downloadFileMock = jest.spyOn(utils, 'downloadFile'); const uploadFileMock = jest.spyOn(utils, 'uploadFile'); const transferFileMock = jest.spyOn(utils, 'transferFile'); beforeEach(() => { jest.clearAllMocks(); }); describe('Text -> Message', () => { it('should call the api with the correct parameters', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/gemini-2.5-flash'; case 'messages.values': return [{ role: 'user', content: 'Hello, world!' }]; case 'simplify': return true; case 'jsonOutput': return true; case 'options': return { systemMessage: 'You are a helpful assistant.', codeExecution: true, frequencyPenalty: 0, maxOutputTokens: 100, candidateCount: 1, presencePenalty: 0, temperature: 0.5, topP: 0.5, topK: 10, }; default: return undefined; } }); executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }, { type: 'ai_tool' }]); getConnectedToolsMock.mockResolvedValue([]); apiRequestMock.mockResolvedValue({ candidates: [ { content: { parts: [{ text: 'Hello, world!' }], role: 'model', }, }, ], }); const result = await text.message.execute.call(executeFunctionsMock, 0); expect(result).toEqual([ { json: { content: { parts: [{ text: 'Hello, world!' }], role: 'model', }, }, pairedItem: { item: 0 }, }, ]); expect(apiRequestMock).toHaveBeenCalledWith( 'POST', '/v1beta/models/gemini-2.5-flash:generateContent', expect.objectContaining({ body: { contents: [ { parts: [{ text: 'Hello, world!' }], role: 'user', }, ], tools: [ { codeExecution: {}, }, ], generationConfig: { candidateCount: 1, frequencyPenalty: 0, maxOutputTokens: 100, presencePenalty: 0, temperature: 0.5, topP: 0.5, topK: 10, responseMimeType: 'application/json', }, systemInstruction: { parts: [{ text: 'You are a helpful assistant.' }], }, }, }), ); }); it('should include thinking options when the thinking budget is specified', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/gemini-2.5-flash'; case 'messages.values': return [{ role: 'user', content: 'Hello, world!' }]; case 'simplify': return true; case 'jsonOutput': return false; case 'options': return { thinkingBudget: 1024, maxOutputTokens: 100, temperature: 0.5, }; default: return undefined; } }); executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]); apiRequestMock.mockResolvedValue({ candidates: [ { content: { parts: [{ text: 'Hello with thinking!' }], role: 'model', }, }, ], }); const result = await text.message.execute.call(executeFunctionsMock, 0); expect(result).toEqual([ { json: { content: { parts: [{ text: 'Hello with thinking!' }], role: 'model', }, }, pairedItem: { item: 0 }, }, ]); expect(apiRequestMock).toHaveBeenCalledWith( 'POST', '/v1beta/models/gemini-2.5-flash:generateContent', { body: { contents: [ { parts: [{ text: 'Hello, world!' }], role: 'user', }, ], tools: [], generationConfig: { maxOutputTokens: 100, temperature: 0.5, thinkingConfig: { thinkingBudget: 1024, }, }, }, }, ); }); }); describe('Audio -> Analyze', () => { it('should analyze audio from URL', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/gemini-2.5-flash'; case 'inputType': return 'url'; case 'audioUrls': return 'https://example.com/audio.mp3'; case 'text': return "What's in this audio?"; case 'simplify': return true; case 'options': return { maxOutputTokens: 300, }; default: return undefined; } }); downloadFileMock.mockResolvedValue({ fileContent: Buffer.from('test'), mimeType: 'audio/mpeg', }); uploadFileMock.mockResolvedValue({ fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'audio/mpeg', }); apiRequestMock.mockResolvedValue({ candidates: [ { content: { parts: [{ text: 'This audio contains a person speaking about AI.' }], role: 'model', }, }, ], }); const result = await audio.analyze.execute.call(executeFunctionsMock, 0); expect(result).toEqual([ { json: { content: { parts: [{ text: 'This audio contains a person speaking about AI.' }], role: 'model', }, }, pairedItem: { item: 0 }, }, ]); expect(downloadFileMock).toHaveBeenCalledWith('https://example.com/audio.mp3', 'audio/mpeg'); expect(uploadFileMock).toHaveBeenCalledWith(Buffer.from('test'), 'audio/mpeg'); expect(apiRequestMock).toHaveBeenCalledWith( 'POST', '/v1beta/models/gemini-2.5-flash:generateContent', { body: { contents: [ { parts: [ { fileData: { fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'audio/mpeg', }, }, { text: "What's in this audio?", }, ], role: 'user', }, ], generationConfig: { maxOutputTokens: 300, }, }, }, ); }); it('should analyze audio from binary data', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/gemini-2.5-flash'; case 'inputType': return 'binary'; case 'binaryPropertyName': return 'data'; case 'text': return "What's in this audio?"; case 'simplify': return true; case 'options': return { maxOutputTokens: 300, }; default: return undefined; } }); const mockBinaryData: IBinaryData = { mimeType: 'audio/mpeg', fileName: 'test.mp3', fileSize: '1024', fileExtension: 'mp3', data: 'test', }; executeFunctionsMock.helpers.assertBinaryData.mockReturnValue(mockBinaryData); executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(Buffer.from('test')); uploadFileMock.mockResolvedValue({ fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'audio/mpeg', }); apiRequestMock.mockResolvedValue({ candidates: [ { content: { parts: [{ text: 'This audio contains a person speaking about AI.' }], role: 'model', }, }, ], }); const result = await audio.analyze.execute.call(executeFunctionsMock, 0); expect(result).toEqual([ { json: { content: { parts: [{ text: 'This audio contains a person speaking about AI.' }], role: 'model', }, }, pairedItem: { item: 0 }, }, ]); expect(uploadFileMock).toHaveBeenCalledWith(Buffer.from('test'), 'audio/mpeg'); expect(apiRequestMock).toHaveBeenCalledWith( 'POST', '/v1beta/models/gemini-2.5-flash:generateContent', { body: { contents: [ { parts: [ { fileData: { fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'audio/mpeg', }, }, { text: "What's in this audio?", }, ], role: 'user', }, ], generationConfig: { maxOutputTokens: 300, }, }, }, ); }); it('should analyze audio from Google API URL', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/gemini-2.5-flash'; case 'inputType': return 'url'; case 'audioUrls': return 'https://generativelanguage.googleapis.com/v1/files/abc123'; case 'text': return "What's in this audio?"; case 'simplify': return true; case 'options': return { maxOutputTokens: 300, }; default: return undefined; } }); apiRequestMock.mockImplementation(async (method: string) => { if (method === 'GET') { return { mimeType: 'audio/mpeg' }; } return { candidates: [ { content: { parts: [{ text: 'This audio contains a person speaking about AI.' }], role: 'model', }, }, ], }; }); const result = await audio.analyze.execute.call(executeFunctionsMock, 0); expect(result).toEqual([ { json: { content: { parts: [{ text: 'This audio contains a person speaking about AI.' }], role: 'model', }, }, pairedItem: { item: 0 }, }, ]); expect(downloadFileMock).not.toHaveBeenCalled(); expect(uploadFileMock).not.toHaveBeenCalled(); expect(apiRequestMock).toHaveBeenCalledWith('GET', '', { option: { url: 'https://generativelanguage.googleapis.com/v1/files/abc123' }, }); expect(apiRequestMock).toHaveBeenCalledWith( 'POST', '/v1beta/models/gemini-2.5-flash:generateContent', { body: { contents: [ { parts: [ { fileData: { fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'audio/mpeg', }, }, { text: "What's in this audio?", }, ], role: 'user', }, ], generationConfig: { maxOutputTokens: 300, }, }, }, ); }); }); describe('Audio -> Transcribe', () => { it('should transcribe audio from URL', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/gemini-2.5-flash'; case 'inputType': return 'url'; case 'audioUrls': return 'https://example.com/audio.mp3'; case 'simplify': return true; case 'options': return { startTime: '00:15', endTime: '02:15', }; default: return undefined; } }); downloadFileMock.mockResolvedValue({ fileContent: Buffer.from('test'), mimeType: 'audio/mpeg', }); uploadFileMock.mockResolvedValue({ fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'audio/mpeg', }); apiRequestMock.mockResolvedValue({ candidates: [ { content: { parts: [{ text: 'This is the transcribed text from 00:15 to 02:15.' }], role: 'model', }, }, ], }); const result = await audio.transcribe.execute.call(executeFunctionsMock, 0); expect(result).toEqual([ { json: { content: { parts: [{ text: 'This is the transcribed text from 00:15 to 02:15.' }], role: 'model', }, }, pairedItem: { item: 0 }, }, ]); expect(downloadFileMock).toHaveBeenCalledWith('https://example.com/audio.mp3', 'audio/mpeg'); expect(uploadFileMock).toHaveBeenCalledWith(Buffer.from('test'), 'audio/mpeg'); expect(apiRequestMock).toHaveBeenCalledWith( 'POST', '/v1beta/models/gemini-2.5-flash:generateContent', { body: { contents: [ { parts: [ { fileData: { fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'audio/mpeg', }, }, { text: 'Generate a transcript of the speech from 00:15 to 02:15', }, ], role: 'user', }, ], }, }, ); }); it('should transcribe audio from binary data', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/gemini-2.5-flash'; case 'inputType': return 'binary'; case 'binaryPropertyName': return 'data'; case 'simplify': return true; case 'options': return {}; default: return undefined; } }); const mockBinaryData: IBinaryData = { mimeType: 'audio/mpeg', fileName: 'test.mp3', fileSize: '1024', fileExtension: 'mp3', data: 'test', }; executeFunctionsMock.helpers.assertBinaryData.mockReturnValue(mockBinaryData); executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(Buffer.from('test')); uploadFileMock.mockResolvedValue({ fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'audio/mpeg', }); apiRequestMock.mockResolvedValue({ candidates: [ { content: { parts: [{ text: 'This is the transcribed text.' }], role: 'model', }, }, ], }); const result = await audio.transcribe.execute.call(executeFunctionsMock, 0); expect(result).toEqual([ { json: { content: { parts: [{ text: 'This is the transcribed text.' }], role: 'model', }, }, pairedItem: { item: 0 }, }, ]); expect(uploadFileMock).toHaveBeenCalledWith(Buffer.from('test'), 'audio/mpeg'); expect(apiRequestMock).toHaveBeenCalledWith( 'POST', '/v1beta/models/gemini-2.5-flash:generateContent', { body: { contents: [ { parts: [ { fileData: { fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'audio/mpeg', }, }, { text: 'Generate a transcript of the speech', }, ], role: 'user', }, ], }, }, ); }); it('should transcribe audio from Google API URL', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/gemini-2.5-flash'; case 'inputType': return 'url'; case 'audioUrls': return 'https://generativelanguage.googleapis.com/v1/files/abc123'; case 'text': return "What's in this audio?"; case 'simplify': return true; case 'options': return { startTime: '00:15', endTime: '02:15', }; default: return undefined; } }); apiRequestMock.mockImplementation(async (method: string) => { if (method === 'GET') { return { mimeType: 'audio/mpeg' }; } return { candidates: [ { content: { parts: [{ text: 'This is the transcribed text from 00:15 to 02:15.' }], role: 'model', }, }, ], }; }); const result = await audio.transcribe.execute.call(executeFunctionsMock, 0); expect(result).toEqual([ { json: { content: { parts: [{ text: 'This is the transcribed text from 00:15 to 02:15.' }], role: 'model', }, }, pairedItem: { item: 0 }, }, ]); expect(downloadFileMock).not.toHaveBeenCalled(); expect(uploadFileMock).not.toHaveBeenCalled(); expect(apiRequestMock).toHaveBeenCalledWith('GET', '', { option: { url: 'https://generativelanguage.googleapis.com/v1/files/abc123' }, }); expect(apiRequestMock).toHaveBeenCalledWith( 'POST', '/v1beta/models/gemini-2.5-flash:generateContent', { body: { contents: [ { parts: [ { fileData: { fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'audio/mpeg', }, }, { text: 'Generate a transcript of the speech from 00:15 to 02:15', }, ], role: 'user', }, ], }, }, ); }); }); describe('File -> Upload', () => { it('should upload file from URL', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'inputType': return 'url'; case 'fileUrl': return 'https://example.com/file.pdf'; default: return undefined; } }); transferFileMock.mockResolvedValue({ fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'application/pdf', }); const result = await file.upload.execute.call(executeFunctionsMock, 0); expect(result).toEqual([ { json: { fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'application/pdf', }, pairedItem: { item: 0 }, }, ]); expect(transferFileMock).toHaveBeenCalledWith( 0, 'https://example.com/file.pdf', 'application/octet-stream', ); }); it('should upload file from binary data', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'inputType': return 'binary'; case 'binaryPropertyName': return 'data'; default: return undefined; } }); transferFileMock.mockResolvedValue({ fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'application/pdf', }); const result = await file.upload.execute.call(executeFunctionsMock, 0); expect(result).toEqual([ { json: { fileUri: 'https://generativelanguage.googleapis.com/v1/files/abc123', mimeType: 'application/pdf', }, pairedItem: { item: 0 }, }, ]); expect(transferFileMock).toHaveBeenCalledWith(0, undefined, 'application/octet-stream'); }); }); describe('Image -> Generate', () => { it('should generate image using Gemini model', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/gemini-2.0-flash-preview-image-generation'; case 'prompt': return 'A cute cat eating a dinosaur'; case 'options.binaryPropertyOutput': return 'data'; default: return undefined; } }); apiRequestMock.mockResolvedValue({ candidates: [ { content: { parts: [ { inlineData: { data: 'abcdefgh', mimeType: 'image/png', }, }, ], }, }, ], }); executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({ mimeType: 'image/png', fileName: 'image.png', fileSize: '100', data: 'abcdefgh', }); const result = await image.generate.execute.call(executeFunctionsMock, 0); expect(result).toEqual([ { binary: { data: { mimeType: 'image/png', fileName: 'image.png', fileSize: '100', data: 'abcdefgh', }, }, json: { mimeType: 'image/png', fileName: 'image.png', fileSize: '100', }, pairedItem: { item: 0 }, }, ]); expect(apiRequestMock).toHaveBeenCalledWith( 'POST', '/v1beta/models/gemini-2.0-flash-preview-image-generation:generateContent', { body: { contents: [ { role: 'user', parts: [{ text: 'A cute cat eating a dinosaur' }], }, ], generationConfig: { responseModalities: ['IMAGE', 'TEXT'], }, }, }, ); }); it('should generate multiple images using Imagen model', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/imagen-3.0-generate-002'; case 'prompt': return 'A cute cat eating a dinosaur'; case 'options.sampleCount': return 2; case 'options.binaryPropertyOutput': return 'data'; default: return undefined; } }); apiRequestMock.mockResolvedValue({ predictions: [ { bytesBase64Encoded: 'abcdefgh', mimeType: 'image/png', }, { bytesBase64Encoded: 'abcdefgh', mimeType: 'image/png', }, ], }); executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({ mimeType: 'image/png', fileName: 'image.png', fileSize: '100', data: 'abcdefgh', }); const result = await image.generate.execute.call(executeFunctionsMock, 0); expect(result).toEqual([ { binary: { data: { mimeType: 'image/png', fileName: 'image.png', fileSize: '100', data: 'abcdefgh', }, }, json: { mimeType: 'image/png', fileName: 'image.png', fileSize: '100', }, pairedItem: { item: 0 }, }, { binary: { data: { mimeType: 'image/png', fileName: 'image.png', fileSize: '100', data: 'abcdefgh', }, }, json: { mimeType: 'image/png', fileName: 'image.png', fileSize: '100', }, pairedItem: { item: 0 }, }, ]); expect(apiRequestMock).toHaveBeenCalledWith( 'POST', '/v1beta/models/imagen-3.0-generate-002:predict', { body: { instances: [ { prompt: 'A cute cat eating a dinosaur', }, ], parameters: { sampleCount: 2, }, }, }, ); }); it('should throw error for unsupported model', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/unsupported-model'; case 'prompt': return 'A cute cat eating a dinosaur'; default: return undefined; } }); executeFunctionsMock.getNode.mockReturnValue({ id: '1', name: 'Google Gemini', } as INode); await expect(image.generate.execute.call(executeFunctionsMock, 0)).rejects.toThrow( new NodeOperationError( executeFunctionsMock.getNode(), 'Model models/unsupported-model is not supported for image generation', { description: 'Please check the model ID and try again.', }, ), ); }); }); describe('Video -> Generate', () => { beforeEach(() => { jest.useFakeTimers({ advanceTimers: true }); }); afterEach(() => { jest.useRealTimers(); }); it('should generate video using Veo model', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/veo-3.0-generate-002'; case 'prompt': return 'Panning wide shot of a calico kitten sleeping in the sunshine'; case 'options': return { aspectRatio: '16:9', personGeneration: 'dont_allow', sampleCount: 1, durationSeconds: 8, }; case 'options.binaryPropertyOutput': return 'data'; case 'returnAs': return 'video'; default: return undefined; } }); executeFunctionsMock.getCredentials.mockResolvedValue({ apiKey: 'test-api-key' }); let pollCount = 0; apiRequestMock.mockImplementation(async (_method: string, path: string) => { if (path.includes(':predictLongRunning')) { return { name: 'operations/123', done: false, }; } pollCount++; return { name: 'operations/123', done: pollCount > 1, response: pollCount > 1 ? { generateVideoResponse: { generatedSamples: [ { video: { uri: 'https://example.com/video.mp4', }, }, ], }, } : undefined, }; }); downloadFileMock.mockResolvedValue({ fileContent: Buffer.from('abcdefgh'), mimeType: 'video/mp4', }); executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({ mimeType: 'video/mp4', fileName: 'video.mp4', fileSize: '1000', data: 'abcdefgh', }); const promise = video.generate.execute.call(executeFunctionsMock, 0); await jest.advanceTimersByTimeAsync(5000); await jest.advanceTimersByTimeAsync(5000); const result = await promise; expect(result).toEqual([ { binary: { data: { mimeType: 'video/mp4', fileName: 'video.mp4', fileSize: '1000', data: 'abcdefgh', }, }, json: { mimeType: 'video/mp4', fileName: 'video.mp4', fileSize: '1000', }, pairedItem: { item: 0 }, }, ]); expect(apiRequestMock).toHaveBeenCalledWith( 'POST', '/v1beta/models/veo-3.0-generate-002:predictLongRunning', { body: { instances: [ { prompt: 'Panning wide shot of a calico kitten sleeping in the sunshine', }, ], parameters: { aspectRatio: '16:9', personGeneration: 'dont_allow', sampleCount: 1, durationSeconds: 8, }, }, }, ); expect(apiRequestMock).toHaveBeenCalledWith('GET', '/v1beta/operations/123'); expect(pollCount).toBe(2); expect(downloadFileMock).toHaveBeenCalledWith('https://example.com/video.mp4', 'video/mp4', { key: 'test-api-key', }); }); it('should not pass durationSeconds if not provided', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/veo-3.0-generate-002'; case 'prompt': return 'Panning wide shot of a calico kitten sleeping in the sunshine'; case 'options': return { aspectRatio: '16:9', personGeneration: 'dont_allow', sampleCount: 1, }; case 'returnAs': return 'url'; default: return undefined; } }); executeFunctionsMock.getCredentials.mockResolvedValue({ apiKey: 'test-api-key' }); apiRequestMock.mockResolvedValue({ name: 'operations/123', done: true, response: { generateVideoResponse: { generatedSamples: [ { video: { uri: 'https://example.com/video.mp4', }, }, ], }, }, }); const result = await video.generate.execute.call(executeFunctionsMock, 0); expect(result).toEqual([ { json: { url: 'https://example.com/video.mp4', }, pairedItem: { item: 0 }, }, ]); expect(apiRequestMock).toHaveBeenCalledWith( 'POST', '/v1beta/models/veo-3.0-generate-002:predictLongRunning', { body: { instances: [ { prompt: 'Panning wide shot of a calico kitten sleeping in the sunshine', }, ], parameters: { aspectRatio: '16:9', personGeneration: 'dont_allow', sampleCount: 1, }, }, }, ); }); it('should handle errors from video generation', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/veo-3.0-generate-002'; case 'prompt': return 'Panning wide shot of a calico kitten sleeping in the sunshine'; case 'options': return {}; default: return undefined; } }); executeFunctionsMock.getCredentials.mockResolvedValue({ apiKey: 'test-api-key' }); apiRequestMock.mockImplementationOnce(async () => { return { name: 'operations/123', done: true, error: { message: 'Failed to generate video', }, }; }); executeFunctionsMock.getNode.mockReturnValue({ name: 'Google Gemini' } as INode); await expect(video.generate.execute.call(executeFunctionsMock, 0)).rejects.toThrow( new NodeOperationError(executeFunctionsMock.getNode(), 'Failed to generate video', { description: 'Error generating video', }), ); }); it('should throw error for non-Veo model', async () => { executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => { switch (parameter) { case 'modelId': return 'models/gemini-2.0-flash'; case 'prompt': return 'Panning wide shot of a calico kitten sleeping in the sunshine'; default: return undefined; } }); executeFunctionsMock.getNode.mockReturnValue({ name: 'Google Gemini' } as INode); await expect(video.generate.execute.call(executeFunctionsMock, 0)).rejects.toThrow( new NodeOperationError( executeFunctionsMock.getNode(), 'Model models/gemini-2.0-flash is not supported for video generation. Please use a Veo model', { description: 'Video generation is only supported by Veo models', }, ), ); }); }); });