Files
timmy-talking-turd/tests/abstention-policy.test.js
T
timmy 209e21a951
Quality gates / quality (pull_request) Successful in 13m29s
Gate visual prefill on a versioned abstention policy (#32)
Confidence was the only thing standing between a model answer and a
prefilled journal entry, so a confident reading of a blurry photo could
still be offered as fact. Quality is now part of the policy: `poor`
abstains however confident the reply looked, with its own fixed copy.

The confidence floor, accepted quality levels, and refusal copy move into
src/abstention-policy.js so the parser, the corpus, and the browser cannot
drift into three explanations of the same refusal. parseVisionResponse
takes an optional policy, which is how the tests prove the parser carries
no hidden limits of its own.

evaluatePrefillGate turns the #29 calibration report into one release
decision: GO plus a validation-selected threshold scored on the held-out
test split, or prefill is blocked with each reason named. A missing report
is a block, never a default allow. scripts/abstention_policy_receipt.mjs
exposes that as a receipt (exit 0 / 3 / 2).

The committed synthetic predictions currently report BLOCKED and the
receipt says so. No candidate exists until #31, and the builder does not
hard-gate on the receipt yet; the on-camera quality gate is the evidence
this release carries.

The corpus gains a case that replays a 0.94-confidence answer about the
synthetic motion-blur fixture, and the release demo asserts in the browser
that nothing is prefilled before captioning it.

Verified: 226 unit tests, calibration harness (19), corpus manifest (8),
vision-refusal, photo-first, and provisional acceptance, check:syntax,
check:diff, and a full real-browser demo recording whose caption receipt
confirms the new scene painted on screen.
2026-10-26 10:58:38 +00:00

118 lines
5.3 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { mkdtemp, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { ABSTENTION_COPY, ABSTENTION_POLICY, abstainFor, evaluatePrefillGate } from '../src/abstention-policy.js';
test('every abstention reason has one fixed piece of copy the user sees', () => {
assert.equal(abstainFor('not_stool').reason, 'The image does not clearly show stool.');
assert.equal(abstainFor('low_confidence').reason, 'The image is too uncertain to prefill safely.');
assert.equal(abstainFor('unusable_visual_fields').reason, 'The photo does not show a form Timmy can read safely.');
assert.equal(abstainFor('low_quality').reason, 'The photo is too blurry or poorly lit to suggest a form safely.');
assert.deepEqual(abstainFor('low_quality'), {
status: 'needs_user_input',
isStool: true,
abstainedFor: 'low_quality',
reason: ABSTENTION_COPY.low_quality,
});
assert.equal(abstainFor('not_stool', false).isStool, false);
assert.throws(() => abstainFor('looked_wrong'), /unknown abstention reason/);
});
test('the policy states the confidence floor and the quality levels that may prefill', () => {
assert.ok(Number.isInteger(ABSTENTION_POLICY.version));
assert.equal(ABSTENTION_POLICY.minConfidence, 0.55);
assert.deepEqual([...ABSTENTION_POLICY.acceptedImageQuality], ['good', 'fair']);
assert.equal(ABSTENTION_POLICY.acceptedImageQuality.includes('poor'), false);
assert.equal(Object.isFrozen(ABSTENTION_POLICY), true, 'the shipped policy must not be mutated at runtime');
});
test('prefill is authorised only by a GO report whose threshold was selected on validation', () => {
const passing = {
verdict: 'GO',
recommended: 'specialist-candidate',
models: [{
model: 'specialist-candidate',
selectionSplit: 'validation',
evaluatedSplit: 'test',
gate: { passed: true, failed: [] },
}],
};
assert.deepEqual(evaluatePrefillGate(passing), {
prefillAllowed: true,
verdict: 'GO',
calibrationModel: 'specialist-candidate',
blockedBy: [],
});
const blocked = evaluatePrefillGate({
verdict: 'BLOCKED',
recommended: null,
models: [{ model: 'specialist-candidate', selectionSplit: 'validation', evaluatedSplit: 'test', gate: { passed: false, failed: ['precisionAtCoverage'] } }],
});
assert.equal(blocked.prefillAllowed, false);
assert.deepEqual(blocked.blockedBy, ['calibration-verdict-not-go', 'no-recommended-calibration']);
const selectedOnTest = evaluatePrefillGate({
verdict: 'GO',
recommended: 'opportunist',
models: [{ model: 'opportunist', selectionSplit: 'test', evaluatedSplit: 'test', gate: { passed: true, failed: [] } }],
});
assert.equal(selectedOnTest.prefillAllowed, false, 'a threshold chosen on the split it is scored on is not evidence');
assert.deepEqual(selectedOnTest.blockedBy, ['threshold-not-selected-on-validation']);
const noReport = evaluatePrefillGate(undefined);
assert.equal(noReport.prefillAllowed, false);
assert.match(noReport.blockedBy.join(','), /calibration-verdict-not-go/);
});
const execFileAsync = promisify(execFile);
const receiptScript = fileURLToPath(new URL('../scripts/abstention_policy_receipt.mjs', import.meta.url));
async function runReceipt(report) {
const directory = await mkdtemp(join(tmpdir(), 'timmy-policy-'));
const reportPath = join(directory, 'calibration-report.json');
await writeFile(reportPath, JSON.stringify(report));
try {
const { stdout } = await execFileAsync(process.execPath, [receiptScript, '--report', reportPath]);
return { code: 0, stdout };
} catch (error) {
return { code: error.code, stdout: error.stdout || '', stderr: error.stderr || '' };
}
}
test('the receipt refuses to authorise prefill without a passing held-out calibration', async () => {
const blocked = await runReceipt({
verdict: 'BLOCKED',
recommended: null,
models: [{ model: 'specialist-candidate', selectionSplit: 'validation', evaluatedSplit: 'test', gate: { passed: false, failed: ['precisionAtCoverage'] } }],
});
assert.equal(blocked.code, 3, 'a blocked calibration must exit non-zero instead of shipping prefill');
assert.match(blocked.stdout, /prefill: BLOCKED/);
assert.match(blocked.stdout, /calibration-verdict-not-go/);
assert.match(blocked.stdout, /policy: version 1 · confidence ≥ 0\.55 · quality good\|fair/);
const passing = await runReceipt({
verdict: 'GO',
recommended: 'specialist-candidate',
models: [{ model: 'specialist-candidate', selectionSplit: 'validation', evaluatedSplit: 'test', gate: { passed: true, failed: [] } }],
});
assert.equal(passing.code, 0);
assert.match(passing.stdout, /prefill: AUTHORISED for specialist-candidate/);
});
test('the receipt treats a missing report as a block and never as a default allow', async () => {
const directory = await mkdtemp(join(tmpdir(), 'timmy-policy-'));
try {
await execFileAsync(process.execPath, [receiptScript, '--report', join(directory, 'absent.json')]);
assert.fail('a missing calibration report must not exit 0');
} catch (error) {
assert.equal(error.code, 2);
assert.match(String(error.stderr), /could not read|absent/i);
}
});