io-resources.test.mjs (11357B)
1 import test from 'node:test'; 2 import assert from 'node:assert/strict'; 3 import {lifecycle} from '../dist/io-resources.js'; 4 import {HostWork} from '../dist/io-host-work.js'; 5 const ok=value=>({kind:'completed',value}); 6 const setup=(limits={})=>lifecycle(1n,'owner-stopped','cycle',['stdin','stdout','stderr'],limits); 7 const create=(s,owner,backend,flags={},kind='file')=>s.resources.publish(s.resources.beginAcquire(owner),kind,backend,flags); 8 async function finish(s,scope,release=()=>{},outcome=ok(null)){ 9 s.tasks.bodyDone(scope,outcome);assert(s.tasks.beginRelease(scope)); 10 const errors=await s.resources.releaseScope(scope,release,error=>error,'fallback');return s.tasks.settle(scope,errors); 11 } 12 function retired(s){assert.deepEqual(s.tasks.counts,{scopes:0,tasks:0,hosts:0});assert.deepEqual(s.resources.counts,{resources:0,admissions:0,leases:0});} 13 14 test('admission closure preserves admitted opens and existing descendant borrowing',async()=>{ 15 const s=setup(),child=s.tasks.spawn(0,true,false),admission=s.resources.beginAcquire(0); 16 s.tasks.bodyDone(0,ok(null));assert.equal(s.tasks.beginRelease(0),false); 17 assert.throws(()=>s.resources.beginAcquire(child.key,0),/closed-scope/); 18 const file=s.resources.publish(admission,'file','backend'),lease=s.resources.beginLease(child.key,file,'file','read'); 19 assert.equal(lease.backend,'backend');s.tasks.bodyDone(child.key,ok(null));assert.equal(s.tasks.beginRelease(child.key),false); 20 s.resources.endLease(lease.token);assert(s.tasks.beginRelease(child.key));s.tasks.settle(child.key); 21 assert(s.tasks.beginRelease(0));assert.throws(()=>s.tasks.settle(0),/busy/); 22 const released=[];await s.resources.releaseScope(0,r=>released.push(r.backend),e=>e,'fallback');s.tasks.settle(0); 23 assert.deepEqual(released,['backend']);retired(s); 24 }); 25 test('pending ancestor acquisition blocks owner even after acquiring child has stopped',async()=>{ 26 const s=setup(),child=s.tasks.spawn(0,true,false),admission=s.resources.beginAcquire(child.key,0); 27 s.tasks.cancel(0,child,'cancel');await finish(s,child.key);s.tasks.bodyDone(0,ok(null)); 28 assert.equal(s.tasks.beginRelease(0),false);s.resources.abortAcquire(admission); 29 assert.throws(()=>s.resources.abortAcquire(admission),/invalid-admission/); 30 assert(s.tasks.beginRelease(0));await s.resources.releaseScope(0,()=>assert.fail('borrowed close'),e=>e,'fallback');s.tasks.settle(0);retired(s); 31 }); 32 test('transfer validation is atomic; rekeying invalidates aliases and preserves acquisition order',async()=>{ 33 const s=setup(),a=create(s,0,'first'),b=create(s,0,'second'); 34 const before={tasks:s.tasks.counts,resources:s.resources.counts}; 35 for(const [tokens,message] of [[[a,a],/duplicate/],[[a,{...b,run:2n}],/stale/],[[a,{run:1n,key:1}],/borrowed/]]){ 36 assert.throws(()=>s.resources.spawn(0,false,true,tokens),message); 37 assert.deepEqual({tasks:s.tasks.counts,resources:s.resources.counts},before);assert.equal(s.resources.lookup(0,a).backend,'first'); 38 } 39 const {task,handles}=s.resources.spawn(0,true,true,[b,a]); 40 assert.throws(()=>s.resources.lookup(0,a),/stale/);assert.throws(()=>s.resources.lookup(task.key,b),/stale/); 41 assert.throws(()=>s.resources.lookup(0,handles[0]),/out-of-scope/); 42 assert.equal(s.resources.lookup(task.key,handles[0]).backend,'second');assert.deepEqual(s.resources.newestOwned(task.key),handles[0]); 43 const nested=s.tasks.nested(task.key);assert.throws(()=>s.resources.spawn(nested,true,false,[handles[1]]),/not-owner/); 44 await finish(s,nested);const released=[];await finish(s,task.key,r=>released.push(r.backend)); 45 assert.deepEqual(released,['second','first']);await finish(s,0);retired(s); 46 }); 47 test('failed task admission cannot move resources, start a child or consume resource IDs',async()=>{ 48 const s=setup({taskIdentities:1}),file=create(s,0,'owner'),only=s.tasks.spawn(0,true,false);await finish(s,only.key); 49 const before=s.resources.counts; 50 assert.throws(()=>s.resources.spawn(0,true,true,[file]),/scope-identity-exhausted/); 51 assert.equal(s.resources.lookup(0,file).owner,0);assert.deepEqual(s.resources.counts,before); 52 assert.deepEqual(s.tasks.counts,{scopes:1,tasks:0,hosts:0}); 53 const next=create(s,0,'next');assert.equal(next.key,file.key+2,'failed spawn consumed resource identities'); 54 await finish(s,0);retired(s); 55 }); 56 test('direction leases, wrong-kind rejection and exactly-once close acknowledgements',async()=>{ 57 const s=setup(),file=create(s,0,'duplex',{duplex:true}); 58 assert.throws(()=>s.resources.beginLease(0,file,'reader','read'),/wrong-kind/);assert.equal(s.resources.counts.leases,0); 59 const r=s.resources.beginLease(0,file,'file','read'),w=s.resources.beginLease(0,file,'file','write'); 60 assert.throws(()=>s.resources.beginLease(0,file,'file','read'),/busy/);assert.throws(()=>s.resources.spawn(0,true,false,[file]),/busy/); 61 assert.equal(s.resources.claimClose(0,file),null);assert.throws(()=>s.resources.beginLease(0,file,'file','read'),/closed-handle/); 62 s.resources.endLease(r.token);assert.throws(()=>s.resources.endLease(r.token),/invalid-lease/);s.resources.endLease(w.token); 63 assert(s.resources.claimClose(0,file));assert.equal(s.resources.claimClose(0,file),null); 64 s.tasks.bodyDone(0,ok(null));assert(s.tasks.beginRelease(0));assert.throws(()=>s.tasks.settle(0),/busy/); 65 s.resources.finishClose(file);assert.throws(()=>s.resources.finishClose(file),/stale-handle/); 66 await s.resources.releaseScope(0,()=>{},e=>e,'fallback');s.tasks.settle(0);retired(s); 67 }); 68 test('ordinary single-cursor resources, internal resources and borrowed standards stay distinct',async()=>{ 69 const s=setup(),file=create(s,0,'file'),lease=s.resources.beginLease(0,file,'file','read'); 70 assert.throws(()=>s.resources.beginLease(0,file,'file','write'),/busy/);s.resources.endLease(lease.token); 71 const hidden=create(s,0,'hidden',{internal:true}); 72 assert.throws(()=>s.resources.beginLease(0,hidden,'file','read'),/internal-resource/); 73 assert.throws(()=>s.resources.claimClose(0,hidden),/internal-resource/); 74 assert.throws(()=>s.resources.spawn(0,true,false,[hidden]),/internal-resource/); 75 assert.throws(()=>s.resources.claimClose(0,{run:1n,key:2}),/borrowed-resource/); 76 const internal=s.resources.beginLease(0,hidden,'file','exclusive',true);s.resources.endLease(internal.token); 77 const view=s.resources.lookup(0,file);view.owner=999;assert.equal(s.resources.lookup(0,file).owner,0); 78 assert.throws(()=>{view.flags.borrowed=true;},TypeError); 79 await finish(s,0);retired(s); 80 }); 81 test('reverse publication order, not open-start order, determines cleanup',async()=>{ 82 const s=setup(),first=s.resources.beginAcquire(0),second=s.resources.beginAcquire(0); 83 s.resources.publish(second,'file','published first');s.resources.publish(first,'file','published second'); 84 const order=[];await finish(s,0,r=>order.push(r.backend));assert.deepEqual(order,['published second','published first']);retired(s); 85 }); 86 test('publication identity exhaustion leaves admission live for raw release/abort; cleanup needs no IDs',async()=>{ 87 const s=setup({resourceIdentities:4}),admission=s.resources.beginAcquire(0); 88 assert.throws(()=>s.resources.publish(admission,'file','raw backend'),/identity-exhausted/); 89 assert.deepEqual(s.resources.counts,{resources:3,admissions:1,leases:0}); 90 assert.throws(()=>s.resources.beginAcquire(0),/identity-exhausted/); 91 assert.throws(()=>s.resources.beginLease(0,{run:1n,key:1},'reader','read'),/identity-exhausted/); 92 s.resources.abortAcquire(admission);await finish(s,0);retired(s); 93 }); 94 test('physical cleanup attempts every provider once and diagnostics failure cannot skip releases',async()=>{ 95 const s=setup(),order=[];let classified=0;for(let i=0;i<20;i++)create(s,0,i); 96 s.tasks.bodyDone(0,{kind:'failed',value:'primary'});assert(s.tasks.beginRelease(0)); 97 const errors=await s.resources.releaseScope(0,r=>{order.push(r.backend);throw 'release failed';},()=>{classified++;throw 'classifier failed';},'fallback'); 98 assert.deepEqual(order,Array.from({length:20},(_,i)=>19-i));assert.equal(errors.items.length,16);assert.equal(errors.omitted,4);assert.equal(classified,16); 99 const out=s.tasks.settle(0,errors);assert.equal(out.value,'primary');assert.deepEqual(out.cleanup,errors);retired(s); 100 }); 101 test('asynchronous release stays registered until provider acknowledgement, even on failure',async()=>{ 102 const s=setup(),work=new HostWork(s.tasks),file=create(s,0,'file');let acknowledge; 103 s.tasks.bodyDone(0,ok(null));assert(s.tasks.beginRelease(0)); 104 const pending=new Promise(resolve=>{acknowledge=resolve;}); 105 work.release(0,()=>s.resources.releaseScope(0,async()=>{await pending;throw 'close failed';},e=>e,'fallback')); 106 assert(s.resources.lookup(0,file).releasing);assert.throws(()=>s.tasks.settle(0),/invalid-transition/); 107 const wait=work.wait();acknowledge();await wait;const reply=work.take();assert(reply.outcome.ok); 108 assert.throws(()=>s.resources.lookup(0,file),/stale-handle/); 109 const out=s.tasks.settle(0,reply.outcome.value);assert.equal(out.kind,'failed');assert.equal(out.value,'close failed');retired(s); 110 }); 111 test('repeated scoped resources and leases retain no registry history',async()=>{ 112 const s=setup();for(let i=0;i<1000;i++){ 113 const child=s.resources.spawn(0,true,false,[]).task,file=create(s,child.key,i),lease=s.resources.beginLease(child.key,file,'file','exclusive'); 114 s.resources.endLease(lease.token);await finish(s,child.key); 115 assert.deepEqual(s.resources.counts,{resources:3,admissions:0,leases:0});assert.deepEqual(s.tasks.counts,{scopes:1,tasks:0,hosts:0}); 116 } 117 await finish(s,0);retired(s); 118 }); 119 test('explicit close cancels a leased operation, not its task, and waits for lease acknowledgement',async()=>{ 120 const s=setup(),work=new HostWork(s.tasks),file=create(s,0,'reader',{},'reader'),child=s.tasks.spawn(0,true,false);let acknowledge; 121 const drained=new Promise(resolve=>{acknowledge=resolve;}); 122 const read=work.start(child.key,async signal=>{ 123 const lease=s.resources.beginLease(child.key,file,'reader','read'); 124 try {await new Promise(resolve=>signal.addEventListener('abort',resolve,{once:true}));await drained;throw 'resource closed';} 125 finally{s.resources.endLease(lease.token);} 126 }); 127 assert.equal(s.resources.claimClose(0,file),null); 128 assert.equal(work.cancelBody({...read,run:2n}),false);assert.equal(work.cancelBody(read),true);assert.equal(work.cancelBody(read),false); 129 assert.equal(s.resources.claimClose(0,file),null);assert.equal(s.tasks.state(child.key).reason,null); 130 const waiting=work.wait();acknowledge();await waiting;const reply=work.take(); 131 assert.equal(reply.outcome.ok,false);assert.equal(reply.outcome.error,'resource closed');assert.equal(s.tasks.state(child.key).reason,null); 132 let physical=0;work.start(0,()=>{assert(s.resources.claimClose(0,file));physical++;s.resources.finishClose(file);}); 133 await work.wait();assert(work.take().outcome.ok);assert.equal(physical,1); 134 await finish(s,child.key);await finish(s,0);retired(s); 135 }); 136 137 test('resource exhaustion and oversize transfers do not admit tasks or invalidate aliases',async()=>{ 138 const s=setup({resourceIdentities:5}),file=create(s,0,'file'); 139 for(const [tokens,error] of [[[file],/identity-exhausted/],[Array(257).fill(file),/transfer-limit/]]){ 140 assert.throws(()=>s.resources.spawn(0,true,false,tokens),error); 141 assert.equal(s.resources.lookup(0,file).owner,0);assert.deepEqual(s.tasks.counts,{scopes:1,tasks:0,hosts:0}); 142 } 143 await finish(s,0);retired(s); 144 });