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