pediatric-ai-scribe-v3/test/generated-images.integration.js

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},'pptxgenjs':require('pptxgenjs')
});
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);
});