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", }]} assert result["records"]["issue:r:2:"] == "2026-08-10T09:00:00.000Z" assert result["status"] == "saved" 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, } ], "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_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:"]] @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) =>" in html assert "onExpire: ids =>" in html assert "laterSync.enqueue(action, itemId, wakeAt)" 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 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-v54" in source assert "BASE + 'static/later-sync.js'" in source