feat: add offline delivery center (Closes #481)
This commit is contained in:
1 parent
b667963968
commit
9dede4bf2a
8 files changed
+177
-7
No files matched your search
@@ -77,6 +77,30 @@ outbox.flush('timmy').then(result => process.stdout.write(JSON.stringify({{persi
|
||||
assert output["remaining"] == []
|
||||
|
||||
|
||||
def test_authored_outbox_exposes_sending_then_preserves_transient_attempt_details():
|
||||
script = f"""
|
||||
const createAuthoredOutbox = 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 release; const gate = new Promise(resolve => release = resolve);
|
||||
const outbox = createAuthoredOutbox({{
|
||||
storage, getOwnerLogin:()=>'timmy', now:()=>777,
|
||||
fetchJson:async()=>{{await gate; const error=new Error('Network unavailable');error.status=503;throw error;}},
|
||||
}});
|
||||
outbox.enqueue({{kind:'issue-comment',repository:'o/r',number:1,body:'Wait',operationId:'attempt'}});
|
||||
const pending=outbox.flush('timmy');
|
||||
const sending=outbox.list()[0];
|
||||
release();
|
||||
pending.then(()=>process.stdout.write(JSON.stringify({{sending,after:outbox.list()[0]}})));
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["sending"]["status"] == "sending"
|
||||
assert output["after"]["status"] == "queued"
|
||||
assert output["after"]["lastAttemptAt"] == 777
|
||||
assert output["after"]["lastAttemptError"] == "Network unavailable"
|
||||
|
||||
|
||||
def test_authored_outbox_persists_and_delivers_complete_pull_review_payload():
|
||||
script = f"""
|
||||
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
|
||||
|
||||
@@ -225,6 +225,36 @@ process.stdout.write(JSON.stringify(drafts));
|
||||
assert by_id["mine"]["quarantined"] is False
|
||||
|
||||
|
||||
def test_draft_inbox_partitions_delivery_center_and_counts_only_safe_waiting_retries():
|
||||
script = f"""
|
||||
const createDraftInbox = require({json.dumps(str(DRAFTS))});
|
||||
const values = new Map([
|
||||
['stackchain.issue-comment.v1:stackchain/api#17', 'Unfinished comment'],
|
||||
['stackchain.issue-outbox.v1', JSON.stringify({{version:3,items:[
|
||||
{{id:'waiting',repository:'o/r',title:'Waiting issue',body:'Body',ownerLogin:'timmy',status:'queued',queuedAt:100,lastAttemptAt:90,lastAttemptError:'Network unavailable'}},
|
||||
{{id:'sending',repository:'o/r',title:'Sending issue',body:'Body',ownerLogin:'timmy',status:'sending',queuedAt:200}},
|
||||
{{id:'attention',repository:'o/r',title:'Broken issue',body:'Body',ownerLogin:'timmy',status:'attention',queuedAt:300}},
|
||||
{{id:'other-user',repository:'o/r',title:'Private issue',body:'Body',ownerLogin:'alexander',status:'queued',queuedAt:400}}
|
||||
]}})],
|
||||
['stackchain.authored-outbox.v1', JSON.stringify({{version:2,items:[
|
||||
{{id:'uncertain',kind:'issue-comment',repository:'o/r',number:4,body:'Maybe sent',ownerLogin:'timmy',status:'attention',deliveryState:'uncertain',queuedAt:500}}
|
||||
]}})],
|
||||
]);
|
||||
const storage = {{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
|
||||
const inbox = createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}});
|
||||
process.stdout.write(JSON.stringify(inbox.partition()));
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert len(output["drafts"]) == 1
|
||||
assert len(output["deliveries"]) == 5
|
||||
assert output["counts"] == {"waiting": 2, "sending": 1, "attention": 2}
|
||||
assert [item["outbox_id"] for item in output["retryable"]] == ["waiting"]
|
||||
waiting = next(item for item in output["deliveries"] if item["outbox_id"] == "waiting")
|
||||
assert waiting["last_attempt_at"] == 90
|
||||
assert waiting["last_attempt_error"] == "Network unavailable"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_dashboard_exposes_touch_safe_draft_recovery_lane():
|
||||
html = await dashboard()
|
||||
@@ -245,6 +275,24 @@ async def test_mobile_dashboard_exposes_touch_safe_draft_recovery_lane():
|
||||
assert "error.code = payload.detail?.code" in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_dashboard_renders_delivery_center_separately_and_retries_waiting_work():
|
||||
html = await dashboard()
|
||||
|
||||
assert 'class="delivery-center"' in html
|
||||
assert 'id="retry-waiting-deliveries"' in html
|
||||
assert 'Waiting <strong data-delivery-count="waiting">' in html
|
||||
assert 'Sending <strong data-delivery-count="sending">' in html
|
||||
assert 'Needs attention <strong data-delivery-count="attention">' in html
|
||||
assert "const deliveryCenter = draftInbox.partition(lastDrafts);" in html
|
||||
assert "await Promise.all([issueOutbox.flush(activeFlushLogin), authoredOutbox.flush(activeFlushLogin)])" in html
|
||||
assert "deliveryCenter.retryable.length" in html
|
||||
assert "item.status === 'sending' ? 'Sending'" in html
|
||||
assert "item.last_attempt_error ? '<span class=\"delivery-attempt small\">Last attempt '" in html
|
||||
assert '.delivery-center { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;' in html
|
||||
assert '.delivery-center button { min-height:44px;' in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dashboard_only_flushes_account_bound_outboxes_after_a_fresh_identity_snapshot():
|
||||
html = await dashboard()
|
||||
|
||||
@@ -202,6 +202,30 @@ outbox.flush('timmy').then(result => process.stdout.write(JSON.stringify({{resul
|
||||
assert output["remaining"][0]["status"] == "queued"
|
||||
|
||||
|
||||
def test_issue_outbox_exposes_sending_then_preserves_transient_attempt_details():
|
||||
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 release; const gate = new Promise(resolve => release = resolve);
|
||||
const outbox = createIssueOutbox({{
|
||||
storage, getOwnerLogin:()=>'timmy', createOperationId:()=>'attempt', now:()=>888,
|
||||
fetchJson:async()=>{{await gate;const error=new Error('Gateway timeout');error.status=503;throw error;}},
|
||||
}});
|
||||
outbox.enqueue({{repository:'o/r',title:'Wait',body:'Context'}});
|
||||
const pending=outbox.flush('timmy');
|
||||
const sending=outbox.list()[0];
|
||||
release();
|
||||
pending.then(()=>process.stdout.write(JSON.stringify({{sending,after:outbox.list()[0]}})));
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["sending"]["status"] == "sending"
|
||||
assert output["after"]["status"] == "queued"
|
||||
assert output["after"]["lastAttemptAt"] == 888
|
||||
assert output["after"]["lastAttemptError"] == "Gateway timeout"
|
||||
|
||||
|
||||
def test_foreground_attachment_retry_does_not_create_a_second_issue():
|
||||
script = f"""
|
||||
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
||||
|
||||
Reference in new issue
Block a user