/* Pure unit tests: no network or browser dependencies. */ const fs=require('node:fs'),vm=require('node:vm'),assert=require('node:assert/strict'); const path=require('node:path'); vm.runInThisContext(fs.readFileSync(path.join(__dirname,'../engine.js'),'utf8')); const {Demo,validateReply,MINE}=IgneumSiteEngine;const results=[]; function check(name,fn){try{fn();results.push({name,pass:true});}catch(e){results.push({name,pass:false,error:e.message});}} const now=Date.now(),demo=new Demo(now),reply=demo.snapshot(now); const edit=fn=>{const r=JSON.parse(JSON.stringify(reply));fn(r);return r;}; check('Seeded demo obeys observer contract',()=>assert.equal(validateReply(reply).ok,true)); check('All hashes unique',()=>assert.equal(new Set(reply.blocks.map(b=>b.hash)).size,reply.blocks.length)); check('Parent links are never self-links',()=>assert(reply.blocks.every(b=>!b.parents.includes(b.hash)))); check('Parents precede children',()=>{const byHash=new Map(reply.blocks.map(b=>[b.hash,b]));assert(reply.blocks.every(b=>b.parents.every(h=>!byHash.has(h)||byHash.get(h).ts<=b.ts)));}); check('Included, excluded, selected remain separate',()=>{assert(reply.blocks.some(b=>b.color==='red'));assert(reply.blocks.some(b=>b.color==='blue'&&!b.chain));}); check('Proving completion does not implicitly become final',()=>{const x=demo.trigger('mine');demo.trigger('proof');assert(x.proven);assert(!x.final);assert(!x.locked);}); check('Own block uses the tracked demo vote key',()=>assert.equal(demo.trigger('mine').miner,MINE)); check('Checkpoint effect needs an explicit simulated checkpoint',()=>{const x=demo.trigger('lock');assert(demo.snapshot().finality.checkpoints.some(c=>c.state==='locked'&&c.blue_score===x.blue_score));}); check('Validation does not age or promote observer proof',()=>{const r=edit(r=>{r.blocks[0].ts=now-1e8;r.blocks[0].shards=[{state:'planned'}];r.blocks[0].proven=false;r.blocks[0].final=false;r.blocks[0].locked=false;});const out=validateReply(r);assert.equal(out.blocks[0].shards[0].state,'planned');assert.equal(out.blocks[0].final,false);}); check('Observer false finality remains false',()=>assert.equal(validateReply(edit(r=>{r.blocks[0].chain=true;r.blocks[0].proven=true;r.blocks[0].final=false;})).blocks[0].final,false)); check('Payload is defensively cloned',()=>{const v=validateReply(reply);v.blocks[0].hash='changed';assert.notEqual(reply.blocks[0].hash,'changed');}); function reject(name, mutate) { check(name, function(){ assert.throws(function(){validateReply(edit(mutate));});}); } reject('HTML hashes rejected', r => { r.blocks[0].hash=''; }); reject('Repeated hashes rejected', r => { r.blocks.push(r.blocks[0]); }); reject('Invalid parent rejected', r => { r.blocks[0].parents=['javascript:bad']; }); reject('Self parent rejected', r => { r.blocks[0].parents=[r.blocks[0].hash]; }); reject('Future time rejected', r => { r.blocks[0].ts=now+1e8; }); reject('Seconds are not silently read as milliseconds', r => { r.blocks[0].ts=now/1000; }); reject('Unknown proof state rejected', r => { r.blocks[0].shards=[{state:'jackpot'}]; }); reject('String proof flag rejected', r => { r.blocks[0].proven='true'; }); reject('String finality flag rejected', r => { r.blocks[0].final='false'; }); reject('Invalid inclusion rejected', r => { r.blocks[0].color='winning'; }); reject('Negative observer age rejected', r => { r.state.age_s=-1; }); reject('Invalid checkpoint fraction rejected', r => { r.finality.checkpoints[0].fraction_total=2; }); check('Empty observer window accepted',()=>assert.equal(validateReply({ok:true,state:{stale:false,age_s:0},blocks:[]}).blocks.length,0)); check('Demo retained history is bounded',()=>{for(let i=0;i<1000;i++)demo.create(now+i*2000,false,false);demo.update(now+2000000);assert(demo.blocks.length<=161);assert(demo.cps.length<=12);}); console.log(JSON.stringify({passed:results.filter(x=>x.pass).length,total:results.length,results},null,2)); if(results.some(x=>!x.pass))process.exitCode=1;