Quality gates / quality (pull_request) Successful in 1m45s
Suggested, confirmed, and corrected AI states are now distinct in the log sheet, and correcting a suggested value clears the AI-confirmed state while keeping every user-entered field. Screen-reader copy states the AI confidence and who is in control. - src/suggestion.js: provenance state machine (pending/confirmed/corrected/manual) - app.js: renders provenance on the scan result and log steps, tracks corrections - tests/suggestion.test.js: unit coverage for the state machine and a11y copy - tests/provisional-suggestion.acceptance.mjs: browser acceptance + screenshots - record_release_demo.mjs: demo now reviews, confirms, and corrects the suggestion - CI, release builder, and release notes gate the new contract
93 lines
4.9 KiB
JavaScript
93 lines
4.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { changeVisualField, confirmSuggestion, createSuggestionState, provenanceCopy } from '../src/suggestion.js';
|
|
|
|
test('keeps an AI suggestion provisional until the user acts on it', () => {
|
|
const state = createSuggestionState({ status: 'suggestion', bristolType: 4, color: 'brown', confidence: 0.83 });
|
|
|
|
assert.equal(state.status, 'pending');
|
|
assert.equal(state.affirmed, false);
|
|
assert.deepEqual(state.suggestion, { bristolType: 4, color: 'brown', confidence: 0.83 });
|
|
});
|
|
|
|
test('treats a missing or unusable suggestion as no suggestion at all', () => {
|
|
assert.deepEqual(createSuggestionState(null).status, 'none');
|
|
assert.deepEqual(createSuggestionState(undefined).suggestion, null);
|
|
assert.deepEqual(createSuggestionState({ status: 'needs_user_input', reason: 'The image is too uncertain to prefill safely.' }).status, 'none');
|
|
});
|
|
|
|
test('confirms a suggestion only when the user explicitly accepts it', () => {
|
|
const pending = createSuggestionState({ status: 'suggestion', bristolType: 4, color: 'brown', confidence: 0.83 });
|
|
const confirmed = confirmSuggestion(pending);
|
|
|
|
assert.equal(confirmed.status, 'confirmed');
|
|
assert.equal(confirmed.affirmed, true);
|
|
assert.deepEqual(confirmed.suggestion, pending.suggestion);
|
|
|
|
const nothing = confirmSuggestion(createSuggestionState(null));
|
|
assert.equal(nothing.status, 'none');
|
|
assert.equal(nothing.affirmed, false);
|
|
assert.equal(confirmSuggestion(confirmed).status, 'confirmed');
|
|
});
|
|
|
|
test('changing a suggested field clears AI-confirmed state but keeps the context', () => {
|
|
const confirmed = confirmSuggestion(createSuggestionState({ status: 'suggestion', bristolType: 4, color: 'brown', confidence: 0.83 }));
|
|
const corrected = changeVisualField(confirmed, 'bristolType', 2);
|
|
|
|
assert.equal(corrected.status, 'corrected');
|
|
assert.equal(corrected.affirmed, false);
|
|
assert.deepEqual(corrected.suggestion, { bristolType: 4, color: 'brown', confidence: 0.83 });
|
|
assert.deepEqual(corrected.correctedFields, ['bristolType']);
|
|
|
|
const recorrected = changeVisualField(corrected, 'color', 'black');
|
|
assert.deepEqual(recorrected.correctedFields, ['bristolType', 'color']);
|
|
assert.deepEqual(changeVisualField(recorrected, 'note', 'ignored').correctedFields, ['bristolType', 'color']);
|
|
});
|
|
|
|
test('does not invent an AI correction when the user never had a suggestion', () => {
|
|
const manual = changeVisualField(createSuggestionState(null), 'bristolType', 6);
|
|
|
|
assert.equal(manual.status, 'manual');
|
|
assert.equal(manual.suggestion, null);
|
|
assert.deepEqual(manual.correctedFields, []);
|
|
});
|
|
|
|
test('re-picking the suggested value is not treated as a correction', () => {
|
|
const confirmed = confirmSuggestion(createSuggestionState({ status: 'suggestion', bristolType: 4, color: 'brown', confidence: 0.83 }));
|
|
|
|
assert.equal(changeVisualField(confirmed, 'bristolType', 4).status, 'confirmed');
|
|
assert.deepEqual(changeVisualField(confirmed, 'bristolType', 4).correctedFields, []);
|
|
});
|
|
|
|
test('labels suggested, confirmed, corrected, and manual states distinctly', () => {
|
|
const pending = createSuggestionState({ status: 'suggestion', bristolType: 4, color: 'brown', confidence: 0.83 });
|
|
const confirmed = confirmSuggestion(pending);
|
|
const corrected = changeVisualField(confirmed, 'bristolType', 2);
|
|
const manual = changeVisualField(createSuggestionState(null), 'bristolType', 6);
|
|
|
|
const labels = [pending, confirmed, corrected, manual].map(state => provenanceCopy(state, { bristolType: 2, color: 'brown' }).label);
|
|
|
|
assert.deepEqual(labels, ['AI SUGGESTION', 'AI PREFILLED', 'CORRECTED BY YOU', 'YOUR OWN ENTRY']);
|
|
assert.equal(provenanceCopy(createSuggestionState(null), {}).label, '');
|
|
});
|
|
|
|
test('screen-reader copy carries the AI confidence and who is in control', () => {
|
|
const pending = createSuggestionState({ status: 'suggestion', bristolType: 4, color: 'brown', confidence: 0.83 });
|
|
const confirmed = confirmSuggestion(pending);
|
|
const corrected = changeVisualField(confirmed, 'bristolType', 2);
|
|
const manual = changeVisualField(createSuggestionState(null), 'bristolType', 6);
|
|
|
|
assert.match(provenanceCopy(pending, { bristolType: 4, color: 'brown' }).aria, /83 percent confidence/i);
|
|
assert.match(provenanceCopy(pending, { bristolType: 4, color: 'brown' }).aria, /not saved until you confirm/i);
|
|
assert.match(provenanceCopy(confirmed, { bristolType: 4, color: 'brown' }).aria, /83 percent confidence/i);
|
|
assert.match(provenanceCopy(confirmed, { bristolType: 4, color: 'brown' }).aria, /You confirmed/i);
|
|
|
|
const correctedAria = provenanceCopy(corrected, { bristolType: 2, color: 'brown' }).aria;
|
|
assert.match(correctedAria, /You changed/i);
|
|
assert.match(correctedAria, /Type 4/);
|
|
assert.match(correctedAria, /Type 2/);
|
|
assert.equal(corrected.affirmed, false);
|
|
assert.match(provenanceCopy(manual, { bristolType: 6, color: 'brown' }).aria, /No AI suggestion/i);
|
|
});
|