tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

io-v2-public.test.mjs (9029B)


      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-v2-private.js';
      6 import {Wire} from '../dist/io-v2-core.js';
      7 import {Session} from '../dist/io-v2-session.js';
      8 import {envelope,available} from '../dist/io-v2-profile.js';
      9 import {drive} from '../dist/io-v2-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(),v2:{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(),v2:{
     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(()=>{});},v2:{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;},v2:{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(),v2:{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(),v2:{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(),v2:{stepping:true,open(){},event(){},close(){}}});assert.equal(listeners,0);
    102  }
    103  assert.equal(peak,1);
    104 });