import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard ROOT = Path(__file__).resolve().parents[1] RECOVERY = ROOT / "frontend" / "mobile-delivery-recovery.js" def run_node(script: str) -> dict: result = subprocess.run( ["node", "-e", script], cwd=ROOT, text=True, capture_output=True, check=False, ) assert result.returncode == 0, result.stderr return json.loads(result.stdout) def test_recovery_orders_deliveries_by_safest_required_intervention(): script = f""" const createRecovery = require({json.dumps(str(RECOVERY))}); const recovery = createRecovery({{ getItems: () => [ {{outbox_id:'waiting', delivery_state:'waiting', title:'Waiting'}}, {{outbox_id:'uncertain', delivery_state:'uncertain', title:'Uncertain'}}, {{outbox_id:'attention', status:'attention', title:'Attention'}}, {{outbox_id:'authorization', status:'authorization', title:'Authorization'}}, ], }}); process.stdout.write(JSON.stringify(recovery.snapshot())); """ assert run_node(script) == { "count": 4, "current": { "id": "authorization", "state": "authorization", "primary": "Authorize & send", "position": 1, "total": 4, }, } def test_recovery_advances_only_after_the_current_delivery_is_resolved(): script = f""" const createRecovery = require({json.dumps(str(RECOVERY))}); (async () => {{ let items = [ {{outbox_id:'authorize', status:'authorization', title:'Approve review'}}, {{outbox_id:'retry', delivery_state:'waiting', title:'Post comment'}}, ]; let resolves = false; const recovery = createRecovery({{ getItems: () => items, activate: async item => {{ if (resolves) items = items.filter(candidate => candidate.outbox_id !== item.outbox_id); return resolves; }}, }}); recovery.open(); const retained = await recovery.activate(); resolves = true; const advanced = await recovery.activate(); process.stdout.write(JSON.stringify({{retained, advanced}})); }})(); """ assert run_node(script) == { "retained": { "count": 2, "current": { "id": "authorize", "state": "authorization", "primary": "Authorize & send", "position": 1, "total": 2, }, }, "advanced": { "count": 1, "current": { "id": "retry", "state": "waiting", "primary": "Retry now", "position": 1, "total": 1, }, }, } def test_recovery_keeps_one_attempt_locked_until_delivery_settles(): script = f""" const createRecovery = require({json.dumps(str(RECOVERY))}); (async () => {{ let finish; let attempts = 0; const settled = new Promise(resolve => {{finish = resolve}}); const element = () => ({{textContent:'', disabled:false}}); const elements = {{ dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}}, title:element(), destination:element(), reason:element(), progress:element(), action:element(), status:element(), }}; const recovery = createRecovery({{ getItems: () => [{{outbox_id:'retry', delivery_state:'waiting', title:'Post comment'}}], activate: async () => {{attempts += 1; await settled; return true}}, elements, }}); recovery.open(); const first = recovery.activate(); const second = recovery.activate(); await Promise.resolve(); const pending = {{attempts, disabled:elements.action.disabled, status:elements.status.textContent}}; finish(); await Promise.all([first, second]); process.stdout.write(JSON.stringify({{ pending, attempts, disabled:elements.action.disabled, }})); }})(); """ assert run_node(script) == { "pending": {"attempts": 1, "disabled": True, "status": "Working…"}, "attempts": 1, "disabled": False, } def test_attention_handoff_closes_modal_focuses_destination_and_awaits_work(): script = f""" const createRecovery = require({json.dumps(str(RECOVERY))}); (async () => {{ let finish; let handoffs = 0; const settled = new Promise(resolve => {{finish = resolve}}); const destination = {{ focused:false, scrolled:false, focus(){{this.focused=true}}, scrollIntoView(){{this.scrolled=true}}, async onclick(){{handoffs += 1; await settled; return true}}, }}; global.document = {{ getElementById: () => null, querySelector: selector => selector.includes('draft-resume') ? destination : null, }}; const element = () => ({{textContent:'', disabled:false}}); const elements = {{ dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}}, title:element(), destination:element(), reason:element(), progress:element(), action:element(), status:element(), }}; const recovery = createRecovery({{ getItems: () => [{{outbox_id:'review', status:'attention', title:'Review feedback'}}], getIndex: () => 3, elements, }}); recovery.open(); const first = recovery.activate(); const second = recovery.activate(); await Promise.resolve(); const pending = {{ dialogOpen:elements.dialog.open, disabled:elements.action.disabled, focused:destination.focused, scrolled:destination.scrolled, handoffs, }}; finish(); await Promise.all([first, second]); process.stdout.write(JSON.stringify({{pending, handoffs, dialogOpen:elements.dialog.open}})); }})(); """ assert run_node(script) == { "pending": { "dialogOpen": False, "disabled": True, "focused": True, "scrolled": True, "handoffs": 1, }, "handoffs": 1, "dialogOpen": False, } def test_failed_attention_handoff_reopens_the_same_retryable_item(): script = f""" const createRecovery = require({json.dumps(str(RECOVERY))}); (async () => {{ const destination = {{ focus(){{}}, scrollIntoView(){{}}, async onclick(){{throw new Error('Latest checklist unavailable')}}, }}; global.document = {{ getElementById: () => null, querySelector: () => destination, }}; const element = () => ({{textContent:'', disabled:false}}); const elements = {{ dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}}, title:element(), destination:element(), reason:element(), progress:element(), action:element(), status:element(), }}; const recovery = createRecovery({{ getItems: () => [{{outbox_id:'conflict', checklist_conflict:true, title:'Checklist conflict'}}], getIndex: () => 0, elements, }}); recovery.open(); const result = await recovery.activate(); process.stdout.write(JSON.stringify({{ result, dialogOpen:elements.dialog.open, status:elements.status.textContent, action:elements.action.textContent, }})); }})(); """ assert run_node(script) == { "result": { "count": 1, "current": { "id": "conflict", "state": "attention", "primary": "Review changes", "position": 1, "total": 1, }, }, "dialogOpen": True, "status": "Latest checklist unavailable Try again.", "action": "Review changes", } def test_recovery_retains_failed_delivery_with_actionable_status(): script = f""" const createRecovery = require({json.dumps(str(RECOVERY))}); (async () => {{ const element = () => ({{textContent:'', disabled:false}}); const elements = {{ dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}}, title:element(), destination:element(), reason:element(), progress:element(), action:element(), status:element(), }}; const recovery = createRecovery({{ getItems: () => [{{outbox_id:'retry', delivery_state:'waiting', title:'Post comment'}}], activate: async () => {{throw new Error('Network unavailable')}}, elements, }}); recovery.open(); const result = await recovery.activate(); process.stdout.write(JSON.stringify({{ result, disabled:elements.action.disabled, status:elements.status.textContent, }})); }})(); """ assert run_node(script) == { "result": { "count": 1, "current": { "id": "retry", "state": "waiting", "primary": "Retry now", "position": 1, "total": 1, }, }, "disabled": False, "status": "Network unavailable Try again.", } def test_recovery_renders_delivery_context_and_hands_off_when_cleared(): script = f""" const createRecovery = require({json.dumps(str(RECOVERY))}); (async () => {{ let items = [{{ outbox_id:'close-7', status:'authorization', outbox_kind:'issue-close', title:'Close resolved incident', repository:'stackchain/ops', details:'#7', last_attempt_error:'Fresh authorization expired', }}]; let completed = 0; const element = () => ({{textContent:'', hidden:false}}); const elements = {{ dialog: {{open:false, showModal(){{this.open=true}}, close(){{this.open=false}}}}, title: element(), destination: element(), reason: element(), progress: element(), action: element(), status: element(), }}; const recovery = createRecovery({{ getItems: () => items, activate: async () => {{items = []; return true}}, onComplete: () => {{completed += 1}}, elements, }}); const opened = recovery.open(); const rendered = {{ opened, dialogOpen:elements.dialog.open, title:elements.title.textContent, destination:elements.destination.textContent, reason:elements.reason.textContent, progress:elements.progress.textContent, action:elements.action.textContent, }}; const cleared = await recovery.activate(); process.stdout.write(JSON.stringify({{rendered, cleared, completed, dialogOpen:elements.dialog.open}})); }})(); """ assert run_node(script) == { "rendered": { "opened": "opened", "dialogOpen": True, "title": "Close resolved incident", "destination": "stackchain/ops · #7", "reason": "Fresh authorization expired", "progress": "Delivery 1 of 1 · Authorization required", "action": "Authorize & close", }, "cleared": {"count": 0, "current": None}, "completed": 1, "dialogOpen": False, } @pytest.mark.anyio async def test_dashboard_packages_phone_safe_guided_delivery_recovery(): html = await dashboard() service_worker = (ROOT / "frontend" / "service-worker.js").read_text() bundle = (ROOT / "src" / "frontend_bundle.py").read_text() assert '' in html assert "const mobileDeliveryRecovery = createMobileDeliveryRecovery({" in html assert "activate: recoverMobileDelivery," in html assert "button.onclick=async()=>" in html assert "await (authored ? authoredOutbox : issueOutbox).retry(item.outbox_id, activeFlushLogin)" in html assert "(authored ? applyAuthoredOutboxResult : applyOutboxResult)(result)" in html assert "openDelivery: () => mobileDeliveryRecovery.open()" in html assert "mobileStartDay.completePhase('delivery')" in html assert "BASE + 'static/mobile-delivery-recovery.js'" in service_worker assert '"static/mobile-delivery-recovery.js"' in bundle assert ".mobile-delivery-recovery-panel" in html assert "min-height:48px" in html assert "env(safe-area-inset-bottom)" in html