tricu

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

io-v2-tasks.test.mjs (8671B)


      1 import test from 'node:test';
      2 import assert from 'node:assert/strict';
      3 import {Tasks} from '../dist/io-v2-tasks.js';
      4 const ok=value=>({kind:'completed',value}),failed=value=>({kind:'failed',value}),cancelled=value=>({kind:'cancelled',value});
      5 const diagnostics=items=>({items,omitted:0});
      6 const fresh=(run=1n,limit)=>new Tasks(run,'owner-stopped','join-cycle',limit);
      7 function finish(s,id,outcome=ok(null),cleanup){s.bodyDone(id,outcome);assert.equal(s.beginRelease(id),true);return s.settle(id,cleanup);}
      8 function retired(s){assert.deepEqual(s.counts,{scopes:0,tasks:0,hosts:0});assert.deepEqual([...s.payloads()],['owner-stopped','join-cycle']);}
      9 
     10 test('normal exit closes admission but waits for children; ordinary errors remain values',()=>{
     11  const s=fresh(),child=s.spawn(0,false,false),ordinary={err:'domain error'};
     12  s.bodyDone(0,ok(ordinary));assert.equal(s.beginRelease(0),false);
     13  assert.throws(()=>s.spawn(0,false,false),/invalid-transition/);
     14  const grandchild=s.spawn(child.key,false,false); // existing child remains live during ancestor join
     15  finish(s,grandchild.key);finish(s,child.key);
     16  assert.equal(s.beginRelease(0),true);const out=s.settle(0);
     17  assert.equal(out.kind,'completed');assert.equal(out.value,ordinary);retired(s);
     18 });
     19 test('normal joining remains cancellable and the first reason is sticky',()=>{
     20  const s=fresh(),child=s.spawn(0,false,false);
     21  s.bodyDone(0,ok('return'));s.stop(0,cancelled('stop'));s.stop(0,failed('late'));
     22  assert.deepEqual(s.state(child.key).reason,cancelled('owner-stopped'));
     23  finish(s,child.key,failed('late child'));
     24  assert(s.beginRelease(0));assert.equal(s.settle(0).value,'stop');retired(s);
     25 });
     26 test('supervision contains failure; unsupervised failure stops siblings immediately',()=>{
     27  const s=fresh(),contained=s.spawn(0,true,true);
     28  finish(s,contained.key,failed('contained'));assert.equal(s.state(0).reason,null);
     29  s.claimJoin(0,contained);assert.equal(s.pollJoin(0).value,'contained');
     30  const a=s.spawn(0,false,true),b=s.spawn(0,false,true);
     31  s.stop(a.key,failed('first'));s.stop(b.key,failed('second'));
     32  assert.deepEqual(s.state(0).reason,failed('first'));
     33  assert.deepEqual(s.state(b.key).reason,cancelled('owner-stopped'));
     34  finish(s,a.key);finish(s,b.key);assert.equal(finish(s,0).value,'first');retired(s);
     35 });
     36 test('joins consume before waiting; abandoned joins never restore tickets or retain results',()=>{
     37  const s=fresh(),child=s.spawn(0,true,true),waiter=s.spawn(0,true,true);
     38  s.claimJoin(waiter.key,child);assert.equal(s.pollJoin(waiter.key),null);
     39  assert.throws(()=>s.discard(0,child),/stale-task/);
     40  s.cancel(0,waiter,'cancel wait');finish(s,waiter.key);
     41  const result={unique:'unclaimed'};finish(s,child.key,ok(result));
     42  assert(![...s.payloads()].includes(result));
     43  s.claimJoin(0,waiter);assert.equal(s.pollJoin(0).value,'cancel wait');
     44  assert.throws(()=>s.claimJoin(0,waiter),/stale-task/);
     45  finish(s,0);retired(s);
     46 });
     47 test('explicit and implicit cycles use task identities across lexical scopes',()=>{
     48  const s=fresh(),a=s.spawn(0,true,true);let b=s.spawn(0,true,true);
     49  const inner=s.nested(a.key);
     50  assert.throws(()=>s.claimJoin(inner,a),/join-cycle/);
     51  s.claimJoin(inner,b);assert.throws(()=>s.claimJoin(b.key,a),/join-cycle/);
     52  s.abandonJoin(inner);finish(s,b.key);b=s.spawn(0,true,true);
     53  const c=s.spawn(inner,true,true);
     54  s.claimJoin(b.key,a);s.claimJoin(c.key,b);s.bodyDone(inner,ok(null));
     55  assert.equal(s.state(inner).reason.value,'join-cycle');assert.equal(s.state(a.key).reason.value,'join-cycle');
     56  finish(s,c.key);assert(s.beginRelease(inner));s.settle(inner);finish(s,a.key);
     57  assert.equal(s.pollJoin(b.key).value,'join-cycle');finish(s,b.key);finish(s,0);retired(s);
     58 });
     59 test('out-of-scope, cross-run and completed task cancellation reject without consumption',()=>{
     60  const s=fresh(),a=s.spawn(0,true,true),b=s.spawn(0,true,true),owned=s.spawn(a.key,true,true);
     61  assert.throws(()=>s.claimJoin(b.key,owned),/out-of-scope/);
     62  assert.throws(()=>s.cancel(0,{...a,run:2n},'bad'),/stale-task/);
     63  finish(s,owned.key,ok('child'));s.claimJoin(a.key,owned);assert.equal(s.pollJoin(a.key).value,'child');
     64  finish(s,a.key);assert.throws(()=>s.cancel(0,a,'late'),/stale-task/);s.discard(0,a);
     65  finish(s,b.key);finish(s,0);retired(s);
     66 });
     67 test('cleanup defeats success, preserves the primary and propagates bounded diagnostics once',()=>{
     68  const s=fresh(),inner=s.nested(0),child=s.spawn(inner,false,false);
     69  assert.equal(finish(s,child.key,ok(null),diagnostics([17])).value,17);
     70  const lexical=finish(s,inner,ok(null),diagnostics(Array.from({length:25},(_,i)=>100+i)));
     71  assert.equal(lexical.cleanup,undefined);
     72  const out=finish(s,0,failed(999),diagnostics([200]));
     73  assert.equal(out.kind,'failed');assert.equal(out.value,17);
     74  assert.deepEqual(out.cleanup,{items:Array.from({length:16},(_,i)=>100+i),omitted:10});retired(s);
     75 });
     76 test('pending host cancellation is delivered once but release requires acknowledgement',async()=>{
     77  const s=fresh(),reader=s.spawn(0,true,true),read=s.beginHost(reader.key),controller=new AbortController(),events=[];
     78  let acknowledge;
     79  const physicalWork=new Promise(resolve=>{acknowledge=resolve;});
     80  controller.signal.addEventListener('abort',()=>events.push('abort-requested'),{once:true});
     81  s.cancel(0,reader,'cancel input');s.bodyDone(reader.key,ok('ignored value'));
     82  assert.deepEqual(s.takeCancellations(),[read]);controller.abort();
     83  assert.deepEqual(s.takeCancellations(),[]);assert.equal(s.beginRelease(reader.key),false);
     84  s.bodyDone(0,ok('parent'));assert.equal(s.beginRelease(0),false);
     85  await Promise.resolve();assert.deepEqual(events,['abort-requested']);assert.equal(s.beginRelease(reader.key),false);
     86  acknowledge();await physicalWork;events.push('physical-work-settled');
     87  assert.equal(s.endHost(read),'ignored');assert.equal(s.endHost(read),'stale');
     88  assert(s.beginRelease(reader.key));assert.equal(s.settle(reader.key).kind,'cancelled');
     89  assert(s.beginRelease(0));s.settle(0);retired(s);
     90 });
     91 test('wrong/stale host resumes leave the correct wait intact',()=>{
     92  const s=fresh(),child=s.spawn(0,true,false),host=s.beginHost(child.key);
     93  for(const bad of [{...host,scope:0},{...host,run:2n},{...host,key:host.key+1}])assert.equal(s.endHost(bad),'stale');
     94  assert.equal(s.state(child.key).hostCalls,1);assert.equal(s.endHost(host),'reply');
     95  const next=s.beginHost(child.key);assert.notEqual(next.key,host.key);assert.equal(s.endHost(host),'stale');
     96  assert.equal(s.endHost(next),'reply');finish(s,child.key);finish(s,0);retired(s);
     97 });
     98 test('all callback completion orders retain cancelled scopes until their own work settles',()=>{
     99  for(const order of [[0,1,2],[0,2,1],[1,0,2],[1,2,0],[2,0,1],[2,1,0]]){
    100   const s=fresh(),children=order.map(()=>s.spawn(0,false,false)),hosts=children.map(c=>s.beginHost(c.key));
    101   s.stop(0,failed('stop'));s.bodyDone(0,ok(null));
    102   for(const child of children)s.bodyDone(child.key,ok(null));
    103   assert.equal(s.takeCancellations().length,3);assert.equal(s.takeCancellations().length,0);
    104   for(const index of order){
    105    assert.equal(s.beginRelease(0),false);assert.equal(s.beginRelease(children[index].key),false);
    106    assert.equal(s.endHost(hosts[index]),'ignored');assert(s.beginRelease(children[index].key));s.settle(children[index].key);
    107   }
    108   assert(s.beginRelease(0));assert.equal(s.settle(0).value,'stop');retired(s);
    109  }
    110 });
    111 test('nonwrapping identities, scope state copies and immutable diagnostic capture',()=>{
    112  for(const bad of [0n,-1n,1,2n**64n])assert.throws(()=>fresh(bad),/identity/);
    113  const s=fresh(1n,2),a=s.spawn(0,true,true),copy=s.state(a.key);copy.phase='releasing';
    114  assert.equal(s.state(a.key).phase,'open');
    115  const items=['cleanup'];s.bodyDone(a.key,failed('primary'));assert(s.beginRelease(a.key));
    116  const result=s.settle(a.key,diagnostics(items));items[0]='changed';result.cleanup.items[0]='also changed';
    117  s.claimJoin(0,a);assert.equal(s.pollJoin(0).cleanup.items[0],'cleanup');
    118  const b=s.spawn(0,true,false);finish(s,b.key);
    119  assert.throws(()=>s.spawn(0,true,false),/identity-exhausted/);assert.deepEqual(s.counts,{scopes:1,tasks:0,hosts:0});
    120  const h=s.beginHost(0);assert(Object.isFrozen(h));s.endHost(h);s.endHost(s.beginHost(0));
    121  assert.throws(()=>s.beginHost(0),/identity-exhausted/);finish(s,0);retired(s);
    122 });
    123 test('joined, discarded and non-result tasks retain no history or payload roots',()=>{
    124  const s=fresh();
    125  for(let i=0;i<2000;i++){
    126   const t=s.spawn(0,true,i%3!==2),payload={unique:i};finish(s,t.key,ok(payload));
    127   if(i%3===0){s.claimJoin(0,t);assert.equal(s.pollJoin(0).value,payload);}
    128   if(i%3===1)s.discard(0,t);
    129   assert.deepEqual(s.counts,{scopes:1,tasks:0,hosts:0});assert(![...s.payloads()].includes(payload));
    130  }
    131  finish(s,0);retired(s);
    132 });