414 lines
38 KiB
JavaScript
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\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: '' })).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);
|
|
});
|