import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard LATER_SYNC = Path(__file__).parents[1] / "frontend" / "later-sync.js" LATER_WORK = Path(__file__).parents[1] / "frontend" / "later-work.js" COORDINATOR = Path(__file__).parents[1] / "frontend" / "outbox-coordinator.js" def run_node(script): return json.loads( subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ).stdout ) def test_two_tabs_cannot_erase_each_others_later_edits_or_double_drain(): script = f""" const createLaterSync=require({json.dumps(str(LATER_SYNC))}); const createCoordinator=require({json.dumps(str(COORDINATOR))}); const values=new Map();const storage={{get length(){{return values.size}},key:i=>[...values.keys()][i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}; const channels=[];const channelFactory=()=>{{const channel={{onmessage:null,postMessage:data=>channels.filter(x=>x!==channel).forEach(x=>x.onmessage?.({{data}})),close(){{}}}};channels.push(channel);return channel}}; const held=new Set();const locks={{request:async(name,_options,work)=>{{if(held.has(name))return work(null);held.add(name);try{{return await work({{name}})}}finally{{held.delete(name)}}}}}}; let sequence=0,releaseFirst;const delivered=[]; const make=tab=>createLaterSync({{storage,getLogin:()=> 'timmy',createOperationId:()=>tab+'-'+(++sequence),coordinator:createCoordinator({{storage,locks,channelFactory,tabId:tab}}),fetchJson:async(_url,options={{}})=>{{if(!options.method)return {{revision:0,records:{{}}}};const operation=JSON.parse(options.body).operations[0];delivered.push(operation);if(delivered.length===1)await new Promise(resolve=>releaseFirst=resolve);return {{revision:delivered.length,records:{{[operation.item_id]:operation.wake_at}}}}}},onRemoteRecords:()=>{{}},onStatus:()=>{{}}}}); const first=make('first'),second=make('second');first.enqueue('defer','issue:r:1:','2026-08-10T09:00:00.000Z'); (async()=>{{const draining=first.flush();while(!releaseFirst)await Promise.resolve();second.enqueue('defer','issue:r:2:','2026-08-11T09:00:00.000Z');const competing=second.flush();releaseFirst();await Promise.all([draining,competing]);await first.flush();process.stdout.write(JSON.stringify({{delivered,pending:first.pending(),keys:[...values.keys()]}}));}})(); """ result = run_node(script) assert sorted(operation["operation_id"] for operation in result["delivered"]) == ["first-1", "second-2"] assert len(result["delivered"]) == 2 assert result["pending"] == [] assert not any(".operation." in key for key in result["keys"]) def test_concurrent_later_records_for_one_item_deliver_only_the_newest_intent(): script = f""" const createLaterSync=require({json.dumps(str(LATER_SYNC))}); const values=new Map();const storage={{get length(){{return values.size}},key:i=>[...values.keys()][i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}; const prefix='stackchain.later-sync.v1.timmy.operation.'; values.set(prefix+'old',JSON.stringify({{queued_at:100,operation:{{operation_id:'old',action:'defer',item_id:'issue:r:2:',wake_at:'2026-08-10T09:00:00.000Z'}}}})); values.set(prefix+'new',JSON.stringify({{queued_at:101,operation:{{operation_id:'new',action:'restore',item_id:'issue:r:2:',wake_at:null}}}})); const delivered=[];const sync=createLaterSync({{storage,getLogin:()=> 'timmy',fetchJson:async(_url,options={{}})=>{{if(!options.method)return {{revision:0,records:{{}}}};delivered.push(...JSON.parse(options.body).operations);return {{revision:1,records:{{}}}}}},onRemoteRecords:()=>{{}},onStatus:()=>{{}}}}); (async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{delivered,pending:sync.pending(),keys:[...values.keys()]}}));}})(); """ result = run_node(script) assert [operation["operation_id"] for operation in result["delivered"]] == ["new"] assert result["pending"] == [] assert not any(".operation." in key for key in result["keys"]) def test_offline_deferral_replays_once_and_adopts_server_records(): script = f""" const createLaterSync = require({json.dumps(str(LATER_SYNC))}); const values = new Map(); const requests = []; let remote = {{revision:1,records:{{'issue:r:9:':'2026-08-11T09:00:00.000Z'}}}}; const sync = createLaterSync({{ storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}, getLogin:()=> 'timmy', createOperationId:()=> 'offline-op', fetchJson:async (url,options={{}})=>{{ requests.push({{url,body:options.body&&JSON.parse(options.body)}}); if (!options.method) return remote; const operation=JSON.parse(options.body).operations[0]; remote={{revision:2,records:{{...remote.records,[operation.item_id]:operation.wake_at}}}}; return remote; }}, onRemoteRecords:records=>{{globalThis.records=records}}, onStatus:status=>{{globalThis.status=status}}, }}); sync.enqueue('defer','issue:r:2:','2026-08-10T09:00:00.000Z'); (async()=>{{await sync.flush();await sync.flush();process.stdout.write(JSON.stringify({{ requests,records:globalThis.records,status:globalThis.status,pending:sync.pending() }}));}})(); """ result = run_node(script) assert [request["url"] for request in result["requests"]] == [ "api/v1/later", "api/v1/later", ] assert result["requests"][0]["body"] == {"operations": [{ "operation_id": "offline-op", "action": "defer", "item_id": "issue:r:2:", "wake_at": "2026-08-10T09:00:00.000Z", "base_revision": 0, }]} assert result["records"]["issue:r:2:"] == "2026-08-10T09:00:00.000Z" assert result["status"] == "saved" assert result["pending"] == [] def test_today_handoff_intent_replays_through_account_sync(): script = f""" const createLaterSync=require({json.dumps(str(LATER_SYNC))}); const values=new Map(); const requests=[]; const sync=createLaterSync({{ storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}, getLogin:()=> 'timmy',createOperationId:()=> 'wrap-up-op', fetchJson:async(_url,options={{}})=>{{ const body=JSON.parse(options.body); requests.push(body); return {{revision:1,records:{{[body.operations[0].item_id]:{{wake_at:body.operations[0].wake_at,handoff:body.operations[0].handoff}}}},accepted_operation_ids:['wrap-up-op'],duplicate_operation_ids:[],rejected_operations:[]}}; }},onRemoteRecords:records=>{{globalThis.records=records}},onStatus:()=>{{}}, }}); sync.enqueue('defer','issue:r:2:','2026-08-17T09:00:00.000Z','today'); (async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{requests,records:globalThis.records}}));}})(); """ result = run_node(script) assert result["requests"] == [{"operations": [{ "operation_id": "wrap-up-op", "action": "defer", "item_id": "issue:r:2:", "wake_at": "2026-08-17T09:00:00.000Z", "handoff": "today", "base_revision": 0, }]}] assert result["records"] == {"issue:r:2:": { "wake_at": "2026-08-17T09:00:00.000Z", "handoff": "today", }} def test_later_queue_expires_ancient_edits_before_replay(): script = f""" const createLaterSync=require({json.dumps(str(LATER_SYNC))}); const values=new Map();const statuses=[];const requests=[]; const storage={{get length(){{return values.size}},key:i=>[...values.keys()][i]||null, getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}; const sync=createLaterSync({{storage,getLogin:()=> 'timmy',createOperationId:()=> 'old-later', now:()=>1_800_000_000_000,maxOfflineMs:1000, fetchJson:async(_url,options={{}})=>{{requests.push(options.method||'GET');return {{revision:4,records:{{}}}}}}, onRemoteRecords:()=>{{}},onStatus:(state,detail={{}})=>statuses.push([state,detail.count||0])}}); (async()=>{{await sync.flush();sync.enqueue('restore','issue:r:2:'); const key=[...values.keys()].find(value=>value.includes('.operation.')); const record=JSON.parse(values.get(key));record.queued_at=1_799_999_000_000;values.set(key,JSON.stringify(record)); await sync.flush();process.stdout.write(JSON.stringify({{requests,statuses,pending:sync.pending()}}));}})(); """ result = run_node(script) assert result["requests"] == ["GET", "GET"] assert ["expired", 1] in result["statuses"] assert result["statuses"][-1] == ["expired", 1] assert result["pending"] == [] def test_stale_receipt_adopts_server_truth_and_reports_another_device_conflict(): script = f""" const createLaterSync=require({json.dumps(str(LATER_SYNC))}); const values=new Map();const requests=[];const statuses=[];const adopted=[]; const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}; const sync=createLaterSync({{ storage,getLogin:()=> 'timmy',createOperationId:()=> 'offline-old', fetchJson:async(_url,options={{}})=>{{ if(!options.method)return {{revision:3,records:{{'issue:r:1:':'2026-08-12T09:00:00.000Z'}}}}; const body=JSON.parse(options.body);requests.push(body); return {{revision:4,records:{{'issue:r:1:':'2026-08-13T09:00:00.000Z'}},accepted_operation_ids:[],duplicate_operation_ids:[],rejected_operations:[{{operation_id:'offline-old',reason:'stale_intent'}}]}}; }},onRemoteRecords:records=>adopted.push(records),onStatus:(state,detail={{}})=>statuses.push([state,detail]), }}); (async()=>{{await sync.flush();sync.enqueue('defer','issue:r:1:','2026-08-10T09:00:00.000Z');await sync.flush(); process.stdout.write(JSON.stringify({{requests,statuses,adopted,pending:sync.pending()}}));}})(); """ result = run_node(script) assert result["requests"] == [{"operations": [{ "operation_id": "offline-old", "action": "defer", "item_id": "issue:r:1:", "wake_at": "2026-08-10T09:00:00.000Z", "base_revision": 3, }]}] assert result["statuses"][-1] == ["conflict", {"count": 1}] assert result["adopted"][-1] == { "issue:r:1:": "2026-08-13T09:00:00.000Z" } assert result["pending"] == [] def test_pending_later_edits_are_sent_as_one_batch_without_a_preflight_get(): script = f""" const createLaterSync=require({json.dumps(str(LATER_SYNC))}); const values=new Map();const requests=[];let sequence=0; const sync=createLaterSync({{ storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}, getLogin:()=> 'timmy',createOperationId:()=> 'op-'+(++sequence), fetchJson:async(_url,options={{}})=>{{const body=JSON.parse(options.body);requests.push(body);return {{revision:2,records:{{}},accepted_operation_ids:body.operations.map(x=>x.operation_id),duplicate_operation_ids:[],rejected_operations:[]}}}}, onRemoteRecords:()=>{{}},onStatus:()=>{{}}, }}); sync.enqueue('defer','issue:r:1:','2026-08-10T09:00:00.000Z'); sync.enqueue('restore','issue:r:2:'); (async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{requests,pending:sync.pending()}}));}})(); """ result=run_node(script) assert len(result["requests"]) == 1 assert [item["operation_id"] for item in result["requests"][0]["operations"]] == ["op-1", "op-2"] assert result["pending"] == [] def test_latest_offline_intent_wins_and_failed_delivery_stays_pending(): script = f""" const createLaterSync = require({json.dumps(str(LATER_SYNC))}); const values=new Map(); let sequence=0; const sync=createLaterSync({{ storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}, getLogin:()=> 'timmy',createOperationId:()=> 'op-'+(++sequence), fetchJson:async()=>{{throw new Error('offline')}},onRemoteRecords:()=>{{}},onStatus:s=>{{globalThis.status=s}}, }}); sync.enqueue('defer','issue:r:2:','2026-08-10T09:00:00.000Z'); sync.enqueue('restore','issue:r:2:'); (async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{pending:sync.pending(),status:globalThis.status}}));}})(); """ assert run_node(script) == { "pending": [ { "operation_id": "op-2", "action": "restore", "item_id": "issue:r:2:", "wake_at": None, "base_revision": 0, } ], "status": "retrying", } def test_retry_is_single_flight_and_does_not_cross_account_boundary(): script = f""" const createLaterSync = require({json.dumps(str(LATER_SYNC))}); const values=new Map(); const timers=[]; const requests=[]; let login='timmy'; const sync=createLaterSync({{ storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}, getLogin:()=>login,createOperationId:()=> 'stable-later-op', setTimer:(callback,delay)=>{{timers.push({{callback,delay}});return timers.length}},clearTimer:()=>{{}}, fetchJson:async (_url,options={{}})=>{{requests.push(options.method||'GET');throw new Error('offline')}}, onRemoteRecords:()=>{{}},onStatus:()=>{{}}, }}); sync.enqueue('defer','issue:r:2:','2026-08-10T09:00:00.000Z'); (async()=>{{ const first=sync.flush(); const same=sync.flush(); await Promise.all([first,same]); const scheduledBeforeSwitch=timers.length; login='alexander'; await timers[0].callback(); process.stdout.write(JSON.stringify({{scheduledBeforeSwitch,timers:timers.length,requests,pendingForAlexander:sync.pending()}})); }})(); """ assert run_node(script) == { "scheduledBeforeSwitch": 1, "timers": 1, "requests": ["PATCH"], "pendingForAlexander": [], } def test_change_queued_during_delivery_is_drained_before_flush_settles(): script = f""" const createLaterSync=require({json.dumps(str(LATER_SYNC))}); const values=new Map(); const actions=[]; let sequence=0; let releaseFirst; const sync=createLaterSync({{ storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}, getLogin:()=> 'timmy',createOperationId:()=> 'op-'+(++sequence), fetchJson:async (_url,options={{}})=>{{ if (!options.method) return {{revision:0,records:{{}}}}; const operation=JSON.parse(options.body).operations[0]; actions.push(operation.action); if (operation.action==='defer') await new Promise(resolve=>releaseFirst=resolve); return {{revision:actions.length,records:operation.action==='defer'?{{[operation.item_id]:operation.wake_at}}:{{}}}}; }},onRemoteRecords:r=>{{globalThis.records=r}},onStatus:()=>{{}}, }}); sync.enqueue('defer','issue:r:2:','2026-08-10T09:00:00.000Z'); (async()=>{{const flushing=sync.flush();while(!releaseFirst) await Promise.resolve(); sync.enqueue('restore','issue:r:2:');releaseFirst();await flushing; process.stdout.write(JSON.stringify({{actions,pending:sync.pending(),records:globalThis.records}}));}})(); """ assert run_node(script) == { "actions": ["defer", "restore"], "pending": [], "records": {}, } def test_tabs_reject_an_older_snapshot_after_a_newer_revision(): script = f""" const createLaterSync=require({json.dumps(str(LATER_SYNC))}); const values=new Map(); const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}; const listeners=[]; const createChannel=()=>({{addEventListener:(_n,h)=>listeners.push(h),postMessage:data=>listeners.forEach(h=>h({{data}}))}}); let resolveOld,resolveNew; const oldHistory=[],newHistory=[]; const oldTab=createLaterSync({{storage,getLogin:()=> 'timmy',createChannel,fetchJson:()=>new Promise(r=>resolveOld=r),onRemoteRecords:r=>oldHistory.push(r),onStatus:()=>{{}}}}); const newTab=createLaterSync({{storage,getLogin:()=> 'timmy',createChannel,fetchJson:()=>new Promise(r=>resolveNew=r),onRemoteRecords:r=>newHistory.push(r),onStatus:()=>{{}}}}); (async()=>{{const oldFlush=oldTab.flush();const newFlush=newTab.flush();await Promise.resolve(); resolveNew({{revision:2,records:{{new:'2026-08-11T09:00:00.000Z'}}}});await newFlush; resolveOld({{revision:1,records:{{old:'2026-08-10T09:00:00.000Z'}}}});await oldFlush; process.stdout.write(JSON.stringify({{oldHistory,newHistory}}));}})(); """ result = run_node(script) assert result["oldHistory"][-1] == {"new": "2026-08-11T09:00:00.000Z"} assert result["newHistory"][-1] == {"new": "2026-08-11T09:00:00.000Z"} def test_existing_browser_records_migrate_once_and_lifecycle_replays(): script = f""" const createLaterSync=require({json.dumps(str(LATER_SYNC))}); const values=new Map(); const handlers={{}}; let sequence=0; const requests=[]; const sync=createLaterSync({{ storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}, getLogin:()=> 'timmy',createOperationId:()=> 'migration-'+(++sequence), fetchJson:async (_url,options={{}})=>{{requests.push(options.method||'GET');return {{revision:options.method?1:0,records:{{}}}}}}, onRemoteRecords:()=>{{}},onStatus:()=>{{}}, }}); const records={{'issue:r:1:':'2026-08-10T09:00:00.000Z'}}; const first=sync.migrate(records),second=sync.migrate(records); sync.startLifecycle({{window:{{addEventListener:(n,h)=>handlers[n]=h}},document:{{hidden:false,addEventListener:()=>{{}}}}}}); (async()=>{{await handlers.online();process.stdout.write(JSON.stringify({{first,second,requests,pending:sync.pending()}}));}})(); """ assert run_node(script) == { "first": True, "second": False, "requests": ["PATCH"], "pending": [], } def test_unsynced_wrap_up_handoff_migrates_after_browser_restart(): script = f""" const createLaterSync=require({json.dumps(str(LATER_SYNC))}); const values=new Map();const requests=[]; const sync=createLaterSync({{ storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}, getLogin:()=> 'timmy',createOperationId:()=> 'migrated-handoff', fetchJson:async(_url,options={{}})=>{{const body=JSON.parse(options.body);requests.push(body);return {{revision:1,records:{{}},accepted_operation_ids:['migrated-handoff'],duplicate_operation_ids:[],rejected_operations:[]}}}}, onRemoteRecords:()=>{{}},onStatus:()=>{{}}, }}); sync.migrate({{'issue:r:1:':{{wake_at:'2026-08-17T09:00:00.000Z',handoff:'today'}}}}); (async()=>{{await sync.flush();process.stdout.write(JSON.stringify(requests));}})(); """ assert run_node(script) == [{"operations": [{ "operation_id": "migrated-handoff", "action": "defer", "item_id": "issue:r:1:", "wake_at": "2026-08-17T09:00:00.000Z", "handoff": "today", "base_revision": 0, }]}] def test_later_work_emits_local_changes_adopts_remote_truth_and_retires_expiry(): script = f""" const createLaterWork=require({json.dumps(str(LATER_WORK))}); const values=new Map(); const changes=[]; const expired=[]; let clock=new Date('2026-08-08T12:00:00Z'); const item={{kind:'issue',repository:'stackchain/api',number:17}}; const work=createLaterWork({{ storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}, getLogin:()=> 'timmy',now:()=>clock,setTimer:()=>1,clearTimer:()=>{{}}, onChange:(action,id,wakeAt)=>changes.push([action,id,wakeAt]), onExpire:ids=>expired.push(ids), }}); work.defer(item,new Date('2026-08-08T16:00:00Z')); work.restore(item); work.adopt({{'issue:stackchain/api:17:':'2026-08-09T09:00:00.000Z'}}); const remote=work.partition([item]); clock=new Date('2026-08-09T09:00:01Z'); const awake=work.partition([item]); process.stdout.write(JSON.stringify({{changes,expired,remote:remote.later,awake:awake.active}})); """ result = run_node(script) assert result["changes"] == [ ["defer", "issue:stackchain/api:17:", "2026-08-08T16:00:00.000Z"], ["restore", "issue:stackchain/api:17:", None], ] assert result["remote"][0]["deferred_until"] == "2026-08-09T09:00:00.000Z" assert result["awake"] == [{"kind": "issue", "repository": "stackchain/api", "number": 17}] assert result["expired"] == [["issue:stackchain/api:17:"]] def test_later_work_preserves_synced_today_handoff_until_it_wakes(): script = f""" const createLaterWork=require({json.dumps(str(LATER_WORK))}); const values=new Map(); const changes=[]; const expired=[]; let clock=new Date('2026-08-16T20:00:00Z'); const item={{kind:'issue',repository:'stackchain/dashboard',number:992,title:'Commitment'}}; const work=createLaterWork({{ storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}, getLogin:()=> 'timmy',now:()=>clock,setTimer:()=>1,clearTimer:()=>{{}}, onChange:(action,id,wakeAt,handoff)=>changes.push([action,id,wakeAt,handoff||null]), onExpire:(ids,handoffs)=>expired.push([ids,handoffs]), }}); work.defer(item,new Date('2026-08-17T09:00:00Z'),{{handoff:'today'}}); const stored=work.read(); clock=new Date('2026-08-17T09:00:01Z'); const awake=work.partition([item]); process.stdout.write(JSON.stringify({{stored,changes,expired,awake:awake.active}})); """ result = run_node(script) assert result["stored"] == { "issue:stackchain/dashboard:992:": { "wake_at": "2026-08-17T09:00:00.000Z", "handoff": "today", } } assert result["changes"] == [[ "defer", "issue:stackchain/dashboard:992:", "2026-08-17T09:00:00.000Z", "today", ]] assert result["expired"] == [[ ["issue:stackchain/dashboard:992:"], ["issue:stackchain/dashboard:992:"], ]] assert result["awake"] == [{ "kind": "issue", "repository": "stackchain/dashboard", "number": 992, "title": "Commitment", }] @pytest.mark.anyio async def test_dashboard_syncs_every_later_change_and_exposes_account_status(): html = await dashboard() assert '' in html assert 'id="later-sync-status"' in html assert "const laterSync = createLaterSync({" in html assert "onRemoteRecords: records =>" in html assert "onChange: (action, itemId, wakeAt, handoff) =>" in html assert "onExpire: (ids, handoffs) =>" in html assert "laterSync.enqueue(action, itemId, wakeAt, handoff)" in html assert "ids.map(id => laterSync.enqueue('restore', id)).every(Boolean)" in html assert "laterSync.migrate(laterWork.read());" in html assert "laterSync.flush();" in html assert "Later saved to account." in html assert "Another device changed this Later item" in html assert "Today edit expired after 30 days offline" in html assert "Later edit expired after 30 days offline" in html def test_later_sync_ships_atomically_in_the_offline_shell(): source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text() assert "stackchain-dashboard-shell-v123" in source assert "BASE + 'static/later-sync.js'" in source