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)); async function fixture(run){ const originals=Object.fromEntries(['window','document','fetch','roverEffects','setInterval','clearInterval'].map(k=>[k,globalThis[k]])); let focus=true,hidden=false;const calls=[],timers=[];globalThis.roverEffects=[]; globalThis.window={addEventListener(){},removeEventListener(){}}; globalThis.document={get hidden(){return hidden;},hasFocus:()=>focus,addEventListener(){},removeEventListener(){}}; globalThis.setInterval=(f,ms)=>{const timer={f,ms};timers.push(timer);return timer;};globalThis.clearInterval=()=>{}; globalThis.fetch=async(url,init)=>{const body=init.body?JSON.parse(init.body):null;calls.push({url,body});return {ok:true,json:async()=>url.endsWith('/arm')?{session_id:'fixture-session'}:{fresh:true,controlling:true,snapshot:{state:'ready'}}};}; const c=useRoverControl('fixture',true);const cleanup=globalThis.roverEffects.map(f=>f()); try{await flush();await c.arm(30,2000);await flush();await run({c,calls,timers,blur:()=>{focus=false;},focus:()=>{focus=true;},hide:()=>{hidden=true;}});} 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('command heartbeat sends neutral on lost blur, keeps session, and accepts new focused input',async()=>fixture(async f=>{ f.c.setInputGuard(()=>true);f.c.setDemand({left:1,right:-1});await flush(); assert.equal(f.calls.at(-1).body.left,1);f.blur();f.timers.find(t=>t.ms===100).f();await flush(); assert.equal(f.calls.at(-1).body.stop,false);assert.equal(f.calls.at(-1).body.left,0); f.c.setDemand({left:1,right:1});await flush();assert.equal(f.calls.at(-1).body.left,0); f.focus();f.timers.find(t=>t.ms===100).f();await flush();assert.equal(f.calls.at(-1).body.left,0); f.c.setDemand({left:1,right:1});await flush();assert.equal(f.calls.at(-1).body.left,1); assert.equal(f.calls.filter(x=>x.url.endsWith('/arm')).length,1); })); test('expired physical input prevents heartbeat from renewing remembered demand',async()=>fixture(async f=>{ let valid=true;f.c.setInputGuard(()=>valid);f.c.setDemand({left:1,right:-1});await flush();valid=false; f.timers.find(t=>t.ms===100).f();await flush();assert.equal(f.calls.at(-1).body.stop,false); })); test('no registered input scope cannot issue motion',async()=>fixture(async f=>{ f.c.setDemand({left:1,right:1});await flush();assert.equal(f.calls.at(-1).body.stop,false); assert.equal(f.calls.some(x=>x.body?.left===1),false); })); test('hidden document sends only neutral without a visibility event',async()=>fixture(async f=>{ f.c.setInputGuard(()=>true);f.hide();f.c.setDemand({left:1,right:1});await flush(); assert.equal(f.calls.at(-1).body.stop,false);assert.equal(f.calls.some(x=>x.body?.left===1),false); })); test('explicit pause clears demand without revoking session; explicit stop still revokes it',async()=>fixture(async f=>{ f.c.setInputGuard(()=>true);f.c.setDemand({left:1,right:-1});await flush();f.c.pauseInput();await flush(); assert.equal(f.calls.at(-1).body.stop,false);assert.equal(f.calls.at(-1).body.left,0); f.c.setDemand({left:1,right:1});await flush();assert.equal(f.calls.at(-1).body.left,1); f.c.stop();await flush();assert.equal(f.calls.at(-1).body.stop,true); const count=f.calls.length;f.c.setDemand({left:1,right:1});await flush();assert.equal(f.calls.length,count); }));