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