Reference source · 0.1.0-dev.11
validation-jobs.test.ts
tests/validation-jobs.test.ts · Download original file
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync,rmSync,mkdirSync,writeFileSync,symlinkSync,cpSync,readFileSync } from 'node:fs';
import { join,resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { snapshotDependencies,verifyDependencies } from '../src/validator-lock.ts';
import { LocalTerminology } from '../src/local-terminology.ts';
import { DurableValidationAdmission } from '../src/validation-jobs.ts';
import { validationDigest,type ExternalFhirValidator } from '../src/external-fhir.ts';
import { fixture,PATIENT } from '../src/reference/fixtures.ts';
import { SCOPES } from '../src/authorization.ts';
import { ProtocolError } from '../src/core.ts';
import { ReferenceHost } from '../src/host.ts';
import { DirectoryTrust } from '../src/discovery.ts';
import { ReferenceDirectory } from '../src/reference/directory.ts';
import { makeIdentity,signCredential } from '../src/identity.ts';
import { verifyArchive } from '../src/archive.ts';
import { DurableQueue } from '../src/queue.ts';
async function authorize(f:ReturnType<typeof fixture>){const d=await f.client.requestAccess({patient:PATIENT,scopes:Object.values(SCOPES),requested_duration_seconds:1800,refresh_requested:true});if(d.status==='pending'){f.host.authorization.approve(f.app,d.request_id,PATIENT,f.clock());await f.client.approvalStatus(d.request_id);}}
const system='https://www.convey.care/fhir/CodeSystem/oprp-synthetic-clinical';
const resource=()=>({resourceType:'Condition',subject:{reference:`Patient/${PATIENT}`},code:{coding:[{system,code:'condition'}]}});
function engine(validate?: (r:any[])=>Promise<any[]>,binding='test-binding'){const result={binding,engine:'test-engine',manifest:{version:'test'},profile:()=>['test-profile'],validateMany:validate??(async(r:any[])=>r.map(value=>({accepted:true,digest:validationDigest(value),target:'oprp',profiles:['test-profile'],engine:'test-engine',terminology:'disabled',issues:[]})))};return result as unknown as ExternalFhirValidator;}
function setup(validator=engine()){const dir=mkdtempSync(join(tmpdir(),'oprp-validation-'));let now=100;const options={path:join(dir,'jobs.sqlite'),owner:'host-key-fingerprint',validator,clock:()=>now,lifetime:10,leaseSeconds:20};return {dir,options,advance:(n:number)=>{now+=n;},close:()=>rmSync(dir,{recursive:true,force:true})};}
test('dependency tree lock rejects changed bytes, extra packages, missing files and symbolic links',()=>{const dir=mkdtempSync(join(tmpdir(),'oprp-lock-'));try{const p=join(dir,'example#1/package');mkdirSync(p,{recursive:true});writeFileSync(join(p,'package.json'),JSON.stringify({name:'example',version:'1'}));writeFileSync(join(p,'data.json'),'original');const lock=snapshotDependencies(dir);verifyDependencies(dir,lock);writeFileSync(join(p,'data.json'),'mutation');assert.throws(()=>verifyDependencies(dir,lock),/validator_dependency_mismatch/);writeFileSync(join(p,'data.json'),'original');symlinkSync('/etc/passwd',join(p,'link'));assert.throws(()=>verifyDependencies(dir,lock),/validator_cache_unsafe/);rmSync(join(p,'link'));rmSync(join(p,'data.json'));assert.throws(()=>verifyDependencies(dir,lock),/validator_dependency_mismatch/);mkdirSync(join(dir,'unknown#2/package'),{recursive:true});writeFileSync(join(dir,'unknown#2/package/package.json'),JSON.stringify({name:'unknown',version:'2'}));assert.throws(()=>verifyDependencies(dir,lock),/validator_dependency_mismatch/);}finally{rmSync(dir,{recursive:true,force:true});}});
test('local catalogue validates coded concepts and detects unavailable systems, versions and invalid codes',()=>{const tx=new LocalTerminology(resolve('profiles/terminology'));assert.deepEqual(tx.check(resource()),[]);for(const coding of [{system,code:'invented'},{system:'http://loinc.org',code:'2345-7'},{system,code:'condition',version:'wrong'}])assert(tx.check({...resource(),code:{coding:[coding]}}).some(i=>i.severity==='error'));assert(tx.check({...resource(),code:{text:'no coding'}}).some(i=>i.details?.text==='coded-concept-required'));});
test('local value sets enforce immunization status subsets and ActCode encounter ancestry',()=>{const tx=new LocalTerminology(resolve('profiles/terminology'));const encounter={resourceType:'Encounter',status:'finished',class:{system:'http://terminology.hl7.org/CodeSystem/v3-ActCode',code:'AMB'}};assert.deepEqual(tx.check(encounter),[]);assert(tx.check({...encounter,class:{...encounter.class,code:'_ActEncounterCode'}}).some(i=>i.details?.text==='value-set-invalid'));const immunization={resourceType:'Immunization',status:'in-progress',vaccineCode:{coding:[{system,code:'vaccine'}]}};assert(tx.check(immunization).some(i=>i.details?.text==='value-set-invalid'));assert.deepEqual(tx.check({...immunization,status:'completed'}),[]);});
test('terminology source mutation is detected and unsupported compose filters fail startup',()=>{const dir=mkdtempSync(join(tmpdir(),'oprp-tx-'));try{cpSync('profiles/terminology',dir,{recursive:true});const tx=new LocalTerminology(dir);writeFileSync(join(dir,'manifest.json'),'{}');assert.throws(()=>tx.verify());cpSync('profiles/terminology',dir,{recursive:true});const value=JSON.parse(readFileSync(join(dir,'definitions/ValueSet-v3-ActEncounterCode.json'),'utf8'));value.compose.include[0].filter[0].op='regex';writeFileSync(join(dir,'definitions/ValueSet-v3-ActEncounterCode.json'),JSON.stringify(value));assert.throws(()=>new LocalTerminology(dir),/unsupported_terminology_catalogue/);}finally{rmSync(dir,{recursive:true,force:true});}});
test('validation jobs deduplicate, persist approved content across restart and expire exactly',async()=>{const f=setup();let gate=new DurableValidationAdmission(f.options);try{assert.throws(()=>gate.check(resource(),PATIENT,100),/external_validation_required/);assert.throws(()=>gate.check(resource(),PATIENT,100),/external_validation_required/);assert.equal(gate.jobs().length,1);assert.equal((await gate.runOne())!.state,'accepted');gate.close();gate=new DurableValidationAdmission(f.options);gate.check(resource(),PATIENT,109);assert.throws(()=>gate.check({...resource(),id:'changed'},PATIENT,109),/external_validation_required/);f.advance(10);assert.throws(()=>gate.check(resource(),PATIENT,110),/external_validation_required/);}finally{gate.close();f.close();}});
test('jobs are bound to Host ownership and the complete validation configuration',async()=>{const f=setup();const gate=new DurableValidationAdmission(f.options);try{assert.throws(()=>gate.check(resource(),PATIENT,100));await gate.runOne();assert.throws(()=>new DurableValidationAdmission({...f.options,owner:'other'}),/wrong_validation_owner/);const changed=new DurableValidationAdmission({...f.options,validator:engine(undefined,'new-rules')});try{assert.throws(()=>changed.check(resource(),PATIENT,100),/external_validation_required/);assert.equal(changed.jobs().length,1);}finally{changed.close();}}finally{gate.close();f.close();}});
test('rejected reports are durable and ordered requester queues stop at the rejection',async()=>{const validator=engine(async r=>r.map(v=>({accepted:false,digest:validationDigest(v),target:'oprp',profiles:['test-profile'],engine:'test-engine',terminology:'disabled',issues:[{severity:'error',code:'code-invalid'}]})));const f=setup(validator),gate=new DurableValidationAdmission(f.options);try{assert.throws(()=>gate.check(resource(),PATIENT,100));assert.equal((await gate.runOne())!.state,'rejected');assert.throws(()=>gate.check(resource(),PATIENT,101),/external_validation_rejected/);const fixture_=fixture(),queue=new DurableQueue(':memory:',fixture_.client);try{fixture_.client.createResource=async()=>{throw new ProtocolError('external_validation_rejected');};queue.createResource(PATIENT,resource(),'first');queue.createResource(PATIENT,resource(),'second');await queue.flush();assert.deepEqual(queue.entries().map(e=>e.state),['rejected','pending']);}finally{queue.close();fixture_.host.close();fixture_.trust.close();}}finally{gate.close();f.close();}});
test('tool failures retry with bounded backoff, then require trusted intervention',async()=>{const f=setup(engine(async()=>{throw new ProtocolError('validator_timeout');})),gate=new DurableValidationAdmission({...f.options,maxAttempts:2});try{assert.throws(()=>gate.check(resource(),PATIENT,100));assert.equal((await gate.runOne())!.state,'queued');assert.equal(await gate.runOne(),undefined);f.advance(2);const failed=(await gate.runOne())!;assert.equal(failed.state,'failed');assert.equal(failed.attempts,2);gate.retry(failed.id);assert.equal(gate.jobs()[0]!.attempts,0);assert.throws(()=>gate.check(resource(),PATIENT,102),/external_validation_required/);}finally{gate.close();f.close();}});
test('unsupported validation targets stop without futile tool retries or admission',async()=>{const f=setup(engine(async()=>{throw new ProtocolError('unsupported_us_core_resource');})),gate=new DurableValidationAdmission(f.options);try{assert.throws(()=>gate.check(resource(),PATIENT,100));const job=(await gate.runOne())!;assert.equal(job.state,'failed');assert.equal(job.error,'unsupported_us_core_resource');assert.equal(await gate.runOne(),undefined);assert.throws(()=>gate.check(resource(),PATIENT,100),/external_validation_required/);}finally{gate.close();f.close();}});
test('expired worker lease is reclaimed and stale completion cannot approve any content',async()=>{let release!:(r:any[])=>void;const deferred=new Promise<any[]>(r=>{release=r;});const f=setup(engine(()=>deferred));const first=new DurableValidationAdmission(f.options),second=new DurableValidationAdmission({...f.options,validator:engine()});try{assert.throws(()=>first.check(resource(),PATIENT,100));const running=first.runOne();assert.throws(()=>first.close(),/validation_worker_busy/);await assert.rejects(first.runOne(),/validation_worker_busy/);assert.equal(await second.runOne(),undefined);f.advance(21);await second.runOne();release([{accepted:false,digest:validationDigest(resource()),target:'oprp',profiles:['test-profile'],engine:'test-engine',terminology:'disabled',issues:[{severity:'error',code:'invalid'}]}]);await running;first.check(resource(),PATIENT,121);assert.equal(first.jobs()[0]!.state,'accepted');}finally{first.close();second.close();f.close();}});
test('unreclaimed expired completion cannot install approval and malformed reports fail closed',async()=>{const f=setup(engine(async()=>{f.advance(21);return [];})),gate=new DurableValidationAdmission(f.options);try{assert.throws(()=>gate.check(resource(),PATIENT,100));await gate.runOne();assert.equal(gate.jobs()[0]!.state,'running');assert.throws(()=>gate.check(resource(),PATIENT,121),/external_validation_required/);}finally{gate.close();f.close();}const g=setup(engine(async r=>r.map(v=>({accepted:true,digest:validationDigest(v),target:'oprp',profiles:['wrong-profile'],engine:'test-engine',terminology:'disabled',issues:[]})))),bad=new DurableValidationAdmission(g.options);try{assert.throws(()=>bad.check(resource(),PATIENT,100));assert.equal((await bad.runOne())!.state,'queued');assert.equal(bad.jobs()[0]!.error,'invalid_validation_report');}finally{bad.close();g.close();}});
test('archive versions are all scheduled in bounded batches before readiness and quota failures roll back job admission',async()=>{const f=setup(),gate=new DurableValidationAdmission(f.options);try{const versions=Array.from({length:40},(_,i)=>({...resource(),id:`entry-${i}`}));gate.prepareArchive([...versions,versions[0]!],100);assert.equal(gate.jobs().length,2);await gate.runOne();await gate.runOne();for(const value of versions)gate.check(value,PATIENT,100);const bounded=new DurableValidationAdmission({...f.options,path:join(f.dir,'bounded.sqlite'),maxJobs:1});try{assert.throws(()=>bounded.prepareArchive(versions,100),/validation_queue_full/);assert.equal(bounded.jobs().length,0);}finally{bounded.close();}}finally{gate.close();f.close();}});
test('authorized Host intake schedules outside its rolled-back mutation; restart resumes work, current authorization still applies',async()=>{const f=fixture(),storage=setup();let gate=new DurableValidationAdmission({...storage.options,clock:f.clock});try{f.host.clinical.admission=gate;const decision=await f.client.requestAccess({patient:PATIENT,scopes:Object.values(SCOPES),requested_duration_seconds:1800,refresh_requested:true});if(decision.status==='pending'){f.host.authorization.approve(f.app,decision.request_id,PATIENT,f.clock());await f.client.approvalStatus(decision.request_id);}await assert.rejects(f.client.createResource(PATIENT,resource(),'durable'),/external_validation_required/);assert.equal(gate.jobs().length,1);assert.equal((f.host.db.prepare('SELECT count(*) AS n FROM clinical_resources').get() as any).n,0);gate.close();gate=new DurableValidationAdmission({...storage.options,clock:f.clock});f.host.clinical.admission=gate;await gate.runOne();const created=await f.client.createResource(PATIENT,resource(),'durable');assert.equal(created.version,1);f.advance(11);assert.deepEqual(await f.client.createResource(PATIENT,resource(),'durable'),created);await assert.rejects(f.client.createResource(PATIENT,{...resource(),code:{...resource().code,text:'Changed'}},'later'),/external_validation_required/);await gate.runOne();f.host.authorization.revoke(f.client.authorization!.grant_id);await assert.rejects(f.client.createResource(PATIENT,{...resource(),code:{...resource().code,text:'Changed'}},'later'));assert.equal((f.host.db.prepare('SELECT count(*) AS n FROM clinical_resources').get() as any).n,1);}finally{gate.close();storage.close();f.host.close();f.trust.close();}});
test('destination restore queues full authoritative history and remains atomic until durable worker completes',async()=>{const f=fixture(),storage=setup();const gate=new DurableValidationAdmission({...storage.options,clock:f.clock});const destination=new ReferenceHost(':memory:',f.hostIdentity,f.hostCredential,f.trust,f.policy);try{const d=await f.client.requestAccess({patient:PATIENT,scopes:Object.values(SCOPES),requested_duration_seconds:1800,refresh_requested:true});if(d.status==='pending'){f.host.authorization.approve(f.app,d.request_id,PATIENT,f.clock());await f.client.approvalStatus(d.request_id);}await f.client.createResource(PATIENT,resource(),'archive');const archive=f.host.exportRecord(PATIENT,f.clock());destination.clinical.admission=gate;assert.throws(()=>destination.importRecord(archive,PATIENT,f.clock()),/external_validation_required/);assert.equal(gate.jobs()[0]!.kind,'migration');assert.equal((destination.db.prepare('SELECT count(*) AS n FROM patients').get() as any).n,0);await gate.runOne();assert.equal(destination.importRecord(archive,PATIENT,f.clock()).imported,true);}finally{destination.close();gate.close();storage.close();f.host.close();f.trust.close();}});
test('durable migration gate issues no ready proof before destination preflight and recovers expired admission after release',async()=>{const f=fixture();const identity=makeIdentity('urn:oprp:demo:gated-destination'),credential=signCredential(f.hostingAuthority,identity.public,'host',f.clock());const destination=new ReferenceHost(':memory:',identity,credential,f.trust,f.policy);const storage=setup();const bindings=new ReferenceDirectory(':memory:',f.hostingAuthority,['patients.example'],f.trust);try{await authorize(f);await f.client.createResource(PATIENT,resource(),'migrating');const address='synthetic@patients.example';bindings.enroll(address,PATIENT,f.hostCredential,'https://source.example/oprp/exchange',f.clock());const directory=new DirectoryTrust({[f.hostingAuthority.issuer]:{publicKey:f.hostingAuthority.publicKey,domains:['patients.example']}},f.trust);const plan=f.host.migration.begin(PATIENT,credential,'https://destination.example/oprp/exchange',bindings.document(address,f.clock()),directory,PATIENT,f.clock(),'gated-transfer');destination.migration.admit(plan,PATIENT,f.clock());const archive=f.host.migration.freeze('gated-transfer',f.clock());const caller=f.trust.validate(f.hostCredential,f.clock());const gate=new DurableValidationAdmission({...storage.options,clock:f.clock});destination.clinical.admission=gate;assert.throws(()=>destination.migration.stage(plan,archive,true,caller,f.clock()),/external_validation_required/);assert.equal(destination.migration.incoming('gated-transfer').ready,undefined);assert.equal(f.host.migration.custody(PATIENT).state,'frozen');const body=verifyArchive(archive,f.trust,f.clock(),PATIENT),versions=[...body.resources,...body.clinical_history!].filter(e=>e.resource.resourceType==='Condition').map(e=>e.resource);await gate.runOne();const ready=destination.migration.stage(plan,archive,true,caller,f.clock()).ready;const release=f.host.migration.release('gated-transfer',ready,f.clock());f.advance(301);assert.throws(()=>destination.migration.activate('gated-transfer',release,caller,f.clock()),/external_validation_required/);assert.equal(destination.migration.custody(PATIENT).state,'staged');await gate.runOne();destination.migration.activate('gated-transfer',release,caller,f.clock());assert.equal(destination.migration.custody(PATIENT).state,'active');assert.equal(f.host.migration.custody(PATIENT).state,'moved');}finally{destination.clinical.admission && (destination.clinical.admission as DurableValidationAdmission).close();bindings.close();destination.close();f.host.close();f.trust.close();storage.close();}});