io-public.test.mjs (9070B)
1 import test from 'node:test'; 2 import assert from 'node:assert/strict'; 3 import {Graph} from '../dist/graph.js'; 4 import {readText,readNatural,readList,view} from '../dist/codecs.js'; 5 import {constructors} from '../dist/io-private.js'; 6 import {Wire} from '../dist/io-core.js'; 7 import {Session} from '../dist/io-session.js'; 8 import {envelope,available} from '../dist/io-profile.js'; 9 import {drive} from '../dist/io-driver.js'; 10 function fixture(){ 11 const g=new Graph(),t=constructors({leaf:g.leaf,observe:g.observe.bind(g),stem:g.stem.bind(g),fork:g.fork.bind(g),app:g.app.bind(g)}),w=new Wire(t,1n); 12 const n=x=>t.natural(BigInt(x)),a=(tag,p=t.leaf)=>t.fork(n(tag),p),request=(group,op,p=t.leaf,revision=1)=>a(10,t.fork(t.text(group),t.fork(n(revision),t.fork(t.text(op),p)))); 13 return {g,t,w,n,a,request,session:(action,steps=128)=>new Session(t,action,'1',roots=>g.collect([...roots]),{summarize:false,steps,backend:'test-native'})}; 14 } 15 const host=()=>({before:async()=>{},after(){},read(){throw Error('legacy read');},write(){throw Error('legacy write');},running(){}}); 16 const driver=(s,read)=>({run:s.run,turn:c=>s.turn(c),view:async()=>read(s.core.result.value)}); 17 async function finish(s){for(let i=0;i<100;i++){const r=await s.turn({run:s.run,kind:'poll'});if(r.done)return r;}throw Error('stalled');} 18 const result=(f,s)=>f.w.pair(f.w.pair(s.core.result.value)[1])[0]; 19 test('public framing is bounded and leaves action opaque; unknown versions never trial-execute',()=>{ 20 const f=fixture(),poison=f.g.app(f.g.leaf,f.g.leaf),original=f.t.observe;f.t.observe=h=>{if(h===poison)throw Error('payload forced');return original(h);}; 21 for(const version of [2])assert.deepEqual(envelope(f.t,f.t.fork(f.t.text('tricuIO'),f.t.fork(f.n(version),poison))),{version,action:poison}); 22 const padded=f.t.fork(f.t.stem(f.t.leaf),f.t.fork(f.t.leaf,f.t.leaf)); 23 assert.throws(()=>envelope(f.t,f.t.fork(f.t.text('tricuIO'),f.t.fork(padded,poison))),/version/); 24 for(const version of [0,1,3,4])assert.throws(()=>envelope(f.t,f.t.fork(f.t.text('tricuIO'),f.t.fork(f.n(version),poison))),/version/); 25 assert.throws(()=>envelope(f.t,f.t.fork(f.t.text('tricuIOx'),poison)),/sentinel/); 26 }); 27 test('browser capabilities and inert inspection agree on exact implemented facilities',async()=>{ 28 const f=fixture(),s=f.session(f.request('runtime','capabilities'));await finish(s); 29 const rows=new Map(readList(f.g,result(f,s),20).map(h=>{const [k,v]=readList(f.g,h,2);return [readText(f.g,k),v];})); 30 assert.equal(readText(f.g,rows.get('backend')),'test-native');assert.equal(readText(f.g,rows.get('authority')),'browser-terminal'); 31 const groups=readList(f.g,rows.get('groups'),20).map(h=>{const [group,revision,ops]=readList(f.g,h,3);return [readText(f.g,group),readNatural(f.g,revision).toString(),readList(f.g,ops,10).map(h=>readText(f.g,h))];}); 32 assert.deepEqual(groups,[['runtime','1',['capabilities','inspect']],['standard','1',['input','output','error']],['stream','1',['read','write']],['resource','1',['close']],['utf8','1',['encode','decode']]]); 33 for(const [group,,ops]of groups)for(const op of ops)assert(available(group,1,op)); 34 for(const group of ['file','tcp','process','clock','__proto__','constructor'])assert(!available(group,1,'open')); 35 for(const [group,op,rev,label]of [['stream','write',1,'available'],['stream','write',2,'unsupported'],['file','open',1,'unsupported']]){ 36 const q=fixture(),poison=q.g.app(q.g.leaf,q.g.leaf),observe=q.t.observe;q.t.observe=h=>{if(h===poison)throw Error('argument forced');return observe(h);}; 37 const inspect=q.session(q.request('runtime','inspect',q.request(group,op,poison,rev)));await finish(inspect); 38 const out=readList(q.g,result(q,inspect),5);assert.equal(readText(q.g,out[4]),label);assert(!inspect.core.hasFault); 39 } 40 }); 41 test('public sessions never summarize a completed payload before the selected full codec',async()=>{ 42 const f=fixture(),payload=f.t.text('λ😀'),s=f.session(f.a(0,payload)); 43 let rendered=false,closed; 44 const out=await drive(driver(s,h=>{rendered=true;return view(f.g,h,'string');}),{...host(),structured:{open(){},event(){throw Error('unexpected event');},close(_run,settled){closed=settled;assert(!rendered);}}}); 45 assert.equal(out.value,'λ😀');assert.equal(closed,true); 46 const q=fixture(),opaque=q.g.app(q.g.leaf,q.g.leaf),observe=q.t.observe;q.t.observe=h=>{if(h===opaque)throw Error('summary forced payload');return observe(h);}; 47 const retained=q.session(q.a(0,opaque));assert.equal((await finish(retained)).outcome.value,null); 48 }); 49 test('public Stop cancels a pending read and waits for physical acknowledgement',async()=>{ 50 const f=fixture(),s=f.session(f.request('stream','read',f.t.fork(f.w.marker('io.v2.resource',1),f.n(4)))); 51 let send,closed=false,cancelled=false,stale; 52 await assert.rejects(drive(driver(s,()=>{throw Error('cancelled payload viewed');}),{...host(),structured:{ 53 open(_run,command){send=command;stale=command;}, 54 event(event){if(event.kind==='read')send({run:'1',kind:'stop'});else{ 55 assert.equal(event.kind,'cancel');cancelled=true; 56 setTimeout(()=>{assert(!closed);send({run:'1',kind:'reply',reply:{...event,kind:'cancelled'}});},10); 57 }},close(_run,settled){assert(cancelled);assert(settled);closed=true;} 58 }}),/cancelled/); 59 assert(closed);stale({run:'1',kind:'stop'});assert.equal(s.events.counts.pending,0); 60 }); 61 test('step hooks cannot block structured stop, including a never-resolving pause',async()=>{ 62 const f=fixture(),s=f.session(f.a(0,f.n(42)),1);let send,seen=0; 63 await assert.rejects(drive(driver(s,()=>{throw Error('unexpected view');}),{...host(),before(){seen++;queueMicrotask(()=>send({run:'1',kind:'stop'}));return new Promise(()=>{});},structured:{stepping:true,open(_r,c){send=c;},event(){throw Error('unexpected event');},close(_r,settled){assert(settled);}}}),/cancelled/); 64 assert.equal(seen,1); 65 }); 66 test('undefined step-hook rejection is preserved after cleanup, and renderer failure occurs after settlement',async()=>{ 67 for(const phase of ['step','view']){ 68 const f=fixture(),s=f.session(f.a(0,f.n(42)),1);let closed=false; 69 try{await drive(driver(s,()=>{if(phase==='view')throw undefined;return {}; }),{...host(),before:async()=>{if(phase==='step')throw undefined;},structured:{stepping:true,open(){},event(){},close(_r,settled){closed=settled;}}});assert.fail('must reject');} 70 catch(e){assert.equal(e,undefined);assert(closed);} 71 } 72 }); 73 74 test('unexpected driver loss is labelled unacknowledged, never a successful cleanup',async()=>{ 75 let settled,viewed=false,discarded=false; 76 await assert.rejects(drive({run:'1',discard(){discarded=true;},turn:async()=>{throw Error('runtime lost');},view:async()=>{viewed=true;}},{...host(),structured:{open(){},event(){},close(_r,value){settled=value;}}}),/runtime lost/); 77 assert.equal(settled,false);assert.equal(viewed,false);assert(discarded); 78 }); 79 test('unknown facilities/revisions are ordinary opaque errors; malformed inspection is not execution',async()=>{ 80 for(const [group,operation,revision]of [['file','open',1],['runtime','capabilities',2]]){ 81 const f=fixture(),poison=f.g.app(f.g.leaf,f.g.leaf),observe=f.t.observe;f.t.observe=h=>{if(h===poison)throw Error('unknown argument forced');return observe(h);}; 82 const s=f.session(f.request(group,operation,poison,revision));await finish(s);assert(!s.core.hasFault); 83 assert.equal(readText(f.g,readList(f.g,result(f,s),4)[0]),'unsupported'); 84 } 85 const f=fixture(),s=f.session(f.request('runtime','inspect',f.t.leaf));await finish(s);assert(!s.core.hasFault); 86 assert.equal(readText(f.g,readList(f.g,result(f,s),4)[0]),'invalid-argument'); 87 }); 88 89 test('emergency disposal interrupts an idle read without inventing physical acknowledgement',async()=>{ 90 const f=fixture(),s=f.session(f.request('stream','read',f.t.fork(f.w.marker('io.v2.resource',1),f.n(4)))); 91 let settled;const lifetime=new AbortController(); 92 await assert.rejects(drive({...driver(s,()=>{throw Error('unexpected view');}),signal:lifetime.signal},{...host(),structured:{open(){},event(event){assert.equal(event.kind,'read');setTimeout(()=>lifetime.abort(Error('runtime disposed')),10);},close(_r,value){settled=value;}}}),/disposed/); 93 assert.equal(settled,false);assert.equal(s.events.counts.pending,1,'no fabricated cancelled reply'); 94 }); 95 96 test('completed runs remove emergency-disposal listeners rather than retaining per-turn history',async()=>{ 97 const controller=new AbortController(),signal=controller.signal,add=signal.addEventListener.bind(signal),remove=signal.removeEventListener.bind(signal);let listeners=0,peak=0; 98 signal.addEventListener=(...args)=>{listeners++;peak=Math.max(peak,listeners);return add(...args);};signal.removeEventListener=(...args)=>{listeners--;return remove(...args);}; 99 for(let run=0;run<5;run++){ 100 const f=fixture();let action=f.a(0,f.n(42));for(let i=0;i<20;i++)action=f.a(1,f.t.fork(f.a(0),f.t.fork(f.t.leaf,action))); 101 const s=f.session(action,1);await drive({...driver(s,h=>view(f.g,h,'number')),signal},{...host(),structured:{stepping:true,open(){},event(){},close(){}}});assert.equal(listeners,0); 102 } 103 assert.equal(peak,1); 104 });