Files
timmy-talking-turd/tests/analysis.test.js
T
timmy d9f82299d0
Quality gates / quality (pull_request) Successful in 2m26s
test: prove schema conformance and refusal with a committed vision corpus (#18)
The worker contract was only proven by one happy-path fixture and a handful of
inline assertions, so nothing continuously checked that a reply Timmy cannot use
is refused instead of guessed at.

Corpus: a CC0 synthetic corpus (stool, non-stool, blur, glare, empty bowl,
malformed response) generated deterministically by scripts/build_vision_corpus.py.
Every fixture records its sha256, provenance, licence, case class, the provider
reply it replays, and the outcome that reply must produce. loadVisionCorpus
refuses a fixture whose bytes no longer match the manifest, so the corpus cannot
be edited into a different test. No real medical imagery, camera, or third-party
content is involved.

Contract fixes the corpus exposed:
- A schema-valid reply with a null form and colour is now an abstention
  (abstainedFor: unusable_visual_fields) instead of a hard provider failure. The
  declared response schema permits those nulls, so treating the reply as an error
  was wrong; it now abstains calmly and still prefill nothing.
- Fail-closed replies carry a machine-readable code (invalid_json, invalid_fields)
  instead of prose matching, and visionFailureResponse() maps them to plain,
  user-safe copy with no provider text, image bytes, or credentials echoed. A
  running server now returns that copy for unparsable and out-of-range replies.

Evidence: unit corpus contract and hash integrity, corpus-wide conformance
through the real analysis path, the same corpus through a running server.mjs with
a stub worker (200 abstain vs 503 fail-closed, no leaked bytes), and a real
browser acceptance run for the refusal UI. The release demo replays three corpus
cases through the real code path so the video shows the status the server would
actually send, with the on-camera caption gated by the release builder.
2026-10-11 08:14:36 +00:00

102 lines
4.3 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
buildVisionRequest,
mergeVisualSuggestion,
parseVisionResponse,
validatePhotoPayload,
} from '../src/analysis.js';
test('validates a confident visual suggestion without inventing nonvisual fields', () => {
const result = parseVisionResponse({
isStool: true,
bristolType: 4,
color: 'brown',
confidence: 0.82,
imageQuality: 'good',
observations: 'Smooth, formed appearance.',
urgency: 4,
discomfort: 3,
symptoms: { blood: true },
diagnosis: 'anything',
});
assert.deepEqual(result, {
status: 'suggestion',
isStool: true,
bristolType: 4,
color: 'brown',
confidence: 0.82,
imageQuality: 'good',
observations: 'Smooth, formed appearance.',
warning: 'Visual suggestion only. Confirm it yourself; this is not a diagnosis.',
});
assert.equal(result.urgency, undefined);
assert.equal(result.symptoms, undefined);
assert.equal(result.diagnosis, undefined);
});
test('fails closed on a non-stool or low-confidence image', () => {
// abstainedFor names which abstention happened, so callers never have to parse prose.
assert.deepEqual(parseVisionResponse({ isStool: false, confidence: 0.9 }), {
status: 'needs_user_input',
isStool: false,
abstainedFor: 'not_stool',
reason: 'The image does not clearly show stool.',
});
assert.deepEqual(parseVisionResponse({ isStool: true, bristolType: 4, color: 'brown', confidence: 0.31 }), {
status: 'needs_user_input',
isStool: true,
abstainedFor: 'low_confidence',
reason: 'The image is too uncertain to prefill safely.',
});
});
test('rejects malformed model output instead of guessing defaults', () => {
assert.throws(() => parseVisionResponse({ isStool: true, bristolType: 9, color: 'purple', confidence: 0.8 }), /invalid/i);
assert.throws(() => parseVisionResponse('not json'), /invalid/i);
});
test('merges only visual fields and preserves user-reported context', () => {
const form = { bristolType: 2, color: 'green', urgency: 3, discomfort: 2, note: 'user note', symptoms: { fever: true } };
const merged = mergeVisualSuggestion(form, { status: 'suggestion', bristolType: 4, color: 'brown', confidence: 0.8 });
assert.deepEqual(merged, { bristolType: 4, color: 'brown', urgency: 3, discomfort: 2, note: 'user note', symptoms: { fever: true } });
});
test('accepts bounded JPEG/PNG/WebP data URLs and rejects oversized or unsupported input', () => {
const payload = validatePhotoPayload({ imageDataUrl: 'data:image/jpeg;base64,' + 'YQ==', consent: true });
assert.equal(payload.mime, 'image/jpeg');
assert.equal(payload.bytes, 1);
assert.throws(() => validatePhotoPayload({ imageDataUrl: 'data:image/svg+xml;base64,PHN2Zz4=', consent: true }), /JPEG, PNG, or WebP/i);
assert.throws(() => validatePhotoPayload({ imageDataUrl: 'data:image/jpeg;base64,YQ==', consent: false }), /consent/i);
assert.throws(() => validatePhotoPayload({ imageDataUrl: 'data:image/jpeg;base64,' + 'A'.repeat(6_000_000), consent: true }), /too large/i);
});
test('builds a structured multimodal request that forbids diagnosis and nonvisual inference', () => {
const body = buildVisionRequest({ imageDataUrl: 'data:image/jpeg;base64,YQ==', model: 'vision-model' });
assert.equal(body.model, 'vision-model');
assert.equal(body.response_format.type, 'json_schema');
const prompt = body.messages[0].content.find(part => part.type === 'text').text;
assert.match(prompt, /do not infer urgency/i);
assert.match(prompt, /not a diagnosis/i);
assert.equal(body.messages[0].content.find(part => part.type === 'image_url').image_url.url, 'data:image/jpeg;base64,YQ==');
});
test('abstains instead of failing when a schema-valid response carries no readable visual fields', () => {
// The declared JSON schema allows a null form and color whenever the model cannot read them,
// so a schema-valid response must come back as an abstention — never as a provider failure.
assert.deepEqual(parseVisionResponse({
isStool: true,
bristolType: null,
color: null,
confidence: 0.66,
imageQuality: 'poor',
observations: 'Large specular highlight hides the surface.',
}), {
status: 'needs_user_input',
isStool: true,
abstainedFor: 'unusable_visual_fields',
reason: 'The photo does not show a form Timmy can read safely.',
});
});