import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard OUTBOX = Path(__file__).parents[1] / "frontend" / "issue-outbox.js" def run_node(script: str): result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) return json.loads(result.stdout) def test_issue_outbox_binds_items_and_only_flushes_for_the_matching_account(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; const calls = []; let confirmedLogin = 'timmy'; const outbox = createIssueOutbox({{ storage, getOwnerLogin:() => confirmedLogin, createOperationId:() => 'bound-op', fetchJson:async url => {{ calls.push(url); return {{number:1}}; }}, }}); const queued = outbox.enqueue({{repository:'stackchain/api',title:'Bound',body:'Context'}}); confirmedLogin = 'alexander'; (async () => {{ const mismatch = await outbox.flush('alexander'); const unknown = await outbox.retry(queued.id, ''); const matched = await outbox.flush('timmy'); process.stdout.write(JSON.stringify({{queued,mismatch,unknown,matched,calls}})); }})(); """ output = run_node(script) assert output["queued"]["ownerLogin"] == "timmy" assert output["mismatch"]["blocked"] == 1 assert output["unknown"]["blocked"] == 1 assert len(output["matched"]["confirmed"]) == 1 assert len(output["calls"]) == 1 def test_issue_outbox_queues_multiple_planned_issues_with_stable_operation_ids(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const storage = {{ getItem:key => values.has(key) ? values.get(key) : null, setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key), }}; let sequence = 0; const outbox = createIssueOutbox({{storage, getOwnerLogin:()=>'timmy', createOperationId:() => 'op-' + (++sequence), now:() => 1000 + sequence}}); outbox.enqueue({{repository:'stackchain/api',title:'First',body:'One',labelIds:[3]}}); outbox.enqueue({{repository:'stackchain/web',title:'Second',body:'Two',milestoneId:4,dueDate:'2026-08-09'}}); const reloaded = createIssueOutbox({{storage}}).list(); process.stdout.write(JSON.stringify({{items:reloaded, stored:JSON.parse(values.get('stackchain.issue-outbox.v1'))}})); """ output = run_node(script) assert [(item["repository"], item["title"]) for item in output["items"]] == [ ("stackchain/api", "First"), ("stackchain/web", "Second"), ] assert [item["operationId"] for item in output["items"]] == ["op-1", "op-2"] assert all(item["status"] == "queued" for item in output["items"]) assert output["stored"]["version"] == 3 def test_issue_outbox_persists_create_and_start_intent_and_returns_it_with_confirmation(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; const outbox = createIssueOutbox({{ storage, getOwnerLogin:()=> 'timmy', createOperationId:()=> 'create-start-1', fetchJson:async () => ({{repository:'stackchain/dashboard',number:389,title:'Resume me'}}), }}); outbox.enqueue({{ repository:'stackchain/dashboard',title:'Resume me',body:'',completionIntent:'create-and-start' }}); const reloaded = createIssueOutbox({{ storage, fetchJson:async () => ({{repository:'stackchain/dashboard',number:389,title:'Resume me'}}) }}); reloaded.flush('timmy').then(result => process.stdout.write(JSON.stringify({{ persisted:JSON.parse(values.get('stackchain.issue-outbox.v1')), result, }}))); """ output = run_node(script) assert output["persisted"]["version"] == 3 completion = output["result"]["completions"][0] assert completion["intent"] == "create-and-start" assert completion["ownerLogin"] == "timmy" assert completion["operationId"] == "create-start-1" assert completion["issue"]["number"] == 389 def test_editing_a_queued_issue_updates_its_completion_intent_without_rekeying_delivery(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values=new Map();let sequence=0; const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; const outbox=createIssueOutbox({{ storage,getOwnerLogin:()=>'timmy',createOperationId:()=> 'intent-' + (++sequence) }}); const queued=outbox.enqueue({{repository:'o/r',title:'Work',body:''}}); const started=outbox.update(queued.id,{{...queued,completionIntent:'create-and-start'}}); const ordinary=outbox.update(queued.id,{{...started,completionIntent:undefined}}); process.stdout.write(JSON.stringify({{started,ordinary}})); """ output = run_node(script) assert output["started"]["completionIntent"] == "create-and-start" assert "completionIntent" not in output["ordinary"] assert output["started"]["operationId"] == output["ordinary"]["operationId"] == "intent-1" def test_issue_outbox_flushes_sequentially_and_keeps_transient_failures_queued(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}; let sequence = 0; const calls = []; const outbox = createIssueOutbox({{ storage, getOwnerLogin:()=>'timmy', createOperationId:() => 'stable-' + (++sequence), fetchJson:async (url, options) => {{ calls.push({{url, key:options.headers['Idempotency-Key']}}); if (calls.length === 2) {{ const error = new Error('upstream unavailable'); error.status = 503; throw error; }} return {{repository:'stackchain/api',number:41,title:'First'}}; }}, }}); outbox.enqueue({{repository:'stackchain/api',title:'First',body:'One'}}); outbox.enqueue({{repository:'stackchain/web',title:'Second',body:'Two'}}); outbox.flush('timmy').then(result => process.stdout.write(JSON.stringify({{result,calls,remaining:outbox.list()}}))); """ output = run_node(script) assert output["result"]["confirmed"][0]["number"] == 41 assert output["calls"] == [ {"url": "api/v1/repos/stackchain/api/issues", "key": "stable-1"}, {"url": "api/v1/repos/stackchain/web/issues", "key": "stable-2"}, ] assert len(output["remaining"]) == 1 assert output["remaining"][0]["title"] == "Second" assert output["remaining"][0]["status"] == "queued" def test_issue_outbox_preserves_permanent_failures_for_edit_and_retry(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}; let invalid = true; let operationSequence = 0; const calls = []; const outbox = createIssueOutbox({{ storage, getOwnerLogin:()=>'timmy', createOperationId:() => 'edit-' + (++operationSequence), fetchJson:async (_url, options) => {{ calls.push({{key:options.headers['Idempotency-Key'],body:JSON.parse(options.body)}}); if (invalid) {{ const error = new Error('Title is invalid'); error.status = 422; throw error; }} return {{repository:'stackchain/api',number:42,title:'Fixed title'}}; }}, }}); const queued = outbox.enqueue({{repository:'stackchain/api',title:'Bad',body:'Context'}}); outbox.flush('timmy').then(async () => {{ const attention = outbox.list()[0]; outbox.update(queued.id, {{...attention,title:'Fixed title'}}); invalid = false; const result = await outbox.retry(queued.id, 'timmy'); process.stdout.write(JSON.stringify({{attention,result,calls,remaining:outbox.list()}})); }}); """ output = run_node(script) assert output["attention"]["status"] == "attention" assert output["attention"]["error"] == "Title is invalid" assert [call["key"] for call in output["calls"]] == ["edit-1", "edit-2"] assert output["calls"][1]["body"]["title"] == "Fixed title" assert output["result"]["confirmed"][0]["number"] == 42 assert output["remaining"] == [] def test_issue_outbox_rekeys_only_after_user_retries_uncertain_delivery(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); let sequence=0; const calls=[]; const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; const outbox = createIssueOutbox({{ storage, getOwnerLogin:()=>'timmy', createOperationId:()=> 'operation-' + (++sequence), fetchJson:async (_url, options) => {{ const key=options.headers['Idempotency-Key']; calls.push(key); if (key === 'operation-1') {{ const e=new Error('Verify it was not posted before retrying.'); e.status=422; e.code='delivery_uncertain'; throw e; }} return {{number:319}}; }}, }}); const item=outbox.enqueue({{repository:'o/r',title:'Possibly sent',body:'Check first'}}); (async()=>{{ await outbox.flush('timmy'); const attention=outbox.list()[0]; await outbox.retry(item.id,'timmy'); process.stdout.write(JSON.stringify({{attention,calls,remaining:outbox.list()}})); }})(); """ output = run_node(script) assert output["attention"]["status"] == "attention" assert output["attention"]["deliveryState"] == "uncertain" assert output["calls"] == ["operation-1", "operation-2"] assert output["remaining"] == [] def test_issue_outbox_rotates_operation_id_only_when_delivery_payload_changes(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; let sequence = 0; const outbox = createIssueOutbox({{ storage, getOwnerLogin:()=>'timmy', createOperationId:() => 'payload-' + (++sequence), }}); const queued = outbox.enqueue({{ repository:'stackchain/api',title:'Plan',body:'Context',labelIds:[1],milestoneId:2,dueDate:'2026-08-09' }}); const unchanged = outbox.update(queued.id, {{...queued}}); const fields = [ ['repository', 'stackchain/web'], ['title', 'Revised'], ['body', 'More context'], ['labelIds', [1, 3]], ['milestoneId', 4], ['dueDate', '2026-08-10'], ]; const edits = []; for (const [field, value] of fields) {{ const current = outbox.list()[0]; edits.push(outbox.update(queued.id, {{...current, [field]:value}})); }} process.stdout.write(JSON.stringify({{queued,unchanged,edits,final:outbox.list()[0]}})); """ output = run_node(script) assert output["unchanged"]["operationId"] == output["queued"]["operationId"] == "payload-1" assert [item["operationId"] for item in output["edits"]] == [ "payload-2", "payload-3", "payload-4", "payload-5", "payload-6", "payload-7" ] assert output["final"]["milestoneId"] == 4 assert output["final"]["dueDate"] == "2026-08-10" def test_issue_outbox_is_single_flight_when_reconnect_and_send_now_overlap(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}; let calls = 0; let release; const gate = new Promise(resolve => {{ release = resolve; }}); const outbox = createIssueOutbox({{ storage, getOwnerLogin:()=>'timmy', createOperationId:() => 'single-flight', fetchJson:async () => {{ calls += 1; await gate; return {{repository:'stackchain/api',number:43,title:'Once'}}; }}, }}); const queued = outbox.enqueue({{repository:'stackchain/api',title:'Once',body:'Context'}}); const reconnect = outbox.flush('timmy'); const sendNow = outbox.retry(queued.id, 'timmy'); release(); Promise.all([reconnect,sendNow]).then(results => process.stdout.write(JSON.stringify({{calls,results,remaining:outbox.list()}}))); """ output = run_node(script) assert output["calls"] == 1 assert output["remaining"] == [] assert sum(len(result["confirmed"]) for result in output["results"]) >= 1 def test_issue_outbox_mirrors_queue_and_registers_background_sync(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; const state = {{snapshots:[],syncs:0}}; const backgroundSync = {{ reconcile: async items => state.snapshots.push(items.map(item => ({{...item}}))), requestSync: async () => {{ state.syncs += 1; }}, }}; const outbox = createIssueOutbox({{ storage, backgroundSync, getOwnerLogin:()=> 'timmy', createOperationId:()=> 'background-1', }}); outbox.enqueue({{repository:'stackchain/api',title:'Close the app',body:'Still deliver'}}); setTimeout(() => process.stdout.write(JSON.stringify(state)), 0); """ output = run_node(script) assert output["snapshots"][0][0]["operationId"] == "background-1" assert output["snapshots"][0][0]["ownerLogin"] == "timmy" assert output["syncs"] == 1 def test_issue_outbox_confirms_durable_admission_only_after_mirror_and_sync(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; let releaseMirror; let releaseSync; const mirrorGate = new Promise(resolve => releaseMirror = resolve); const syncGate = new Promise(resolve => releaseSync = resolve); const events = []; const outbox = createIssueOutbox({{ storage, getOwnerLogin:()=> 'timmy', createOperationId:()=> 'durable-1', backgroundSync: {{ reconcile: async items => {{ events.push('mirror-start'); await mirrorGate; events.push('mirror-committed'); }}, requestSync: async () => {{ events.push('sync-start'); await syncGate; events.push('sync-registered'); }}, }}, }}); const admission = outbox.enqueueDurably({{repository:'stackchain/api',title:'Keep this',body:'Draft'}}) .then(result => {{ events.push('confirmed'); return result; }}); Promise.resolve().then(async () => {{ const beforeMirror = events.slice(); releaseMirror(); await new Promise(resolve => setTimeout(resolve, 0)); const beforeSync = events.slice(); releaseSync(); const result = await admission; process.stdout.write(JSON.stringify({{beforeMirror,beforeSync,events,result,items:outbox.list()}})); }}); """ output = run_node(script) assert output["beforeMirror"] == ["mirror-start"] assert output["beforeSync"] == ["mirror-start", "mirror-committed", "sync-start"] assert output["events"][-1] == "confirmed" assert output["result"]["background"] is True assert output["result"]["item"]["id"] == "durable-1" assert output["items"][0]["title"] == "Keep this" def test_issue_outbox_reports_degraded_admission_without_losing_foreground_item(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; const outbox = createIssueOutbox({{ storage, getOwnerLogin:()=> 'timmy', createOperationId:()=> 'foreground-1', backgroundSync: {{ reconcile: async () => {{}}, requestSync: async () => {{ throw new Error('Background Sync unavailable'); }}, }}, }}); outbox.enqueueDurably({{repository:'stackchain/api',title:'Retain me',body:'Draft'}}).then(result => process.stdout.write(JSON.stringify({{ background:result.background, durability:result.durability, error:result.error.message, item:result.item, persisted:outbox.list() }})) ); """ output = run_node(script) assert output["background"] is False assert output["durability"] == "foreground-only" assert output["error"] == "Background Sync unavailable" assert output["persisted"] == [output["item"]] def test_foreground_delivery_uses_same_atomic_background_claim(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; const state = {{backgroundCalls:0,directCalls:0}}; const backgroundSync = {{ reconcile: async () => {{}}, requestSync: async () => {{}}, send: async (item, login) => {{ state.backgroundCalls += 1; return {{issue:{{number:9}}, claimed:item.id + ':' + login}}; }}, }}; const outbox = createIssueOutbox({{ storage, backgroundSync, getOwnerLogin:()=> 'timmy', createOperationId:()=> 'shared-claim', fetchJson:async () => {{ state.directCalls += 1; return {{number:10}}; }}, }}); outbox.enqueue({{repository:'o/r',title:'One owner'}}); outbox.flush('timmy').then(result => process.stdout.write(JSON.stringify({{state,result,remaining:outbox.list()}}))); """ output = run_node(script) assert output["state"] == {"backgroundCalls": 1, "directCalls": 0} assert output["result"]["confirmed"][0]["number"] == 9 assert output["remaining"] == [] def test_foreground_background_delivery_surfaces_permanent_failure_immediately(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values=new Map();const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; const error=new Error('Title is invalid');error.status=422; const backgroundSync={{reconcile:async()=>{{}},requestSync:async()=>{{}},send:async()=>({{attention:true,error}})}}; const outbox=createIssueOutbox({{storage,backgroundSync,getOwnerLogin:()=>'timmy',createOperationId:()=>'invalid'}}); outbox.enqueue({{repository:'o/r',title:'Bad'}}); outbox.flush('timmy').then(result=>process.stdout.write(JSON.stringify({{result,items:outbox.list()}}))); """ output = run_node(script) assert output["items"][0]["status"] == "attention" assert output["items"][0]["error"] == "Title is invalid" assert output["result"]["confirmed"] == [] def test_page_reconciles_worker_success_and_attention_into_visible_outbox(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values=new Map(); const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; values.set('stackchain.issue-outbox.v1',JSON.stringify({{version:2,items:[ {{id:'sent',operationId:'sent',ownerLogin:'timmy',status:'queued',title:'Done'}}, {{id:'bad',operationId:'bad',ownerLogin:'timmy',status:'queued',title:'Fix me'}}, ]}})); const outbox=createIssueOutbox({{storage}}); outbox.reconcileBackground([ {{id:'sent',status:'sent'}}, {{id:'bad',status:'attention',error:'Title is invalid'}}, ]); process.stdout.write(JSON.stringify(outbox.list())); """ output = run_node(script) assert output == [ { "id": "bad", "operationId": "bad", "ownerLogin": "timmy", "status": "attention", "title": "Fix me", "error": "Title is invalid", } ] def test_page_preserves_background_create_and_start_until_the_matching_account_continues_it(): script = f""" const createIssueOutbox = require({json.dumps(str(OUTBOX))}); const values=new Map(); const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; values.set('stackchain.issue-outbox.v1',JSON.stringify({{version:3,items:[{{ id:'resume',operationId:'resume',ownerLogin:'timmy',status:'queued', repository:'stackchain/dashboard',title:'Resume',body:'',completionIntent:'create-and-start' }}]}})); const outbox=createIssueOutbox({{storage}}); outbox.reconcileBackground([{{ id:'resume',status:'sent',ownerLogin:'timmy',completionIntent:'create-and-start', deliveredIssue:{{repository:'stackchain/dashboard',number:389,title:'Resume'}} }}]); const blocked=outbox.pendingCompletions('alexander'); const pending=outbox.pendingCompletions('timmy'); const retained=outbox.list(); outbox.completeIntent('resume','timmy'); process.stdout.write(JSON.stringify({{blocked,pending,retained,remaining:outbox.list()}})); """ output = run_node(script) assert output["blocked"] == [] assert output["pending"] == [{ "id": "resume", "intent": "create-and-start", "ownerLogin": "timmy", "operationId": "resume", "issue": {"repository": "stackchain/dashboard", "number": 389, "title": "Resume"}, }] assert output["retained"][0]["status"] == "completion" assert output["remaining"] == [] @pytest.mark.anyio async def test_mobile_dashboard_queues_offline_captures_and_exposes_outbox_actions(): html = await dashboard() assert '' in html assert "const issueOutbox = createIssueOutbox({" in html assert "await issueOutbox.enqueueDurably(durableDraft)" in html assert "Saving for background delivery…" in html assert "Saved for next launch; background delivery unavailable." in html assert "throw new Error('Background Sync unavailable')" in html assert "issueOutbox.retry(queued.id, activeFlushLogin)" in html assert "navigator.onLine" in html assert "activeFlushLogin = contextIdentityFresh ?" in html assert "flushIssueOutbox();" in html assert 'class="draft-send"' in html assert 'class="draft-edit"' in html assert 'Queued for sync' in html assert 'Needs attention' in html assert "issueOutbox.discard(item.outbox_id)" in html assert '.draft-actions { display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr));' in html