Quality gates / quality (pull_request) Successful in 1m54s
Logging must never depend on AI availability, and an unsaved entry must not disappear when the user navigates away mid-log. - add src/vision-readiness.js so the photo flow consumes one readiness contract - when the worker is offline/disabled/unknown, offer no AI action and no analysis-consent prompt, and keep manual Bristol entry one action away - add src/draft.js and make the draft own the photo so a chosen photo and entered context survive a recoverable close or navigation, with a visible kept-draft summary and an explicit start over - drafts stay in session memory only: a photo awaiting consent is never persisted, and reload starts clean - service worker now refuses to cache image bodies in addition to analysis requests and no-store responses - tests: 2 new unit files, 3 service-worker privacy assertions (1 hold-out), 1 browser acceptance file, release-demo and CI/package assertions - release demo now visibly replays the vision-offline pass and the retained draft with synthetic data and an in-script assertion
55 lines
2.2 KiB
JavaScript
55 lines
2.2 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createDraft, draftNotice, isDraftDirty } from '../src/draft.js';
|
|
|
|
test('a fresh draft is empty and reports nothing to resume', () => {
|
|
const draft = createDraft();
|
|
assert.equal(isDraftDirty(draft), false);
|
|
assert.equal(draftNotice(draft), '');
|
|
assert.equal(draft.bristolType, 4);
|
|
assert.equal(draft.color, 'brown');
|
|
assert.deepEqual(draft.symptoms, {});
|
|
});
|
|
|
|
test('a fresh draft is independent so one log cannot leak into the next', () => {
|
|
const first = createDraft();
|
|
first.symptoms.blood = true;
|
|
first.note = 'leaked';
|
|
const second = createDraft();
|
|
assert.equal(second.symptoms.blood, undefined);
|
|
assert.equal(second.note, '');
|
|
assert.equal(isDraftDirty(second), false);
|
|
});
|
|
|
|
test('any user-entered context makes the draft worth resuming', () => {
|
|
const cases = [
|
|
[{ typeChosen: true, bristolType: 6 }, /type 6/i],
|
|
[{ note: 'After lunch' }, /note/i],
|
|
[{ urgency: 3 }, /urgency/i],
|
|
[{ discomfort: 2 }, /discomfort/i],
|
|
[{ photoDataUrl: 'data:image/jpeg;base64,abc' }, /photo/i],
|
|
[{ symptoms: { blood: true } }, /symptom/i],
|
|
];
|
|
for (const [patch, expected] of cases) {
|
|
const draft = { ...createDraft(), ...patch };
|
|
assert.equal(isDraftDirty(draft), true, JSON.stringify(patch));
|
|
assert.match(draftNotice(draft), expected);
|
|
}
|
|
});
|
|
|
|
test('untouched defaults are not treated as user context', () => {
|
|
assert.equal(isDraftDirty({ ...createDraft(), bristolType: 7 }), false);
|
|
assert.equal(isDraftDirty({ ...createDraft(), color: 'brown', urgency: 0, discomfort: 0, note: ' ' }), false);
|
|
assert.equal(draftNotice({ ...createDraft(), color: 'brown' }), '');
|
|
});
|
|
|
|
test('a changed color is user context and unconfirmed AI defaults are not', () => {
|
|
assert.equal(isDraftDirty({ ...createDraft(), color: 'black' }), true);
|
|
assert.match(draftNotice({ ...createDraft(), color: 'black' }), /color/i);
|
|
});
|
|
|
|
test('draft copy never implies the photo or note left this device', () => {
|
|
const notice = draftNotice({ ...createDraft(), typeChosen: true, photoDataUrl: 'data:image/jpeg;base64,abc', note: 'x' });
|
|
assert.doesNotMatch(notice, /upload|server|sent|cloud/i);
|
|
});
|