Files
timmy-talking-turd/tests/draft.test.js
T
timmy dce668e72b
Quality gates / quality (pull_request) Successful in 1m54s
feat: guarantee manual fallback and in-session draft recovery (#12)
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
2026-10-07 08:12:43 +00:00

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);
});