Files
timmy-talking-turd/tests/vision-boundary.test.js
timmy 5c759078c4
Quality gates / quality (pull_request) Failing after 14s
fix: fail closed when a filled-in visual field is out of range (#73)
The blank-first ordering let one blank field short-circuit the whole reply, so a
response carrying an impossible value came back as `unusable_visual_fields`:
the user was told the photo was unreadable when the model had in fact returned
something Timmy must never apply.

Blank fields still abstain and still prefill nothing. A filled-in field is a
claim, and a claim outside the contract now fails closed even when the other
field came back blank.

The corpus earns its label too. `out-of-range-fields` drops from confidence 1.4
to 0.9 so the case reaches the field-range check instead of dying at the
confidence gate, and a new `blank-plus-out-of-range` case covers the class the
old ordering let through. The release demo replays that case on camera and the
fail-closed 503 budget moves from one to two, so the scene cannot be dropped
silently.

Closes #73
2026-10-12 08:07:22 +00:00

111 lines
6.0 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { analyzePhoto, visionFailureResponse } from '../src/vision-service.js';
import { loadVisionCorpus } from '../src/vision-corpus.js';
import { CORPUS_MODEL, caseFor, corpusDir, corpusPayload, startProvider, startServer, stopServer } from './helpers/vision-harness.mjs';
const FAIL_CLOSED_COPY = /no suggestion was applied/i;
test('a malformed provider response fails closed with user-safe copy instead of echoing provider detail', () => {
const failure = visionFailureResponse(Object.assign(new Error('Invalid AI response: visual fields are out of range.'), { code: 'invalid_fields' }));
assert.equal(failure.status, 503);
assert.match(failure.body.error, FAIL_CLOSED_COPY);
assert.doesNotMatch(failure.body.error, /invalid ai response|out of range/i);
const consent = visionFailureResponse(new Error('Explicit consent is required before AI analysis.'));
assert.equal(consent.status, 400);
assert.match(consent.body.error, /consent/i);
const offline = visionFailureResponse(new Error('AI analysis is temporarily unavailable. Continue manually.'));
assert.equal(offline.status, 503);
assert.match(offline.body.error, /temporarily unavailable/i);
});
test('the corpus replays through the real provider call path without inventing a suggestion', async () => {
const corpus = await loadVisionCorpus(corpusDir);
for (const entry of corpus.cases) {
const fetchImpl = async () => ({ ok: true, json: async () => ({ choices: [{ message: { content: entry.providerContent } }] }) });
const options = {
payload: { imageDataUrl: 'data:image/png;base64,YQ==', consent: true },
fetchImpl,
config: { baseUrl: 'http://127.0.0.1:9/v1', apiKey: 'corpus-key', model: CORPUS_MODEL },
};
if (entry.expectedOutcome === 'suggestion') {
const result = await analyzePhoto(options);
assert.equal(result.status, 'suggestion', `${entry.id} must return a suggestion`);
assert.equal(result.imageQuality, 'good');
} else if (entry.expectedOutcome === 'abstained') {
const result = await analyzePhoto(options);
assert.equal(result.status, 'needs_user_input', `${entry.id} must abstain`);
assert.equal(result.abstainedFor, entry.expectedReason, `${entry.id} must name its abstention`);
assert.equal('bristolType' in result, false, `${entry.id} must not prefill a form`);
} else {
await assert.rejects(() => analyzePhoto(options), (error) => {
assert.equal(visionFailureResponse(error).status, 503, `${entry.id} must fail closed`);
return true;
});
}
}
});
test('the running server answers every corpus case with a fail-closed status and no leaked image bytes', async () => {
const corpus = await loadVisionCorpus(corpusDir);
const provider = await startProvider();
const server = await startServer({ providerPort: provider.port });
const postAnalyze = async (payload) => {
const response = await fetch('http://127.0.0.1:4199/api/analyze', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
});
return { status: response.status, body: await response.json() };
};
try {
const suggestion = caseFor(corpus, 'stool-like-formed');
provider.state.content = suggestion.providerContent;
const payload = await corpusPayload(suggestion);
const ok = await postAnalyze(payload);
assert.equal(ok.status, 200, 'a schema-valid suggestion is the only case that returns 200 with fields');
assert.equal(ok.body.bristolType, 4);
const glare = caseFor(corpus, 'glare-overexposed');
provider.state.content = glare.providerContent;
const abstained = await postAnalyze(await corpusPayload(glare));
assert.equal(abstained.status, 200, 'a schema-valid response with unreadable fields abstains, it is not a server failure');
assert.equal(abstained.body.status, 'needs_user_input');
assert.equal(abstained.body.abstainedFor, 'unusable_visual_fields');
assert.equal('bristolType' in abstained.body, false);
const malformed = caseFor(corpus, 'malformed-response');
provider.state.content = malformed.providerContent;
const unparsable = await postAnalyze(await corpusPayload(malformed));
assert.equal(unparsable.status, 503, 'unparsable provider content must not be served as a suggestion');
assert.match(unparsable.body.error, FAIL_CLOSED_COPY);
assert.doesNotMatch(JSON.stringify(unparsable.body), /base64|data:image|corpus-key|Bearer/i);
const outOfRangeCase = caseFor(corpus, 'out-of-range-fields');
provider.state.content = outOfRangeCase.providerContent;
const outOfRange = await postAnalyze(await corpusPayload(outOfRangeCase));
assert.equal(outOfRange.status, 503, 'out-of-range fields must not be served as a suggestion');
assert.match(outOfRange.body.error, FAIL_CLOSED_COPY);
// Issue #73: a filled-in field that is out of range fails closed even when the other field is blank,
// so the user is never told the photo was unreadable when the model returned an impossible value.
const blankAndOutOfRange = caseFor(corpus, 'blank-plus-out-of-range');
provider.state.content = blankAndOutOfRange.providerContent;
const blankOutOfRange = await postAnalyze(await corpusPayload(blankAndOutOfRange));
assert.equal(blankOutOfRange.status, 503, 'a filled-in out-of-range field must not be served as an abstention');
assert.match(blankOutOfRange.body.error, FAIL_CLOSED_COPY);
assert.equal('abstainedFor' in blankOutOfRange.body, false, 'a contract breach must not be reported as an abstention');
assert.doesNotMatch(JSON.stringify(blankOutOfRange.body), /base64|data:image|corpus-key|Bearer/i);
const noConsent = await postAnalyze({ imageDataUrl: payload.imageDataUrl, consent: false });
assert.equal(noConsent.status, 400, 'analysis without explicit consent is a client error');
assert.doesNotMatch(JSON.stringify(noConsent.body), /base64|data:image/i);
} finally {
await stopServer(server);
await provider.close();
}
});