import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createRunTracker } from '../../src/lib/canvas-runtime/runs.ts'; const tick = async () => { for (let index=0;index<30;index++)await Promise.resolve(); }; const deferred = () => { let resolve;const promise=new Promise(yes=>{resolve=yes;});return{promise,resolve}; }; function fixture() { let now=0,next=0;const timers=new Map(),runs=new Map([['run',{id:'run',status:'running',cursor:0,hasMore:true,attempt:0}]]),calls=[],saved=[],response=deferred(); const tracker=createRunTracker({clock:{now:()=>now,setTimer(callback,delay){const id=++next;timers.set(id,{callback,due:now+delay});return id;},clearTimer:id=>timers.delete(id)},getRuns:()=>runs,async persist(){saved.push(structuredClone([...runs]));},async poll(run){calls.push(run);return calls.length===1?response.promise:{status:'succeeded',events:[],next:2,hasMore:false,outputs:[],steps:[]};},accept(run,page){runs.set(run.id,{...runs.get(run.id),status:page.status,cursor:page.next,hasMore:page.hasMore});},update(id,reduce){runs.set(id,reduce(runs.get(id)));},onError(error){throw error;}}); return{tracker,runs,calls,saved,response,timers,async advance(ms){now+=ms;for(const[id,timer]of[...timers])if(timer.due<=now){timers.delete(id);timer.callback();}await tick();}}; } test('run holds preserve a completed cursor and notifications without launching another poll',async()=>{ const f=fixture();f.tracker.notify(['run']);await f.advance(0);assert.equal(f.calls.length,1); const releaseA=f.tracker.hold(),releaseB=f.tracker.hold(); f.response.resolve({status:'running',events:[],next:1,hasMore:true,outputs:[],steps:[]});await f.tracker.settle(); assert.equal(f.runs.get('run').cursor,1);assert.equal(f.saved.at(-1)[0][1].cursor,1);assert.equal(f.timers.size,0); await f.advance(10000);await f.tracker.retry();assert.equal(f.calls.length,1);assert.equal(f.timers.size,0); releaseA();releaseA();await f.advance(0);assert.equal(f.calls.length,1); releaseB();await f.advance(0);await f.tracker.settle();assert.equal(f.calls.length,2);assert.equal(f.calls[1].cursor,1);assert.equal(f.runs.get('run').cursor,2); f.tracker.stop(); }); test('releasing a run hold after stop cannot restart a poll',async()=>{ const f=fixture(),release=f.tracker.hold();f.tracker.notify(['run']);f.tracker.stop();release();await f.advance(10000); assert.equal(f.calls.length,0);assert.equal(f.timers.size,0);assert.throws(()=>f.tracker.hold(),/run_tracker_closed/); });