import json import subprocess from pathlib import Path SOURCE = Path(__file__).parents[1] / "frontend" / "today-session-sync.js" def run_node(script: str) -> dict: completed = subprocess.run( ["node", "-e", SOURCE.read_text() + "\n" + script], check=True, capture_output=True, text=True, ) return json.loads(completed.stdout) def test_second_device_sees_and_claims_the_running_session(): result = run_node( r""" const calls = []; const remote = {revision:3, device_id:'desktop-a', identity:'issue:r:42:', elapsed_ms:90000, running:true, updated_at:10}; let adopted = null; const offers = []; const sync = createTodaySessionSync({ getDeviceId:()=>'phone-b', fetchJson:async (url, options={}) => { calls.push({url, body:options.body ? JSON.parse(options.body) : null}); return options.method === 'PATCH' ? {...remote, revision:4, device_id:'phone-b'} : remote; }, timer:{adopt:(identity, elapsed, running)=>{adopted={identity, elapsed, running};}}, onRemote:session=>{offers.push(session);}, }); (async()=>{ await sync.refresh(); const claimed = await sync.claim(); process.stdout.write(JSON.stringify({offers, claimed, adopted, calls})); })().catch(error=>{console.error(error);process.exit(1);}); """ ) assert result["offers"][0]["device_id"] == "desktop-a" assert result["claimed"]["device_id"] == "phone-b" assert result["adopted"] == { "identity": "issue:r:42:", "elapsed": 90000, "running": True, } assert result["calls"][1] == { "url": "api/v1/today/session", "body": { "base_revision": 3, "device_id": "phone-b", "identity": "issue:r:42:", "elapsed_ms": 90000, "running": True, }, } def test_previous_owner_pauses_after_another_device_claims(): result = run_node( r""" let response = {revision:1, device_id:'desktop-a', identity:'issue:r:42:', elapsed_ms:1000, running:true}; let pauses = 0; let transferred = null; const sync = createTodaySessionSync({ getDeviceId:()=> 'desktop-a', fetchJson:async()=>response, timer:{pause:()=>{pauses += 1;}}, onTransferred:session=>{transferred=session;}, }); (async()=>{ await sync.refresh(); response = {...response, revision:2, device_id:'phone-b', elapsed_ms:2500}; await sync.refresh(); await sync.refresh(); process.stdout.write(JSON.stringify({pauses, transferred})); })().catch(error=>{console.error(error);process.exit(1);}); """ ) assert result["pauses"] == 1 assert result["transferred"]["device_id"] == "phone-b" def test_owner_poll_publishes_current_elapsed_time(): result = run_node( r""" const calls = []; let tick = null; let elapsed = 1000; const owned = {revision:1, device_id:'desktop-a', identity:'issue:r:42:', elapsed_ms:1000, running:true}; const sync = createTodaySessionSync({ getDeviceId:()=> 'desktop-a', fetchJson:async (url, options={}) => { calls.push({method:options.method || 'GET', body:options.body ? JSON.parse(options.body) : null}); return options.method === 'PATCH' ? {...owned, revision:2, elapsed_ms:elapsed} : owned; }, timer:{snapshot:()=>({identity:owned.identity, elapsed_ms:elapsed, running:true})}, setInterval:callback=>{tick=callback; return 7;}, }); (async()=>{ await sync.refresh(); sync.start(); elapsed = 6500; await tick(); process.stdout.write(JSON.stringify(calls)); })().catch(error=>{console.error(error);process.exit(1);}); """ ) assert result[-1] == { "method": "PATCH", "body": { "base_revision": 1, "device_id": "desktop-a", "identity": "issue:r:42:", "elapsed_ms": 6500, "running": True, }, }