pediatric-ai-scribe-v3/test/generated-images.integration.js
Daniel 15a8b399ba
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 57s
Forgejo Docker Build / Root app tests (push) Successful in 53s
Forgejo Android APK / Build signed APK (push) Successful in 1m59s
Forgejo Docker Build / Build Docker image (push) Has been cancelled
Forgejo Docker Build / Deploy to the host (push) Has been cancelled
feat: slides are built by pandoc from markdown, with a reference template
pptxgenjs is gone, and with it 269 lines of hand-rolled markdown parsing.

It stretched every image. Reading the slide XML it emitted shows why: it writes
the target box verbatim with <a:stretch/> and a no-op srcRect, so a 200x800
image handed an 11.8x3.9 box came out 1:4 squashed to 3:1. It could not do
better — it never measures an image, and its own getSizeFromImage is commented
out and marked "currently unused", reaching for a package called sizeof that
does not exist. pandoc measures them: a 300x175 source renders at aspect 1.714
and a 160x360 at 0.445, verified by rendering the deck to PDF and looking at it.

Tables, ordered and unordered lists, bold, italic and subscripts all come out
natively, and the fonts, palette and slide layouts come from
assets/learning/slides-reference.pptx. Design now lives in that file: restyling
the decks means editing it in PowerPoint, not editing this route.

Only images the requester owns can reach a deck. pandoc resolves an image link
against the filesystem, so a markdown link naming any local path would read that
file into the presentation. Images are fetched by id through the ownership
check, written into a per-request temporary directory under names we choose, and
every image link that did not resolve is removed rather than passed through. The
directory is removed in a finally block, and the conversion has a 60s timeout so
it cannot hang a request.

pandoc is in the image rather than a sidecar, because an export must not fail
for reasons outside this container. It costs 197MB (307 -> 504).

Removing pptxgenjs also removed image-size, and with it both high-severity
advisories — GHSA-w3rx-r6r6-pgpr and GHSA-5p2g-fcmc-qvqq, ICNS/JXL/HEIF parser
denial of service, ranged <=2.0.2 with no fixed release to upgrade to. npm audit
goes from 2 high and 2 moderate to 2 moderate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-11 13:25:24 +02:00

414 lines
38 KiB
JavaScript

// Run only with scripts/test-generated-images.sh: disposable internal-network PG + private S3.
const test = require('node:test');
const assert = require('node:assert/strict');
const { Pool } = require('pg');
const { createImageService, requestKey } = require('../src/utils/generatedImages');
const { createStorage, inspect } = require('../src/utils/generatedImageStorage');
const links = require('../src/utils/generatedImageLinks');
const { savedChatPayload } = require('../src/utils/clinicalConversation');
const revisions = require('../src/utils/promptRevisions');
const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aL9sAAAAASUVORK5CYII=', 'base64');
if (!process.env.GENERATED_IMAGES_TEST_DB || new URL(process.env.GENERATED_IMAGES_TEST_DB).hostname !== 'test-pg' || new URL(process.env.GENERATED_IMAGES_TEST_DB).pathname !== '/image_lane') throw Error('Use the disposable test script, never an application database');
const pool = new Pool({ connectionString: process.env.GENERATED_IMAGES_TEST_DB });
const db = { pool, query: (sql, params) => pool.query(sql, params), async all(sql, params) { return (await pool.query(sql, params)).rows; }, async get(sql, params) { return (await pool.query(sql, params)).rows[0]; } };
let paid = 0;
const generate = async () => { paid++; return inspect(png); };
const storage = createStorage();
const service = () => createImageService({ db, storage, generate });
let image;
test.before(async () => {
await pool.query('CREATE TABLE users(id INTEGER PRIMARY KEY); INSERT INTO users VALUES(101),(102),(103); CREATE TABLE app_settings(key TEXT PRIMARY KEY,value TEXT,updated_at TIMESTAMPTZ DEFAULT NOW()); CREATE TABLE learning_content(id SERIAL PRIMARY KEY,body TEXT,published BOOLEAN DEFAULT false);');
for (const migration of ['1777700000000_add-prompt-revisions','1777800000000_generated-images','1777900000000_image-context']) {
let sql; require('../migrations/' + migration).up({ sql: value => { sql = value; } }); await pool.query(sql);
}
await pool.query("INSERT INTO app_settings(key,value) VALUES('learning_hub.image_model','synthetic-learning-image'),('clinical_assistant.image_model','synthetic-clinical-image')");
});
test.after(async () => { storage.close(); await pool.end(); });
test('private S3 asset, encrypted durable snapshot, idempotency and owner/workflow isolation', async () => {
const jobs = service(); const before = paid;
const input = { prompt: 'Synthetic flowchart comparison', layout: 'portrait' };
image = await jobs.enqueue(101, 'clinical_assistant', input, 'first');
const dup = await jobs.enqueue(101, 'clinical_assistant', input, 'first'); assert.equal(dup.jobId, image.jobId);
await assert.rejects(jobs.enqueue(101, 'clinical_assistant', { prompt: 'different' }, 'first'), e => e.statusCode === 409);
const row = await db.get('SELECT * FROM generated_image_jobs WHERE id=$1', [image.jobId]);
assert.match(row.prompt_cipher, /^enc1:/); assert.ok(!row.prompt_cipher.includes(input.prompt)); assert.equal(row.budget, 32000); assert.equal(row.model, 'synthetic-clinical-image');
await jobs.tick(); assert.equal(paid, before + 1);
const done = await jobs.get(image.jobId, 101, 'clinical_assistant'); assert.equal(done.status, 'done'); assert.equal(done.imageUrl, '/api/generated-images/' + image.jobId);
assert.deepEqual((await jobs.asset(image.jobId, { id: 101 })).bytes, png);
await assert.rejects(jobs.get(image.jobId, 102, 'clinical_assistant'), e => e.statusCode === 404);
await assert.rejects(jobs.get(image.jobId, 101, 'learning_hub'), e => e.statusCode === 404);
await assert.rejects(jobs.asset(image.jobId, { id: 102, role: 'admin' }), e => e.statusCode === 404);
assert.equal((await db.get('SELECT staged_bytes FROM generated_image_jobs WHERE id=$1', [image.jobId])).staged_bytes, null);
const anonymous = await fetch('http://test-s3:9000/generated-images/assets/' + image.jobId); assert.equal(anonymous.status, 403);
});
test('saved chat keeps exact body/citations, validates owned asset and durable job references', async () => {
const body = '## Exact\nDose [3, 1].\n| A | Source |\n| --- | --- |\n| 5 mg | [1] |';
const sources = [{ number: 3, page: 19, title: 'Synthetic three' }, { number: 1, page: 4, title: 'Synthetic one' }];
const payload = savedChatPayload({ messages: [{ role: 'assistant', content: body, sources, imageJobs: [image] }], lastAnswer: body, sources, generatedImage: '/api/generated-images/' + image.jobId });
await links.validateChat(db, payload, 101);
assert.equal(payload.messages[0].content, body); assert.deepEqual(payload.sources, sources);
assert.deepEqual(JSON.parse(JSON.stringify(payload)), payload);
await assert.rejects(links.validateChat(db, payload, 102), e => e.statusCode === 403);
});
test('Learning has independent model/prompt revision and authenticated current-publication grants; clinical UUID cannot be published', async () => {
const jobs = service();
const revision = await revisions.mutate(db, 'learning_hub.image_behavior', { action: 'save', value: 'Synthetic learning instructions.', expectedRevision: 0, actor: 101 });
const job = await jobs.enqueue(101, 'learning_hub', { prompt: 'Learning diagram' }, 'learning');
const row = await db.get('SELECT * FROM generated_image_jobs WHERE id=$1', [job.jobId]);
assert.equal(row.prompt_revision, revision.revision); assert.equal(row.model, 'synthetic-learning-image');
assert.match(require('../src/utils/crypto').decryptString(row.prompt_cipher), /Synthetic learning instructions/);
const changed = await revisions.mutate(db, 'learning_hub.image_behavior', { action: 'save', value: 'Second synthetic Learning instructions.', expectedRevision: revision.revision, actor: 101 });
const restored = await revisions.mutate(db, 'learning_hub.image_behavior', { action: 'restore', revisionId: revision.revision, expectedRevision: changed.revision, actor: 101 });
assert.equal(restored.value, revision.value);
assert.equal((await revisions.read(db, 'learning_hub.image_behavior', restored.revision)).restoredFrom, revision.revision);
assert.equal((await db.get('SELECT prompt_revision FROM generated_image_jobs WHERE id=$1', [job.jobId])).prompt_revision, revision.revision);
await jobs.tick();
const id = (await db.query("INSERT INTO learning_content(body,published) VALUES('',false) RETURNING id")).rows[0].id;
const client = await pool.connect();
try {
await client.query('BEGIN');
await assert.rejects(links.validateLearning(client, '<img src="/api/generated-images/' + image.jobId + '">', 101, id), e => e.statusCode === 403);
const ids = await links.validateLearning(client, '<img src="/api/generated-images/' + job.jobId + '">', 101, id);
await links.setLinks(client, id, ids); await client.query('COMMIT');
} finally { client.release(); }
await assert.rejects(jobs.asset(job.jobId, { id: 102, role: 'user' }), e => e.statusCode === 404);
assert.deepEqual((await jobs.asset(job.jobId, { id: 102, role: 'moderator' })).bytes, png);
await db.query('UPDATE learning_content SET published=true WHERE id=$1', [id]);
assert.deepEqual((await jobs.asset(job.jobId, { id: 102, role: 'user' })).bytes, png);
await db.query('UPDATE learning_content SET published=false WHERE id=$1', [id]);
await assert.rejects(jobs.asset(job.jobId, { id: 102, role: 'user' }), e => e.statusCode === 404);
await db.query('DELETE FROM generated_image_links WHERE content_id=$1', [id]);
await assert.rejects(jobs.asset(job.jobId, { id: 102, role: 'moderator' }), e => e.statusCode === 404);
await assert.rejects(links.validateLearning(db, '/api/generated-images/' + job.jobId, 102, id), e => e.statusCode === 403);
await assert.rejects(db.query('INSERT INTO generated_image_links VALUES($1,$2)', [image.jobId, id]), /Only Learning assets/);
await assert.rejects(db.query("UPDATE generated_image_jobs SET workflow='learning_hub' WHERE id=$1", [image.jobId]), /immutable/);
});
test('restart resumes queued/storage stages, but never retries ambiguous paid stages; lease fencing and SKIP LOCKED are native', async () => {
const jobs = service(); const before = paid;
const queued = await jobs.enqueue(101, 'clinical_assistant', { prompt: 'Queued across restart' }, 'restart');
await service().tick(); assert.equal((await jobs.get(queued.jobId,101,'clinical_assistant')).status, 'done');
const unknown = await jobs.enqueue(101, 'clinical_assistant', { prompt: 'Crash during provider request' }, 'unknown');
const old = await jobs.claim(); assert.equal(old.id, unknown.jobId);
await db.query("UPDATE generated_image_jobs SET lease_until=NOW()-interval '1 second' WHERE id=$1", [unknown.jobId]);
await service().tick(); assert.equal((await jobs.get(unknown.jobId,101,'clinical_assistant')).outcome, 'unknown');
assert.equal(paid, before + 1);
const fenced = await db.query("UPDATE generated_image_jobs SET stage='storing' WHERE id=$1 AND lease_token=$2 AND stage='generating' RETURNING id", [old.id,old.lease_token]); assert.equal(fenced.rows.length, 0);
const storing = await jobs.enqueue(101,'clinical_assistant',{ prompt: 'Storage crash' },'storage');
await createImageService({ db, generate, storage: { ...storage, put: async () => { throw Error('synthetic unavailable'); } } }).tick();
const staged = await db.get('SELECT stage,staged_bytes FROM generated_image_jobs WHERE id=$1', [storing.jobId]);
assert.equal(staged.stage, 'storing'); assert.notDeepEqual(staged.staged_bytes, png);
assert.deepEqual(require('../src/utils/crypto').decryptBuffer(staged.staged_bytes), png);
await db.query("UPDATE generated_image_jobs SET lease_until=NOW()-interval '1 second' WHERE id=$1",[storing.jobId]);
await service().tick(); assert.equal((await jobs.get(storing.jobId,101,'clinical_assistant')).status, 'done'); assert.equal(paid,before+2);
const first = await jobs.enqueue(101,'clinical_assistant',{ prompt:'lock one' },'lock-one');
const second = await jobs.enqueue(102,'clinical_assistant',{ prompt:'lock two' },'lock-two');
const client = await pool.connect();
try {
await client.query('BEGIN'); await client.query('SELECT id FROM generated_image_jobs WHERE id=$1 FOR UPDATE',[first.jobId]);
const claim = await service().claim(); assert.equal(claim.id, second.jobId);
await client.query('COMMIT');
} finally { client.release(); }
await db.query("UPDATE generated_image_jobs SET stage='interrupted' WHERE id=ANY($1::uuid[])", [[first.jobId,second.jobId]]);
});
test('budget exact UTF16 assembly, malformed input and missing storage prevent paid calls; provider timeout is explicit unknown', async () => {
const jobs = service(); const before = paid;
await db.query("INSERT INTO app_settings(key,value) VALUES('clinical_assistant.image_behavior','X'),('clinical_assistant.image_budget','1000')");
const base = await jobs.snapshot('clinical_assistant',{ prompt:'x',layout:'square' });
const prompt = '😀'.repeat(Math.floor((1001-base.rendered.length)/2)) + ('x'.repeat((1001-base.rendered.length)%2));
const exact = await jobs.snapshot('clinical_assistant',{prompt,layout:'square'}); assert.equal(exact.rendered.length, 1000);
await assert.rejects(jobs.enqueue(101,'clinical_assistant',{prompt:prompt+'x',layout:'square'},'large'), e => e.statusCode === 413);
for (const input of [{prompt:''},{prompt:'x',model:'forbidden'},{prompt:'x',layout:'url'},{prompt:'x'.repeat(32001)}]) await assert.rejects(jobs.enqueue(101,'clinical_assistant',input,'bad'));
const unavailable = createImageService({ db, generate, storage: { ready: async () => { throw Error('synthetic storage outage'); } } });
await assert.rejects(unavailable.enqueue(101,'clinical_assistant',{prompt:'no call'},'offline'),e => e.statusCode === 503);
assert.equal(paid,before);
const unknown = await jobs.enqueue(101,'clinical_assistant',{prompt:'ambiguous timeout'},'timeout');
await createImageService({ db, storage, generate: async () => { paid++; throw Error('synthetic timeout after possible billing'); } }).tick();
await jobs.tick(); assert.equal(paid,before+1); assert.equal((await jobs.get(unknown.jobId,101,'clinical_assistant')).outcome,'unknown');
});
test('actual authenticated asset/settings and Learning content write routes enforce grants and publication atomically', async () => {
const express = require('express'); const fs = require('fs'); const vm = require('vm'); const jwt = require('jsonwebtoken');
const jobs = service();
await pool.query("ALTER TABLE learning_content ADD title TEXT, ADD slug TEXT, ADD category_id INTEGER, ADD subject TEXT, ADD content_type TEXT, ADD author_id INTEGER, ADD updated_at TIMESTAMPTZ DEFAULT NOW()");
const convert = sql => { let n=0; return sql.replace(/\?/g,()=>'$'+(++n)); };
let lockNotice;
const routeDb = { ...db, pool: { async connect() {
const client=await pool.connect(); return { release:()=>client.release(), query(sql,params) {
if(sql.includes('FOR UPDATE') && lockNotice) { lockNotice(); lockNotice=null; }
return client.query(sql,params);
} };
} }, getSetting: async key => (await db.get('SELECT value FROM app_settings WHERE key=$1',[key]))?.value,
async get(sql,params) { return db.get(convert(sql),params); },
async all(sql,params) { return db.all(convert(sql),params); },
async run(sql,params) { const r=await db.query(convert(sql),params);return {lastInsertRowid:r.rows[0]?.id,changes:r.rowCount}; }
};
function load(file,mocks) {
const module={exports:{}};
vm.runInNewContext(fs.readFileSync(file,'utf8'),{module,Buffer,console:{warn(){},error(){}},process:{env:{JWT_SECRET:'synthetic-signing-only',CLINICAL_ASSISTANT_MCP_WARMUP:'false'}},setTimeout(){},require:n=>{assert.ok(n in mocks,n);return mocks[n];}});
return module.exports;
}
const roles={101:'admin',102:'user',103:'moderator'};
const auth = load('src/middleware/auth.js',{'jsonwebtoken':jwt,'../db/database':{get:async(sql,params)=>sql.includes('user_sessions')?{id:1,last_activity:new Date()}:roles[params[0]]?{id:params[0],role:roles[params[0]]}:null},'../utils/sessions':{hashToken:()=> 'synthetic-hash'},'../utils/platform':{isMobileClient:()=>false}});
const imageRoutes=load('src/routes/generatedImages.js',{'express':express,'../middleware/auth':auth,'../utils/generatedImages':{...require('../src/utils/generatedImages'),service:()=>jobs},'../db/database':routeDb});
const learningRoutes=load('src/routes/learningAdmin.js',{'express':express,'../db/database':routeDb,'../middleware/auth':auth,'../utils/embeddings':{isEmbeddingsAvailable:()=>false},'../utils/generatedImageLinks':links});
await db.query('CREATE TABLE clinical_assistant_chats(id SERIAL PRIMARY KEY,user_id INTEGER,title TEXT,payload TEXT,created_at TIMESTAMPTZ DEFAULT NOW(),updated_at TIMESTAMPTZ DEFAULT NOW())');
const learningAI=load('src/routes/learningAI.js',{
express,multer:require('multer'),axios:{},path:require('path'),'../utils/ai':{},'../utils/imageTool':require('../src/utils/imageTool'),
'../middleware/auth':auth,'../db/database':routeDb,'../utils/crypto':require('../src/utils/crypto'),'../utils/urlSafety':require('../src/utils/urlSafety'),
'../utils/policy':{requireFeature:()=>()=>{}},'../utils/generatedImageLinks':links,'../utils/generatedImages':{service:()=>jobs}
});
await db.query('CREATE TABLE learning_categories(id SERIAL PRIMARY KEY,name TEXT);CREATE TABLE learning_questions(id SERIAL PRIMARY KEY,content_id INTEGER,sort_order INTEGER)');
const clinicalRoutes = load('src/routes/clinicalAssistant.js', {
express, axios: {}, crypto: require('crypto'), '../db/database': routeDb, '../middleware/auth': auth,
'../utils/ai': {}, '../utils/generatedImages': { ...require('../src/utils/generatedImages'), service: () => jobs },
'../utils/imageTool': require('../src/utils/imageTool'), '../utils/generatedImageLinks': links,
'../utils/logger': { audit() {}, error() {} }, '../utils/crypto': require('../src/utils/crypto'), '../utils/redis': {},
'../utils/clinicalPromptPool': { createClinicalPromptPool: () => ({}) }, '../utils/clinicalMcpClient': {}, '../utils/clinicalRetrieval': {},
'../utils/clinicalPrompts': require('../src/utils/clinicalPrompts'), '../utils/clinicalConversation': require('../src/utils/clinicalConversation'), '../utils/clinicalAnswer': require('../src/utils/clinicalAnswer')
});
// Execute REAL server registrations in order, including the actual blanket adminConfig guard.
const configRoutes=load('src/routes/adminConfig.js',{
express,'../db/database':routeDb,'../middleware/auth':auth,'../utils/prompts':{getAllPrompts:()=>[]},
'../utils/promptCatalog':{},'../utils/promptRevisions':{},'../utils/clinicalConversation':require('../src/utils/clinicalConversation'),
'../utils/logger':{},'../utils/errors':{},'../utils/ttsProvider':{},'../utils/litellm':{},'../utils/sttProvider':{},'../utils/embeddings':{}
});
const app=express();app.use(express.json());
const composition=fs.readFileSync('server.js','utf8');
vm.runInNewContext(composition.slice(composition.indexOf('// Routes\n'),composition.indexOf('// User-level preference:')),{
app,APP_VERSION:'synthetic',process:{env:{}},require(name){
if(name==='./src/routes/generatedImages') return imageRoutes;
if(name==='./src/routes/learningAdmin') return learningRoutes;
if(name==='./src/routes/learningAI') return learningAI;
if(name==='./src/routes/clinicalAssistant') return clinicalRoutes;
if(name==='./src/routes/adminConfig') return configRoutes;
if(name==='./src/middleware/auth') return auth;
if(name==='./src/db/database') return routeDb;
if(name==='./src/utils/models') return {activeProvider:'synthetic',getAvailableModelsWithOverrides:async()=>[],getEffectiveDefaultModel:async()=>''};
if(name==='./src/utils/ai') return {};
if(name==='./src/utils/generatedImages') return {service:()=>({start(){}})};
assert.match(name,/^\.\/src\/routes\//);return express.Router(); // no unrelated application modules/services
}
});
const server=app.listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));
const base='http://127.0.0.1:'+server.address().port;
const request=(path,owner,method='GET',body)=>fetch(base+path,{method,headers:{'Content-Type':'application/json',...(owner?{Authorization:'Bearer '+jwt.sign({userId:owner},'synthetic-signing-only')}: {})},body:body?JSON.stringify(body):undefined});
try {
assert.equal((await request('/api/health')).status,200);
assert.equal((await request('/api/models')).status,200);
assert.equal((await request('/api/admin/learning/image/jobs',null,'POST',{prompt:'denied'})).status,401);
assert.equal((await request('/api/admin/learning/image/jobs',102,'POST',{prompt:'denied'})).status,403);
assert.equal((await request('/api/admin/config',103)).status,403);
const moderatorJob=await request('/api/admin/learning/image/jobs',103,'POST',{prompt:'Moderator image',idempotencyKey:'moderator-image'});
assert.equal(moderatorJob.status,200,await moderatorJob.clone().text());
const moderatorId=(await moderatorJob.json()).jobId;
assert.equal((await request('/api/admin/learning/image/jobs/'+moderatorId,103)).status,200);await jobs.tick();
assert.equal((await request('/api/generated-images/'+image.jobId)).status,401);
assert.equal((await request('/api/generated-images/'+image.jobId,102)).status,404);
const bytes=await request('/api/generated-images/'+image.jobId+'?download=1',101);
assert.equal(bytes.status,200);assert.equal(bytes.headers.get('x-image-owner'),'101');assert.equal(bytes.headers.get('cache-control'),'private, no-store');assert.equal(bytes.headers.get('x-content-type-options'),'nosniff');assert.deepEqual(Buffer.from(await bytes.arrayBuffer()),png);
const denied=await request('/api/admin/learning/content',101,'POST',{title:'Forbidden',body:'<img src="/api/generated-images/'+image.jobId+'">',published:true});assert.equal(denied.status,403);
assert.equal((await db.get("SELECT COUNT(*)::int AS n FROM learning_content WHERE title='Forbidden'")).n,0);
const job=await jobs.enqueue(101,'learning_hub',{prompt:'Attach through actual CMS'},'route-attach');await jobs.tick();
const draft=await request('/api/admin/learning/content',101,'POST',{title:'Teaching',body:'<p>Exact body.</p><img src="/api/generated-images/'+job.jobId+'">',published:false});assert.equal(draft.status,200);const contentId=(await draft.json()).id;
assert.equal((await request('/api/generated-images/'+job.jobId,102)).status,404);
assert.equal((await request('/api/generated-images/'+job.jobId,103)).status,200);
assert.equal((await request('/api/admin/learning/content/'+contentId,103,'PUT',{published:true})).status,200);
assert.equal((await request('/api/generated-images/'+job.jobId,102)).status,200);
assert.equal((await request('/api/admin/learning/content/'+contentId,103,'PUT',{published:false})).status,200);
assert.equal((await request('/api/generated-images/'+job.jobId,102)).status,404);
// Concurrent unpublish holds the row while a body-only request reaches its lock.
await db.query('UPDATE learning_content SET published=true WHERE id=$1',[contentId]);
const unpublish=await pool.connect();
try {
await unpublish.query('BEGIN');await unpublish.query('UPDATE learning_content SET published=false WHERE id=$1',[contentId]);
let lockReached;const atLock=new Promise(r=>{lockReached=r;});lockNotice=lockReached;
const update=request('/api/admin/learning/content/'+contentId,103,'PUT',{body:'<p>Concurrent body [3].</p><img src="/api/generated-images/'+job.jobId+'">'});
await atLock;await unpublish.query('COMMIT');assert.equal((await update).status,200);
assert.equal((await db.get('SELECT published FROM learning_content WHERE id=$1',[contentId])).published,false,'body edit must not restore stale publication');
assert.equal((await request('/api/generated-images/'+job.jobId,102)).status,404);
assert.equal((await db.get('SELECT COUNT(*)::int AS n FROM generated_image_links WHERE content_id=$1',[contentId])).n,1);
} finally { await unpublish.query('ROLLBACK');unpublish.release(); }
const markdown='---\nmarp: true\n---\n# Original slide [3]\nDose 5 mg, page 19 [3].\n---\n# Image\n![Generated teaching visual](/api/generated-images/'+job.jobId+')\n';
const presentation=await request('/api/admin/learning/content',101,'POST',{title:'Presentation',content_type:'presentation',body:markdown,published:false});
assert.equal(presentation.status,200);const presentationId=(await presentation.json()).id;
const reopenedPresentation=await request('/api/admin/learning/content/'+presentationId,103);
assert.equal(reopenedPresentation.status,200);assert.equal((await reopenedPresentation.json()).content.body,markdown);
const pptx=await request('/api/admin/learning/generate-pptx',103,'POST',{markdown,title:'Synthetic presentation'});
assert.equal(pptx.status,200,await pptx.clone().text());
const zip=await require('jszip').loadAsync(Buffer.from(await pptx.arrayBuffer()));
const media=Object.keys(zip.files).filter(f=>/^ppt\/media\/.+\.png$/.test(f));assert.equal(media.length,1);
assert.deepEqual(await zip.files[media[0]].async('nodebuffer'),png);
assert.match(await zip.files['ppt/slides/slide1.xml'].async('string'),/Original slide \[3\]/);
assert.equal((await request('/api/admin/learning/content',102,'POST',{title:'No permission'})).status,403);
assert.equal((await request('/api/admin/image-settings/learning_hub',103,'PUT',{model:'synthetic',budget:32000})).status,403);
assert.equal((await request('/api/admin/image-settings/learning_hub',101,'PUT',{model:'synthetic-own-learning',budget:1500})).status,200);
assert.equal((await request('/api/admin/image-settings/learning_hub',101,'PUT',{model:'synthetic',budget:32001})).status,400);
const listing=await (await request('/api/image-jobs/clinical_assistant',101)).text();assert.ok(!listing.includes('prompt_cipher'));assert.ok(!listing.includes('staged_bytes'));
const body = ' Exact [3, 1].\n| Dose | Page |\n| 5 mg | 19 [3] |\n';
const payload = { lastAnswer: body, messages: [{ role: 'assistant', content: body, sources: [{ number: 3, page: 19 }], imageJobs: [{ jobId: image.jobId, status: 'forged', imageUrl: 'https://invalid.test' }] }], generatedImage: '/api/generated-images/' + image.jobId };
const saved = await request('/api/clinical-assistant/chats', 101, 'POST', payload); assert.equal(saved.status, 200); const savedId = (await saved.json()).id;
const reopened = await (await request('/api/clinical-assistant/chats/' + savedId, 101)).json();
assert.equal(reopened.chat.payload.lastAnswer, body); assert.equal(reopened.chat.payload.messages[0].content, body);
assert.deepEqual(reopened.chat.payload.messages[0].sources, [{ number: 3, page: 19 }]);
assert.deepEqual(reopened.chat.payload.messages[0].imageJobs, [{ jobId: image.jobId }]);
assert.equal(reopened.chat.payload.generatedImage, undefined, 'offbox image is session-only and never stored');
assert.match((await db.get('SELECT payload FROM clinical_assistant_chats WHERE id=$1', [savedId])).payload, /^enc1:/);
assert.equal((await request('/api/clinical-assistant/chats/' + savedId, 102)).status, 404);
assert.equal((await request('/api/clinical-assistant/chats', 102, 'POST', payload)).status, 403);
assert.equal((await request('/api/clinical-assistant/chats', 102, 'POST', { messages: [], lastAnswer: '![image](' + payload.generatedImage + ')' })).status, 403);
assert.equal((await request('/api/clinical-assistant/chats', 101, 'POST', { messages: [], generatedImageJobs: [{ jobId: 'bad' }] })).status, 400);
} finally { await new Promise(r=>server.close(r)); }
});
test('both workflows enforce exact 32000 UTF16 assembly and durable snapshots cannot change with admin settings', async () => {
const jobs = service(); const before = paid;
for (const workflow of ['clinical_assistant', 'learning_hub']) {
await db.query("INSERT INTO app_settings(key,value) VALUES($1,'32000') ON CONFLICT(key) DO UPDATE SET value='32000'", [workflow + '.image_budget']);
const base = await jobs.snapshot(workflow, { prompt: 'x', layout: 'square' });
const units = 32001 - base.rendered.length;
const prompt = '😀'.repeat(Math.floor(units / 2)) + 'x'.repeat(units % 2);
assert.equal((await jobs.snapshot(workflow, { prompt, layout: 'square' })).rendered.length, 32000);
await assert.rejects(jobs.enqueue(101, workflow, { prompt: prompt + 'x', layout: 'square' }, 'over-32000'), e => e.statusCode === 413);
const job = await jobs.enqueue(101, workflow, { prompt, layout: 'square' }, 'exact-32000');
const original = await db.get('SELECT * FROM generated_image_jobs WHERE id=$1', [job.jobId]);
assert.equal(original.prompt_units, 32000); assert.match(original.prompt_cipher, /^enc1:/);
for (const [field, value] of [['owner_id', 102], ['model', 'replacement'], ['budget', 1000], ['prompt_cipher', 'enc1:replacement'], ['prompt_revision', original.prompt_revision + 1], ['context_total', 9], ['context_included', 1]]) {
await assert.rejects(db.query(`UPDATE generated_image_jobs SET ${field}=$2 WHERE id=$1`, [job.jobId, value]), /immutable/);
}
await db.query("UPDATE app_settings SET value='1000' WHERE key=$1", [workflow + '.image_budget']);
let sent;
await createImageService({ db, storage, generate: async (snapshot, text) => { sent = { snapshot, text }; paid++; return inspect(png); } }).tick();
assert.equal(sent.snapshot.id, job.jobId); assert.equal(sent.snapshot.budget, 32000); assert.equal(sent.snapshot.model, original.model);
assert.equal(sent.text, require('../src/utils/crypto').decryptString(original.prompt_cipher));
}
assert.equal(paid, before + 2);
});
test('concurrent tool replay, owner/workflow identity and preflight failures never create duplicate paid jobs', async () => {
const jobs = service(); const before = paid; const tool = require('../src/utils/imageTool');
const opts = { owner: 101, workflow: 'clinical_assistant', body: { idempotencyKey: 'concurrent-tool' }, imageContext:{request:'Concurrent original request',history:[]}, images: jobs };
const result = prompt => ({ content: 'Exact body [3].', toolCalls: [{ id: 'one', type: 'function', function: { name: 'generate_image', arguments: JSON.stringify({ prompt }) } }] });
const [a, b] = await Promise.all([tool.dispatch(result('First replay diagram'), opts), tool.dispatch(result('Second replay diagram'), opts)]);
assert.equal(a.imageJobs[0].jobId, b.imageJobs[0].jobId);
const other = await jobs.enqueue(102, 'clinical_assistant', { prompt: 'Other owner' }, 'tool:concurrent-tool');
const learning = await jobs.enqueue(101, 'learning_hub', { prompt: 'Other workflow' }, 'tool:concurrent-tool');
assert.notEqual(other.jobId, a.imageJobs[0].jobId); assert.notEqual(learning.jobId, a.imageJobs[0].jobId);
await jobs.tick(); await jobs.tick(); await jobs.tick(); await jobs.tick(); assert.equal(paid, before + 3);
const noMigration = createImageService({ db: { query: async () => { throw Error('synthetic missing migration'); } }, storage, generate });
await assert.rejects(noMigration.ready(), e => e.statusCode === 503);
const noEncryption = createImageService({ db, storage, generate, encryption: { hasKey: () => false } });
await assert.rejects(noEncryption.enqueue(101, 'clinical_assistant', { prompt: 'No encryption' }, 'no-key'), e => e.statusCode === 503);
const noGateway = createImageService({ db, storage, env: {}, encryption: require('../src/utils/crypto') });
await assert.rejects(noGateway.ready(), e => e.statusCode === 503);
assert.equal(paid, before + 3);
});
test('worker stop during preflight or claim never starts a new paid request; queued work resumes safely', async () => {
for (const pauseAt of ['preflight', 'claim']) {
const jobs = service(); const before = paid;
const job = await jobs.enqueue(101, 'clinical_assistant', { prompt: 'Stop before payment' }, 'stop-' + pauseAt);
let reached, release;
const paused = new Promise(resolve => { reached = resolve; });
const gate = new Promise(resolve => { release = resolve; });
const worker = createImageService({ generate,
storage: { ...storage, ready: async () => { await storage.ready(); if (pauseAt === 'preflight') { reached(); await gate; } } },
db: { ...db, pool: { async connect() {
const client = await pool.connect();
return { release: () => client.release(), async query(...args) {
const result = await client.query(...args);
if (pauseAt === 'claim' && args[0] === 'COMMIT') { reached(); await gate; }
return result;
} };
} } }
});
worker.start(); await paused;
const stopped = worker.stop(); release(); await stopped;
assert.equal(paid, before, pauseAt); assert.equal((await jobs.get(job.jobId, 101, 'clinical_assistant')).status, 'pending');
await service().tick(); assert.equal(paid, before + 1); assert.equal((await jobs.get(job.jobId, 101, 'clinical_assistant')).status, 'done');
}
});
test('exact IMAGE HTTP input binds original request and contiguous whole recent turns; snapshots, UTF16 metadata and replay remain honest', async () => {
const http = require('node:http'); const captured = [];
const server = http.createServer(async (req,res) => {
const chunks=[]; for await (const c of req) chunks.push(c);
captured.push({path:req.url,body:JSON.parse(Buffer.concat(chunks))});
res.writeHead(200,{'Content-Type':'application/json'}); res.end(JSON.stringify({data:[{b64_json:png.toString('base64')}]}));
});
server.listen(0,'127.0.0.1'); await new Promise(r=>server.once('listening',r));
const old = process.env.LITELLM_API_BASE; process.env.LITELLM_API_BASE='http://127.0.0.1:'+server.address().port+'/v1';
try {
await db.query("UPDATE app_settings SET value='12000' WHERE key='clinical_assistant.image_budget'");
const jobs=createImageService({db,storage});
const context={request:' ORIGINAL request: draw the latest corrected dose 😀 [3].\n',history:[
{role:'user',content:'Old tiny turn must not jump a gap.'},
{role:'assistant',content:'too large boundary '+ '😀'.repeat(7000)},
{role:'user',content:'Recent correction '+ '😀'.repeat(1600)},
{role:'assistant',content:' Exact table [3, 1].\n| Dose | Page |\n| 5 mg | 19 [3] |\n'}]};
const original=JSON.stringify(context); const input={prompt:'MODEL DESCRIPTION ONLY',layout:'portrait'};
const tool=require('../src/utils/imageTool');
const opts={owner:101,workflow:'clinical_assistant',body:{idempotencyKey:'exact-image-input'},imageContext:context,images:jobs};
const ai={content:'Unchanged [3, 1].',toolCalls:[{id:'ctx',type:'function',function:{name:'generate_image',arguments:JSON.stringify(input)}}]};
const job=(await tool.dispatch(ai,opts)).imageJobs[0];
await jobs.tick(); assert.equal(captured.length,1);
const sent=captured[0].body.prompt;
assert.ok(sent.includes(context.request),'ORIGINAL request must reach IMAGE provider');
assert.ok(sent.includes(input.prompt));
assert.ok(sent.includes(context.history[2].content)); assert.ok(sent.includes(context.history[3].content));
assert.ok(sent.indexOf(context.history[2].content)<sent.indexOf(context.history[3].content));
assert.ok(!sent.includes(context.history[0].content)); assert.ok(!sent.includes(context.history[1].content));
assert.match(sent,/image only/i); assert.match(sent,/citations, reference numbers, footnotes, bibliography, or source lists/i);
assert.equal(captured[0].path,'/v1/images/generations'); assert.equal(captured[0].body.model,'synthetic-clinical-image');
assert.deepEqual(job.context,{includedTurns:2,totalTurns:4,used:sent.length,limit:12000,unit:'UTF-16 code units'});
assert.deepEqual((await jobs.get(job.jobId,101,'clinical_assistant')).context,job.context);
const row=await db.get('SELECT * FROM generated_image_jobs WHERE id=$1',[job.jobId]);
assert.equal(require('../src/utils/crypto').decryptString(row.prompt_cipher),sent); assert.equal(row.prompt_units,sent.length);
assert.ok(!row.prompt_cipher.includes(context.request)); assert.equal(JSON.stringify(context),original);
const replay=await tool.dispatch({...ai,toolCalls:[{...ai.toolCalls[0],function:{name:'generate_image',arguments:'{"prompt":"changed model wording"}'}}]},opts);
assert.equal(replay.imageJobs[0].jobId,job.jobId);
await assert.rejects(tool.dispatch(ai,{...opts,imageContext:{...context,history:context.history.concat({role:'user',content:'new'})}}),e=>e.statusCode===409);
for (const workflow of ['clinical_assistant','learning_hub']) {
await db.query("UPDATE app_settings SET value='32000' WHERE key=$1",[workflow+'.image_budget']);
const base=await jobs.snapshot(workflow,{prompt:'x'});
const text='😀'.repeat(Math.floor((32001-base.rendered.length)/2))+'x'.repeat((32001-base.rendered.length)%2);
const exact=await jobs.enqueue(101,workflow,{prompt:text},'http-exact-'+workflow);
await jobs.tick(); assert.equal(captured.at(-1).body.prompt.length,32000);
assert.match(captured.at(-1).body.prompt,/image only/i); assert.equal(exact.context.totalTurns,0);
await assert.rejects(jobs.enqueue(101,workflow,{prompt:text+'x'},'http-over-'+workflow),e=>e.statusCode===413);
}
assert.equal(captured.length,3,'mandatory overflow never calls IMAGE provider');
} finally { if(old===undefined) delete process.env.LITELLM_API_BASE; else process.env.LITELLM_API_BASE=old; await new Promise(r=>server.close(r)); }
});
test('expired PAID lease becomes explicit unknown even while storage and gateway readiness fail', async () => {
const jobs=service(),before=paid;
const job=await jobs.enqueue(101,'clinical_assistant',{prompt:'Crash plus outage'},'combined-outage');
const claim=await jobs.claim();assert.equal(claim.id,job.jobId);
await db.query("UPDATE generated_image_jobs SET lease_until=NOW()-interval '1 second' WHERE id=$1",[job.jobId]);
const down=createImageService({db,env:{},storage:{ready:async()=>{throw Error('synthetic outage');}}});
await down.tick().catch(()=>{});
assert.equal((await jobs.get(job.jobId,101,'clinical_assistant')).outcome,'unknown');assert.equal(paid,before);
await jobs.tick();assert.equal(paid,before);
});
test('partial schema without links/asset-read relation fails readiness and queued payment against real PG', async () => {
const jobs=service(),before=paid;
const job=await jobs.enqueue(101,'clinical_assistant',{prompt:'Do not pay with missing links'},'partial-schema');
await db.query('ALTER TABLE generated_image_links RENAME TO unavailable_links');
try {
await assert.rejects(jobs.ready(),e=>e.statusCode===503);
await assert.rejects(jobs.tick(),e=>e.statusCode===503);
await assert.rejects(jobs.enqueue(101,'clinical_assistant',{prompt:'blocked too'},'partial-new'),e=>e.statusCode===503);
assert.equal(paid,before);
} finally { await db.query('ALTER TABLE unavailable_links RENAME TO generated_image_links');await db.query("UPDATE generated_image_jobs SET stage='interrupted' WHERE id=$1",[job.jobId]); }
});
test('whole-turn context exactly fills the cap or is omitted in full; mandatory original request overflow never pays', async () => {
const jobs=service(),before=paid;
for(const workflow of ['clinical_assistant','learning_hub']) {
await db.query("UPDATE app_settings SET value='32000' WHERE key=$1",[workflow+'.image_budget']);
const input={prompt:'Image description',layout:'square'},context={request:' Original image request 😀\n',history:[{role:'user',content:'x'}]};
const base=await jobs.snapshot(workflow,input,context);const remaining=32001-base.rendered.length;
context.history[0].content='😀'.repeat(Math.floor(remaining/2))+'x'.repeat(remaining%2);
const original=JSON.stringify(context),exact=await jobs.snapshot(workflow,input,context);
assert.equal(exact.rendered.length,32000);assert.equal(exact.included,1);assert.ok(exact.rendered.includes(context.history[0].content));
context.history[0].content+='x';const over=await jobs.snapshot(workflow,input,context);
assert.equal(over.included,0);assert.equal(over.total,1);assert.ok(!over.rendered.includes(context.history[0].content));
assert.ok(over.rendered.includes(context.request));assert.ok(over.rendered.endsWith(require('../src/utils/generatedImages').IMAGE_OUTPUT_RULE));
context.history[0].content=context.history[0].content.slice(0,-1);assert.equal(JSON.stringify(context),original);
await assert.rejects(jobs.enqueue(101,workflow,input,'mandatory-original-overflow',false,{request:'😀'.repeat(16000),history:[]}),e=>e.statusCode===413);
}
assert.equal(paid,before);
});