packages.test.mjs (10730B)
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 {packageIndex,packageReleases,environmentProfile} from '../dist/packages.js'; 6 import {decodeBundle,loadBundle} from '../dist/bundle.js'; 7 import {Graph} from '../dist/graph.js'; 8 import {BROWSER_BUDGET} from '../dist/execution.js'; 9 const read=name=>readFile(new URL('../'+name,import.meta.url)); 10 11 test('named versions and demo environments are validated and pinned',async()=>{ 12 const releases=packageIndex(JSON.parse(await read('packages.json'))); 13 const catalogue=JSON.parse(await read('catalogue.json')); 14 const environments=JSON.parse(await read('environments.json')); 15 const compiler=JSON.parse(await read('compiler.json')); 16 assert.equal(environments.compiler,compiler.hash); 17 for(const entry of catalogue){ 18 const release=releases.find(r=>r.demo===entry.id&&r.hash===entry.hash);assert.ok(release); 19 if(entry.environment===undefined){assert.equal(entry.preparation,'native');continue;} 20 const {profile}=environmentProfile(environments,entry.environment); 21 if(profile){ 22 const bytes=await read('bundles/'+profile.hash+'.arboricx'); 23 assert.equal(createHash('sha256').update(bytes).digest('hex'),profile.hash); 24 const values=loadBundle(new Graph(),decodeBundle(bytes)); 25 for(const name of profile.names)assert.ok(values.has(name),name); 26 } 27 } 28 }); 29 test('index and environment boundaries reject malformed paths, pins, duplicates and profiles',()=>{ 30 const r={name:'demo',version:'0.1',title:'A demo',hash:'a'.repeat(64),manifest:'b'.repeat(64)}; 31 assert.equal(packageIndex({schema:1,releases:[r]})[0].name,'demo'); 32 for(const change of [{name:'../x'},{version:'x/y'},{hash:'A'.repeat(64)},{manifest:'x'},{title:[]},{demo:'x/y'}])assert.throws(()=>packageIndex({schema:1,releases:[{...r,...change}]})); 33 assert.throws(()=>packageIndex({schema:1,releases:[r,r]}),/Duplicate/); 34 assert.throws(()=>packageIndex({schema:2,releases:[r]})); 35 const descriptor={schema:1,compiler:'a'.repeat(64),profiles:{empty:null}}; 36 assert.equal(environmentProfile(descriptor,'empty').profile,null); 37 assert.throws(()=>environmentProfile(descriptor,'toString'),/Unknown/); 38 for(const names of [['bad name'],['x','x'],Array(129).fill('x'),['../x']])assert.throws(()=>environmentProfile({...descriptor,profiles:{test:{hash:r.hash,names}}},'test')); 39 }); 40 test('browser execution has no application ceilings and retains GC',()=>{ 41 assert.deepEqual(BROWSER_BUDGET,{nodes:Infinity,fuel:Infinity,stack:Infinity}); 42 const open=new Graph(BROWSER_BUDGET,{threshold:2});let root=open.leaf; 43 for(let i=0;i<100;i++){root=open.stem(root);open.collect([root]);} 44 assert.ok(open.stats.nodes>10);assert.ok(open.stats.collections>0); 45 }); 46 47 test('published native libraries have authenticated complete interfaces and no fabricated demo source',async()=>{ 48 const releases=packageIndex(JSON.parse(await read('packages.json'))); 49 const expected={ 50 'tricu-base':['append','matchResult','showNumber'], 51 'tricu-file-io':['io','bind','readFile','sha256Hex','putStr','abortIO'], 52 'arboricx-inspection':['inspectBundle','renderSummary','describeError'], 53 }; 54 for(const [name,names] of Object.entries(expected))for(const version of ['0.1.0','0.1.1']){ 55 const release=releases.find(r=>r.name===name&&r.version===version);assert.ok(release);assert.equal(release.demo,undefined); 56 const bytes=await read('bundles/'+release.hash+'.arboricx'); 57 assert.equal(createHash('sha256').update(bytes).digest('hex'),release.hash); 58 assert.deepEqual([...loadBundle(new Graph(),decodeBundle(bytes)).keys()].sort(),names.sort()); 59 } 60 }); 61 62 test('showcase packages retain browser-bounded complete library roots and native-only server identity',async()=>{ 63 const releases=packageIndex(JSON.parse(await read('packages.json'))); 64 for(const [name,required] of Object.entries({ 65 'arboricx-server':['arboricxServer','arboricxRouter','bundleHashValid?'], 66 'tricu-http':['parseRequestLine','response','httpHandlerIO'], 67 'tricu-intensional-contracts':['fullTree?','powerOfTwo?','sortedList?'], 68 'tricu-graphs':['graphSort','graphCycle'], 69 'tricu-parser':['parserBind','parserCommit','parserMatchReply','parserEof'], 70 'tricu-formatter':['formatTri'], 71 })){ 72 const release=releases.find(r=>r.name===name);assert.ok(release);assert.equal(release.demo,undefined); 73 const bytes=await read('bundles/'+release.hash+'.arboricx'); 74 assert.equal(createHash('sha256').update(bytes).digest('hex'),release.hash); 75 assert.ok(bytes.length<=16777216); 76 const bundle=decodeBundle(bytes);assert.ok(bundle.nodes.length<=100000); 77 const exports=loadBundle(new Graph(),bundle); 78 for(const name of required)assert.ok(exports.has(name),name); 79 if(release.name==='arboricx-server')assert.match(release.title,/native sockets and filesystem/); 80 } 81 }); 82 83 test('native parser demo source and goldens are authenticated with an editable native module environment',async()=>{ 84 const catalogue=JSON.parse(await read('catalogue.json')),demo=catalogue.find(e=>e.id==='parser'); 85 const goldens=JSON.parse(await read('parser-demo.json')); 86 assert.equal(demo.preparation,'native');assert.equal(demo.environment,'empty'); 87 assert.equal(createHash('sha256').update(demo.source).digest('hex'),demo.sourceSha256); 88 assert.equal(demo.source,await read('demos/parser.tri').then(b=>b.toString())); 89 assert.equal(goldens.hash,demo.hash);assert.equal(goldens.sourceSha256,demo.sourceSha256); 90 assert.ok(goldens.haskellCases.some(c=>c.record.status==='error'&&c.record.committed)); 91 assert.ok(goldens.haskellCases.some(c=>c.export==='parsePrefix'&&c.record.status==='ok'&&c.record.remaining)); 92 }); 93 94 test('both indexed compiler packages retain their exact independently authenticated artifacts',async()=>{ 95 const releases=packageIndex(JSON.parse(await read('packages.json'))),descriptor=JSON.parse(await read('compiler.json')); 96 for(const [name,path,entrypoint] of [ 97 ['tricu-compiler','test/fixtures/compiler-library.arboricx','source_unit.compileStableModuleDefinitions'], 98 ['tricu-source-builder','bootstrap/build-stable.arboricx','sourceBuildGraph'], 99 ]){ 100 const reference=await readFile(new URL('../../../'+path,import.meta.url)); 101 const referenceHash=createHash('sha256').update(reference).digest('hex'); 102 const release=releases.find(r=>r.name===name&&r.hash===referenceHash);assert.ok(release);assert.equal(release.demo,undefined); 103 const bytes=await read('bundles/'+release.hash+'.arboricx'); 104 assert.ok(bytes.equals(reference),'indexed current artifact matches authenticated reference'); 105 assert.equal(createHash('sha256').update(bytes).digest('hex'),release.hash); 106 const {COMPILER_BUNDLE_LIMIT}=await import('../dist/bundle.js'); 107 const bundle=decodeBundle(bytes,COMPILER_BUNDLE_LIMIT);assert.ok(bundle.nodes.length<=100000); 108 assert.ok(bundle.exports.some(e=>e.name===entrypoint)); 109 if(name==='tricu-compiler')assert.equal(release.hash,descriptor.hash); 110 else{assert.equal(bundle.exports.length,1);assert.match(release.title,/native application compiler/);} 111 } 112 }); 113 114 test('new practical libraries expose complete interfaces and lexer reference source is authenticated',async()=>{ 115 const releases=packageIndex(JSON.parse(await read('packages.json'))); 116 for(const [name,required] of Object.entries({ 117 'tricu-math':['add','divMod','inRange','decimal','printable'], 118 'tricu-lexer':['lexerScanWhile','lexerAdvance','lexerDispatch','lexerToken'], 119 'tricu-binary':['readU8','readBytes','binaryUtf8?','errInvalidBinaryInput'], 120 'tricu-list-traversal':['listContinue','listStop','listFoldUntil','listFoldTailUntil'], 121 'tricu-patterns':['match','otherwise','cond','guard'], 122 })){ 123 const release=releases.find(r=>r.name===name);assert.ok(release);assert.equal(release.demo,undefined); 124 const bytes=await read('bundles/'+release.hash+'.arboricx'); 125 assert.equal(createHash('sha256').update(bytes).digest('hex'),release.hash); 126 const names=decodeBundle(bytes).exports.map(e=>e.name); 127 for(const name of required)assert.ok(names.includes(name),name); 128 if(name==='tricu-list-traversal'||name==='tricu-patterns')assert.deepEqual(names.sort(),required.sort()); 129 } 130 const demo=JSON.parse(await read('catalogue.json')).find(e=>e.id==='lexer'),goldens=JSON.parse(await read('lexer-demo.json')); 131 assert.equal(demo.preparation,'native');assert.equal(demo.environment,'empty'); 132 assert.equal(demo.source,await read('demos/lexer.tri').then(b=>b.toString())); 133 assert.equal(createHash('sha256').update(demo.source).digest('hex'),demo.sourceSha256); 134 assert.equal(goldens.hash,demo.hash);assert.equal(goldens.sourceSha256,demo.sourceSha256); 135 assert.ok(goldens.haskellCases.some(c=>c.record.line===2&&c.record.offset===3)); 136 assert.ok(goldens.haskellCases.some(c=>c.record.remaining==='é')); 137 }); 138 139 test('release choices are newest-first, numeric, independent of publication order and non-mutating',()=>{ 140 const versions=['0.1.0','0.10.0','0.2.0','1.0.0-rc.2','1.0.0-rc.10','1.0.0','1.0.0-alpha','1.0.0-1']; 141 const releases=versions.map(version=>({name:'demo',version,hash:'a'.repeat(64),manifest:'b'.repeat(64),title:version})); 142 const expected=['1.0.0','1.0.0-rc.10','1.0.0-rc.2','1.0.0-alpha','1.0.0-1','0.10.0','0.2.0','0.1.0']; 143 for(const input of [releases,[...releases].reverse()]){ 144 const snapshot=[...input]; 145 assert.deepEqual(packageReleases([...input,{...input[0],name:'other',version:'99.0.0'}],'demo').map(r=>r.version),expected); 146 assert.deepEqual(input,snapshot); 147 } 148 assert.deepEqual(packageReleases(releases,'missing'),[]); 149 const labels=['build2','build10','0.1.0'].map(version=>({...releases[0],version})); 150 assert.deepEqual(packageReleases(labels,'demo').map(r=>r.version),['0.1.0','build10','build2']); 151 }); 152 153 test('every catalogue demo is editable, source-authenticated and explicitly imports indexed manifests',async()=>{ 154 const catalogue=JSON.parse(await read('catalogue.json')),index=packageIndex(JSON.parse(await read('packages.json'))); 155 assert.equal(catalogue.length,16); 156 assert.equal(catalogue[0].id,'names-values'); 157 assert.ok(!catalogue.some(e=>e.id==='intro')); 158 for(const demo of catalogue){ 159 assert.equal(demo.environment,'empty',demo.id); 160 assert.equal(demo.source,(await read('demos/'+demo.id+'.tri')).toString()); 161 assert.equal(createHash('sha256').update(demo.source).digest('hex'),demo.sourceSha256); 162 assert.match(demo.source,/^!export /m); 163 for(const line of demo.source.split('\n').filter(s=>s.startsWith('!import '))){ 164 const match=line.match(/^!import ([0-9a-f]{64}) (?:\*|as [A-Za-z_][A-Za-z0-9_]*)$/);assert.ok(match,line); 165 assert.ok(index.some(r=>r.manifest===match[1]),line); 166 } 167 assert.doesNotMatch(demo.source,/Tree graph|copy it into Compiler|source is read-only|input box uses JSON/); 168 } 169 });