reprochain-ai / test_engine.js
Hugdrigo's picture
Upload 35 files
c04328a verified
Raw History Blame Contribute Delete
6.44 kB
#!/usr/bin/env node
/* ReproChain AI unit tests, no external dependencies. */
'use strict';const assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto');
const R=require('./reprochain_engine.js'),E=require('./evalflow_engine.js');const root=__dirname, sha=x=>crypto.createHash('sha256').update(x).digest('hex');let n=0;
function test(name,fn){fn();n++;console.log('PASS',name);}
const m=JSON.parse(fs.readFileSync(path.join(root,'manifest.json'),'utf8'));const snap=JSON.parse(fs.readFileSync(path.join(root,'robustness_snapshots.json'),'utf8'));
const policy=JSON.parse(fs.readFileSync(path.join(root,'release_policy.json'),'utf8'));
const good=Object.fromEntries(m.artifacts.map(a=>[a.id,{observed:a.sha256,match:true}]));
test('manifest graph valid',()=>assert.equal(R.validateManifest(m).count,m.artifacts.length));
test('all node IDs unique',()=>assert.equal(new Set(m.artifacts.map(a=>a.id)).size,m.artifacts.length));
test('all referenced files exist',()=>assert.ok(m.artifacts.every(a=>fs.existsSync(path.join(root,a.file)))));
test('all file hashes match',()=>assert.ok(m.artifacts.every(a=>sha(fs.readFileSync(path.join(root,a.file)))===a.sha256)));
test('inventory root valid',()=>assert.equal(sha(R.merkleInput(m)),m.bundleRoot));
test('merkle input stable independent artifact order',()=>assert.equal(R.merkleInput({...m,artifacts:[...m.artifacts].reverse()}),R.merkleInput(m)));
test('integrity 100 percent verified',()=>assert.equal(R.integrity(m,good).passed,true));
test('tampered digest detected',()=>assert.equal(R.integrity(m,{...good,predictions:{observed:'0'.repeat(64),match:false}}).passed,false));
test('missing artifact detected',()=>assert.equal(R.integrity(m,{...good,predictions:{observed:null,match:false}}).missing,1));
test('lineage tracks training data upstream',()=>assert.ok(R.lineage(m,'predictions').ancestors.includes('train')));
test('lineage tracks decisions downstream',()=>assert.ok(R.lineage(m,'predictions').descendants.includes('reference-decision')));
test('no lineage for invalid ID',()=>assert.throws(()=>R.lineage(m,'nope')));
test('duplicate node rejected',()=>{const bad={...m,artifacts:[...m.artifacts,m.artifacts[0]]};assert.throws(()=>R.validateManifest(bad));});
test('cycle rejected',()=>{const bad=JSON.parse(JSON.stringify(m));bad.artifacts.find(a=>a.id==='train').parents=['predictions'];assert.throws(()=>R.validateManifest(bad));});
test('path traversal rejected',()=>{const bad=JSON.parse(JSON.stringify(m));bad.artifacts[0].file='../secret';assert.throws(()=>R.validateManifest(bad));});
test('missing dependency rejected',()=>{const bad=JSON.parse(JSON.stringify(m));bad.artifacts[0].parents=['not-real'];assert.throws(()=>R.validateManifest(bad));});
test('bad hash rejected',()=>{const bad=JSON.parse(JSON.stringify(m));bad.artifacts[0].sha256='1234';assert.throws(()=>R.validateManifest(bad));});
test('canonical JSON key order stable',()=>assert.equal(R.stable({b:2,a:1}),R.stable({a:1,b:2})));
test('GraphML renders nodes',()=>assert.ok(R.graphml(m).includes('<node id="predictions"')));
test('GraphML renders lineage edge',()=>assert.ok(R.graphml(m).includes('source="train" target="predictions"')));
test('inventory covers artifact hashes',()=>assert.equal(R.inventory(m,good).filter(a=>a.verified).length,m.artifacts.length));
test('original prediction count 4416',()=>assert.equal(snap.rows.length,4416));
test('base evidence verification',()=>E.verify(snap.rows));
test('source dataset 842 train rows',()=>assert.equal(JSON.parse(fs.readFileSync(path.join(root,'train.json'))).length,842));
test('source dataset 130 IID rows',()=>assert.equal(JSON.parse(fs.readFileSync(path.join(root,'iid_test.json'))).length,130));
test('source dataset 54 challenge rows',()=>assert.equal(JSON.parse(fs.readFileSync(path.join(root,'challenge.json'))).length,54));
const result=E.evaluate(snap.rows,'nb','hybrid','challenge',policy);
test('default synthetic research gate PASS',()=>assert.equal(result.status,'PASS'));
test('challenge matched sample size 54',()=>assert.equal(result.pair.n,54));
test('challenge hybrid Accuracy 42/54',()=>assert.ok(Math.abs(result.candidateMetrics.accuracy-42/54)<1e-12));
test('default nine model gates',()=>assert.equal(result.gates.length,9));
test('default governance pass',()=>assert.equal(R.evaluateRelease(result,R.integrity(m,good),{rootVerified:true}).status,'PASS'));
test('governance fails on tamper',()=>assert.equal(R.evaluateRelease(result,R.integrity(m,{...good,predictions:{observed:'bad',match:false}}),{rootVerified:true}).status,'BLOCK'));
test('governance fails when root mismatch',()=>assert.equal(R.evaluateRelease(result,R.integrity(m,good),{rootVerified:false}).status,'BLOCK'));
test('governance fails on simulated tamper',()=>assert.equal(R.evaluateRelease(result,R.integrity(m,good),{rootVerified:true,simulatedTamper:true}).status,'BLOCK'));
test('strict policy blocks model gate',()=>{const r=E.evaluate(snap.rows,'nb','hybrid','challenge',{...policy,minAccuracy:.95});assert.equal(r.status,'BLOCK');});
test('reverse comparison blocks defaults',()=>{const r=E.evaluate(snap.rows,'hybrid','nb','challenge',policy);assert.equal(r.status,'BLOCK');});
const g=R.evaluateRelease(result,R.integrity(m,good),{rootVerified:true});const run=R.buildRun(result,g,{bundleRoot:m.bundleRoot,integrity:'matched',replayId:1});
test('run schema',()=>assert.equal(run.schema,'reprochain.run.v1'));
test('run provenance is synthetic',()=>assert.match(run.source,/synthetic-curated/));
test('run preserves policy',()=>assert.equal(run.policy.minAccuracy,.75));
test('run digest computed stable',()=>assert.equal(sha(R.stable(run)),sha(R.stable({...run}))));
test('imported runs never trusted',()=>assert.equal(R.validateImportedRun(run).governance,'UNVERIFIED'));
test('invalid imported run rejected',()=>assert.throws(()=>R.validateImportedRun({...run,accuracy:7})));
test('run registry CSV contains headers',()=>assert.match(R.summaryCsv([run]),/bundleRoot/));
test('CSV neutralizes formula injection',()=>assert.match(R.summaryCsv([{...run,provenance:'=HYPERLINK(1)'}]),/\'=HYPERLINK/));
const upstream=JSON.parse(fs.readFileSync(path.join(root,'source_hashes.json')));
for(const [name,expected] of Object.entries(upstream.sha256))test('preserved upstream hash '+name,()=>assert.equal(sha(fs.readFileSync(path.join(root,name))),expected));
console.log('UNIT TESTS PASSED',n);