pediatric-ai-scribe-v3/test/assistant-image-done.test.js
Daniel 500fe2c12a fix: image Done state, chat titles that keep whole words, and readable extension cards
The Create image popup showed "Generating image…" forever even after the job
finished. The status poll called fetchAssistantImageJob, which was never
imported, so every tick threw ReferenceError — and the catch treated that like a
transient network failure and rescheduled, permanently. The import is added, a
test now asserts that every api.js function the assistant calls is actually
imported, and the poll distinguishes a programming error (surface it) from a
transient one (retry, but not forever).

Chat titles were hard-cut at 60 characters mid-word, so "Rickets Radiographic
Fea" was all the Create image picker could ever show. The server already allows
160, so titles now keep whole words up to that, and each view decides its own
visible length from the width it actually has rather than inheriting one cut made
at save time. A single very long token still falls back to a hard cut.

Extension cards led with the number at 20px with word-break:break-all, so
"5616/3764/5619" wrapped as "5616/3764/56 19" — unreadable, and unsafe to dial
from. The name leads now, since that is what the eye hunts for in a list of
fifty; the number follows in tabular figures and may only break between groups,
never inside a run of digits. Cards share a minimum height so a grid reads as
rows rather than a ragged mosaic.

The collapsed rail's brand kept its expanded margin-right:auto, which pushed the
stethoscope off the axis the two buttons sat on. Every child of the collapsed
head is now the same centred fixed-size box.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018e1PLqrKgAM9jQhFKRnbLd
2026-09-10 06:44:22 +02:00

69 lines
4 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const root = path.join(__dirname, '..');
const read = f => fs.readFileSync(path.join(root, f), 'utf8');
// The Create image popup showed "Generating image…" forever even though the job
// had finished. Cause: the status poll called fetchAssistantImageJob, which was
// never imported, so every tick threw ReferenceError — and the catch treated it
// as a transient failure and rescheduled, permanently.
test('every api.js function the assistant calls is actually imported', () => {
const src = read('public/js/clinicalAssistant.js');
const api = read('public/js/assistant/api.js');
const marker = "from './assistant/api.js';";
const importBlock = src.slice(0, src.indexOf(marker));
const body = src.slice(importBlock.length);
const exported = [...api.matchAll(/export function (\w+)/g)].map(m => m[1]);
assert.ok(exported.length > 5, 'found the api surface');
const missing = exported.filter(name =>
new RegExp('\\b' + name + '\\s*\\(').test(body) && !new RegExp('\\b' + name + '\\b').test(importBlock));
assert.deepEqual(missing, [], 'called without being imported: ' + missing.join(', '));
});
test('a broken poll surfaces instead of retrying forever', () => {
const src = read('public/js/clinicalAssistant.js');
const poll = src.slice(src.indexOf('var pollAttempts = 0;'), src.indexOf('// ── Images gallery'));
// A programming error must not look like a slow image.
assert.match(poll, /pollError instanceof ReferenceError \|\| pollError instanceof TypeError/);
assert.match(poll, /hooks\.onError.*Cannot check image status/, 'and the user is told');
assert.match(poll, /console\.error\('\[clinical-assistant\] image polling is broken'/);
// Even genuine transient failures cannot retry indefinitely.
assert.match(poll, /if \(\+\+pollAttempts > 240\)/);
assert.match(poll, /the image may still finish/, 'and the message says the work was not lost');
});
test('the done handler still reports success when a later step fails', () => {
const src = read('public/js/clinicalAssistant.js');
// Rendering history or opening a preview must not turn a finished image into
// an error state.
assert.match(src, /catch \(doneError\) \{\s*\n\s*if \(typeof hooks\.onError === 'function'\) hooks\.onError\('Image ready: '/);
});
test('extension cards put the name first and never wrap a number mid-digit', () => {
const js = read('public/js/extensions.js');
const css = read('public/css/styles.css');
// The number led at 20px with word-break:break-all, so "5616/3764/5619"
// wrapped as "5616/3764/56 19" — unreadable, and unsafe to dial.
// Scope to the card renderer; other handlers reference these classes too.
const card = js.slice(js.indexOf("html += '<div class=\"ext-card\""), js.indexOf('return html;'));
assert.match(card, /<span class="ext-name">/);
assert.ok(card.indexOf('ext-name') < card.indexOf('ext-number'), 'the name is the scan target and leads');
assert.match(card, /<wbr>/, 'numbers may only break between groups');
assert.doesNotMatch(css, /\.ext-number\{[^}]*word-break:break-all/, 'never inside a run of digits');
assert.match(css, /\.ext-number\{[^}]*word-break:normal/);
// Uniform boxes so a grid reads as rows rather than a ragged mosaic.
assert.match(css, /\.ext-card\{[\s\S]*?min-height:96px;/);
assert.match(css, /@media\(max-width:640px\)\{ \.ext-card\{ min-height:0; \}/, 'except on a phone');
});
test('a saved chat title keeps whole words', () => {
const src = read('public/js/clinicalAssistant.js');
// slice(0, 60) cut mid-word, and every view inherited that one arbitrary cut.
assert.doesNotMatch(src, /function deriveChatTitle\(\)[\s\S]{0,240}?slice\(0, 60\)/);
assert.match(src, /truncateOnWord\(String\(first && first\.content/);
assert.match(src, /if \(lastSpace > limit \* 0\.5\) cut = cut\.slice\(0, lastSpace\);/,
'a single very long token still falls back to a hard cut');
});