stackchain-dashboard/tests/test_mobile_delivery_recovery.py
timmy 3fde7cf3ad
All checks were successful
CI / lint (pull_request) Successful in 3m1s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 2m22s
CI / release-candidate (pull_request) Has been skipped
fix: foreground mobile delivery attention handoffs (Closes #1018)
2026-08-17 10:42:19 +00:00

351 lines
12 KiB
Python

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 '<dialog id="mobile-delivery-recovery"' in html
assert 'aria-labelledby="mobile-delivery-recovery-title"' in html
assert 'id="mobile-delivery-recovery-action"' in html
assert '<script src="static/mobile-delivery-recovery.js"></script>' 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