Quality gates / quality (pull_request) Successful in 2m53s
Field finding #67: a hard/lumpy (Bristol 1-2) entry now asks a short follow-up before saving — difficult to pass, soreness, visible red blood, truly black/sticky/tarry versus dark brown, and dehydration. Every answer is user-reported and starts unanswered; nothing is inferred from the photo, and answering "dark brown" never silently escalates as black stool. A confirmed red-flag answer writes the same symptom flag manual logging already uses, so the existing deterministic escalation is the only escalation path. Mild reports get concise non-diagnostic self-care that states the photo cannot rule out bleeding, and it is withdrawn whenever a red flag is confirmed. - src/followup.js: pure follow-up contract (9 unit tests, tests/followup.test.js) - app.js + styles.css: follow-up block on the safety step for Type 1-2 entries only - tests/followup.acceptance.mjs: browser acceptance incl. that the AI suggestion surface still contains no symptom wording - CI + release builder gates: npm run test:followup, syntax checks, manifest gate - scripts/record_release_demo.mjs: records the real follow-up, mild guidance, and the black/tarry alert handoff with synthetic data
100 lines
5.7 KiB
JavaScript
100 lines
5.7 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
|
|
const demoPath = new URL('../scripts/record_release_demo.mjs', import.meta.url);
|
|
const builderPath = new URL('../scripts/build_release.py', import.meta.url);
|
|
|
|
test('release demo visibly explains the CI-protected browser path without overstating safety', async () => {
|
|
const demo = await readFile(demoPath, 'utf8');
|
|
|
|
assert.match(demo, /Automated checks replay this synthetic path before review/);
|
|
assert.match(demo, /One clear photo action\. Manual logging stays one tap away\./);
|
|
assert.match(demo, /Camera closed cleanly — gallery and manual logging are still available/);
|
|
assert.match(demo, /#camera-photo.*dispatchEvent\('cancel'\)/s);
|
|
assert.match(demo, /#gallery-photo.*synthetic-type4\.jpg/s);
|
|
assert.match(demo, /tests\/fixtures\/synthetic-type4\.jpg/);
|
|
assert.match(demo, /The pinned bootstrap verifies both model files before starting on private loopback/);
|
|
assert.match(demo, /AI may suggest visible form, broad color, and image quality — never symptoms or diagnosis/);
|
|
assert.match(demo, /Hermes Agent connected/);
|
|
assert.match(demo, /Photos stay out of chat/);
|
|
assert.match(demo, /I barfed/);
|
|
assert.match(demo, /Urgent language is intercepted deterministically before Hermes/);
|
|
assert.match(demo, /SLEEK\. SIMPLE\.<br>HERMES-POWERED\./);
|
|
});
|
|
|
|
test('release demo exercises the vision-offline manual path and the retained draft', async () => {
|
|
const demo = await readFile(demoPath, 'utf8');
|
|
|
|
assert.match(demo, /Vision worker offline — the AI and consent steps disappear instead of failing after upload/);
|
|
assert.match(demo, /No consent prompt and no upload when analysis cannot run/);
|
|
assert.match(demo, /One tap still reaches manual Bristol entry/);
|
|
assert.match(demo, /Closing the log keeps the draft in this session instead of throwing it away/);
|
|
assert.match(demo, /Start over clears the kept draft — nothing lingers silently/);
|
|
assert.match(demo, /#manual-from-scan/);
|
|
assert.match(demo, /#discard-draft/);
|
|
assert.match(demo, /\.draft-resume/);
|
|
// The offline pass must be a real state change driven by the app's own readiness contract.
|
|
assert.match(demo, /providerReady: false|visionOffline/);
|
|
});
|
|
|
|
test('release demo shows the provisional, confirmed, and corrected suggestion states on camera', async () => {
|
|
const demo = await readFile(demoPath, 'utf8');
|
|
|
|
assert.match(demo, /\[data-provenance="pending"\]/);
|
|
assert.match(demo, /The suggestion stays provisional — nothing is applied or saved until you confirm it/);
|
|
assert.match(demo, /Confirming is explicit, and every suggested value is still yours to overrule/);
|
|
assert.match(demo, /Change a suggested value and Timmy clears the AI-confirmed state/);
|
|
assert.match(demo, /\[data-provenance="corrected"\]/);
|
|
assert.match(demo, /Only visible form and color were ever suggested — urgency, discomfort, and symptoms stay yours/);
|
|
assert.match(demo, /Type 2/, 'the retained draft in the demo must show the value the user corrected to');
|
|
});
|
|
|
|
test('release builder gates the sleek shell, Hermes chat, and bootstrap syntax', async () => {
|
|
const builder = await readFile(builderPath, 'utf8');
|
|
assert.match(builder, /"test:sleek"/);
|
|
assert.match(builder, /"test:mobile-capture"/);
|
|
assert.match(builder, /"test:manual-fallback"/);
|
|
assert.match(builder, /Manual fallback: vision-offline logging stays one action away with an in-session draft/);
|
|
assert.match(builder, /"manual_fallback_draft_recovery": "passed"/);
|
|
assert.match(builder, /"src\/draft\.js"/);
|
|
assert.match(builder, /"src\/vision-readiness\.js"/);
|
|
assert.match(builder, /offline vision status/);
|
|
assert.match(builder, /"sleek_hermes_chat_acceptance": "passed"/);
|
|
assert.match(builder, /Sleek three-destination shell/);
|
|
assert.match(builder, /"bash", "-n", "scripts\/bootstrap_selfhost_smolvlm\.sh"/);
|
|
});
|
|
|
|
test('release builder gates the provisional suggestion states the same way CI does', async () => {
|
|
const builder = await readFile(builderPath, 'utf8');
|
|
|
|
assert.match(builder, /"test:provisional"/);
|
|
assert.match(builder, /"provisional_suggestion_state": "passed"/);
|
|
assert.match(builder, /"src\/suggestion\.js"/);
|
|
assert.match(builder, /AI suggestions stay provisional/);
|
|
});
|
|
|
|
test('release demo visibly exercises the user-reported hard-stool follow-up and its red-flag handoff', async () => {
|
|
const demo = await readFile(demoPath, 'utf8');
|
|
|
|
assert.match(demo, /\[data-followup-block\]/);
|
|
assert.match(demo, /Type 2 is hard or lumpy, so Timmy asks about soreness, hydration, blood, and black-or-tarry — never guessed from the photo/);
|
|
assert.match(demo, /\[data-followup="bloodVisible"\]\[data-answer="no"\]/);
|
|
assert.match(demo, /Every answer is yours to give, and a skipped question stays unanswered/);
|
|
assert.match(demo, /#followup-guidance/);
|
|
assert.match(demo, /Mild report: steady water, no straining, gentle care — general self-care, not a diagnosis/);
|
|
assert.match(demo, /And it never claims the photo ruled out bleeding/);
|
|
assert.match(demo, /\[data-followup="tarryBlack"\]\[data-answer="yes"\]/);
|
|
assert.match(demo, /Say it was truly black and tarry and the same deterministic red-flag path fires instead/);
|
|
assert.match(demo, /Dark brown is not silently escalated, and the flag clears the moment the answer changes/);
|
|
});
|
|
|
|
test('release builder gates the hard-stool follow-up the same way CI does', async () => {
|
|
const builder = await readFile(builderPath, 'utf8');
|
|
|
|
assert.match(builder, /"test:followup"/);
|
|
assert.match(builder, /"hard_stool_followup": "passed"/);
|
|
assert.match(builder, /"src\/followup\.js"/);
|
|
assert.match(builder, /Hard-stool follow-up: a Bristol 1-2 entry asks user-reported questions/);
|
|
});
|