50 lines
3.9 KiB
JavaScript
50 lines
3.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {build} from 'esbuild';
|
|
const result=await build({entryPoints:[new URL('../src/core/fleet/useRoverControl.ts',import.meta.url).pathname],bundle:true,write:false,platform:'node',format:'esm',plugins:[{name:'hook-fixture',setup(b){
|
|
b.onResolve({filter:/^react$/},()=>({path:'react',namespace:'fixture'}));
|
|
b.onLoad({filter:/.*/,namespace:'fixture'},()=>({contents:`export const useRef=v=>({current:v});export const useState=v=>[v,()=>{}];export const useCallback=f=>f;export const useEffect=f=>globalThis.roverEffects.push(f);`}));
|
|
}}]});
|
|
const {useRoverControl}=await import('data:text/javascript;base64,'+Buffer.from(result.outputFiles[0].contents).toString('base64'));
|
|
const flush=()=>new Promise(resolve=>setImmediate(resolve));
|
|
const deferred=()=>{let resolve;const promise=new Promise(r=>resolve=r);return {promise,resolve};};
|
|
const response=value=>({ok:true,json:async()=>value});
|
|
const idle={fresh:true,controlling:false,snapshot:{supported:true,state:'observing'}};
|
|
async function fixture(run){
|
|
const originals=Object.fromEntries(['window','document','fetch','roverEffects','setInterval','clearInterval'].map(k=>[k,globalThis[k]]));
|
|
const calls=[],timers=[];globalThis.roverEffects=[];
|
|
globalThis.window={addEventListener(){},removeEventListener(){}};globalThis.document={hidden:false,hasFocus:()=>true,addEventListener(){},removeEventListener(){}};
|
|
globalThis.setInterval=(f,ms)=>{timers.push({f,ms});return timers.length;};globalThis.clearInterval=()=>{};
|
|
let armResponse=null,nextPoll=null;
|
|
globalThis.fetch=async(url,init)=>{const body=init.body?JSON.parse(init.body):null;calls.push({url,body});
|
|
if(url.endsWith('/arm'))return armResponse?armResponse.promise:response({session_id:'new-session'});
|
|
if(!body)return nextPoll?nextPoll.promise:response(idle);
|
|
return response({accepted_sequence:body.sequence});
|
|
};
|
|
const c=useRoverControl('fixture',true),cleanup=globalThis.roverEffects.map(f=>f());
|
|
try{await flush();await run({c,calls,poll:()=>timers.find(t=>t.ms===200).f(),heartbeat:()=>timers.find(t=>t.ms===100).f(),delayArm:()=>armResponse=deferred(),delayPoll:()=>nextPoll=deferred()});}
|
|
finally{cleanup.forEach(f=>f?.());await flush();for(const [key,value]of Object.entries(originals)){if(value===undefined)delete globalThis[key];else globalThis[key]=value;}}
|
|
}
|
|
test('idle poll begun during pending arm cannot revoke newly acknowledged session',async()=>fixture(async f=>{
|
|
const a=f.delayArm();const arming=f.c.arm(30,2000);const p=f.delayPoll();f.poll();
|
|
a.resolve(response({session_id:'new-session'}));assert.equal(await arming,true);await flush();
|
|
p.resolve(response(idle));await flush();f.heartbeat();await flush();
|
|
assert.equal(f.calls.filter(x=>x.body?.stop===true).length,0,'old idle poll revoked the new session');
|
|
assert.equal(f.calls.at(-1).body.session_id,'new-session');
|
|
}));
|
|
test('failed poll begun during pending arm cannot revoke newly acknowledged session',async()=>fixture(async f=>{
|
|
const a=f.delayArm();const arming=f.c.arm(30,2000);const p=f.delayPoll();f.poll();
|
|
a.resolve(response({session_id:'new-session'}));assert.equal(await arming,true);await flush();
|
|
p.resolve({ok:false,json:async()=>({detail:'stale read failure'})});await flush();
|
|
assert.equal(f.calls.filter(x=>x.body?.stop===true).length,0,'old failed poll revoked the new session');
|
|
}));
|
|
test('a current poll still revokes control when server reports lease lost',async()=>fixture(async f=>{
|
|
assert.equal(await f.c.arm(30,2000),true);await flush();f.poll();await flush();
|
|
assert.equal(f.calls.at(-1).body.stop,true);
|
|
}));
|
|
test('duplicate arm clicks while request pending create exactly one lease request',async()=>fixture(async f=>{
|
|
const a=f.delayArm();const first=f.c.arm(30,2000),second=f.c.arm(30,2000);
|
|
a.resolve(response({session_id:'new-session'}));await Promise.all([first,second]);await flush();
|
|
assert.equal(f.calls.filter(x=>x.url.endsWith('/arm')).length,1);
|
|
}));
|