feat: review ready mobile drafts in one tap (Closes #861)
CI / lint (pull_request) Successful in 1m46s
CI / build-release (pull_request) Successful in 6s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-15 01:12:10 +00:00
1 parent ff41eb1d57
commit c1c4ff8e31
8 files changed
+170 -33

No files matched your search

+9 -4
View File
@@ -270,13 +270,17 @@ const values = new Map([
]);
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()));
const partition=inbox.partition();
process.stdout.write(JSON.stringify({{...partition,labels:partition.deliveries.map(inbox.deliveryLabel)}}));
"""
output = run_node(script)
assert len(output["drafts"]) == 1
assert len(output["deliveries"]) == 5
assert output["counts"] == {"waiting": 2, "sending": 1, "attention": 2, "authorization": 0}
assert output["labels"] == [
"Needs attention", "Identity protected", "Needs attention", "Sending", "Queued for sync"
]
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
@@ -293,7 +297,7 @@ async def test_mobile_dashboard_exposes_touch_safe_draft_recovery_lane():
assert 'class="draft-resume"' in html
assert 'class="draft-discard"' in html
assert 'Discard this unfinished draft?' in html
assert '.draft-actions button { min-height:44px;' in html
assert '.draft-actions button, .draft-filing-session-actions button, .agenda-replan-actions button { min-height:44px;' in html
assert 'createDraftInbox({ storage: localStorage' in html
assert "captureDraft.repository && !issueCaptureRepositories.includes(captureDraft.repository)" in html
assert "Verified not posted — retry" in html
@@ -316,7 +320,8 @@ async def test_mobile_dashboard_renders_delivery_center_separately_and_retries_w
assert "const deliveryCenter = draftInbox.partition(displayedDrafts);" 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 "draftInbox.deliveryLabel(item)" in html
assert "if (item.status === 'sending') return 'Sending'" in DRAFTS.read_text()
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
@@ -330,7 +335,7 @@ async def test_mobile_dashboard_requires_an_explicit_authorize_and_close_gesture
assert '>Authorize &amp; close</button>' in html
assert "list.querySelectorAll('.draft-authorize')" in html
assert "authoredOutbox.retry(item.outbox_id, activeFlushLogin)" in html
assert "item.status === 'authorization' ? 'Awaiting authorization'" in html
assert "if (item.status === 'authorization') return 'Awaiting authorization'" in DRAFTS.read_text()
@pytest.mark.anyio
+1 -1
View File
@@ -4246,7 +4246,7 @@ async def test_every_mobile_repository_selection_uses_the_resilient_metadata_loa
assert "renderLabels(repository, metadata.labels" in source
assert "renderMilestones(repository, metadata.milestones" in source
assert "templatePicker.render(repository, metadata.templates" in source
assert html.count("loadIssueFilingMetadata(") == 4
assert html.count("loadIssueFilingMetadata(") == 5
assert "loadIssueLabels(repository);\n loadIssueMilestones(repository);" not in html
assert "loadIssueLabels(event.target.value).then" not in html
assert "loadIssueLabels(qs('#create-issue-repository').value" not in html
+94 -6
View File
@@ -60,6 +60,78 @@ session.start('planned');
assert run_node(script) == [[{"filename": "one.png"}, {"filename": "two.png"}]]
def test_ready_draft_summary_exposes_truthful_mobile_action_and_plan_details():
script = f"""
const createUnfiledCaptures=require({json.dumps(str(UNFILED))});
const planned=createUnfiledCaptures.summary({{
filingPlan:{{repository:'stackchain/dashboard',completionIntent:'create-and-start',estimateMinutes:45}},
attachmentCount:2,
}});
const minimal=createUnfiledCaptures.summary({{}});
process.stdout.write(JSON.stringify({{planned,minimal}}));
"""
assert run_node(script) == {
"planned": {
"label": "Ready to file",
"repository": "stackchain/dashboard",
"ready": True,
"action": "Review & start",
"details": "Create & start · 45 min · 2 screenshots",
},
"minimal": {
"label": "Needs filing",
"repository": "",
"ready": False,
"action": "Choose repository",
"details": "Create issue · No screenshots",
},
}
def test_ready_draft_opens_final_review_with_restored_intent_and_evidence():
script = f"""
const createDraftFilingSession=require({json.dumps(str(DRAFT_SESSION))});
const events=[];
const trigger={{id:'ready-action'}};
const resumed={{repository:'o/r',title:'Planned',body:'Evidence',labelIds:[7],
completionIntent:'create-and-start',estimateMinutes:30,
attachments:[{{filename:'one.png'}},{{filename:'two.png'}}]}};
const elements=new Map();
const qs=selector=>{{
if(!elements.has(selector)) elements.set(selector,{{id:selector.slice(1),hidden:false,disabled:false,textContent:'',addEventListener:()=>{{}},
requestSubmit:button=>events.push(['review',resumed,button.id])}});
return elements.get(selector);
}};
const session=createDraftFilingSession({{list:()=>[{{id:'planned'}}]}});
session.attach(qs,{{
captures:{{resume:async()=>resumed}},
capture:()=>({{saveDraft:value=>events.push(['save',value])}}),
attachment:{{restore:value=>events.push(['evidence',value]),clear:()=>{{}}}},
getLogin:()=>'timmy',setResumedId:id=>events.push(['source',id]),
openSheet:async()=>events.push(['sheet']),setFilingMode:()=>events.push(['filing']),
review:async()=>{{events.push(['metadata']);events.push(['duplicates']);}},
}});
session.start('planned');
(async()=>{{await session.reviewCapture('planned',trigger);process.stdout.write(JSON.stringify({{
events,origin:session.takeTrigger().id
}}));}})();
"""
output = run_node(script)
events = output["events"]
assert [event[0] for event in events] == [
"save", "source", "sheet", "evidence", "filing", "metadata",
"duplicates", "review"
]
assert events[-1][1]["labelIds"] == [7]
assert events[-1][1]["attachments"] == [
{"filename": "one.png"}, {"filename": "two.png"}
]
assert events[-1][2] == "create-and-start-issue"
assert output["origin"] == "ready-action"
def test_unfiled_captures_block_at_capacity_until_oldest_is_explicitly_replaced():
script = f"""
const createUnfiledCaptures = require({json.dumps(str(UNFILED))});
@@ -599,7 +671,7 @@ async def test_mobile_composer_exposes_cold_offline_save_and_account_safe_resume
assert "await unfiledCaptures.completeResume(rUC)" in html
assert "...(rUC ? { sourceCaptureId: rUC } : {})" in html
assert "item.hasAttachment ? ' · Screenshot attached' : ''" in html
assert "dependencies.issueCapture.saveDraft(resumed)" in DRAFT_SESSION.read_text()
assert "issueCapture.saveDraft(resumed)" in DRAFT_SESSION.read_text()
assert "item.kind === 'unfiled-issue'" in html
assert '.create-issue-actions button { min-height:44px;' in html
assert '.draft-card:focus-visible' in html
@@ -618,11 +690,28 @@ async def test_mobile_planned_draft_can_be_saved_and_resumed_without_replanning(
assert "qs('#create-issue-estimate').value = captureDraft.estimateMinutes || '';" in html
assert "qs('#file-new-issue').hidden = enabled" in feature
assert "qs('.create-issue-capture-actions').hidden = false" in feature
assert "label:item.filingPlan?.repository ? 'Ready to file' : 'Needs filing'" in feature
assert "repository:item.filingPlan?.repository || ''" in feature
assert "label:ready ? 'Ready to file' : 'Needs filing'" in feature
assert "repository:plan.repository || ''" in feature
assert "Saved planned Draft" in feature
@pytest.mark.anyio
async def test_mobile_ready_draft_card_opens_validated_final_review_in_one_tap():
html = await dashboard()
assert "isUnfiled ? item.action" in html
assert "[item.label, item.repository, item.details].filter(Boolean).join(' · ')" in html
assert "escapeHtml([item.label" in html
assert "await dFS.open(item.capture_id, item.ready ? button : null)" in html
assert "capture:()=>issueCapture" in html
assert "review:r=>loadIssueFilingMetadata(r.repository,r)" in html
assert ".then(()=>issueCapture.findDuplicates(currentIssueCaptureDraft())).then(renderIssueDuplicates)" in html
assert "qs('#create-issue-form').requestSubmit(submitter)" in DRAFT_SESSION.read_text()
assert "dFS.takeTrigger() || event.submitter || qs('#submit-new-issue')" in html
assert '.draft-actions button, .draft-filing-session-actions button, .agenda-replan-actions button { min-height:44px; width:100%; }' in html
assert '@media(max-width:320px)' in html
@pytest.mark.anyio
async def test_mobile_drafts_expose_a_safe_sequential_filing_session():
html = await dashboard()
@@ -632,13 +721,12 @@ async def test_mobile_drafts_expose_a_safe_sequential_filing_session():
assert 'id="draft-filing-progress"' in html
assert 'id="previous-draft"' in html
assert 'id="skip-draft"' in html
assert "dFS.start(item.capture_id)" in html
assert "await dFS.nextCapture()" in html
assert "await dFS.open(item.capture_id, item.ready ? button : null)" in html
assert "await dFS.advance(fS.id)" in html
feature = DRAFT_SESSION.read_text()
assert "Draft ${state.position} of ${state.total}" in feature
assert "qs('#submit-new-issue').textContent = state.remaining ? 'File & next' : 'File final Draft'" in feature
assert '.draft-filing-session-actions button { min-height:44px;' in html
assert '.draft-actions button, .draft-filing-session-actions button, .agenda-replan-actions button { min-height:44px;' in html
assert 'padding-bottom:calc(10px + env(safe-area-inset-bottom))' in html