import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import { buildPromptWithImages } from '../src/prompt-builder'; describe('prompt-builder.ts', () => { let tempDir: string; beforeEach(async () => { tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'prompt-builder-test-')); }); afterEach(async () => { try { await fs.rm(tempDir, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } }); describe('buildPromptWithImages - no images', () => { it('should return plain text when no images provided', async () => { const basePrompt = 'Hello, world!'; const result = await buildPromptWithImages(basePrompt); expect(result.content).toBe('Hello, world!'); expect(result.hasImages).toBe(false); }); it('should return plain text when empty image array provided', async () => { const basePrompt = 'Test prompt'; const result = await buildPromptWithImages(basePrompt, []); expect(result.content).toBe('Test prompt'); expect(result.hasImages).toBe(false); }); it('should handle multiline prompts', async () => { const basePrompt = 'Line 1\nLine 2\nLine 3'; const result = await buildPromptWithImages(basePrompt); expect(result.content).toBe('Line 1\nLine 2\nLine 3'); }); }); describe('buildPromptWithImages - with images', () => { it('should build content blocks with single image', async () => { const imagePath = path.join(tempDir, 'test.png'); await fs.writeFile(imagePath, Buffer.from('image data')); const result = await buildPromptWithImages('Check this image', [imagePath]); expect(result.hasImages).toBe(true); expect(Array.isArray(result.content)).toBe(true); const blocks = result.content as Array<{ type: string; text?: string; source?: object; }>; expect(blocks).toHaveLength(2); expect(blocks[0]).toMatchObject({ type: 'text', text: 'Check this image', }); expect(blocks[1]).toMatchObject({ type: 'image', }); }); it('should build content blocks with multiple images', async () => { const image1 = path.join(tempDir, 'img1.jpg'); const image2 = path.join(tempDir, 'img2.png'); await fs.writeFile(image1, Buffer.from('jpg data')); await fs.writeFile(image2, Buffer.from('png data')); const result = await buildPromptWithImages('Two images', [image1, image2]); expect(result.hasImages).toBe(true); const blocks = result.content as Array<{ type: string; text?: string; source?: object; }>; expect(blocks).toHaveLength(3); // 1 text + 2 images expect(blocks[0].type).toBe('text'); expect(blocks[1].type).toBe('image'); expect(blocks[2].type).toBe('image'); }); it('should resolve relative paths with workDir', async () => { const imagePath = 'test.png'; const fullPath = path.join(tempDir, imagePath); await fs.writeFile(fullPath, Buffer.from('data')); const result = await buildPromptWithImages('Test', [imagePath], tempDir); expect(result.hasImages).toBe(true); expect(Array.isArray(result.content)).toBe(true); }); it('should handle absolute paths without workDir', async () => { const imagePath = path.join(tempDir, 'absolute.png'); await fs.writeFile(imagePath, Buffer.from('data')); const result = await buildPromptWithImages('Test', [imagePath]); expect(result.hasImages).toBe(true); }); }); describe('buildPromptWithImages - includeImagePaths option', () => { it('should not include image paths by default', async () => { const imagePath = path.join(tempDir, 'test.png'); await fs.writeFile(imagePath, Buffer.from('data')); const result = await buildPromptWithImages('Prompt', [imagePath]); const blocks = result.content as Array<{ type: string; text?: string; }>; const textBlock = blocks.find((b) => b.type === 'text'); expect(textBlock?.text).not.toContain('Attached images:'); expect(textBlock?.text).toBe('Prompt'); }); it('should include image paths when requested', async () => { const imagePath = path.join(tempDir, 'test.png'); await fs.writeFile(imagePath, Buffer.from('data')); const result = await buildPromptWithImages('Prompt', [imagePath], undefined, true); const blocks = result.content as Array<{ type: string; text?: string; }>; const textBlock = blocks.find((b) => b.type === 'text'); expect(textBlock?.text).toContain('Prompt'); expect(textBlock?.text).toContain('Attached images:'); expect(textBlock?.text).toContain(imagePath); }); it('should format multiple image paths when included', async () => { const img1 = path.join(tempDir, 'img1.png'); const img2 = path.join(tempDir, 'img2.jpg'); await fs.writeFile(img1, Buffer.from('data1')); await fs.writeFile(img2, Buffer.from('data2')); const result = await buildPromptWithImages('Test', [img1, img2], undefined, true); const blocks = result.content as Array<{ type: string; text?: string; }>; const textBlock = blocks.find((b) => b.type === 'text'); expect(textBlock?.text).toContain('Attached images:'); expect(textBlock?.text).toContain(img1); expect(textBlock?.text).toContain(img2); }); }); describe('buildPromptWithImages - edge cases', () => { it('should handle empty prompt with images', async () => { const imagePath = path.join(tempDir, 'test.png'); await fs.writeFile(imagePath, Buffer.from('data')); const result = await buildPromptWithImages('', [imagePath]); expect(result.hasImages).toBe(true); const blocks = result.content as Array<{ type: string; text?: string; source?: object; }>; // Should only have image block, no text block for empty string expect(blocks.length).toBeGreaterThan(0); expect(blocks.some((b) => b.type === 'image')).toBe(true); }); it('should handle whitespace-only prompt with images', async () => { const imagePath = path.join(tempDir, 'test.png'); await fs.writeFile(imagePath, Buffer.from('data')); const result = await buildPromptWithImages(' ', [imagePath]); expect(result.hasImages).toBe(true); const blocks = result.content as Array<{ type: string; text?: string; source?: object; }>; // Whitespace-only is trimmed, so no text block should be added expect(blocks.every((b) => b.type !== 'text')).toBe(true); }); it('should skip failed image loads', async () => { const validImage = path.join(tempDir, 'valid.png'); const invalidImage = path.join(tempDir, 'nonexistent.png'); await fs.writeFile(validImage, Buffer.from('data')); const result = await buildPromptWithImages('Test', [validImage, invalidImage]); expect(result.hasImages).toBe(true); const blocks = result.content as Array<{ type: string; text?: string; source?: object; }>; const imageBlocks = blocks.filter((b) => b.type === 'image'); // Only valid image should be included expect(imageBlocks).toHaveLength(1); }); it('should handle mixed case in includeImagePaths parameter', async () => { const imagePath = path.join(tempDir, 'test.png'); await fs.writeFile(imagePath, Buffer.from('data')); const resultFalse = await buildPromptWithImages('Test', [imagePath], undefined, false); const resultTrue = await buildPromptWithImages('Test', [imagePath], undefined, true); const blocksFalse = resultFalse.content as Array<{ type: string; text?: string; }>; const blocksTrue = resultTrue.content as Array<{ type: string; text?: string; }>; expect(blocksFalse[0].text).not.toContain('Attached images:'); expect(blocksTrue[0].text).toContain('Attached images:'); }); }); describe('buildPromptWithImages - content format', () => { it('should return string when only text and includeImagePaths false', async () => { const result = await buildPromptWithImages('Just text', undefined); expect(typeof result.content).toBe('string'); }); it('should return array when has images', async () => { const imagePath = path.join(tempDir, 'test.png'); await fs.writeFile(imagePath, Buffer.from('data')); const result = await buildPromptWithImages('Text', [imagePath]); expect(Array.isArray(result.content)).toBe(true); }); it('should preserve prompt formatting', async () => { const basePrompt = 'Line 1\n\nLine 2\n Indented line'; const imagePath = path.join(tempDir, 'test.png'); await fs.writeFile(imagePath, Buffer.from('data')); const result = await buildPromptWithImages(basePrompt, [imagePath]); const blocks = result.content as Array<{ type: string; text?: string; }>; const textBlock = blocks.find((b) => b.type === 'text'); expect(textBlock?.text).toBe(basePrompt); }); }); });