#!/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('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);