bundle.test.mjs (8250B)
1 import test from 'node:test'; 2 import assert from 'node:assert/strict'; 3 import {readFile} from 'node:fs/promises'; 4 import {createHash} from 'node:crypto'; 5 import {Graph} from '../dist/graph.js'; 6 import {COMPILER_BUNDLE_LIMIT,decodeBundle,loadBundle,bundleHash,fetchBytes,fetchBundle,BUNDLE_LIMIT} from '../dist/bundle.js'; 7 import {makeBundle,sizedBundle} from './bundle-fixture.mjs'; 8 // Independently counted from the authenticated compiler manifest with the 9 // Python bundle reader; older indexed compiler releases remain retained. 10 const COMPILER_EXPORT_COUNT=1191; 11 const data=JSON.parse(await readFile(new URL('../demos.json',import.meta.url))); 12 test('stable packaged sources/pins and real compiler bundle decode',async()=>{ 13 for(const [name,demo] of Object.entries(data.demos)){ 14 const bytes=await readFile(new URL(`../bundles/${demo.hash}.arboricx`,import.meta.url)); 15 assert.equal(createHash('sha256').update(bytes).digest('hex'),demo.hash); 16 assert.equal(createHash('sha256').update(demo.source).digest('hex'),demo.sourceSha256); 17 assert.equal(await readFile(new URL(`../demos/${name}.tri`,import.meta.url),'utf8'),demo.source); 18 assert.ok(decodeBundle(bytes).exports.length>0); 19 if(demo.supportSourceSha256)assert.equal(createHash('sha256').update(await readFile(new URL('../demos/practical-support.tri',import.meta.url))).digest('hex'),demo.supportSourceSha256); 20 } 21 const descriptor=JSON.parse(await readFile(new URL('../compiler.json',import.meta.url))); 22 const bytes=await readFile(new URL('../bundles/'+descriptor.hash+'.arboricx',import.meta.url)); 23 assert.equal(createHash('sha256').update(bytes).digest('hex'),data.compilerBundleSha256); 24 assert.equal(decodeBundle(bytes).exports.length,COMPILER_EXPORT_COUNT); 25 const bundle=decodeBundle(bytes,COMPILER_BUNDLE_LIMIT);assert.equal(bundle.exports.length,COMPILER_EXPORT_COUNT); 26 const g=new Graph();loadBundle(g,bundle);assert.equal(g.stats.nodes,bundle.nodes.length); 27 }); 28 test('direct indexed loading preserves exponential sharing and root aliases',()=>{ 29 const records=[[]];for(let i=1;i<32;i++)records.push([i-1,i-1]); 30 const b=decodeBundle(makeBundle(records,[['huge',31],['alias',31]])); 31 const g=new Graph(), roots=loadBundle(g,b); 32 assert.equal(g.stats.nodes,32);assert.equal(roots.get('huge'),roots.get('alias')); 33 const node=g.observe(roots.get('huge')).node;assert.equal(node.left,node.right); 34 assert.equal(g.preview(roots.get('huge')).nodes.length,32); 35 }); 36 test('malformed header, versions, section bounds/duplicates/reserved, trailing data',()=>{ 37 const good=makeBundle([[],[0]]); 38 for(const [offset,value,width] of [[0,0,1],[8,2,2],[10,1,2],[16,1,1],[24,0,8],[32,2,4],[36,2,2],[40,1,2],[42,1,2],[44,0,8],[52,0xffffffff,8],[60,1,4]]){ 39 const bad=Buffer.from(good); 40 if(width===1)bad[offset]=value;else if(width===2)bad.writeUInt16BE(value,offset);else if(width===4)bad.writeUInt32BE(value,offset);else bad.writeBigUInt64BE(BigInt(value),offset); 41 assert.throws(()=>decodeBundle(bad),undefined,`offset ${offset}`); 42 } 43 const critical=Buffer.from(good);critical.writeUInt32BE(100,32);assert.throws(()=>decodeBundle(critical),/critical/); 44 for(const n of [0,5,31,95,good.length-1])assert.throws(()=>decodeBundle(good.subarray(0,n))); 45 assert.throws(()=>decodeBundle(Buffer.concat([good,Buffer.from([0])])),/Trailing/); 46 assert.throws(()=>decodeBundle(new Uint8Array(BUNDLE_LIMIT+1)),/limit/); 47 }); 48 test('manifest semantics, export ambiguity/ABI, DAG acyclicity/duplicate control',()=>{ 49 assert.throws(()=>decodeBundle(makeBundle([[],[0]],undefined,{evaluation:'eager'})),/semantics/); 50 assert.throws(()=>decodeBundle(makeBundle([[],[0]],undefined,{abi:'other'})),/ABI/); 51 assert.throws(()=>decodeBundle(makeBundle([[],[0]],[['',1]])),/export/); 52 assert.throws(()=>decodeBundle(makeBundle([[],[0]],[['x',1],['x',1]])),/export/); 53 assert.throws(()=>decodeBundle(makeBundle([[],[0]],[['x',9]])),/bounds/); 54 assert.throws(()=>decodeBundle(makeBundle([[],[1]])),/backward/); 55 assert.throws(()=>decodeBundle(makeBundle([[],[]])),/Duplicate/); 56 assert.throws(()=>decodeBundle(makeBundle([[],[0],[0]])),/Duplicate/); 57 }); 58 test('bounded streamed download, corrupt pin, HTTP errors, canonical hash spelling',async()=>{ 59 const good=makeBundle([[],[0]]),hash=bundleHash(createHash('sha256').update(good).digest('hex')); 60 for(const value of ['', 'A'.repeat(64),'../secret', 'f'.repeat(65), 1])assert.throws(()=>bundleHash(value)); 61 const saved=globalThis.fetch; 62 try{ 63 globalThis.fetch=async()=>new Response(good);assert.equal((await fetchBundle(new URL('https://example.test/a'),hash)).nodes.length,2); 64 await assert.rejects(fetchBundle(new URL('https://example.test/a'),bundleHash('0'.repeat(64))),/mismatch/); 65 globalThis.fetch=async()=>new Response('no',{status:404});await assert.rejects(fetchBytes(new URL('https://example.test/a')),/404/); 66 globalThis.fetch=async()=>new Response(good,{headers:{'content-length':String(BUNDLE_LIMIT+1)}});await assert.rejects(fetchBytes(new URL('https://example.test/a')),/limit/); 67 let cancelled=false; 68 globalThis.fetch=async()=>new Response(new ReadableStream({start(controller){controller.enqueue(new Uint8Array(8));controller.enqueue(new Uint8Array(8));},cancel(){cancelled=true;}})); 69 await assert.rejects(fetchBytes(new URL('https://example.test/a'),12),/limit/);assert.equal(cancelled,true); 70 }finally{globalThis.fetch=saved;} 71 }); 72 73 test('modern demo demand controls discard divergent fields under the package budget',async()=>{ 74 const data=JSON.parse(await readFile(new URL('../demos.json',import.meta.url))); 75 let count=0; 76 for(const demo of Object.values(data.demos))for(const c of demo.demandCases){ 77 const g=new Graph({nodes:250_000,fuel:20_000_000,stack:50_000},{threshold:100_000}); 78 const result=g.app(g.fromTernary(c.program,200_000),g.leaf); 79 assert.equal(g.toTernary(result,50000),c.result,c.label);count++; 80 } 81 assert.equal(count,5); 82 }); 83 84 test('compiler download profile retains header/stream cancellation and hash verification',async()=>{ 85 const {fetchBytes,fetchBundle,sha256,BUNDLE_LIMIT}=await import('../dist/bundle.js'); 86 const saved=globalThis.fetch;let cancelled=false; 87 const url=new URL('../compiler.json',import.meta.url); // Mocked fetch only. 88 try{ 89 globalThis.fetch=async()=>new Response(new ReadableStream({cancel(){cancelled=true;}}), 90 {headers:{'content-length':String(COMPILER_BUNDLE_LIMIT+1)}}); 91 await assert.rejects(fetchBytes(url,COMPILER_BUNDLE_LIMIT),/download limit/); 92 assert.equal(cancelled,true); 93 for(const limit of [BUNDLE_LIMIT,COMPILER_BUNDLE_LIMIT]){ 94 cancelled=false; 95 globalThis.fetch=async()=>new Response(new ReadableStream({ 96 start(controller){controller.enqueue(new Uint8Array(limit+1));},cancel(){cancelled=true;} 97 })); 98 await assert.rejects(fetchBytes(url,limit),/download limit/);assert.equal(cancelled,true); 99 } 100 const descriptor=JSON.parse(await readFile(new URL('../compiler.json',import.meta.url))); 101 const bytes=await readFile(new URL('../bundles/'+descriptor.hash+'.arboricx',import.meta.url)); 102 globalThis.fetch=async()=>new Response(bytes); 103 assert.equal((await fetchBundle(url,descriptor.hash)).exports.length,COMPILER_EXPORT_COUNT); 104 assert.equal((await fetchBundle(url,descriptor.hash,COMPILER_BUNDLE_LIMIT)).exports.length,COMPILER_EXPORT_COUNT); 105 await assert.rejects(fetchBundle(url,await sha256(new Uint8Array()),COMPILER_BUNDLE_LIMIT),/SHA-256/); 106 }finally{globalThis.fetch=saved;} 107 }); 108 109 110 test('ordinary packages accept exactly 16 MiB while draft compiler input stays at 2 MiB',async()=>{ 111 assert.equal(BUNDLE_LIMIT,16_777_216);assert.equal(COMPILER_BUNDLE_LIMIT,2_097_152); 112 const bytes=sizedBundle(BUNDLE_LIMIT),hash=bundleHash(createHash('sha256').update(bytes).digest('hex')); 113 assert.equal(decodeBundle(bytes).nodes.length,1); 114 assert.throws(()=>decodeBundle(bytes,COMPILER_BUNDLE_LIMIT),/byte limit/); 115 assert.throws(()=>decodeBundle(sizedBundle(BUNDLE_LIMIT+1)),/byte limit/); 116 const saved=globalThis.fetch; 117 try{ 118 globalThis.fetch=async()=>new Response(bytes); 119 assert.equal((await fetchBundle(new URL('../compiler.json',import.meta.url),hash)).nodes.length,1); 120 }finally{globalThis.fetch=saved;} 121 });