pediatric-ai-scribe-v3/test/assistant-image-done.test.js
Daniel 2f7233f317 perf: store real image previews in MinIO, generated at creation and on demand
Gallery tiles are 56px but were downloading the full ~280kB original. Previews
are now rendered with sharp and stored beside the originals in the same MinIO
bucket under a thumbs/ prefix, so nothing about credentials, lifecycle or backup
changes. Measured on live assets: 216-294kB originals become 13-19kB at 256px,
about 16x smaller; 640px is about 4x.

Both paths, as asked:
- Rendered when a job completes, so the first viewer never waits for a resize.
  A preview failure never unmakes a finished job.
- Rendered on demand for anything that has none — the existing 26 images work
  immediately with no backfill required, and the result is stored for next time.

Boundaries that matter more than the speed:
- Only 256 and 640 are honoured. An open width parameter would let a caller
  drive arbitrary resizes.
- Permission is checked against the ORIGINAL before a preview is served, so a
  preview can never widen who can see an image.
- Previews carry their own SHA-256 and owner headers, because the client
  verifies both on every asset; sending the original's checksum would be
  rejected as tampering, which is that check working correctly.
- Still private, no-store. The client asset pattern was widened to exactly
  ?w=256 and ?w=640 and nothing else.

Client-side downscaling stays as the fallback when a preview cannot be produced.

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

43 lines
2.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: '/);
});