modules.test.mjs (6405B)
1 import test from 'node:test'; 2 import assert from 'node:assert/strict'; 3 import {readFile, readdir} from 'node:fs/promises'; 4 import {decodeBundle, sha256} from '../dist/bundle.js'; 5 import {indexedObject, moduleIdentity, moduleHash} from '../dist/module-identity.js'; 6 import {fetchModule} from '../dist/module-resolver.js'; 7 import {packageIndex, packageReleases} from '../dist/packages.js'; 8 const site = new URL('../', import.meta.url), root = new URL('../../', site); 9 const releases = packageIndex(JSON.parse(await readFile(new URL('packages.json', site)))); 10 const bytes = r => readFile(new URL(`bundles/${r.hash}.arboricx`, site)); 11 const libraries = [...new Set(releases.filter(r => r.name.startsWith('tricu-lib-')).map(r => r.name))] 12 .map(name => packageReleases(releases, name)[0]); 13 14 test('every library source has a complete-interface package; no module allowlist', async () => { 15 const paths = []; 16 async function walk(directory) { 17 for (const entry of await readdir(new URL(directory, root), {withFileTypes:true})) { 18 const path = directory + entry.name; 19 if (entry.isDirectory()) await walk(path + '/'); 20 else if (entry.name.endsWith('.tri')) paths.push(path); 21 } 22 } 23 await walk('lib/'); 24 assert.deepEqual(libraries.map(r => r.title).sort(), paths.sort()); 25 for (const cohort of ['compiler-library', 'support-library']) { 26 const fixture = decodeBundle(await readFile(new URL(`test/fixtures/${cohort}.arboricx`, root))); 27 const metadata = await readFile(new URL(`test/fixtures/${cohort}.v1`, root), 'utf8'); 28 for (const row of metadata.split('\n').filter(s => s.startsWith('module\t'))) { 29 const [, , alias, path] = row.split('\t'); 30 if (!path.startsWith('lib/')) continue; 31 const release = libraries.find(r => r.title === path); 32 assert.ok(release, path); 33 const bundle = decodeBundle(await bytes(release)); 34 assert.deepEqual(bundle.exports.map(e => e.name), fixture.exports.filter(e => e.name.startsWith(alias + '.')).map(e => e.name.slice(alias.length + 1)), path); 35 } 36 } 37 }); 38 39 test('browser manifest identities match every independently native-published library interface', async () => { 40 assert.ok(libraries.length > 0); 41 for (const release of libraries) { 42 const data = await bytes(release); 43 assert.equal(await sha256(data), release.hash, release.name); 44 assert.equal(await moduleIdentity(decodeBundle(data)), release.manifest, release.name); 45 } 46 }); 47 48 test('indexed extraction is name-neutral, ignores unrelated roots/metadata, preserves physical sharing', async () => { 49 const b = decodeBundle(await readFile(new URL('test/fixtures/id.arboricx', root))); 50 const expected = await sha256(indexedObject(b, b.exports[0].root)); 51 b.metadata = {description:'not executable identity'}; 52 b.exports[0].name = 'renamed'; 53 b.nodes.push({kind:'fork',left:0,right:0}); 54 assert.equal(await sha256(indexedObject(b, b.exports[0].root)), expected); 55 const shared = {nodes:[{kind:'leaf'}],roots:[31],exports:[{name:'tree',root:31,kind:'term',abi:'arboricx.abi.tree.v1'}],metadata:{}}; 56 for(let i=1;i<=31;i++) shared.nodes.push({kind:'fork',left:i-1,right:i-1}); 57 const object = indexedObject(shared,31); 58 assert.ok(object.length < 2048); 59 assert.equal(decodeBundle(object).nodes.length,32); 60 assert.equal((await moduleIdentity(shared)).length,64); 61 await assert.rejects(moduleIdentity({...shared,exports:[]}), /nonempty native/); 62 await assert.rejects(moduleIdentity({...shared,exports:[{...shared.exports[0],name:'bad name'}]}), /native module/); 63 assert.throws(()=>moduleHash('../path'), /hash/); 64 }); 65 66 test('manifest transport rejects unknown refs, wrong manifest, corrupt bytes and HTTP failure', async () => { 67 const release = libraries.find(r => r.title === 'lib/packages/io.tri'); 68 const data = await bytes(release), calls = [], original = globalThis.fetch; 69 let response = () => new Response(data); 70 globalThis.fetch = async (url, options) => { calls.push({url:String(url),options}); return response(); }; 71 const base = new URL('https://example.invalid/nested/site/'); 72 try { 73 const loaded = await fetchModule(base, release.manifest, releases); 74 assert.equal(loaded.exports.length,6); 75 assert.equal(calls[0].url, `https://example.invalid/_arboricx/bundle/hash/${release.hash}`); 76 assert.deepEqual(calls[0].options, {redirect:'error',credentials:'omit'}); 77 const count = calls.length; 78 await assert.rejects(fetchModule(base, '0'.repeat(64), releases), /Unknown module manifest/); 79 await assert.rejects(fetchModule(base, release.hash, releases), /Unknown module manifest/); 80 assert.equal(calls.length,count); 81 await assert.rejects(fetchModule(base, '0'.repeat(64), [{...release,manifest:'0'.repeat(64)}]), /Module manifest mismatch/); 82 response = () => new Response(new Uint8Array([1,2,3])); 83 await assert.rejects(fetchModule(base, release.manifest, releases), /SHA-256 mismatch/); 84 response = () => new Response('missing',{status:404}); 85 await assert.rejects(fetchModule(base, release.manifest, releases), /HTTP 404/); 86 response = () => new Response(data); 87 assert.equal((await fetchModule(base, release.manifest, releases)).exports.length,6); 88 } finally { globalThis.fetch = original; } 89 }); 90 91 test('native-parsed export projection retains opaque trees without evaluating them', async () => { 92 const {selectModuleExports} = await import('../dist/module-stage.js'); 93 const value = new Proxy({}, {get(){throw Error('Executable tree must remain opaque');}}); 94 const locals = new Map([['private', value], ['answer', value]]); 95 const dependencies = new Map([['pin', new Map([['id', value]])]]); 96 const stage = exports => ({body:'',imports:[],exports,native:true}); 97 assert.deepEqual([...selectModuleExports(stage([['answer','']]),locals,dependencies).keys()],['answer']); 98 assert.deepEqual([...selectModuleExports(stage([['*',''],['from','pin']]),locals,dependencies).keys()],['private','answer','id']); 99 for(const [exports,pattern] of [[[],/requires an export/],[[['absent','']],/undeclared local/],[[['answer',''],['*','']],/duplicate export/],[[['from','missing']],/Missing re-export/]]){ 100 assert.throws(()=>selectModuleExports(stage(exports),locals,dependencies),pattern); 101 } 102 const huge = new Map(Array.from({length:4097},(_,i)=>['n'+i,value])); 103 assert.throws(()=>selectModuleExports(stage([['*','']]),huge,dependencies),/export limit/); 104 });