io-core.test.mjs (7918B)
1 import test from 'node:test'; 2 import assert from 'node:assert/strict'; 3 import {Graph} from '../dist/graph.js'; 4 import {natural,text,readNatural,readList,readText} from '../dist/codecs.js'; 5 import {Core,Wire} from '../dist/io-core.js'; 6 function fixture(){ 7 const g=new Graph(),tree={leaf:g.leaf,observe:g.observe.bind(g),fork:g.fork.bind(g),stem:g.stem.bind(g),app:g.app.bind(g),natural:n=>natural(g,n),text:s=>text(g,s)}; 8 const w=new Wire(tree,1n),n=x=>natural(g,BigInt(x)),act=(code,p=g.leaf)=>g.fork(n(code),p),k=x=>g.fork(g.leaf,x); 9 const bind=(left,right)=>act(1,g.fork(left,k(right))); 10 const spawn=(child,sup=true,keep=true)=>act(5,g.fork(n(sup?1:0),g.fork(n(keep?1:0),g.fork(g.leaf,k(child))))); 11 const request=(args=g.leaf)=>act(10,g.fork(text(g,'future'),g.fork(n(1),g.fork(text(g,'read'),args)))); 12 const provider={request:e=>()=>e.wire.result(false,e.wire.problem('unsupported','future/read')),release:()=>assert.fail('borrowed backend close')}; 13 const core=(action,p=provider,limit)=>new Core(tree,1n,action,[null,null,null],p,limit); 14 return {g,tree,w,n,act,k,bind,spawn,request,provider,core}; 15 } 16 async function drain(e,turn=()=>{}){ 17 for(let i=0;i<20000&&!e.result;i++){ 18 const progress=e.advance();turn(); 19 if(!progress){assert(e.work.counts.pending,'interpreter stalled');await e.wait();} 20 } 21 assert(e.result,'interpreter did not settle'); 22 assert.deepEqual(e.lifecycle.tasks.counts,{scopes:0,tasks:0,hosts:0}); 23 assert.deepEqual(e.lifecycle.resources.counts,{resources:0,admissions:0,leases:0}); 24 assert.deepEqual(e.work.counts,{pending:0,replies:0});return e.result; 25 } 26 function result(f,value){const [flag,tail]=f.w.pair(value);return {ok:f.g.observe(flag).node.kind==='stem',value:f.w.pair(tail)[0]};} 27 28 test('real tree bind/scope/current-scope execute, with roots collected between turns',async()=>{ 29 const f=fixture(),e=f.core(f.bind(f.act(0,f.n(1)),f.act(3,f.k(f.act(4))))); 30 const out=await drain(e,()=>f.g.collect([...e.roots()]));assert.equal(out.kind,'completed'); 31 const parts=readList(f.g,out.value,3);assert.equal(readText(f.g,parts[0]),'io.v2.scope');assert.equal(readNatural(f.g,parts[2]),1n); 32 }); 33 test('opaque terminal diagnostics never demand payloads or abandoned continuations',async()=>{ 34 for(const tag of [2,9]){ 35 const f=fixture(),poison=f.g.app(f.g.leaf,f.g.leaf),observe=f.tree.observe; 36 f.tree.observe=h=>{if(h===poison)throw Error('forced opaque diagnostic');return observe(h);}; 37 const e=f.core(f.act(1,f.g.fork(f.act(tag,poison),poison))),out=await drain(e); 38 assert.equal(out.kind,tag===2?'failed':'cancelled');assert.equal(out.value,poison);assert.equal(e.fault,undefined); 39 } 40 }); 41 test('spawn/join encode the native outcome contract and consume tickets once',async()=>{ 42 const f=fixture(),join=f.act(6,f.w.marker('io.v2.join',1)); 43 const e=f.core(f.bind(f.spawn(f.act(0,f.n(42))),join)),out=await drain(e),joined=result(f,out.value); 44 assert(joined.ok);const parts=readList(f.g,joined.value,2);assert.equal(readText(f.g,parts[0]),'completed');assert.equal(readNatural(f.g,parts[1]),42n); 45 const other=fixture(),again=other.act(6,other.w.marker('io.v2.join',1)); 46 const repeated=await drain(other.core(other.bind(other.spawn(other.act(0)),other.bind(again,again)))); 47 const stale=result(other,repeated.value);assert(!stale.ok);assert.equal(readText(other.g,readList(other.g,stale.value,4)[0]),'stale-task'); 48 }); 49 test('supervised failure is data; uncontained failure defeats parent success',async()=>{ 50 for(const supervised of [true,false]){ 51 const f=fixture(),out=await drain(f.core(f.bind(f.spawn(f.act(2,f.n(17)),supervised),f.act(6,f.w.marker('io.v2.join',1))))); 52 assert.equal(out.kind,supervised?'completed':'failed'); 53 if(supervised){const joined=result(f,out.value);assert(joined.ok);assert.equal(readText(f.g,readList(f.g,joined.value,3)[0]),'failed');} 54 else assert.equal(readNatural(f.g,out.value),17n); 55 } 56 }); 57 test('unsupported facilities stay ordinary Results without argument demand',async()=>{ 58 const f=fixture(),poison=f.g.app(f.g.leaf,f.g.leaf),observe=f.tree.observe;f.tree.observe=h=>{if(h===poison)throw Error('demanded arguments');return observe(h);}; 59 const e=f.core(f.request(poison)),out=await drain(e,()=>f.g.collect([...e.roots()]));assert.equal(out.kind,'completed');assert.equal(result(f,out.value).ok,false); 60 }); 61 test('tree cancellation drains pending provider before join and suppresses abandoned continuation',async()=>{ 62 const f=fixture(),args=f.n(123);let acknowledge,aborts=0,argumentLive=false; 63 const gate=new Promise(resolve=>{acknowledge=resolve;}); 64 const p={...f.provider,request:async(e,_scope,request,signal)=>{ 65 await new Promise(resolve=>signal.addEventListener('abort',()=>{aborts++;resolve();},{once:true})); 66 await gate;argumentLive=readNatural(f.g,request.arguments)===123n;return ()=>{throw Error('cancelled reply encoder ran');}; 67 }}; 68 const cancel=f.act(8,f.g.fork(f.w.marker('io.v2.task',1),f.n(7))); 69 const e=f.core(f.bind(f.spawn(f.request(args)),f.bind(cancel,f.act(6,f.w.marker('io.v2.join',1)))),p); 70 for(let i=0;i<40;i++){const progress=e.advance();f.g.collect([...e.roots()]);if(!progress)break;} 71 assert.equal(aborts,1);assert.equal(e.result,null);assert.equal(e.work.counts.pending,1); 72 acknowledge();await e.wait();const out=await drain(e,()=>f.g.collect([...e.roots()]));assert(argumentLive); 73 const joined=result(f,out.value),parts=readList(f.g,joined.value,3);assert.equal(readText(f.g,parts[0]),'cancelled');assert.equal(readNatural(f.g,parts[1]),7n); 74 }); 75 test('malformed action is terminal, malformed operation is an ordinary Result',async()=>{ 76 const f=fixture(),terminal=await drain(f.core(f.act(1,f.g.leaf)));assert.equal(terminal.kind,'failed'); 77 const ordinary=await drain(f.core(f.act(6,f.g.leaf)));assert.equal(ordinary.kind,'completed');assert(!result(f,ordinary.value).ok); 78 }); 79 test('evaluator fault after lexical admission drains admitted scopes without retrying evaluation',async()=>{ 80 const f=fixture(),failure=new Error('injected evaluator failure');let applications=0; 81 f.tree.app=()=>{applications++;throw failure;};const e=f.core(f.act(3,f.k(f.act(0)))); 82 assert.equal((await drain(e)).kind,'failed');assert.equal(e.fault,failure);assert.equal(applications,1); 83 }); 84 test('action bound stops the entire run and cleans up',async()=>{ 85 const f=fixture(),e=f.core(f.bind(f.act(0),f.act(0)),f.provider,1); 86 assert.equal((await drain(e)).kind,'failed');assert.equal(e.fault,undefined); 87 }); 88 89 test('evaluator errors cannot masquerade as protocol or ordinary-operation errors',async()=>{ 90 for(const message of ['invalid computation shape','invalid-argument']){ 91 const f=fixture(),fault=new Error(message),action=f.act(6,f.g.leaf),observe=f.tree.observe; 92 f.tree.observe=h=>{if(h===(message==='invalid-argument'?f.g.leaf:action))throw fault;return observe(h);}; 93 const e=f.core(action);assert.equal((await drain(e)).kind,'failed');assert.equal(e.fault,fault);assert(e.hasFault); 94 } 95 }); 96 test('undefined provider rejection is retained as an embedding fault, not successful undefined data',async()=>{ 97 const f=fixture(),e=f.core(f.request(),{...f.provider,request:()=>Promise.reject(undefined)}); 98 assert.equal((await drain(e)).kind,'failed');assert(e.hasFault);assert.equal(e.fault,undefined); 99 }); 100 101 test('lexical physical cleanup failure defeats success once, with uncertain release progress',async()=>{ 102 const f=fixture();let closed=0; 103 const e=f.core(f.act(3,f.k(f.request())),{ 104 request:(engine,scope)=>{ 105 const r=engine.lifecycle.resources;r.publish(r.beginAcquire(scope),'test',null); 106 return ()=>engine.wire.result(true,engine.tree.leaf); 107 },release:()=>{closed++;throw Error('uncertain close');} 108 }); 109 const out=await drain(e);assert.equal(out.kind,'failed');assert.equal(closed,1);assert.equal(out.cleanup.items.length,0); 110 const problem=readList(f.g,out.value,4);assert.equal(readText(f.g,problem[0]),'cleanup-failed');assert.equal(readText(f.g,problem[3]),'unknown'); 111 });