import json import subprocess from pathlib import Path import httpx import pytest from src import main from tests.dashboard_bundle import dashboard SYNC = Path(__file__).parents[1] / "frontend" / "background-issue-sync.js" def run_node(script: str) -> dict: completed = subprocess.run( ["node", "-e", script], capture_output=True, check=True, text=True ) return json.loads(completed.stdout) def test_closed_app_sync_delivers_matching_issue_once_with_original_idempotency_key(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const item = {{ id:'capture-1', operationId:'capture-1', ownerLogin:'timmy', status:'queued', repository:'stackchain/api', title:'Offline report', body:'Full context', labelIds:[3], milestoneId:4, dueDate:'2026-08-09', queuedAt:100, }}; const state = {{item, completed:[], released:[], failed:[], calls:[]}}; const store = {{ claimNext: async owner => state.item && state.item.ownerLogin === owner ? {{...state.item}} : null, complete: async id => {{ state.completed.push(id); state.item = null; }}, release: async id => state.released.push(id), fail: async (id, message) => state.failed.push({{id,message}}), }}; const fetchJson = async (url, options = {{}}) => {{ state.calls.push({{url,options}}); if (url === 'api/v1/background-identity') return {{login:'timmy'}}; return {{repository:'stackchain/api',number:251,title:'Offline report'}}; }}; (async () => {{ const sync = createBackgroundIssueSync({{store,fetchJson}}); const result = await sync.flush(); process.stdout.write(JSON.stringify({{state,result}})); }})(); """ output = run_node(script) assert output["result"]["confirmed"][0]["number"] == 251 assert output["state"]["completed"] == ["capture-1"] assert output["state"]["released"] == [] assert output["state"]["failed"] == [] assert output["state"]["calls"][0]["url"] == "api/v1/background-identity" mutation = output["state"]["calls"][1] assert mutation["url"] == "api/v1/repos/stackchain/api/issues" assert mutation["options"]["headers"]["Idempotency-Key"] == "capture-1" assert json.loads(mutation["options"]["body"]) == { "title": "Offline report", "body": "Full context", "label_ids": [3], "milestone_id": 4, "due_date": "2026-08-09T23:59:59Z", } def test_capture_attachment_retry_resumes_after_creation_without_duplicate_issue(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); let item = {{ id:'capture-image',operationId:'capture-image',ownerLogin:'timmy',status:'queued', repository:'stackchain/dashboard',title:'Broken mobile layout',body:'At 320px',labelIds:[], attachment:{{filename:'phone.png',contentType:'image/png',data:'iVBORw0KGgo='}}, }}; const state = {{calls:[],completed:0,released:0}}; const store = {{ claimNext:async()=>item ? {{...item}} : null, update:async(_id,transform)=>{{item=transform(item);}}, complete:async()=>{{state.completed+=1;item=null;}}, release:async()=>{{state.released+=1;item={{...item,status:'queued'}};}}, fail:async()=>{{}},countBlocked:async()=>0, }}; let uploadAttempts=0; const fetchJson=async(url,options={{}})=>{{ state.calls.push({{url,key:options.headers?.['Idempotency-Key'],body:options.body&&JSON.parse(options.body)}}); if(url==='api/v1/background-identity')return{{login:'timmy'}}; if(url.endsWith('/issues'))return{{repository:'stackchain/dashboard',number:469,title:'Broken mobile layout'}}; if(url.endsWith('/attachments') && uploadAttempts++ === 0){{const error=new Error('Upload unavailable');error.status=503;throw error;}} if(url.endsWith('/attachments'))return{{markdown:'![phone.png]()'}}; if(url.endsWith('/comments'))return{{id:91}}; }}; (async()=>{{ const sync=createBackgroundIssueSync({{store,fetchJson}}); let firstError='';try{{await sync.flush();}}catch(error){{firstError=error.message;}} const afterFirst={{...item}}; const second=await sync.flush(); process.stdout.write(JSON.stringify({{state,firstError,afterFirst,second}})); }})(); """ output = run_node(script) assert output["firstError"] == "Upload unavailable" assert output["afterFirst"]["deliveredIssue"]["number"] == 469 assert [call["url"] for call in output["state"]["calls"]].count( "api/v1/repos/stackchain/dashboard/issues" ) == 1 assert output["state"]["calls"][-2]["key"] == "capture-image:attachment" assert output["state"]["calls"][-1]["key"] == "capture-image:attachment-comment" assert output["state"]["calls"][-1]["body"] == { "body": "![phone.png]()" } assert output["state"]["completed"] == 1 assert output["second"]["confirmed"][0]["number"] == 469 def test_capture_attachment_stage_keys_remain_within_idempotency_limit(): script = f""" const createBackgroundIssueSync=require({json.dumps(str(SYNC))}); const item={{id:'long',operationId:'x'.repeat(128),ownerLogin:'timmy',repository:'o/r',title:'Bug',body:'',labelIds:[],attachment:{{filename:'a.png',contentType:'image/png',data:'abc'}}}}; const keys=[];let queued=true; const store={{claimNext:async()=>queued?(queued=false,item):null,update:async()=>{{}},complete:async()=>{{}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0}}; const fetchJson=async(url,options={{}})=>{{if(url==='api/v1/background-identity')return{{login:'timmy'}};if(options.headers?.['Idempotency-Key'])keys.push(options.headers['Idempotency-Key']);if(url.endsWith('/issues'))return{{repository:'o/r',number:1}};if(url.endsWith('/attachments'))return{{markdown:'![a](url)'}};return{{id:1}};}}; createBackgroundIssueSync({{store,fetchJson}}).flush().then(()=>process.stdout.write(JSON.stringify(keys))); """ keys = run_node(script) assert len(keys) == 3 assert len(set(keys)) == 3 assert all(len(key) <= 128 for key in keys) def test_closed_app_sync_retains_created_issue_for_create_and_start_recovery(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const item = {{ id:'capture-start',operationId:'capture-start',ownerLogin:'timmy',status:'queued', repository:'stackchain/dashboard',title:'Resume',body:'',labelIds:[], completionIntent:'create-and-start', }}; let queued = item; const state = {{completed:[]}}; const store = {{ claimNext:async () => queued ? (queued=null,{{...item}}) : null, complete:async (id, deliveredIssue) => state.completed.push({{id,deliveredIssue}}), release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0, }}; const fetchJson = async url => url === 'api/v1/background-identity' ? {{login:'timmy'}} : {{repository:'stackchain/dashboard',number:389,title:'Resume'}}; (async()=>{{ await createBackgroundIssueSync({{store,fetchJson}}).flush(); process.stdout.write(JSON.stringify(state)); }})(); """ output = run_node(script) assert output["completed"] == [{ "id": "capture-start", "deliveredIssue": { "repository": "stackchain/dashboard", "number": 389, "title": "Resume" }, }] def test_closed_app_sync_returns_privacy_safe_actionable_delivery_receipts(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const queued = [ {{id:'capture-1',operationId:'capture-1',ownerLogin:'timmy',status:'queued',repository:'stackchain/api',title:'Secret title',body:'Secret body',labelIds:[]}}, {{id:'reply-1',operationId:'reply-1',ownerLogin:'timmy',status:'queued',kind:'pull-comment',repository:'stackchain/web',number:8,body:'Secret reply'}}, {{id:'bad-1',operationId:'bad-1',ownerLogin:'timmy',status:'queued',repository:'stackchain/api',title:'Bad',body:'Secret failure',labelIds:[]}}, ]; const store = {{ claimNext: async () => queued.shift() || null, complete: async () => {{}}, release: async () => {{}}, fail: async () => {{}}, countBlocked: async () => 0, }}; let issueMutations = 0; const fetchJson = async (url) => {{ if (url === 'api/v1/background-identity') return {{login:'timmy'}}; if (url.endsWith('/issues') && issueMutations++ === 0) return {{number:44,repository:'stackchain/api',title:'Secret title'}}; if (url.includes('/pulls/8/comments')) return {{id:91,body:'Secret reply'}}; const error = new Error('Sensitive validation detail'); error.status = 422; throw error; }}; (async () => {{ const result = await createBackgroundIssueSync({{store,fetchJson}}).flush(); process.stdout.write(JSON.stringify(result)); }})(); """ output = run_node(script) assert output["login"] == "timmy" assert output["receipts"] == [ { "id": "capture-1", "status": "confirmed", "kind": "issue", "route": "#/my-work/issue/stackchain/api/44", }, { "id": "reply-1", "status": "confirmed", "kind": "message", "route": "#/my-work/pull/stackchain/web/8", }, { "id": "bad-1", "status": "attention", "kind": "issue", "route": "#/my-work/drafts", }, ] serialized = json.dumps(output["receipts"]) assert "Secret" not in serialized assert "Sensitive" not in serialized @pytest.mark.parametrize( ("item", "expected_url"), [ ( {"kind": "issue-comment", "repository": "stackchain/api", "number": 7}, "api/v1/repos/stackchain/api/issues/7/comments", ), ( {"kind": "pull-comment", "repository": "stackchain/web", "number": 8}, "api/v1/repos/stackchain/web/pulls/8/comments", ), ( {"kind": "update-reply", "notificationId": 9}, "api/v1/notifications/9/reply", ), ], ) def test_closed_app_sync_delivers_each_authored_message_kind(item, expected_url): authored = { "id": "message-op", "operationId": "message-op", "ownerLogin": "timmy", "status": "queued", "body": "Ship this reply", **item, } script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); let queued = {json.dumps(authored)}; const calls = []; const store = {{ claimNext: async owner => queued?.ownerLogin === owner ? (queued = null, {json.dumps(authored)}) : null, complete: async () => {{}}, release: async () => {{}}, fail: async () => {{}}, countBlocked: async () => 0, }}; const fetchJson = async (url, options = {{}}) => {{ calls.push({{url, options}}); if (url === 'api/v1/background-identity') return {{login:'timmy'}}; return {{id:42}}; }}; (async () => {{ const result = await createBackgroundIssueSync({{store,fetchJson}}).flush(); process.stdout.write(JSON.stringify({{calls,result}})); }})(); """ output = run_node(script) mutation = output["calls"][1] assert mutation["url"] == expected_url assert mutation["options"]["headers"]["Idempotency-Key"] == "message-op" assert json.loads(mutation["options"]["body"]) == {"body": "Ship this reply"} assert output["result"]["confirmed"] == [{"id": 42}] def test_closed_app_sync_delivers_pull_review_with_stable_identity_and_full_payload(): authored = { "id": "review-op", "operationId": "review-op", "ownerLogin": "timmy", "status": "queued", "kind": "pull-review", "repository": "stackchain/web", "number": 8, "body": "Looks good", "decision": "approve", "expectedHeadSha": "abc123", "comments": [{"path": "app.js", "body": "Nice", "new_position": 4}], } script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); let queued = {json.dumps(authored)}; const calls=[]; const store = {{ claimNext:async owner=>queued?.ownerLogin===owner?(queued=null,{json.dumps(authored)}):null, complete:async()=>{{}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0, }}; const fetchJson=async(url,options={{}})=>{{calls.push({{url,options}});return url==='api/v1/background-identity'?{{login:'timmy'}}:{{id:45}};}}; createBackgroundIssueSync({{store,fetchJson}}).flush().then(result=>process.stdout.write(JSON.stringify({{calls,result}}))); """ output = run_node(script) mutation = output["calls"][1] assert mutation["url"] == "api/v1/repos/stackchain/web/pulls/8/review" assert mutation["options"]["headers"]["Idempotency-Key"] == "review-op" assert json.loads(mutation["options"]["body"]) == { "body": "Looks good", "decision": "approve", "expected_head_sha": "abc123", "comments": [{"path": "app.js", "body": "Nice", "new_position": 4}], } assert output["result"]["confirmed"] == [{"id": 45}] assert output["result"]["receipts"] == [{ "id": "review-op", "status": "confirmed", "kind": "message", "route": "#/my-work/review/stackchain/web/8", }] def test_closed_app_sync_delivers_issue_closure_and_returns_actionable_receipt(): authored = { "id": "close-op", "operationId": "close-op", "ownerLogin": "timmy", "status": "queued", "kind": "issue-close", "repository": "stackchain/dashboard", "number": 27, "body": "", } script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); let queued = {json.dumps(authored)}; const calls=[]; const store = {{ claimNext:async owner=>queued?.ownerLogin===owner?(queued=null,{json.dumps(authored)}):null, complete:async()=>{{}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0, }}; const fetchJson=async(url,options={{}})=>{{calls.push({{url,options}});return url==='api/v1/background-identity'?{{login:'timmy'}}:{{number:27,state:'closed'}};}}; createBackgroundIssueSync({{store,fetchJson}}).flush().then(result=>process.stdout.write(JSON.stringify({{calls,result}}))); """ output = run_node(script) mutation = output["calls"][1] assert mutation["url"] == "api/v1/repos/stackchain/dashboard/issues/27/close" assert mutation["options"]["method"] == "PATCH" assert mutation["options"]["headers"] == { "Accept": "application/json", "Idempotency-Key": "close-op" } assert output["result"]["confirmed"] == [{"number": 27, "state": "closed"}] assert output["result"]["receipts"] == [{ "id": "close-op", "status": "confirmed", "kind": "message", "route": "#/my-work/issue/stackchain/dashboard/27", }] def test_reconciling_one_outbox_lane_preserves_the_other_lane(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const records=new Map();let tail=Promise.resolve(); const transaction=work=>{{const run=tail.then(()=>work({{ getAll:async()=>[...records.values()].map(value=>({{...value}})), put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id), }}));tail=run.catch(()=>{{}});return run;}}; (async()=>{{ const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}}); await store.reconcile([{{id:'issue',ownerLogin:'timmy',status:'queued'}}], 'issue'); await store.reconcile([{{id:'message',kind:'issue-comment',ownerLogin:'timmy',status:'queued'}}], 'authored'); await store.reconcile([], 'issue'); process.stdout.write(JSON.stringify(await store.snapshot())); }})(); """ output = run_node(script) assert output == [ {"id": "message", "kind": "issue-comment", "ownerLogin": "timmy", "status": "queued", "outboxLane": "authored"} ] def test_reconciling_a_lightweight_attachment_reference_preserves_indexeddb_bytes(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const records=new Map();let tail=Promise.resolve(); const transaction=work=>{{const run=tail.then(()=>work({{ getAll:async()=>[...records.values()].map(value=>({{...value}})), put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id), }}));tail=run.catch(()=>{{}});return run;}}; (async()=>{{ const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}}); await store.reconcile([{{ id:'first',operationId:'first',ownerLogin:'timmy',status:'queued', attachment:{{filename:'one.png',contentType:'image/png',data:'first-image-bytes'}}, }}]); await store.reconcile([ {{id:'first',operationId:'first',ownerLogin:'timmy',status:'queued', attachment:{{filename:'one.png',contentType:'image/png',stored:true}}}}, {{id:'second',operationId:'second',ownerLogin:'timmy',status:'queued', attachment:{{filename:'two.png',contentType:'image/png',data:'second-image-bytes'}}}}, ]); process.stdout.write(JSON.stringify(await store.snapshot())); }})(); """ output = run_node(script) assert output[0]["attachment"]["data"] == "first-image-bytes" assert output[1]["attachment"]["data"] == "second-image-bytes" def test_issue_sync_store_hydrates_one_capture_by_key_without_scanning_all_records(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const records=new Map([ ['first',{{id:'first',attachment:{{filename:'one.png',contentType:'image/png',data:'first-bytes'}}}}], ['second',{{id:'second',attachment:{{filename:'two.png',contentType:'image/png',data:'second-bytes'}}}}], ]); const calls=[]; const transaction=work=>work({{ get:async id=>{{calls.push(['get',id]);return records.get(id);}}, getAll:async()=>{{calls.push(['getAll']);return [...records.values()];}}, put:async()=>{{}},delete:async()=>{{}}, }}); (async()=>{{ const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}}); const hydrated=await store.get('second'); process.stdout.write(JSON.stringify({{hydrated,calls}})); }})(); """ output = run_node(script) assert output["hydrated"]["attachment"]["data"] == "second-bytes" assert output["calls"] == [["get", "second"]] def test_stale_foreground_upsert_preserves_confirmed_attachment_delivery_stages(): script = f""" const createBackgroundIssueSync=require({json.dumps(str(SYNC))}); const records=new Map();let tail=Promise.resolve(); const transaction=work=>{{const run=tail.then(()=>work({{getAll:async()=>[...records.values()].map(value=>({{...value}})),put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id)}}));tail=run.catch(()=>{{}});return run;}}; (async()=>{{ const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}}); await store.upsert({{id:'capture',operationId:'same',ownerLogin:'timmy',status:'queued',attachment:{{filename:'a.png'}}}}); await store.update('capture',item=>({{...item,deliveredIssue:{{number:7}},attachmentMarkdown:'![a](url)'}})); const result=await store.upsert({{id:'capture',operationId:'same',ownerLogin:'timmy',status:'queued',attachment:{{filename:'a.png'}}}}); process.stdout.write(JSON.stringify(result)); }})(); """ output = run_node(script) assert output["deliveredIssue"]["number"] == 7 assert output["attachmentMarkdown"] == "![a](url)" def test_indexeddb_store_closes_on_version_change_and_reopens_afterward(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const state={{opens:0,closes:0}}; const indexedDB={{lastDb:null,open:()=>{{ state.opens += 1; const request={{result:null,onsuccess:null,onerror:null,onupgradeneeded:null}}; const db={{ objectStoreNames:{{contains:()=>true}}, close:()=>{{state.closes += 1;}}, transaction:()=>{{ const tx={{oncomplete:null,onerror:null,onabort:null,error:null,objectStore:()=>({{ getAll:()=>{{const get={{onsuccess:null,onerror:null,result:[]}};queueMicrotask(()=>get.onsuccess?.());return get;}}, }})}}; setTimeout(()=>tx.oncomplete?.(),0); return tx; }}, }}; indexedDB.lastDb=db;request.result=db;queueMicrotask(()=>request.onsuccess?.());return request; }}}}; (async()=>{{ const store=createBackgroundIssueSync.createIssueSyncStore({{indexedDB}}); await store.snapshot(); indexedDB.lastDb.onversionchange?.(); await store.snapshot(); process.stdout.write(JSON.stringify(state)); }})(); """ output = run_node(script) assert output == {"opens": 2, "closes": 1} def test_delivery_receipt_preference_is_account_bound_and_hidden_from_outbox(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const records=new Map();let tail=Promise.resolve(); const transaction=work=>{{const run=tail.then(()=>work({{ getAll:async()=>[...records.values()].map(value=>({{...value}})), put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id), }}));tail=run.catch(()=>{{}});return run;}}; (async()=>{{ const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}}); await store.setReceiptPreference('timmy', true); await store.reconcile([{{id:'issue',ownerLogin:'timmy',status:'queued'}}], 'issue'); const result={{ timmy:await store.getReceiptPreference('timmy'), alexander:await store.getReceiptPreference('alexander'), snapshot:await store.snapshot(), blocked:await store.countBlocked('alexander'), }}; await store.setReceiptPreference('timmy', false); result.disabled=await store.getReceiptPreference('timmy'); process.stdout.write(JSON.stringify(result)); }})(); """ output = run_node(script) assert output == { "timmy": True, "alexander": False, "snapshot": [ { "id": "issue", "ownerLogin": "timmy", "status": "queued", "outboxLane": "issue", } ], "blocked": 1, "disabled": False, } def test_closed_app_sync_leaves_another_accounts_issue_queued(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const state = {{owners:[],mutations:0}}; const store = {{ claimNext: async owner => {{ state.owners.push(owner); return null; }}, countBlocked: async owner => owner === 'alexander' ? 1 : 0, complete: async () => {{}}, }}; const fetchJson = async url => {{ if (url === 'api/v1/background-identity') return {{login:'alexander'}}; state.mutations += 1; }}; (async () => {{ const result = await createBackgroundIssueSync({{store,fetchJson}}).flush(); process.stdout.write(JSON.stringify({{state,result}})); }})(); """ output = run_node(script) assert output["state"] == {"owners": ["alexander"], "mutations": 0} assert output["result"]["confirmed"] == [] assert output["result"]["blocked"] == 1 def test_session_expiry_during_delivery_releases_claim_without_attention(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const queued = [ {{id:'capture-auth',operationId:'stable-key',ownerLogin:'timmy',repository:'o/r',title:'Keep me',body:'',labelIds:[]}}, {{id:'capture-later',operationId:'later-key',ownerLogin:'timmy',repository:'o/r',title:'Do not try yet',body:'',labelIds:[]}}, ]; const state = {{released:[],failed:[],mutationKeys:[]}}; const store = {{ claimNext: async () => queued.shift() || null, release: async id => state.released.push(id), fail: async (id,message) => state.failed.push({{id,message}}), countBlocked: async () => 0, }}; const fetchJson = async (url, options={{}}) => {{ if (url === 'api/v1/background-identity') return {{login:'timmy'}}; state.mutationKeys.push(options.headers['Idempotency-Key']); const error = new Error('Authentication required'); error.status = 401; throw error; }}; (async () => {{ let error = null; try {{ await createBackgroundIssueSync({{store,fetchJson}}).flush(); }} catch (caught) {{ error = {{message:caught.message,status:caught.status}}; }} process.stdout.write(JSON.stringify({{state,error}})); }})(); """ output = run_node(script) assert output == { "state": { "released": ["capture-auth"], "failed": [], "mutationKeys": ["stable-key"], }, "error": {"message": "Authentication required", "status": 401}, } def test_transient_delivery_failure_releases_claim_and_requests_another_sync(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); let item = {{id:'capture-2',operationId:'capture-2',ownerLogin:'timmy',repository:'o/r',title:'Retry',body:'Later',labelIds:[]}}; const state = {{released:[],completed:[]}}; const store = {{ claimNext: async () => item ? (item = null, {{id:'capture-2',operationId:'capture-2',ownerLogin:'timmy',repository:'o/r',title:'Retry',body:'Later',labelIds:[]}}) : null, release: async id => state.released.push(id), complete: async id => state.completed.push(id), }}; const fetchJson = async url => {{ if (url === 'api/v1/background-identity') return {{login:'timmy'}}; const error = new Error('Gitea unavailable'); error.status = 503; throw error; }}; (async () => {{ let error = null; try {{ await createBackgroundIssueSync({{store,fetchJson}}).flush(); }} catch (caught) {{ error = caught.message; }} process.stdout.write(JSON.stringify({{state,error}})); }})(); """ output = run_node(script) assert output["state"]["released"] == ["capture-2"] assert output["state"]["completed"] == [] assert output["error"] == "Gitea unavailable" def test_stalled_delivery_times_out_releases_claim_and_retries_with_same_idempotency_key(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const item = {{id:'capture-timeout',operationId:'stable-operation',ownerLogin:'timmy',repository:'o/r',title:'Retry',body:'Later',labelIds:[]}}; const state = {{queued:true,released:[],completed:[],keys:[],attempts:0}}; const store = {{ claimNext: async () => state.queued ? (state.queued=false, {{...item}}) : null, complete: async id => state.completed.push(id), release: async id => {{state.released.push(id); state.queued=true;}}, fail: async () => {{}}, countBlocked: async () => 0, }}; const fetchJson = async (url, options={{}}) => {{ if (url === 'api/v1/background-identity') return {{login:'timmy'}}; state.keys.push(options.headers['Idempotency-Key']); state.attempts += 1; if (state.attempts === 1) return new Promise(() => {{}}); return {{repository:'o/r',number:9}}; }}; (async () => {{ const sync=createBackgroundIssueSync({{store,fetchJson,requestTimeoutMs:10}}); let firstError=''; try {{ await sync.flush(); }} catch (error) {{ firstError=error.message; }} const second=await sync.flush(); process.stdout.write(JSON.stringify({{state,firstError,second}})); }})(); """ output = run_node(script) assert output["firstError"] == "Background request timed out." assert output["state"]["released"] == ["capture-timeout"] assert output["state"]["completed"] == ["capture-timeout"] assert output["state"]["keys"] == ["stable-operation", "stable-operation"] assert output["second"]["confirmed"][0]["number"] == 9 def test_purge_cancels_stalled_delivery_without_reclaiming_it(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const items=[ {{id:'capture-purge-1',operationId:'capture-purge-1',ownerLogin:'timmy',repository:'o/r',title:'Private',body:'Draft',labelIds:[]}}, {{id:'capture-purge-2',operationId:'capture-purge-2',ownerLogin:'timmy',repository:'o/r',title:'Private 2',body:'Draft',labelIds:[]}}, ]; const state={{released:[],closed:0,mutations:0}}; let mutationStarted; const started=new Promise(resolve => mutationStarted=resolve); const store={{ claimBatch:async()=>items.map(item=>({{...item}})), complete:async()=>{{}}, release:async id=>state.released.push(id), fail:async()=>{{}}, countBlocked:async()=>0, close:async()=>{{state.closed+=1;}}, }}; const fetchJson=async url=>{{ if(url==='api/v1/background-identity')return{{login:'timmy'}}; state.mutations+=1;mutationStarted();return new Promise(()=>{{}}); }}; (async()=>{{ const sync=createBackgroundIssueSync({{store,fetchJson,maxConcurrency:1,requestTimeoutMs:1000}}); sync.flush().catch(()=>{{}}); await started; const outcome=await Promise.race([ sync.purge().then(()=> 'purged'), new Promise(resolve=>setTimeout(()=>resolve('blocked'),80)), ]); process.stdout.write(JSON.stringify({{state,outcome}})); process.exit(0); }})(); """ output = run_node(script) assert output["outcome"] == "purged" assert output["state"] == { "released": ["capture-purge-1", "capture-purge-2"], "closed": 1, "mutations": 1, } def test_bounded_flush_delivers_healthy_records_after_transient_failure_without_same_run_retry(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const items = [ {{id:'issue-1',operationId:'issue-1',ownerLogin:'timmy',outboxLane:'issue',repository:'o/r',title:'One',body:'',labelIds:[]}}, {{id:'message-1',operationId:'message-1',ownerLogin:'timmy',outboxLane:'authored',kind:'issue-comment',repository:'o/r',number:1,body:'Reply'}}, {{id:'issue-bad',operationId:'issue-bad',ownerLogin:'timmy',outboxLane:'issue',repository:'o/r',title:'Bad',body:'',labelIds:[]}}, {{id:'message-2',operationId:'message-2',ownerLogin:'timmy',outboxLane:'authored',kind:'issue-comment',repository:'o/r',number:2,body:'Reply'}}, {{id:'issue-2',operationId:'issue-2',ownerLogin:'timmy',outboxLane:'issue',repository:'o/r',title:'Two',body:'',labelIds:[]}}, ]; const state = {{active:0,maxActive:0,attempted:[],completed:[],released:[],batches:0}}; const store = {{ claimBatch: async (owner, limit) => items.slice(0, limit), complete: async id => state.completed.push(id), release: async id => state.released.push(id), fail: async () => {{}}, countBlocked: async () => 0, }}; const fetchJson = async (url, options={{}}) => {{ if (url === 'api/v1/background-identity') return {{login:'timmy'}}; const key = options.headers['Idempotency-Key']; state.attempted.push(key); state.active += 1; state.maxActive = Math.max(state.maxActive, state.active); await new Promise(resolve => setTimeout(resolve, key === 'issue-bad' ? 5 : 20)); state.active -= 1; if (key === 'issue-bad') {{ const error=new Error('Temporary outage'); error.status=503; throw error; }} return {{id:key,number:7}}; }}; (async () => {{ let error; try {{ await createBackgroundIssueSync({{ store,fetchJson,maxConcurrency:3,batchSize:10, batch: async work => {{ state.batches += 1; return work(); }}, }}).flush(); }} catch (caught) {{ error=caught.message; }} process.stdout.write(JSON.stringify({{state,error}})); }})(); """ output = run_node(script) assert output["state"]["maxActive"] == 3 assert output["state"]["batches"] == 1 assert sorted(output["state"]["attempted"]) == [ "issue-1", "issue-2", "issue-bad", "message-1", "message-2" ] assert len(output["state"]["attempted"]) == len(set(output["state"]["attempted"])) assert sorted(output["state"]["completed"]) == [ "issue-1", "issue-2", "message-1", "message-2" ] assert output["state"]["released"] == ["issue-bad"] assert output["error"] == "Temporary outage" def test_bounded_flush_stops_admission_and_releases_unstarted_claims_on_auth_loss(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const items = Array.from({{length:5}}, (_, index) => ({{ id:'item-'+index,operationId:'item-'+index,ownerLogin:'timmy',outboxLane:'issue', repository:'o/r',title:'Item',body:'',labelIds:[], }})); const state = {{attempted:[],released:[],completed:[]}}; const store = {{ claimBatch: async () => items, complete: async id => state.completed.push(id), release: async id => state.released.push(id), fail:async()=>{{}}, countBlocked:async()=>0, }}; const fetchJson = async (url, options={{}}) => {{ if (url === 'api/v1/background-identity') return {{login:'timmy'}}; const key=options.headers['Idempotency-Key']; state.attempted.push(key); if (key === 'item-0') {{ const error=new Error('Authentication required'); error.status=401; throw error; }} await new Promise(resolve => setTimeout(resolve, 20)); return {{number:1}}; }}; (async()=>{{ let error; try {{ await createBackgroundIssueSync({{store,fetchJson,maxConcurrency:2}}).flush(); }} catch (caught) {{ error={{message:caught.message,status:caught.status}}; }} process.stdout.write(JSON.stringify({{state,error}})); }})(); """ output = run_node(script) assert output["state"]["attempted"] == ["item-0", "item-1"] assert sorted(output["state"]["released"]) == ["item-0", "item-2", "item-3", "item-4"] assert output["state"]["completed"] == ["item-1"] assert output["error"] == {"message": "Authentication required", "status": 401} def test_issue_store_claims_one_finite_batch_fairly_across_outbox_lanes(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const records=new Map();let tail=Promise.resolve(); const transaction=work=>{{const run=tail.then(()=>work({{ getAll:async()=>[...records.values()].map(value=>({{...value}})), put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id), }}));tail=run.catch(()=>{{}});return run;}}; (async()=>{{ const store=createBackgroundIssueSync.createIssueSyncStore({{transaction,now:()=>100,claimMs:5000}}); await store.reconcile([ {{id:'issue-1',ownerLogin:'timmy',status:'queued'}}, {{id:'issue-2',ownerLogin:'timmy',status:'queued'}}, {{id:'issue-3',ownerLogin:'timmy',status:'queued'}}, ], 'issue'); await store.reconcile([ {{id:'message-1',ownerLogin:'timmy',status:'queued'}}, {{id:'message-2',ownerLogin:'timmy',status:'queued'}}, ], 'authored'); const claimed=await store.claimBatch('timmy', 4); process.stdout.write(JSON.stringify({{claimed,snapshot:await store.snapshot()}})); }})(); """ output = run_node(script) assert [item["id"] for item in output["claimed"]] == [ "issue-1", "message-1", "issue-2", "message-2" ] assert all(item["status"] == "sending" for item in output["claimed"]) assert all(item["claimUntil"] == 5100 for item in output["claimed"]) assert next(item for item in output["snapshot"] if item["id"] == "issue-3")["status"] == "queued" def test_permanent_delivery_failure_marks_issue_for_foreground_attention(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); let claimed = false; const state = {{failed:[],released:[]}}; const store = {{ claimNext: async () => claimed ? null : (claimed = true, {{id:'capture-3',operationId:'capture-3',ownerLogin:'timmy',repository:'o/r',title:'Invalid',body:'',labelIds:[]}}), fail: async (id, message) => state.failed.push({{id,message}}), release: async id => state.released.push(id), countBlocked: async () => 0, }}; const fetchJson = async url => {{ if (url === 'api/v1/background-identity') return {{login:'timmy'}}; const error = new Error('Title is invalid'); error.status = 422; throw error; }}; (async () => {{ const result = await createBackgroundIssueSync({{store,fetchJson}}).flush(); process.stdout.write(JSON.stringify({{state,result}})); }})(); """ output = run_node(script) assert output["state"]["failed"] == [ {"id": "capture-3", "message": "Title is invalid"} ] assert output["state"]["released"] == [] assert output["result"]["attention"] == 1 def test_uncertain_background_delivery_preserves_verification_state(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); let claimed=false; const state={{failed:[]}}; const store={{ claimNext:async()=>claimed?null:(claimed=true,{{id:'maybe',operationId:'maybe',ownerLogin:'timmy',repository:'o/r',title:'Maybe',labelIds:[]}}), fail:async(id,message,deliveryState)=>state.failed.push({{id,message,deliveryState}}), release:async()=>{{}}, countBlocked:async()=>0, }}; const fetchJson=async url=>{{ if(url==='api/v1/background-identity') return {{login:'timmy'}}; const error=new Error('Verify first'); error.status=422; error.code='delivery_uncertain'; throw error; }}; (async()=>{{ await createBackgroundIssueSync({{store,fetchJson}}).flush(); process.stdout.write(JSON.stringify(state)); }})(); """ output = run_node(script) assert output["failed"] == [ {"id": "maybe", "message": "Verify first", "deliveryState": "uncertain"} ] def test_issue_store_atomically_grants_one_delivery_claim(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const records = new Map(); let tail = Promise.resolve(); const transaction = work => {{ const run = tail.then(() => work({{ getAll: async () => [...records.values()].map(value => ({{...value}})), put: async value => records.set(value.id, {{...value}}), delete: async id => records.delete(id), }})); tail = run.catch(() => {{}}); return run; }}; (async () => {{ const store = createBackgroundIssueSync.createIssueSyncStore({{transaction,now:()=>1000,claimMs:5000}}); await store.reconcile([{{id:'same',operationId:'same',ownerLogin:'timmy',status:'queued',repository:'o/r',title:'Once',labelIds:[]}}]); const claims = await Promise.all([store.claimNext('timmy'),store.claimNext('timmy')]); process.stdout.write(JSON.stringify({{claims,records:[...records.values()]}})); }})(); """ output = run_node(script) assert sum(claim is not None for claim in output["claims"]) == 1 assert output["records"][0]["status"] == "sending" assert output["records"][0]["claimUntil"] == 6000 def test_foreground_and_worker_race_still_posts_one_issue(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const records = new Map(); let tail = Promise.resolve(); const transaction = work => {{ const run = tail.then(() => work({{ getAll:async()=>[...records.values()].map(value=>({{...value}})), put:async value=>records.set(value.id,{{...value}}), delete:async id=>records.delete(id), }})); tail=run.catch(()=>{{}}); return run; }}; const item={{id:'race',operationId:'race',ownerLogin:'timmy',status:'queued',repository:'o/r',title:'Exactly once',body:'',labelIds:[]}}; let mutations=0; const fetchJson=async url=>{{ if(url==='api/v1/background-identity') return {{login:'timmy'}}; mutations+=1; return {{number:77}}; }}; (async()=>{{ const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}}); await store.reconcile([item]); const sync=createBackgroundIssueSync({{store,fetchJson}}); const [foreground,worker]=await Promise.all([sync.send(item,'timmy'),sync.flush()]); process.stdout.write(JSON.stringify({{foreground,worker,mutations,remaining:[...records.values()]}})); }})(); """ output = run_node(script) assert output["mutations"] == 1 assert len(output["remaining"]) == 1 assert output["remaining"][0]["status"] == "sent" delivered = int(bool(output["foreground"].get("issue"))) + len( output["worker"]["confirmed"] ) assert delivered == 1 def test_foreground_send_upserts_before_claim_when_mirror_is_still_pending(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); let record = null; const state={{upserts:0,mutations:0}}; const store={{ upsert:async item=>{{state.upserts+=1;record={{...item}};}}, claim:async(id,owner)=>record?.id===id&&record?.ownerLogin===owner?{{...record}}:null, complete:async()=>{{record=null;}}, release:async()=>{{}}, fail:async()=>{{}}, }}; const fetchJson=async()=>{{state.mutations+=1;return {{number:88}};}}; (async()=>{{ const sync=createBackgroundIssueSync({{store,fetchJson}}); const result=await sync.send({{id:'early',operationId:'early',ownerLogin:'timmy',repository:'o/r',title:'Fast',body:'',labelIds:[]}},'timmy'); process.stdout.write(JSON.stringify({{state,result,record}})); }})(); """ output = run_node(script) assert output["state"] == {"upserts": 1, "mutations": 1} assert output["result"]["issue"]["number"] == 88 assert output["record"] is None def test_completed_delivery_leaves_non_replayable_tombstone_for_next_page(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const records=new Map();let tail=Promise.resolve(); const transaction=work=>{{const run=tail.then(()=>work({{ getAll:async()=>[...records.values()].map(value=>({{...value}})), put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id), }}));tail=run.catch(()=>{{}});return run;}}; (async()=>{{ const store=createBackgroundIssueSync.createIssueSyncStore({{transaction,now:()=>100}}); await store.reconcile([{{id:'done',ownerLogin:'timmy',status:'queued'}}]); await store.claimNext('timmy'); await store.complete('done'); const replay=await store.claimNext('timmy'); const snapshot=await store.snapshot(); process.stdout.write(JSON.stringify({{replay,snapshot}})); }})(); """ output = run_node(script) assert output["replay"] is None assert output["snapshot"] == [ {"id": "done", "ownerLogin": "timmy", "status": "sent", "outboxLane": "issue", "claimUntil": 0} ] def test_user_edit_resets_worker_attention_item_for_retry(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const records=new Map([['edit',{{id:'edit',ownerLogin:'timmy',status:'attention',title:'Bad',error:'Invalid'}}]]); let tail=Promise.resolve(); const transaction=work=>{{const run=tail.then(()=>work({{ getAll:async()=>[...records.values()].map(value=>({{...value}})), put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id), }}));tail=run.catch(()=>{{}});return run;}}; (async()=>{{ const store=createBackgroundIssueSync.createIssueSyncStore({{transaction,now:()=>100}}); await store.reconcile([{{id:'edit',ownerLogin:'timmy',status:'queued',title:'Fixed'}}]); const claimed=await store.claimNext('timmy'); process.stdout.write(JSON.stringify(claimed)); }})(); """ output = run_node(script) assert output["title"] == "Fixed" assert output["status"] == "sending" assert "error" not in output def test_resume_after_purge_reopens_background_delivery_with_fresh_identity(): script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); const state={{closed:0,identityLookups:0,mutations:0,keys:[]}}; let queued={{ id:'capture-after-login',ownerLogin:'timmy',status:'queued',repository:'stackchain/dashboard', title:'Recovered capture',body:'Evidence',labelIds:[],operationId:'op-after-login', }}; const store={{ close:async()=>{{state.closed+=1;}}, claimBatch:async login=>{{ if(queued?.ownerLogin!==login)return []; const claimed=queued;queued=null;return [claimed]; }}, complete:async()=>{{}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0, }}; const fetchJson=async(url,options={{}})=>{{ if(url==='api/v1/background-identity'){{state.identityLookups+=1;return {{login:'timmy'}};}} state.mutations+=1;state.keys.push(options.headers['Idempotency-Key']); return {{repository:'stackchain/dashboard',number:425,title:'Recovered capture'}}; }}; (async()=>{{ const sync=createBackgroundIssueSync({{store,fetchJson}}); await sync.purge(); const blocked=await sync.flush(); await sync.resume(); const delivered=await sync.flush(); process.stdout.write(JSON.stringify({{state,blocked,delivered}})); }})(); """ output = run_node(script) assert output["state"] == { "closed": 1, "identityLookups": 1, "mutations": 1, "keys": ["op-after-login"], } assert output["blocked"]["login"] == "" assert output["delivered"]["login"] == "timmy" assert output["delivered"]["confirmed"] == [ {"repository": "stackchain/dashboard", "number": 425, "title": "Recovered capture"} ] @pytest.mark.anyio async def test_background_identity_is_lightweight_and_never_cacheable(monkeypatch): calls = 0 async def user(): nonlocal calls calls += 1 return {"id": 7, "login": "timmy", "email": "private@example.com"} monkeypatch.setattr(main, "current_user", user) transport = httpx.ASGITransport(app=main.app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: response = await client.get("/api/v1/background-identity") assert response.status_code == 200 assert response.json() == {"login": "timmy"} assert response.headers["cache-control"] == "no-store" assert calls == 1 @pytest.mark.anyio async def test_dashboard_wires_indexeddb_outbox_and_background_sync_fallback(): html = await dashboard() assert '' in html assert "const backgroundIssueStore = createIssueSyncStore();" in html assert "createBackgroundIssueSync({" in html assert "backgroundSync: backgroundIssueSync" in html assert "registration.sync.register('stackchain-issue-outbox-v1')" in html assert "backgroundIssueSync.snapshot().then(records => {" in html assert "issueOutbox.reconcileBackground(records);" in html assert "authoredOutbox.reconcileBackground(records);" in html assert "if ('indexedDB' in window)" in html @pytest.mark.anyio async def test_dashboard_offers_explicit_account_bound_delivery_receipt_opt_in(): html = await dashboard() assert 'id="delivery-receipts" type="checkbox"' in html assert "deliveryReceipts.addEventListener('change', async () => {" in html assert "await Notification.requestPermission()" in html assert "backgroundIssueSync.setReceiptPreference(confirmedOwnerLogin, enabled)" in html assert "await backgroundIssueSync.getReceiptPreference(confirmedOwnerLogin)" in html assert "window.addEventListener('hashchange', openDeliveryReceiptRoute);" in html assert "if (window.location.hash !== '#/my-work/drafts') return;" in html