import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { analyzePhoto } from '../src/vision-service.js'; const cleanJpeg = await readFile(fileURLToPath(new URL('./fixtures/ingress-clean.jpg', import.meta.url))); const imageDataUrl = `data:image/jpeg;base64,${cleanJpeg.toString('base64')}`; test('sends a bounded structured request to the configured provider and validates its response', async () => { let captured; const fetchImpl = async (url, options) => { captured = { url, options }; return { ok: true, json: async () => ({ choices: [{ message: { content: JSON.stringify({ isStool: true, bristolType: 4, color: 'brown', confidence: 0.81, imageQuality: 'good', observations: 'Smooth and formed.' }) } }] }), }; }; const result = await analyzePhoto({ payload: { imageDataUrl, consent: true }, fetchImpl, config: { baseUrl: 'http://127.0.0.1:8645/v1', apiKey: 'secret', model: 'vision-model' }, }); assert.equal(result.status, 'suggestion'); assert.equal(result.bristolType, 4); assert.equal(captured.url, 'http://127.0.0.1:8645/v1/chat/completions'); assert.equal(captured.options.headers.authorization, 'Bearer secret'); assert.doesNotMatch(JSON.stringify(result), /secret/); }); test('fails closed when the provider is unavailable or malformed', async () => { await assert.rejects(() => analyzePhoto({ payload: { imageDataUrl, consent: true }, fetchImpl: async () => ({ ok: false, status: 503, text: async () => 'upstream detail' }), config: { baseUrl: 'http://localhost/v1', apiKey: 'secret', model: 'm' }, }), /temporarily unavailable/i); await assert.rejects(() => analyzePhoto({ payload: { imageDataUrl, consent: true }, fetchImpl: async () => ({ ok: true, json: async () => ({ choices: [] }) }), config: { baseUrl: 'http://localhost/v1', apiKey: 'secret', model: 'm' }, }), /invalid/i); }); test('requires an http(s) provider URL and never accepts credentials from the browser payload', async () => { await assert.rejects(() => analyzePhoto({ payload: { imageDataUrl, consent: true, apiKey: 'injected' }, fetchImpl: async () => { throw new Error('must not call'); }, config: { baseUrl: 'file:///tmp/provider', apiKey: 'secret', model: 'm' }, }), /provider URL/i); });