Skip to main content
Glama
geminiClient.test.ts32.3 kB
import type { GenerativeModel } from '@google/genai' import { type MockedFunction, beforeEach, describe, expect, it, vi } from 'vitest' import type { Config } from '../../utils/config' import { GeminiAPIError, NetworkError } from '../../utils/errors' import { createGeminiClient } from '../geminiClient' // Mock @google/genai vi.mock('@google/genai', () => ({ GoogleGenAI: vi.fn(), })) // Mock the Gemini client instance structure const mockGeminiClientInstance = { models: { generateContent: vi.fn(), }, } const { GoogleGenAI } = await import('@google/genai') const mockGoogleGenAI = vi.mocked(GoogleGenAI) as MockedFunction<typeof GoogleGenAI> describe('geminiClient', () => { const testConfig: Config = { geminiApiKey: 'test-api-key-12345', imageOutputDir: './output', apiTimeout: 30000, } beforeEach(() => { vi.clearAllMocks() mockGoogleGenAI.mockReturnValue(mockGeminiClientInstance as any) }) describe('createGeminiClient', () => { it('should create client with correct model configuration', () => { // Act const result = createGeminiClient(testConfig) // Assert expect(result.success).toBe(true) expect(mockGoogleGenAI).toHaveBeenCalledWith({ apiKey: testConfig.geminiApiKey }) }) it('should return error when API key is invalid', () => { // Arrange mockGoogleGenAI.mockImplementation(() => { throw new Error('Invalid API key') }) // Act const result = createGeminiClient(testConfig) // Assert expect(result.success).toBe(false) if (!result.success) { expect(result.error).toBeInstanceOf(GeminiAPIError) expect(result.error.message).toContain('Failed to initialize Gemini client') } }) }) describe('GeminiClient.generateImage', () => { it('should generate image successfully with text prompt only', async () => { // Arrange const mockResponse = { response: { candidates: [ { content: { parts: [ { inlineData: { data: 'base64-image-data', mimeType: 'image/png', }, }, ], }, }, ], }, } mockGeminiClientInstance.models.generateContent = vi.fn().mockResolvedValue(mockResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate a beautiful landscape', }) // Assert expect(result.success).toBe(true) if (result.success) { expect(result.data.imageData).toBeInstanceOf(Buffer) expect(result.data.metadata.model).toBe('gemini-3-pro-image-preview') expect(result.data.metadata.prompt).toBe('Generate a beautiful landscape') expect(result.data.metadata.mimeType).toBe('image/png') } }) it('should generate image successfully with input image and prompt', async () => { // Arrange const mockResponse = { response: { candidates: [ { content: { parts: [ { inlineData: { data: 'base64-enhanced-image-data', mimeType: 'image/jpeg', }, }, ], }, }, ], }, } mockGeminiClientInstance.models.generateContent = vi.fn().mockResolvedValue(mockResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data const inputImageBuffer = Buffer.from('fake-input-image-data') const inputImageBase64 = inputImageBuffer.toString('base64') // Act const result = await client.generateImage({ prompt: 'Enhance this image', inputImage: inputImageBase64, }) // Assert expect(result.success).toBe(true) if (result.success) { expect(result.data.imageData).toBeInstanceOf(Buffer) expect(result.data.metadata.model).toBe('gemini-3-pro-image-preview') expect(result.data.metadata.prompt).toBe('Enhance this image') expect(result.data.metadata.mimeType).toBe('image/jpeg') } expect(mockGeminiClientInstance.models.generateContent).toHaveBeenCalledWith({ model: 'gemini-3-pro-image-preview', contents: [ { parts: [ { inlineData: { data: inputImageBase64, mimeType: 'image/jpeg', }, }, { text: 'Enhance this image', }, ], }, ], config: { responseModalities: ['IMAGE'], }, }) }) it('should return GeminiAPIError when API returns error', async () => { // Arrange const apiError = new Error('API quota exceeded') mockGeminiClientInstance.models.generateContent = vi.fn().mockRejectedValue(apiError) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate image', }) // Assert expect(result.success).toBe(false) if (!result.success) { expect(result.error).toBeInstanceOf(GeminiAPIError) expect(result.error.message).toContain('Failed to generate image') expect(result.error.message).toContain('API quota exceeded') } }) it('should return NetworkError for network-related failures', async () => { // Arrange const networkError = new Error('ECONNRESET') as Error & { code: string } networkError.code = 'ECONNRESET' mockGeminiClientInstance.models.generateContent = vi.fn().mockRejectedValue(networkError) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate image', }) // Assert expect(result.success).toBe(false) if (!result.success) { expect(result.error).toBeInstanceOf(NetworkError) expect(result.error.message).toContain('Network error') } }) it('should return GeminiAPIError when response is malformed', async () => { // Arrange const mockMalformedResponse = { response: { candidates: [], // Empty candidates }, } mockGeminiClientInstance.models.generateContent = vi .fn() .mockResolvedValue(mockMalformedResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate image', }) // Assert expect(result.success).toBe(false) if (!result.success) { expect(result.error).toBeInstanceOf(GeminiAPIError) expect(result.error.message).toContain('No image generated') } }) it('should handle prompt feedback blocking with safety reasons', async () => { // Arrange const mockBlockedResponse = { response: { promptFeedback: { blockReason: 'SAFETY', blockReasonMessage: 'The prompt was blocked due to safety reasons', safetyRatings: [ { category: 'HARM_CATEGORY_VIOLENCE', probability: 'HIGH', blocked: true, }, ], }, candidates: [], }, } mockGeminiClientInstance.models.generateContent = vi .fn() .mockResolvedValue(mockBlockedResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate violent content', }) // Assert expect(result.success).toBe(false) if (!result.success) { expect(result.error).toBeInstanceOf(GeminiAPIError) expect(result.error.message).toContain('Image generation blocked') expect(result.error.message).toContain('safety reasons') expect(result.error.suggestion).toContain('Rephrase your prompt') expect(result.error.context).toMatchObject({ blockReason: 'SAFETY', stage: 'prompt_analysis', }) } }) it('should handle finish reason SAFETY with detailed information', async () => { // Arrange const mockSafetyStoppedResponse = { response: { candidates: [ { content: { parts: [], // No image parts due to safety stop }, finishReason: 'IMAGE_SAFETY', safetyRatings: [ { category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', probability: 'HIGH', blocked: true, }, { category: 'HARM_CATEGORY_VIOLENCE', probability: 'MEDIUM', blocked: false, }, ], }, ], }, } mockGeminiClientInstance.models.generateContent = vi .fn() .mockResolvedValue(mockSafetyStoppedResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate inappropriate image', }) // Assert expect(result.success).toBe(false) if (!result.success) { expect(result.error).toBeInstanceOf(GeminiAPIError) expect(result.error.message).toContain('Image generation stopped') expect(result.error.message).toContain('safety reasons') expect(result.error.suggestion).toContain('Modify your prompt') expect(result.error.context).toMatchObject({ finishReason: 'IMAGE_SAFETY', stage: 'generation_stopped', }) // Safety ratings should be formatted expect(result.error.context?.safetyRatings).toContain('Sexually Explicit (BLOCKED)') } }) it('should handle finish reason MAX_TOKENS', async () => { // Arrange const mockMaxTokensResponse = { response: { candidates: [ { content: { parts: [], // No image due to token limit }, finishReason: 'MAX_TOKENS', }, ], }, } mockGeminiClientInstance.models.generateContent = vi .fn() .mockResolvedValue(mockMaxTokensResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate extremely complex scene with many details', }) // Assert expect(result.success).toBe(false) if (!result.success) { expect(result.error).toBeInstanceOf(GeminiAPIError) expect(result.error.message).toContain('Maximum token limit reached') expect(result.error.suggestion).toContain('shorter or simpler prompt') expect(result.error.context).toMatchObject({ finishReason: 'MAX_TOKENS', stage: 'generation_stopped', }) } }) it('should handle prohibited content blocking', async () => { // Arrange const mockProhibitedResponse = { response: { promptFeedback: { blockReason: 'PROHIBITED_CONTENT', blockReasonMessage: 'The prompt contains prohibited content', }, candidates: [], }, } mockGeminiClientInstance.models.generateContent = vi .fn() .mockResolvedValue(mockProhibitedResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate prohibited content', }) // Assert expect(result.success).toBe(false) if (!result.success) { expect(result.error).toBeInstanceOf(GeminiAPIError) expect(result.error.message).toContain('prohibited content') expect(result.error.suggestion).toContain('Remove any prohibited content') expect(result.error.context).toMatchObject({ blockReason: 'PROHIBITED_CONTENT', stage: 'prompt_analysis', }) } }) it('should generate image with feature parameters (without processing)', async () => { // Arrange const mockResponse = { response: { candidates: [ { content: { parts: [ { inlineData: { data: 'base64-enhanced-image-data', mimeType: 'image/png', }, }, ], }, }, ], }, } mockGeminiClientInstance.models.generateContent = vi.fn().mockResolvedValue(mockResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act - feature parameters are passed but not processed by GeminiClient const result = await client.generateImage({ prompt: 'Generate character with blending', blendImages: true, maintainCharacterConsistency: true, useWorldKnowledge: false, }) // Assert expect(result.success).toBe(true) if (result.success) { expect(result.data.imageData).toBeInstanceOf(Buffer) expect(result.data.metadata.model).toBe('gemini-3-pro-image-preview') // Features are passed to the API but not stored in metadata expect(result.data.metadata.prompt).toBe('Generate character with blending') } // Verify API was called with original prompt (no enhancement at GeminiClient level) expect(mockGeminiClientInstance.models.generateContent).toHaveBeenCalledWith({ model: 'gemini-3-pro-image-preview', contents: [ { parts: [ { text: 'Generate character with blending', }, ], }, ], config: { responseModalities: ['IMAGE'], }, }) }) it('should generate image with some features enabled (parameters tracked only)', async () => { // Arrange const mockResponse = { response: { candidates: [ { content: { parts: [ { inlineData: { data: 'base64-world-knowledge-image', mimeType: 'image/webp', }, }, ], }, }, ], }, } mockGeminiClientInstance.models.generateContent = vi.fn().mockResolvedValue(mockResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate factually accurate historical scene', useWorldKnowledge: true, }) // Assert expect(result.success).toBe(true) if (result.success) { // Features are passed to the API but not stored in metadata expect(result.data.metadata.prompt).toBe('Generate factually accurate historical scene') expect(result.data.metadata.model).toBe('gemini-3-pro-image-preview') } // Verify API was called with original prompt (no processing at GeminiClient level) expect(mockGeminiClientInstance.models.generateContent).toHaveBeenCalledWith({ model: 'gemini-3-pro-image-preview', contents: [ { parts: [ { text: 'Generate factually accurate historical scene', }, ], }, ], config: { responseModalities: ['IMAGE'], }, }) }) it('should generate image without new features when not specified', async () => { // Arrange const mockResponse = { response: { candidates: [ { content: { parts: [ { inlineData: { data: 'base64-standard-image', mimeType: 'image/png', }, }, ], }, }, ], }, } mockGeminiClientInstance.models.generateContent = vi.fn().mockResolvedValue(mockResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate simple landscape', }) // Assert expect(result.success).toBe(true) if (result.success) { // Features not specified - standard metadata only expect(result.data.metadata.prompt).toBe('Generate simple landscape') expect(result.data.metadata.model).toBe('gemini-3-pro-image-preview') } // Verify API was called with config expect(mockGeminiClientInstance.models.generateContent).toHaveBeenCalledWith({ model: 'gemini-3-pro-image-preview', contents: [ { parts: [ { text: 'Generate simple landscape', }, ], }, ], config: { responseModalities: ['IMAGE'], }, }) }) it('should generate image with features and input image (parameters tracked only)', async () => { // Arrange const mockResponse = { response: { candidates: [ { content: { parts: [ { inlineData: { data: 'base64-blended-image', mimeType: 'image/jpeg', }, }, ], }, }, ], }, } mockGeminiClientInstance.models.generateContent = vi.fn().mockResolvedValue(mockResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data const inputBuffer = Buffer.from('test-image-data') const inputBase64 = inputBuffer.toString('base64') // Act const result = await client.generateImage({ prompt: 'Blend this character with fantasy elements', inputImage: inputBase64, blendImages: true, maintainCharacterConsistency: true, }) // Assert expect(result.success).toBe(true) if (result.success) { expect(result.data.metadata.inputImageProvided).toBe(true) // Features are passed to the API but not stored in metadata expect(result.data.metadata.prompt).toBe('Blend this character with fantasy elements') expect(result.data.metadata.model).toBe('gemini-3-pro-image-preview') } // Verify API was called with input image and original prompt (no processing at GeminiClient level) expect(mockGeminiClientInstance.models.generateContent).toHaveBeenCalledWith({ model: 'gemini-3-pro-image-preview', contents: [ { parts: [ { inlineData: { data: inputBase64, mimeType: 'image/jpeg', }, }, { text: 'Blend this character with fantasy elements', }, ], }, ], config: { responseModalities: ['IMAGE'], }, }) }) }) describe('GeminiClient.generateImage with aspectRatio', () => { it('should call API with imageConfig when aspectRatio is specified', async () => { // Arrange const mockResponse = { response: { candidates: [ { content: { parts: [ { inlineData: { data: 'base64-image-data-16-9', mimeType: 'image/png', }, }, ], }, }, ], }, } mockGeminiClientInstance.models.generateContent = vi.fn().mockResolvedValue(mockResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'test prompt for aspect ratio', aspectRatio: '16:9', }) // Assert expect(result.success).toBe(true) expect(mockGeminiClientInstance.models.generateContent).toHaveBeenCalledWith({ model: 'gemini-3-pro-image-preview', contents: [ { parts: [ { text: 'test prompt for aspect ratio', }, ], }, ], config: { imageConfig: { aspectRatio: '16:9' }, responseModalities: ['IMAGE'], }, }) }) it('should use default when aspectRatio is not specified', async () => { // Arrange const mockResponse = { response: { candidates: [ { content: { parts: [ { inlineData: { data: 'base64-default-image', mimeType: 'image/png', }, }, ], }, }, ], }, } mockGeminiClientInstance.models.generateContent = vi.fn().mockResolvedValue(mockResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'test prompt without aspect ratio', }) // Assert expect(result.success).toBe(true) expect(mockGeminiClientInstance.models.generateContent).toHaveBeenCalledWith({ model: 'gemini-3-pro-image-preview', contents: [ { parts: [ { text: 'test prompt without aspect ratio', }, ], }, ], config: { responseModalities: ['IMAGE'], }, }) // Verify imageConfig.aspectRatio is not included const callArgs = (mockGeminiClientInstance.models.generateContent as any).mock.calls[0][0] expect(callArgs.config.imageConfig).toBeUndefined() }) it('should include responseModalities: ["IMAGE"] in API call with aspectRatio', async () => { // Arrange const mockResponse = { response: { candidates: [ { content: { parts: [ { inlineData: { data: 'base64-image-data-21-9', mimeType: 'image/png', }, }, ], }, }, ], }, } mockGeminiClientInstance.models.generateContent = vi.fn().mockResolvedValue(mockResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'test prompt with 21:9 aspect ratio', aspectRatio: '21:9', }) // Assert expect(result.success).toBe(true) expect(mockGeminiClientInstance.models.generateContent).toHaveBeenCalledWith({ model: 'gemini-3-pro-image-preview', contents: [ { parts: [ { text: 'test prompt with 21:9 aspect ratio', }, ], }, ], config: { imageConfig: { aspectRatio: '21:9' }, responseModalities: ['IMAGE'], }, }) }) }) describe('GeminiClient.generateImage with useGoogleSearch', () => { it('should call API with tools parameter when useGoogleSearch is true', async () => { // Arrange const mockResponse = { response: { candidates: [ { content: { parts: [ { inlineData: { data: 'base64-grounded-image-data', mimeType: 'image/png', }, }, ], }, }, ], }, } mockGeminiClientInstance.models.generateContent = vi.fn().mockResolvedValue(mockResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate current 2025 weather map of Tokyo', useGoogleSearch: true, }) // Assert expect(result.success).toBe(true) expect(mockGeminiClientInstance.models.generateContent).toHaveBeenCalledWith({ model: 'gemini-3-pro-image-preview', contents: [ { parts: [ { text: 'Generate current 2025 weather map of Tokyo', }, ], }, ], config: { responseModalities: ['IMAGE'], }, tools: [{ googleSearch: {} }], }) }) it('should not include tools parameter when useGoogleSearch is false', async () => { // Arrange const mockResponse = { response: { candidates: [ { content: { parts: [ { inlineData: { data: 'base64-standard-image', mimeType: 'image/png', }, }, ], }, }, ], }, } mockGeminiClientInstance.models.generateContent = vi.fn().mockResolvedValue(mockResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate creative fantasy landscape', useGoogleSearch: false, }) // Assert expect(result.success).toBe(true) expect(mockGeminiClientInstance.models.generateContent).toHaveBeenCalledWith({ model: 'gemini-3-pro-image-preview', contents: [ { parts: [ { text: 'Generate creative fantasy landscape', }, ], }, ], config: { responseModalities: ['IMAGE'], }, }) // Verify tools parameter is not included const callArgs = (mockGeminiClientInstance.models.generateContent as any).mock.calls[0][0] expect(callArgs.tools).toBeUndefined() }) it('should not include tools parameter when useGoogleSearch is undefined', async () => { // Arrange const mockResponse = { response: { candidates: [ { content: { parts: [ { inlineData: { data: 'base64-default-image', mimeType: 'image/png', }, }, ], }, }, ], }, } mockGeminiClientInstance.models.generateContent = vi.fn().mockResolvedValue(mockResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate image without grounding', }) // Assert expect(result.success).toBe(true) expect(mockGeminiClientInstance.models.generateContent).toHaveBeenCalledWith({ model: 'gemini-3-pro-image-preview', contents: [ { parts: [ { text: 'Generate image without grounding', }, ], }, ], config: { responseModalities: ['IMAGE'], }, }) // Verify tools parameter is not included const callArgs = (mockGeminiClientInstance.models.generateContent as any).mock.calls[0][0] expect(callArgs.tools).toBeUndefined() }) it('should combine useGoogleSearch with aspectRatio and imageSize', async () => { // Arrange const mockResponse = { response: { candidates: [ { content: { parts: [ { inlineData: { data: 'base64-grounded-4k-image', mimeType: 'image/png', }, }, ], }, }, ], }, } mockGeminiClientInstance.models.generateContent = vi.fn().mockResolvedValue(mockResponse) const clientResult = createGeminiClient(testConfig) expect(clientResult.success).toBe(true) if (!clientResult.success) return const client = clientResult.data // Act const result = await client.generateImage({ prompt: 'Generate 2025 Japan foodtech industry chaos map', useGoogleSearch: true, aspectRatio: '16:9', imageSize: '4K', }) // Assert expect(result.success).toBe(true) expect(mockGeminiClientInstance.models.generateContent).toHaveBeenCalledWith({ model: 'gemini-3-pro-image-preview', contents: [ { parts: [ { text: 'Generate 2025 Japan foodtech industry chaos map', }, ], }, ], config: { imageConfig: { aspectRatio: '16:9', imageSize: '4K', }, responseModalities: ['IMAGE'], }, tools: [{ googleSearch: {} }], }) }) }) })

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/shinpr/mcp-image'

If you have feedback or need assistance with the MCP directory API, please join our Discord server