import json import subprocess from pathlib import Path TODAY_SYNC = Path(__file__).parents[1] / "frontend" / "today-sync.js" COORDINATOR = Path(__file__).parents[1] / "frontend" / "outbox-coordinator.js" DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js" def test_two_tabs_cannot_erase_each_others_today_edits_or_double_drain(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_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=>createTodaySync({{storage,getLogin:()=> 'timmy',createOperationId:()=>tab+'-'+(++sequence),coordinator:createCoordinator({{storage,locks,channelFactory,tabId:tab}}), fetchJson:async(_url,options={{}})=>{{if(!options.method)return {{revision:0,ids:[]}};const operation=JSON.parse(options.body).operations[0];delivered.push(operation.operation_id);if(delivered.length===1)await new Promise(resolve=>releaseFirst=resolve);return {{revision:delivered.length,ids:delivered}}}},onRemoteIds:()=>{{}},onStatus:()=>{{}}}}); const first=make('first'),second=make('second');first.enqueue('add','issue:r:1:'); (async()=>{{const draining=first.flush();while(!releaseFirst)await Promise.resolve();second.enqueue('add','issue:r:2:');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 = json.loads(subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout) assert sorted(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_operations_enqueued_during_an_inflight_flush_are_drained_before_it_settles(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); const patches = []; let sequence = 0; let releaseFirstPatch; const firstPatchBlocked = new Promise(resolve => {{ globalThis.firstPatchStarted = resolve; }}); 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: () => 'op-' + (++sequence), fetchJson: async (_url, options={{}}) => {{ if (!options.method) return {{revision:0, ids:[]}}; const operation = JSON.parse(options.body).operations[0]; patches.push(operation); if (patches.length === 1) {{ globalThis.firstPatchStarted(); await new Promise(resolve => {{ releaseFirstPatch = resolve; }}); }} return {{revision:patches.length, ids:patches.map(item => item.item_id)}}; }}, onRemoteIds: ids => {{ globalThis.ids = ids; }}, onStatus: status => {{ globalThis.status = status; }}, }}); sync.enqueue('add', 'issue:r:1:'); (async () => {{ const firstFlush = sync.flush(); await firstPatchBlocked; sync.enqueue('add', 'issue:r:2:'); const sharedFlush = sync.flush(); const sharesInflight = firstFlush === sharedFlush; releaseFirstPatch(); await sharedFlush; process.stdout.write(JSON.stringify({{ sharesInflight, patches, 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 == { "sharesInflight": True, "patches": [ {"operation_id": "op-1", "action": "add", "item_id": "issue:r:1:", "direction": None, "base_revision": 0}, {"operation_id": "op-2", "action": "add", "item_id": "issue:r:2:", "direction": None, "base_revision": 0}, ], "pending": [], "ids": ["issue:r:1:", "issue:r:2:"], "status": "saved", } def test_activation_queued_during_an_empty_inflight_refresh_is_drained_before_it_settles(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_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)}}; let releaseRefresh; const refreshStarted = new Promise(resolve => {{ globalThis.refreshStarted = resolve; }}); const patches = []; const statuses = []; const sync = createTodaySync({{ storage, getLogin: () => 'timmy', createOperationId: () => 'activation-1', fetchJson: async (_url, options={{}}) => {{ if (!options.method) {{ globalThis.refreshStarted(); await new Promise(resolve => {{ releaseRefresh = resolve; }}); return {{revision:0, ids:[]}}; }} const operations = JSON.parse(options.body).operations; patches.push(...operations); return {{revision:1, ids:[], first_task_state:'complete', accepted_operation_ids:operations.map(item => item.operation_id)}}; }}, onRemoteIds: () => {{}}, onStatus: state => statuses.push(state), }}); (async () => {{ const refreshing = sync.flush(); await refreshStarted; sync.enqueueActivation('complete'); const sharedFlush = sync.flush(); releaseRefresh(); await sharedFlush; process.stdout.write(JSON.stringify({{ sharesInflight: refreshing === sharedFlush, patches, pending:sync.pending(), statuses, }})); }})(); """ result = json.loads(subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ).stdout) assert result["sharesInflight"] is True assert [operation["activation_state"] for operation in result["patches"]] == ["complete"] assert result["pending"] == [] assert result["statuses"][-1] == "saved" def test_operations_queued_in_the_same_millisecond_preserve_enqueue_order(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); const storage = {{ get length() {{ return values.size; }}, key: index => [...values.keys()][index] || null, getItem: key => values.get(key) || null, setItem: (key, value) => values.set(key, value), removeItem: key => values.delete(key), }}; const operationIds = ['z-add-first', 'a-move-second']; const sync = createTodaySync({{ storage, getLogin: () => 'timmy', now: () => 1000, createOperationId: () => operationIds.shift(), fetchJson: async () => ({{revision: 0, ids: []}}), }}); sync.enqueue('add', 'issue:r:1:'); sync.enqueue('move', 'issue:r:1:', 'up'); process.stdout.write(JSON.stringify(sync.pending().map(operation => operation.operation_id))); """ assert json.loads(subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ).stdout) == ["z-add-first", "a-move-second"] def test_corrupt_queue_record_does_not_hide_or_strand_valid_today_edits(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const prefix = 'stackchain.today-sync.v1.timmy.operation.'; const values = new Map([ [prefix + 'first', JSON.stringify({{queued_at: 1000, operation: {{operation_id:'first', action:'add', item_id:'issue:r:1:'}}}})], [prefix + 'broken', '{{not-json'], [prefix + 'invalid', JSON.stringify({{queued_at: 1000, operation: {{operation_id:'invalid', action:'erase-everything'}}}})], [prefix + 'second', JSON.stringify({{queued_at: 1001, operation: {{operation_id:'second', action:'move', item_id:'issue:r:2:', direction:'up'}}}})], ]); 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 patches=[]; const statuses=[]; const sync=createTodaySync({{storage,getLogin:()=> 'timmy', fetchJson:async(_url,options={{}})=>{{const operations=JSON.parse(options.body).operations;patches.push(...operations);return {{revision:1,ids:['issue:r:1:','issue:r:2:'],accepted_operation_ids:operations.map(x=>x.operation_id)}}}}, onRemoteIds:()=>{{}},onStatus:(state,detail={{}})=>statuses.push([state,detail.discarded||0])}}); (async()=>{{const result=await sync.flush();process.stdout.write(JSON.stringify({{ result,patches,statuses,remaining:[...values.keys()].filter(key=>key.startsWith(prefix)),pending:sync.pending() }}));}})(); """ result = json.loads(subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ).stdout) assert result["result"] is True assert [item["operation_id"] for item in result["patches"]] == ["first", "second"] assert result["pending"] == [] assert result["remaining"] == [] assert result["statuses"][-1] == ["recovered", 2] def test_today_sync_reports_queue_recovery_instead_of_plain_saved_status(): source = DASHBOARD.read_text() assert "Today queue recovered" in source assert "discarded ${detail.discarded} unreadable device record" in source def test_today_capacity_configuration_syncs_offline_and_adopts_remote_plan(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); let delivered; let adopted; let broadcast; 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 = createTodaySync({{ storage, getLogin:()=> 'timmy', createOperationId:()=> 'capacity-op', createChannel:()=>({{addEventListener() {{}},postMessage:plan=>{{broadcast=plan}},close() {{}}}}), fetchJson:async(_url, options={{}})=>{{ if (!options.method) return {{revision:2,ids:['issue:r:1:'],capacity_minutes:null,estimates:{{}}}}; delivered=JSON.parse(options.body).operations[0]; return {{revision:3,ids:['issue:r:1:'],capacity_minutes:120,estimates:{{'issue:r:1:':60}},accepted_operation_ids:['capacity-op'],duplicate_operation_ids:[],rejected_operations:[]}}; }}, onRemotePlan:plan=>{{adopted=plan}}, onRemoteIds:()=>{{}}, onStatus:()=>{{}}, }}); sync.enqueueConfiguration(120, {{'issue:r:1:':60}}); (async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{delivered,adopted,broadcast,pending:sync.pending()}}));}})(); """ assert json.loads(subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ).stdout) == { "delivered": { "operation_id": "capacity-op", "action": "configure", "item_id": "plan", "direction": None, "capacity_minutes": 120, "estimates": {"issue:r:1:": 60}, "base_revision": 0, }, "adopted": { "revision": 3, "ids": ["issue:r:1:"], "capacity_minutes": 120, "estimates": {"issue:r:1:": 60}, "accepted_operation_ids": ["capacity-op"], "duplicate_operation_ids": [], "rejected_operations": [], }, "broadcast": { "revision": 3, "ids": ["issue:r:1:"], "capacity_minutes": 120, "estimates": {"issue:r:1:": 60}, }, "pending": [], } def test_today_batch_admission_leaves_no_partial_operations_when_storage_fails(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); let writes = 0; let sequence = 0; const storage = {{ get length() {{ return values.size; }}, key: index => [...values.keys()][index] || null, getItem: key => values.get(key) || null, setItem: (key, value) => {{ writes += 1; throw new Error('quota'); }}, removeItem: key => values.delete(key), }}; const sync = createTodaySync({{ storage, getLogin: () => 'timmy', createOperationId: () => 'batch-' + (++sequence), fetchJson: async () => {{ throw new Error('must not deliver'); }}, onStatus: () => {{}}, }}); const admitted = sync.enqueueBatch([ {{action:'remove', item_id:'issue:r:1:'}}, {{action:'activate', item_id:'first-task', activation_state:'complete'}}, ]); process.stdout.write(JSON.stringify({{admitted, writes, pending:sync.pending(), keys:[...values.keys()]}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "admitted": False, "writes": 1, "pending": [], "keys": [], } def test_today_batch_delivers_remove_and_activation_together_then_clears_receipts(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); let sequence = 0; const patches = []; const storage = {{ get length() {{ return values.size; }}, key: index => [...values.keys()][index] || null, getItem: key => values.get(key) || null, setItem: (key,value) => values.set(key,value), removeItem: key => values.delete(key), }}; const sync = createTodaySync({{ storage, getLogin: () => 'timmy', createOperationId: () => 'batch-' + (++sequence), fetchJson: async (_url, options={{}}) => {{ const operations = JSON.parse(options.body).operations; patches.push(operations); return {{revision:3, ids:[], first_task_state:'complete', accepted_operation_ids:operations.map(item => item.operation_id)}}; }}, onRemoteIds: () => {{}}, onStatus: () => {{}}, }}); const admitted = sync.enqueueBatch([ {{action:'remove', item_id:'issue:r:1:'}}, {{action:'activate', item_id:'first-task', activation_state:'complete'}}, ]); const before = sync.pending(); (async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{ admitted, before, patches, after:sync.pending(), operationKeys:[...values.keys()].filter(key=>key.includes('.operation.')), }}));}})(); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr data = json.loads(result.stdout) assert data["admitted"] is True assert [[item["action"] for item in batch] for batch in data["patches"]] == [["remove", "activate"]] assert data["before"] == data["patches"][0] assert data["after"] == [] assert data["operationKeys"] == [] def test_first_task_activation_uses_today_outbox_and_adopts_remote_completion(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); let sequence=0; const adopted=[]; const delivered=[]; const listeners={{}}; 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 = createTodaySync({{ storage, getLogin:()=> 'timmy', createOperationId:()=> 'activation-' + (++sequence), fetchJson:async(_url, options={{}})=>{{ if (!options.method) return {{revision:4,ids:[],first_task_state:'complete'}}; const operations=JSON.parse(options.body).operations; delivered.push(...operations); return {{revision:5,ids:[],first_task_state:'complete',accepted_operation_ids:operations.map(x=>x.operation_id)}}; }}, onRemotePlan:plan=>adopted.push(plan.first_task_state), onRemoteIds:()=>{{}}, onStatus:()=>{{}}, }}); sync.startLifecycle({{window:{{addEventListener:(name,callback)=>listeners[name]=callback}},document:{{addEventListener(){{}}}}}}); listeners['stackchain:first-task-complete'](); listeners['stackchain:first-task-complete'](); (async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{ delivered,adopted,pending:sync.pending(),hydrated:values.get('stackchain.first-task.v1:timmy') }}));}})(); """ result = json.loads(subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ).stdout) assert result == { "delivered": [{ "operation_id": "activation-1", "action": "activate", "item_id": "first-task", "direction": None, "activation_state": "complete", "base_revision": 0, }], "adopted": ["complete"], "pending": [], "hydrated": "complete", } def test_inflight_today_drain_ships_in_a_new_offline_shell(): source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text() assert "stackchain-dashboard-shell-v145" in source assert "BASE + 'static/today-sync.js'" in source 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:', JSON.parse(options.body).operations[0].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", ] assert result["requests"][0]["body"] == {"operations": [{ "operation_id": "fixed-op", "action": "add", "item_id": "issue:r:2:", "direction": None, "base_revision": 0, }]} assert result["adopted"] == ["issue:r:9:", "issue:r:2:"] assert result["status"] == "saved" assert result["pending"] == [] def test_today_operations_capture_server_revision_and_expire_before_replay(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_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=createTodaySync({{storage,getLogin:()=> 'timmy',createOperationId:()=> 'old-op', now:()=>1_800_000_000_000,maxOfflineMs:1000, fetchJson:async(_url,options={{}})=>{{requests.push(options.method||'GET');return {{revision:7,ids:['server']}}}}, onRemoteIds:()=>{{}},onStatus:(state,detail={{}})=>statuses.push([state,detail.count||0])}}); (async()=>{{await sync.flush();sync.enqueue('move','issue:r:2:','up'); const pendingBefore=sync.pending(); 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({{pendingBefore,requests,statuses,pendingAfter:sync.pending()}}));}})(); """ result = json.loads(subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout) assert result["pendingBefore"] == [{ "operation_id": "old-op", "action": "move", "item_id": "issue:r:2:", "direction": "up", "base_revision": 7, }] assert result["requests"] == ["GET", "GET"] assert ["expired", 1] in result["statuses"] assert result["statuses"][-1] == ["expired", 1] assert result["pendingAfter"] == [] def test_pending_today_edits_are_sent_as_one_batch_without_a_preflight_get(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); const requests=[]; 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:()=> 'op-'+(++sequence), fetchJson:async (_url,options={{}})=>{{const body=JSON.parse(options.body);requests.push(body);return {{revision:2,ids:['issue:r:1:','issue:r:2:'],accepted_operation_ids:body.operations.map(x=>x.operation_id),duplicate_operation_ids:[],rejected_operations:[]}}}}, onRemoteIds:()=>{{}},onStatus:()=>{{}}, }}); sync.enqueue('add','issue:r:1:');sync.enqueue('add','issue:r:2:'); (async()=>{{await sync.flush();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 len(result["requests"]) == 1 assert [item["operation_id"] for item in result["requests"][0]["operations"]] == ["op-1", "op-2"] 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", "base_revision": 0, }], "status": "retrying", } def test_retryable_failure_replays_automatically_once_with_same_operation_id(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); const timers = []; const statuses = []; const patches = []; let attempts = 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:()=> 'stable-op', setTimer:(callback,delay)=>{{timers.push({{callback,delay}});return timers.length}}, clearTimer:()=>{{}}, fetchJson:async (_url,options={{}})=>{{ attempts += 1; if (attempts === 1) {{ const error = new Error('busy'); error.status=503; error.retryAfter=2; throw error; }} if (options.method) patches.push(...JSON.parse(options.body).operations); return options.method ? {{revision:1,ids:['issue:r:2:']}} : {{revision:0,ids:[]}}; }}, onRemoteIds:()=>{{}}, onStatus:(state,detail)=>statuses.push([state,detail?.delayMs||null]), }}); sync.enqueue('add','issue:r:2:'); (async()=>{{ await sync.flush(); const scheduled = timers.map(timer=>timer.delay); await timers[0].callback(); process.stdout.write(JSON.stringify({{scheduled,attempts,patches,pending:sync.pending(),statuses}})); }})(); """ result = json.loads( subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout ) assert result["scheduled"] == [2000] assert result["attempts"] == 2 assert result["patches"] == [{ "operation_id": "stable-op", "action": "add", "item_id": "issue:r:2:", "direction": None, "base_revision": 0, }] assert result["pending"] == [] assert ["retrying", 2000] in result["statuses"] assert result["statuses"][-1] == ["saved", None] 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, "base_revision": 0, }], "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, "base_revision": 0}, {"operation_id": "migration-2", "action": "add", "item_id": "issue:r:2:", "direction": None, "base_revision": 0}, ], } 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={{}}) => {{ patch = true; return {{revision:5, ids:['1','2','3','4','5'], accepted_operation_ids:[], duplicate_operation_ids:[], rejected_operations:[{{operation_id:'sixth',reason:'today_full'}}]}}; }}, 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_server_limit_conflict_drops_only_the_rejected_operation_and_drains_later_edits(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); const values = new Map(); const patches = []; 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: () => 'op-' + (++sequence), fetchJson: async (_url, options={{}}) => {{ const operations = JSON.parse(options.body).operations; patches.push(...operations); return {{revision:6, ids:['1','2','3','4'], accepted_operation_ids:['op-2'], duplicate_operation_ids:[], rejected_operations:[{{operation_id:'op-1',reason:'today_full'}}]}}; }}, onRemoteIds: ids => {{ globalThis.ids = ids; }}, onStatus: status => statuses.push(status), }}); sync.enqueue('add', '6'); sync.enqueue('remove', '5'); (async()=>{{ const result = await sync.flush(); process.stdout.write(JSON.stringify({{result, patches, pending:sync.pending(), ids:globalThis.ids, statuses}})); }})(); """ result = json.loads( subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout ) assert result == { "result": False, "patches": [ {"operation_id": "op-1", "action": "add", "item_id": "6", "direction": None, "base_revision": 0}, {"operation_id": "op-2", "action": "remove", "item_id": "5", "direction": None, "base_revision": 0}, ], "pending": [], "ids": ["1", "2", "3", "4"], "statuses": ["pending", "pending", "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": ["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"]]