import json import subprocess from pathlib import Path SOURCE = Path(__file__).parents[1] / "frontend" / "today-session-sync.js" TIMER = Path(__file__).parents[1] / "frontend" / "today-timer.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_claim_adopts_the_complete_timing_ledger_for_recap(): result = run_node( f""" const createTimer = require({json.dumps(str(TIMER))}); const values = new Map(); const storage = {{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}}; const timer = createTimer({{storage,getLogin:()=> 'timmy',now:()=>100000}}); const remote = {{ revision:3,device_id:'desktop-a',identity:'issue:r:2:',elapsed_ms:90000,running:true, entries:[ {{identity:'issue:r:1:',elapsed_ms:60000}}, {{identity:'issue:r:2:',elapsed_ms:90000}}, ], }}; const sync=createTodaySessionSync({{ getDeviceId:()=> 'phone-b',timer, fetchJson:async (_url,options={{}})=>options.method ? {{...JSON.parse(options.body),revision:4,device_id:'phone-b'}} : remote, }}); (async()=>{{ await sync.refresh(); await sync.claim(); const snapshot=timer.sessionSnapshot(); const recap=timer.recapEntries(); process.stdout.write(JSON.stringify({{recap,snapshot}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ ) assert result["recap"] == [ {"identity": "issue:r:1:", "elapsed_ms": 60_000}, {"identity": "issue:r:2:", "elapsed_ms": 90_000}, ] assert result["snapshot"]["identity"] == "issue:r:2:" assert result["snapshot"]["running"] is True def test_second_device_sees_and_resumes_a_timed_break(): result = run_node( r""" const calls=[]; const remote={revision:3,device_id:'phone-a',identity:'issue:r:42:',elapsed_ms:90000,running:false,break_deadline_at:1800000,updated_at:10}; let adopted=null; const offers=[]; const sync=createTodaySessionSync({ getDeviceId:()=> 'desktop-b', fetchJson:async (url,options={})=>{ calls.push(options.body ? JSON.parse(options.body) : null); return options.method === 'PATCH' ? {...remote,revision:4,device_id:'desktop-b',running:true,break_deadline_at:null} : 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]["break_deadline_at"] == 1_800_000 assert result["calls"][1] == { "base_revision": 3, "device_id": "desktop-b", "identity": "issue:r:42:", "elapsed_ms": 90_000, "running": True, "break_deadline_at": None, } assert result["adopted"] == { "identity": "issue:r:42:", "elapsed": 90_000, "running": True, } assert result["claimed"]["device_id"] == "desktop-b" def test_owner_refresh_recovers_a_server_confirmed_break_missing_from_device_state(): result = run_node( f""" const createTimer = require({json.dumps(str(TIMER))}); const values = new Map(); const storage = {{ getItem:key=>values.get(key)||null, setItem:(key,value)=>values.set(key,value), }}; const timer = createTimer({{storage,getLogin:()=> 'timmy',now:()=>100000}}); timer.adopt('issue:r:42:', 90000, false); let rendered = 0; const remote = {{ revision:3,device_id:'phone-a',identity:'issue:r:42:',elapsed_ms:90000, running:false,break_deadline_at:400000,updated_at:10, }}; const sync = createTodaySessionSync({{ getDeviceId:()=> 'phone-a',fetchJson:async()=>remote,timer, onOwnedRestore:()=>{{rendered += 1;}}, }}); (async()=>{{ await sync.refresh(); process.stdout.write(JSON.stringify({{breakSnapshot:timer.breakSnapshot(),rendered}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ ) assert result == { "breakSnapshot": { "identity": "issue:r:42:", "deadline_at": 400_000, "expired": False, }, "rendered": 1, } def test_break_handoff_summary_is_actionable_and_privacy_safe(): result = run_node( r""" const session={identity:'issue:private/repo:42:',elapsed_ms:90000,running:false,break_deadline_at:1800000}; process.stdout.write(JSON.stringify({ summary:todaySessionHandoffSummary(session,{title:'Secret issue'},0), })); """ ) assert result["summary"].startswith("On break until ") assert "Secret issue" not in result["summary"] assert "private/repo" not in result["summary"] def test_multi_item_handoff_summary_shows_scope_and_total_without_titles(): result = run_node( r""" const session={ identity:'issue:private/repo:42:',elapsed_ms:90000,running:true, entries:[ {identity:'issue:private/repo:41:',elapsed_ms:60000}, {identity:'issue:private/repo:42:',elapsed_ms:90000}, ], }; process.stdout.write(JSON.stringify(todaySessionHandoffSummary(session,{title:'Secret issue'}))); """ ) assert result == "2 tracked items ยท 2 min total" assert "Secret" not in result assert "private/repo" not in result 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, }, } def test_publish_conflict_adopts_remote_owner_and_pauses_once(): result = run_node( r""" const calls = []; const owned = {revision:2, device_id:'desktop-a', identity:'issue:r:42:', elapsed_ms:1000, running:true}; const remote = {...owned, revision:3, device_id:'phone-b', elapsed_ms:2500}; let pauses = 0; const transfers = []; const statuses = []; let reads = 0; const sync = createTodaySessionSync({ getDeviceId:()=> 'desktop-a', fetchJson:async (url, options={}) => { calls.push(options.method || 'GET'); if (options.method === 'PATCH') { const error = new Error('session changed'); error.status = 409; error.code = 'session_changed'; throw error; } reads += 1; return reads === 1 ? owned : remote; }, timer:{ snapshot:()=>({identity:owned.identity, elapsed_ms:1500, running:true}), pause:()=>{pauses += 1;}, }, onTransferred:session=>transfers.push(session.device_id), onStatus:status=>statuses.push(status), }); (async()=>{ await sync.refresh(); await sync.publish(); await sync.refresh(); process.stdout.write(JSON.stringify({calls, pauses, transfers, statuses, session:sync.session()})); })().catch(error=>{console.error(error);process.exit(1);}); """ ) assert result["calls"] == ["GET", "PATCH", "GET", "GET"] assert result["session"]["device_id"] == "phone-b" assert result["pauses"] == 1 assert result["transfers"] == ["phone-b"] assert "conflict" in result["statuses"] def test_rapid_publishes_are_single_flight_and_coalesce_latest_snapshot(): result = run_node( r""" const owned = {revision:1, device_id:'desktop-a', identity:'issue:r:42:', elapsed_ms:0, running:true}; const bodies = []; let active = 0; let peak = 0; let releaseFirst; let patchCount = 0; const firstPatch = new Promise(resolve=>{releaseFirst=resolve;}); const sync = createTodaySessionSync({ getDeviceId:()=> 'desktop-a', fetchJson:async (url, options={}) => { if (!options.method) return owned; active += 1; peak = Math.max(peak, active); patchCount += 1; const body = JSON.parse(options.body); bodies.push(body); if (patchCount === 1) await firstPatch; active -= 1; return {...owned, revision:1 + patchCount, elapsed_ms:body.elapsed_ms, running:body.running}; }, }); (async()=>{ await sync.refresh(); const first = sync.publish({identity:owned.identity, elapsed_ms:1000, running:true}); await Promise.resolve(); const middle = sync.publish({identity:owned.identity, elapsed_ms:2000, running:false}); const latest = sync.publish({identity:owned.identity, elapsed_ms:3000, running:true}); releaseFirst(); await Promise.all([first, middle, latest]); process.stdout.write(JSON.stringify({peak, bodies, session:sync.session()})); })().catch(error=>{console.error(error);process.exit(1);}); """ ) assert result["peak"] == 1 assert [body["elapsed_ms"] for body in result["bodies"]] == [1000, 3000] assert result["session"]["elapsed_ms"] == 3000 def test_remote_conflict_discards_queued_local_publishes_until_explicit_claim(): result = run_node( r""" const owned = {revision:2, device_id:'desktop-a', identity:'issue:r:42:', elapsed_ms:1000, running:true}; const remote = {...owned, revision:3, device_id:'phone-b', elapsed_ms:2500}; const calls = []; let reads = 0; let releaseConflict; const conflictReady = new Promise(resolve=>{releaseConflict=resolve;}); const sync = createTodaySessionSync({ getDeviceId:()=> 'desktop-a', fetchJson:async (url, options={}) => { calls.push(options.method || 'GET'); if (options.method === 'PATCH') { await conflictReady; const error = new Error('session changed'); error.status = 409; throw error; } reads += 1; return reads === 1 ? owned : remote; }, timer:{pause:()=>{}}, }); (async()=>{ await sync.refresh(); const stale = sync.publish({identity:owned.identity, elapsed_ms:1500, running:true}); await Promise.resolve(); const queued = sync.publish({identity:owned.identity, elapsed_ms:2000, running:true}); releaseConflict(); await Promise.all([stale, queued]); process.stdout.write(JSON.stringify({calls, session:sync.session()})); })().catch(error=>{console.error(error);process.exit(1);}); """ ) assert result["calls"] == ["GET", "PATCH", "GET"] assert result["session"]["device_id"] == "phone-b" def test_transient_failure_retains_latest_intent_for_retry(): result = run_node( r""" const owned = {revision:4, device_id:'desktop-a', identity:'issue:r:42:', elapsed_ms:1000, running:true}; const bodies = []; const statuses = []; let attempts = 0; const sync = createTodaySessionSync({ getDeviceId:()=> 'desktop-a', fetchJson:async (url, options={}) => { if (!options.method) return owned; attempts += 1; bodies.push(JSON.parse(options.body)); if (attempts === 1) { const error = new Error('temporarily unavailable'); error.status = 503; throw error; } return {...owned, revision:5, elapsed_ms:bodies.at(-1).elapsed_ms}; }, onStatus:status=>statuses.push(status), }); (async()=>{ await sync.refresh(); const first = await sync.publish({identity:owned.identity, elapsed_ms:4500, running:true}); const retried = await sync.publish(); process.stdout.write(JSON.stringify({first, retried, bodies, statuses})); })().catch(error=>{console.error(error);process.exit(1);}); """ ) assert result["first"] is None assert [body["elapsed_ms"] for body in result["bodies"]] == [4500, 4500] assert result["retried"]["revision"] == 5 assert "offline" in result["statuses"] def test_claim_transport_failure_stays_retryable_without_false_conflict(): result = run_node( r""" const remote = {revision:7, device_id:'phone-b', identity:'issue:r:42:', elapsed_ms:9000, running:true}; const calls = []; const statuses = []; const sync = createTodaySessionSync({ getDeviceId:()=> 'desktop-a', fetchJson:async (url, options={}) => { calls.push(options.method || 'GET'); if (!options.method) return remote; const error = new Error('gateway unavailable'); error.status = 503; throw error; }, onStatus:status=>statuses.push(status), }); (async()=>{ await sync.refresh(); const claimed = await sync.claim(); process.stdout.write(JSON.stringify({claimed, calls, statuses})); })().catch(error=>{console.error(error);process.exit(1);}); """ ) assert result["claimed"] is None assert result["calls"] == ["GET", "PATCH"] assert result["statuses"][-1] == "offline" assert "conflict" not in result["statuses"] def test_owner_publish_includes_every_timed_item(): result = run_node( f""" const createTimer = require({json.dumps(str(TIMER))}); const values = new Map(); const storage = {{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}}; let now=0; const timer=createTimer({{storage,getLogin:()=> 'timmy',now:()=>now}}); timer.activate('issue:r:1:'); now=60000; timer.activate('issue:r:2:'); now=150000; const bodies=[]; const owned={{revision:1,device_id:'desktop-a',identity:'issue:r:2:',elapsed_ms:90000,running:true}}; const sync=createTodaySessionSync({{ getDeviceId:()=> 'desktop-a',timer, fetchJson:async (_url,options={{}})=>{{ if (!options.method) return owned; const body=JSON.parse(options.body); bodies.push(body); return {{...body,revision:2,updated_at:1}}; }}, }}); (async()=>{{ await sync.refresh(); await sync.publish(); process.stdout.write(JSON.stringify(bodies[0].entries)); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ ) assert result == [ {"identity": "issue:r:1:", "elapsed_ms": 60_000}, {"identity": "issue:r:2:", "elapsed_ms": 90_000}, ] def test_timer_snapshot_samples_active_elapsed_once_for_a_consistent_ledger(): result = run_node( f""" const createTimer = require({json.dumps(str(TIMER))}); const values=new Map(); const storage={{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}}; let ticks=0; const timer=createTimer({{storage,getLogin:()=> 'timmy',now:()=>++ticks*1000}}); timer.activate('issue:r:1:'); const snapshot=timer.sessionSnapshot(); process.stdout.write(JSON.stringify(snapshot)); """ ) assert result["elapsed_ms"] == result["entries"][0]["elapsed_ms"]