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
43 lines
2.4 KiB
JavaScript
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: '/);
|
|
});
|