import { afterEach, describe, expect, test } from 'bun:test'; import { classifyImage } from './image-model'; const originalFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = originalFetch; }); function makeImageFile(type = 'image/jpeg') { return new File([new Uint8Array([1, 2, 3])], 'leaf.jpg', { type }); } function mockFetch(handler: (input: Parameters[0], init?: Parameters[1]) => Promise) { globalThis.fetch = Object.assign(handler, { preconnect: originalFetch.preconnect }); } describe('classifyImage', () => { test('maps ML service prediction response to API classification result', async () => { mockFetch(async (input, init) => { expect(String(input)).toBe('http://127.0.0.1:8001/predict'); expect(init?.method).toBe('POST'); expect(init?.body).toBeInstanceOf(FormData); return new Response( JSON.stringify({ label: 'Daun Sehat', confidence: 0.92, probabilities: { 'Bercak Daun': 0.02, 'Daun Sehat': 0.92, 'Karat Daun': 0.03, 'Hawar Daun': 0.03, }, }), { status: 200, headers: { 'content-type': 'application/json' } }, ); }); const result = await classifyImage(makeImageFile()); expect(result.predictedDiseaseSlug).toBe('daun-sehat'); expect(result.confidence).toBe(0.92); expect(result.probabilities).toEqual([ { diseaseSlug: 'daun-sehat', label: 'Daun Sehat', confidence: 0.92 }, { diseaseSlug: 'karat-daun', label: 'Karat Daun', confidence: 0.03 }, { diseaseSlug: 'hawar-daun', label: 'Hawar Daun', confidence: 0.03 }, { diseaseSlug: 'bercak-daun', label: 'Bercak Daun', confidence: 0.02 }, ]); }); test('rejects unsupported file types before calling ML service', async () => { let called = false; mockFetch(async () => { called = true; return new Response('{}'); }); await expect(classifyImage(makeImageFile('image/webp'))).rejects.toThrow('File must be JPEG or PNG'); expect(called).toBe(false); }); test('throws when ML service returns a non-success response', async () => { mockFetch(async () => new Response(JSON.stringify({ detail: 'Model is not loaded' }), { status: 503 })); await expect(classifyImage(makeImageFile())).rejects.toThrow('ML service returned 503: Model is not loaded'); }); test('throws when ML service returns malformed JSON', async () => { mockFetch(async () => new Response('not-json', { status: 200, headers: { 'content-type': 'application/json' } })); await expect(classifyImage(makeImageFile())).rejects.toThrow('Invalid ML service JSON response'); }); test('throws when ML service returns an unknown label', async () => { mockFetch(async () => new Response( JSON.stringify({ label: 'Unknown Disease', confidence: 0.7, probabilities: { 'Unknown Disease': 0.7 }, }), { status: 200, headers: { 'content-type': 'application/json' } }, )); await expect(classifyImage(makeImageFile())).rejects.toThrow('Unknown ML service label: Unknown Disease'); }); });