import json import subprocess from pathlib import Path TODAY_SYNC = Path(__file__).parents[1] / "frontend" / "today-sync.js" def test_local_operations_replay_once_then_adopt_server_order(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); const storage = {{ getItem: key => values.get(key) || null, setItem: (key, value) => values.set(key, value), removeItem: key => values.delete(key), }}; const requests = []; let remote = {{revision: 1, ids:['issue:r:9:']}}; const sync = createTodaySync({{ storage, getLogin: () => 'timmy', createOperationId: () => 'fixed-op', fetchJson: async (url, options={{}}) => {{ requests.push({{url, body: options.body && JSON.parse(options.body)}}); if (!options.method) return remote; remote = {{revision: remote.revision + 1, ids:['issue:r:9:', options.body && JSON.parse(options.body).item_id]}}; return remote; }}, onRemoteIds: ids => {{ globalThis.adopted = ids; }}, onStatus: status => {{ globalThis.status = status; }}, }}); sync.enqueue('add', 'issue:r:2:'); (async () => {{ await sync.flush(); await sync.flush(); process.stdout.write(JSON.stringify({{ requests, adopted: globalThis.adopted, status: globalThis.status, pending: sync.pending(), }})); }})(); """ result = json.loads( subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout ) assert [request["url"] for request in result["requests"]] == [ "api/v1/today", "api/v1/today", "api/v1/today", ] assert result["requests"][1]["body"] == { "operation_id": "fixed-op", "action": "add", "item_id": "issue:r:2:", "direction": None, } assert result["adopted"] == ["issue:r:9:", "issue:r:2:"] assert result["status"] == "saved" assert result["pending"] == [] def test_failed_delivery_stays_pending_for_offline_replay(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); const sync = createTodaySync({{ 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 () => {{ throw new Error('offline'); }}, onRemoteIds: () => {{}}, onStatus: value => {{ globalThis.status = value; }}, }}); sync.enqueue('move', 'issue:r:2:', 'up'); (async () => {{ await sync.flush(); process.stdout.write(JSON.stringify({{pending:sync.pending(),status:globalThis.status}})); }})(); """ result = json.loads( subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout ) assert result == { "pending": [{ "operation_id": "offline-op", "action": "move", "item_id": "issue:r:2:", "direction": "up", }], "status": "pending", } def test_duplicate_pending_retirements_collapse_to_one_effective_remove(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); let sequence = 0; const statuses = []; const sync = createTodaySync({{ storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}, getLogin: () => 'timmy', createOperationId: () => 'remove-' + (++sequence), fetchJson: async () => ({{revision:0,ids:[]}}), onRemoteIds:()=>{{}}, onStatus:value=>statuses.push(value), }}); const first = sync.enqueue('remove', 'issue:r:1:'); const duplicate = sync.enqueue('remove', 'issue:r:1:'); process.stdout.write(JSON.stringify({{first, duplicate, pending:sync.pending(), statuses}})); """ result = json.loads( subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout ) assert result == { "first": True, "duplicate": True, "pending": [{ "operation_id": "remove-1", "action": "remove", "item_id": "issue:r:1:", "direction": None, }], "statuses": ["pending", "pending"], } def test_existing_device_queue_is_migrated_only_once(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); let sequence = 0; const sync = createTodaySync({{ 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 () => ({{revision:0,ids:[]}}), onRemoteIds:()=>{{}}, onStatus:()=>{{}}, }}); const first = sync.migrate(['issue:r:1:', 'issue:r:2:']); const second = sync.migrate(['issue:r:1:', 'issue:r:2:']); process.stdout.write(JSON.stringify({{first, second, pending:sync.pending()}})); """ result = json.loads( subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout ) assert result == { "first": True, "second": False, "pending": [ {"operation_id": "migration-1", "action": "add", "item_id": "issue:r:1:", "direction": None}, {"operation_id": "migration-2", "action": "add", "item_id": "issue:r:2:", "direction": None}, ], } def test_server_limit_conflict_drops_rejected_add_and_adopts_server_truth(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); let patch = false; const sync = createTodaySync({{ storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}, getLogin: () => 'timmy', createOperationId: () => 'sixth', fetchJson: async (_url, options={{}}) => {{ if (options.method) {{ patch = true; const error = new Error('full'); error.status = 409; throw error; }} return {{revision:5, ids:['1','2','3','4','5']}}; }}, onRemoteIds:ids=>{{globalThis.ids=ids}}, onStatus:value=>{{globalThis.status=value}}, }}); sync.enqueue('add', '6'); (async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{patch,pending:sync.pending(),ids:globalThis.ids,status:globalThis.status}}));}})(); """ result = json.loads( subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout ) assert result == { "patch": True, "pending": [], "ids": ["1", "2", "3", "4", "5"], "status": "full", } def test_tabs_ignore_an_older_response_after_a_newer_revision_is_broadcast(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_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: (_name, listener) => listeners.push(listener), postMessage: data => listeners.forEach(listener => listener({{data}})), }}); let resolveOld; let resolveNew; const oldHistory = []; const newHistory = []; const oldTab = createTodaySync({{ storage, getLogin:()=> 'timmy', createChannel, fetchJson:()=>new Promise(resolve=>{{resolveOld=resolve}}), onRemoteIds:ids=>oldHistory.push(ids), onStatus:()=>{{}}, }}); const newTab = createTodaySync({{ storage, getLogin:()=> 'timmy', createChannel, fetchJson:()=>new Promise(resolve=>{{resolveNew=resolve}}), onRemoteIds:ids=>newHistory.push(ids), onStatus:()=>{{}}, }}); (async()=>{{ const oldFlush = oldTab.flush(); const newFlush = newTab.flush(); await Promise.resolve(); resolveNew({{revision:2,ids:['new']}}); await newFlush; resolveOld({{revision:1,ids:['old']}}); await oldFlush; process.stdout.write(JSON.stringify({{oldHistory,newHistory}})); }})(); """ result = json.loads( subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout ) assert result["oldHistory"][-1] == ["new"] assert result["newHistory"][-1] == ["new"] def test_returning_online_replays_pending_operations(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); const handlers = {{}}; const requests = []; let revision = 1; const sync = createTodaySync({{ storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}, getLogin:()=> 'timmy', createOperationId:()=> 'offline-add', fetchJson:async (_url, options={{}})=>{{ requests.push(options.method || 'GET'); if (options.method) return {{revision:++revision,ids:['issue:r:1:']}}; return {{revision,ids:[]}}; }}, onRemoteIds:()=>{{}}, onStatus:()=>{{}}, }}); sync.enqueue('add', 'issue:r:1:'); sync.startLifecycle({{ window: {{addEventListener:(name, handler)=>{{handlers[name]=handler}}}}, document: {{addEventListener:()=>{{}}, hidden:false}}, }}); (async()=>{{ await handlers.online(); process.stdout.write(JSON.stringify({{requests,pending:sync.pending()}})); }})(); """ result = json.loads( subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout ) assert result == {"requests": ["GET", "PATCH"], "pending": []} def test_foregrounding_a_stale_tab_refreshes_the_saved_plan(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const handlers = {{}}; const adopted = []; const documentObject = {{ hidden:true, addEventListener:(name, handler)=>{{handlers[name]=handler}}, }}; const sync = createTodaySync({{ storage: {{getItem:()=>null,setItem:()=>{{}},removeItem:()=>{{}}}}, getLogin:()=> 'timmy', fetchJson:async ()=>({{revision:7,ids:['issue:r:7:']}}), onRemoteIds:ids=>adopted.push(ids), onStatus:()=>{{}}, }}); sync.startLifecycle({{ window: {{addEventListener:()=>{{}}}}, document:documentObject, }}); (async()=>{{ await handlers.visibilitychange(); documentObject.hidden = false; await handlers.visibilitychange(); process.stdout.write(JSON.stringify(adopted)); }})(); """ result = json.loads( subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout ) assert result == [["issue:r:7:"]] def test_old_account_broadcasts_are_ignored_after_identity_changes(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); let login = 'timmy'; let listener; const adopted = []; const sync = createTodaySync({{ storage: {{getItem:()=>null,setItem:()=>{{}},removeItem:()=>{{}}}}, getLogin:()=>login, createChannel:()=>({{addEventListener:(_name, handler)=>{{listener=handler}},postMessage:()=>{{}},close:()=>{{}}}}), fetchJson:async ()=>({{revision:1,ids:['timmy-plan']}}), onRemoteIds:ids=>adopted.push(ids), onStatus:()=>{{}}, }}); (async()=>{{ await sync.flush(); login = 'alexander'; listener({{data:{{revision:2,ids:['timmy-secret']}}}}); process.stdout.write(JSON.stringify(adopted)); }})(); """ result = json.loads( subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout ) assert result == [["timmy-plan"]]