Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
88 lines
3.1 KiB
TypeScript
88 lines
3.1 KiB
TypeScript
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<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => Promise<Response>) {
|
|
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');
|
|
});
|
|
});
|