Skip to main content
Glama
ollama.test.ts9.47 kB
import type { Config } from '@doc-agent/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { extractDocument } from '../index'; // Mock tesseract.js to avoid worker issues in tests vi.mock('tesseract.js', () => ({ default: { recognize: vi.fn().mockResolvedValue({ data: { text: 'Mocked OCR text' }, }), }, })); // Mock fetch globally const mockFetch = vi.fn(); global.fetch = mockFetch; describe('Ollama Extraction', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should extract document data from Ollama API', async () => { const mockResponse = { response: JSON.stringify({ type: 'invoice', vendor: 'Test Company', amount: 100.5, date: '2025-12-07', items: [{ description: 'Test item', total: 100.5 }], }), }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse, }); // Create a temporary file for testing const fs = await import('node:fs'); const path = await import('node:path'); const os = await import('node:os'); const tmpDir = os.tmpdir(); const testFile = path.join(tmpDir, 'test-invoice.pdf'); // Create a minimal PDF file (just for testing file reading) fs.writeFileSync(testFile, Buffer.from('test pdf content')); const config: Config = { aiProvider: 'ollama', ollamaModel: 'llama3.2-vision', }; const result = await extractDocument(testFile, config); expect(result.type).toBe('invoice'); expect(result.vendor).toBe('Test Company'); expect(result.amount).toBe(100.5); expect(result.date).toBe('2025-12-07'); expect(result.items).toHaveLength(1); expect(result.id).toBeDefined(); expect(result.filename).toBe('test-invoice.pdf'); expect(result.extractedAt).toBeInstanceOf(Date); // Verify API call expect(mockFetch).toHaveBeenCalledWith('http://localhost:11434/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: expect.stringContaining('llama3.2-vision'), }); // Cleanup fs.unlinkSync(testFile); }); it('should coerce invalid type to "other"', async () => { // Schema is lenient - invalid types become 'other' const invalidResponse = { response: JSON.stringify({ type: 'invalid_type', // Invalid type - will be coerced to 'other' vendor: 'Test Company', }), }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => invalidResponse, }); const fs = await import('node:fs'); const path = await import('node:path'); const os = await import('node:os'); const tmpDir = os.tmpdir(); const testFile = path.join(tmpDir, 'test-receipt.pdf'); fs.writeFileSync(testFile, Buffer.from('test pdf content')); const config: Config = { aiProvider: 'ollama', ollamaModel: 'llama3.2-vision', }; const result = await extractDocument(testFile, config); // Invalid type should be coerced to 'other' expect(result.type).toBe('other'); expect(result.vendor).toBe('Test Company'); expect(mockFetch).toHaveBeenCalledTimes(1); // No retry needed fs.unlinkSync(testFile); }); it('should coerce string numbers to actual numbers', async () => { // Schema coerces strings like "100.50" to numbers const responseWithStrings = { response: JSON.stringify({ type: 'receipt', vendor: 'Test Store', amount: '50.99', // String instead of number items: [{ description: 'Item', total: '25.50' }], }), }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => responseWithStrings, }); const fs = await import('node:fs'); const path = await import('node:path'); const os = await import('node:os'); const tmpDir = os.tmpdir(); const testFile = path.join(tmpDir, 'test-coerce.pdf'); fs.writeFileSync(testFile, Buffer.from('test pdf content')); const config: Config = { aiProvider: 'ollama', ollamaModel: 'llama3.2-vision', }; const result = await extractDocument(testFile, config); expect(result.amount).toBe(50.99); expect(typeof result.amount).toBe('number'); expect(result.items?.[0].total).toBe(25.5); expect(typeof result.items?.[0].total).toBe('number'); fs.unlinkSync(testFile); }); it('should handle API errors', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, text: async () => 'Internal Server Error', }); const fs = await import('node:fs'); const path = await import('node:path'); const os = await import('node:os'); const tmpDir = os.tmpdir(); const testFile = path.join(tmpDir, 'test-error.pdf'); fs.writeFileSync(testFile, Buffer.from('test pdf content')); const config: Config = { aiProvider: 'ollama', ollamaModel: 'llama3.2-vision', }; await expect(extractDocument(testFile, config)).rejects.toThrow('Ollama API error'); fs.unlinkSync(testFile); }); it('should handle JSON parse errors', async () => { const invalidJsonResponse = { response: 'not valid json {', }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => invalidJsonResponse, }); const fs = await import('node:fs'); const path = await import('node:path'); const os = await import('node:os'); const tmpDir = os.tmpdir(); const testFile = path.join(tmpDir, 'test-parse-error.pdf'); fs.writeFileSync(testFile, Buffer.from('test pdf content')); const config: Config = { aiProvider: 'ollama', ollamaModel: 'llama3.2-vision', }; await expect(extractDocument(testFile, config)).rejects.toThrow('Failed to parse JSON'); fs.unlinkSync(testFile); }); it('should use default model if not specified', async () => { const mockResponse = { response: JSON.stringify({ type: 'receipt', }), }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse, }); const fs = await import('node:fs'); const path = await import('node:path'); const os = await import('node:os'); const tmpDir = os.tmpdir(); const testFile = path.join(tmpDir, 'test-default.pdf'); fs.writeFileSync(testFile, Buffer.from('test pdf content')); const config: Config = { aiProvider: 'ollama', // No ollamaModel specified }; await extractDocument(testFile, config); const callBody = JSON.parse(mockFetch.mock.calls[0][1].body as string); expect(callBody.model).toBe('llama3.2-vision'); // Default model fs.unlinkSync(testFile); }); it('should not retry on API errors', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, text: async () => 'Internal Server Error', }); const fs = await import('node:fs'); const path = await import('node:path'); const os = await import('node:os'); const tmpDir = os.tmpdir(); const testFile = path.join(tmpDir, 'test-no-retry.pdf'); fs.writeFileSync(testFile, Buffer.from('test pdf content')); const config: Config = { aiProvider: 'ollama', ollamaModel: 'llama3.2-vision', }; await expect(extractDocument(testFile, config)).rejects.toThrow('Ollama API error'); // Should not retry on API errors expect(mockFetch).toHaveBeenCalledTimes(1); fs.unlinkSync(testFile); }); it('should handle different image MIME types', async () => { const mockResponse = { response: JSON.stringify({ type: 'receipt', vendor: 'Store', }), }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse, }); const fs = await import('node:fs'); const path = await import('node:path'); const os = await import('node:os'); const tmpDir = os.tmpdir(); const testFile = path.join(tmpDir, 'receipt.png'); fs.writeFileSync(testFile, Buffer.from('test image content')); const config: Config = { aiProvider: 'ollama', ollamaModel: 'llama3.2-vision', }; await extractDocument(testFile, config); const callBody = JSON.parse(mockFetch.mock.calls[0][1].body as string); // With OCR enabled, prompt now includes OCR text expect(callBody.prompt).toContain('OCR Text'); // OCR is applied to images too fs.unlinkSync(testFile); }); it('should handle missing type field gracefully', async () => { // Schema defaults missing type to 'other' const noTypeResponse = { response: JSON.stringify({ vendor: 'Test Company', amount: 100, }), }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => noTypeResponse, }); const fs = await import('node:fs'); const path = await import('node:path'); const os = await import('node:os'); const tmpDir = os.tmpdir(); const testFile = path.join(tmpDir, 'test-no-type.pdf'); fs.writeFileSync(testFile, Buffer.from('test pdf content')); const config: Config = { aiProvider: 'ollama', ollamaModel: 'llama3.2-vision', }; const result = await extractDocument(testFile, config); // Missing type should default to 'other' expect(result.type).toBe('other'); expect(result.vendor).toBe('Test Company'); fs.unlinkSync(testFile); }); });

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/prosdevlab/doc-agent'

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