import json import subprocess from pathlib import Path import pytest from src.views import dashboard DRAFTS = Path(__file__).parents[1] / "frontend" / "drafts.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_draft_inbox_discovers_existing_formats_orders_activity_and_ignores_corruption(): script = f""" const createDraftInbox = require({json.dumps(str(DRAFTS))}); const values = new Map([ ['stackchain.issue-capture.v1', JSON.stringify({{repository:'stackchain/api',title:'Ship recovery',body:'Detailed plan',labelIds:[]}})], ['stackchain.issue-comment.v1:stackchain/api#17', 'Please add a regression test'], ['stackchain.pull-comment.v1:stackchain/web#9', 'Looks good on mobile'], ['stackchain.update-reply.v1.44', 'I can take this'], ['stackchain.review-draft.v1:stackchain/web#12@abc123', JSON.stringify({{notes:{{'app.js':'Handle offline state'}},comments:[],summary:'Needs one fix',decision:'request_changes'}})], ['stackchain.issue-content.v1:stackchain/api#18', '{{broken-json'], ['stackchain.issue-comment.v1:stackchain/api#20:operation', 'not-a-draft'], ]); const storage = {{ get length() {{ return values.size; }}, key: index => Array.from(values.keys())[index] || null, getItem: key => values.has(key) ? values.get(key) : null, setItem: (key, value) => values.set(key, value), removeItem: key => values.delete(key), }}; let clock = 1000; const inbox = createDraftInbox({{storage, now:() => ++clock}}); const first = inbox.list(); values.set('stackchain.issue-comment.v1:stackchain/api#17', 'Newest comment text'); const second = inbox.list(); process.stdout.write(JSON.stringify({{ kinds:first.map(item => item.kind).sort(), newest:second[0], count:second.length, }})); """ output = run_node(script) assert output["kinds"] == ["issue-comment", "new-issue", "pull-comment", "review", "update-reply"] assert output["count"] == 5 assert output["newest"]["kind"] == "issue-comment" assert output["newest"]["repository"] == "stackchain/api" assert output["newest"]["number"] == 17 assert output["newest"]["preview"] == "Newest comment text" assert output["newest"]["route"] == {"kind": "issue", "repository": "stackchain/api", "number": 17} def test_draft_discard_removes_only_selected_content_and_operation_identity(): script = f""" const createDraftInbox = require({json.dumps(str(DRAFTS))}); const values = new Map([ ['stackchain.issue-comment.v1:stackchain/api#17', 'One'], ['stackchain.issue-comment.v1:stackchain/api#17:operation', 'operation-one'], ['stackchain.pull-comment.v1:stackchain/web#9', 'Two'], ]); const storage = {{ get length() {{ return values.size; }}, key:i => Array.from(values.keys())[i] || null, getItem:key => values.has(key) ? values.get(key) : null, setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key), }}; const inbox = createDraftInbox({{storage, now:() => 1000}}); const selected = inbox.list().find(item => item.kind === 'issue-comment'); inbox.discard(selected.id); process.stdout.write(JSON.stringify({{remaining:inbox.list(), keys:Array.from(values.keys()).sort()}})); """ output = run_node(script) assert [item["kind"] for item in output["remaining"]] == ["pull-comment"] assert "stackchain.issue-comment.v1:stackchain/api#17" not in output["keys"] assert "stackchain.issue-comment.v1:stackchain/api#17:operation" not in output["keys"] assert "stackchain.pull-comment.v1:stackchain/web#9" in output["keys"] def test_draft_inbox_recovers_issue_edit_and_review_content_without_exposing_empty_records(): script = f""" const createDraftInbox = require({json.dumps(str(DRAFTS))}); const values = new Map([ ['stackchain.issue-content.v1:stackchain/api#18', JSON.stringify({{title:'Clarify acceptance',body:'Describe mobile flow',expectedUpdatedAt:'2026-08-07T10:00:00Z'}})], ['stackchain.review-draft.v1:stackchain/web#12@abc123', JSON.stringify({{notes:{{}},comments:[],summary:'',decision:'approve'}})], ['stackchain.pull-comment.v1:stackchain/web#10', ' '], ]); 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),removeItem:k=>values.delete(k)}}; const drafts = createDraftInbox({{storage,now:()=>42}}).list(); process.stdout.write(JSON.stringify(drafts)); """ output = run_node(script) assert len(output) == 1 assert output[0]["kind"] == "issue-edit" assert output[0]["preview"] == "Clarify acceptance — Describe mobile flow" assert output[0]["route"] == {"kind": "issue", "repository": "stackchain/api", "number": 18} def test_draft_inbox_expands_issue_outbox_items_with_actionable_states(): script = f""" const createDraftInbox = require({json.dumps(str(DRAFTS))}); const values = new Map([['stackchain.issue-outbox.v1', JSON.stringify({{version:1,items:[ {{id:'queued-1',repository:'stackchain/api',title:'Offline capture',body:'Context',status:'queued',queuedAt:100}}, {{id:'attention-2',repository:'stackchain/web',title:'Fix labels',body:'Details',status:'attention',error:'Unknown label',queuedAt:200}}, ]}})]]); 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),removeItem:k=>values.delete(k)}}; const drafts = createDraftInbox({{storage,now:()=>300}}).list(); process.stdout.write(JSON.stringify(drafts)); """ output = run_node(script) assert [item["outbox_id"] for item in output] == ["attention-2", "queued-1"] assert [item["status"] for item in output] == ["attention", "queued"] assert output[0]["label"] == "Needs attention" assert output[1]["label"] == "Queued issue" def test_draft_inbox_marks_mismatched_and_legacy_outbox_content_copy_only(): script = f""" const createDraftInbox = require({json.dumps(str(DRAFTS))}); const values = new Map([ ['stackchain.issue-outbox.v1', JSON.stringify({{version:2,items:[ {{id:'other',repository:'o/r',title:'Other issue',body:'Private context',ownerLogin:'alexander',status:'queued'}}, {{id:'legacy',repository:'o/r',title:'Legacy issue',body:'Old context',status:'queued'}} ]}})], ['stackchain.authored-outbox.v1', JSON.stringify({{version:2,items:[ {{id:'mine',kind:'issue-comment',repository:'o/r',number:2,body:'My comment',ownerLogin:'timmy',status:'queued'}} ]}})], ]); 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 drafts = createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}}).list(); process.stdout.write(JSON.stringify(drafts)); """ output = run_node(script) by_id = {item["outbox_id"]: item for item in output} assert by_id["other"]["quarantined"] is True assert by_id["other"]["ownership"] == "Queued by alexander — current account is timmy" assert by_id["other"]["copy_text"] == "Other issue\n\nPrivate context" assert by_id["legacy"]["ownership"] == "Queued by an unknown account — current account is timmy" assert by_id["mine"]["quarantined"] is False @pytest.mark.anyio async def test_mobile_dashboard_exposes_touch_safe_draft_recovery_lane(): html = await dashboard() assert '' in html assert 'data-work-filter="draft"' in html assert 'data-work-count="draft"' in html 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 'createDraftInbox({ storage: localStorage' in html assert "captureDraft.repository && !repositories.includes(captureDraft.repository)" in html @pytest.mark.anyio async def test_dashboard_only_flushes_account_bound_outboxes_after_a_fresh_identity_snapshot(): html = await dashboard() assert "let confirmedOwnerLogin = '';" in html assert "let activeFlushLogin = '';" in html assert "getOwnerLogin: () => confirmedOwnerLogin" in html assert "getCurrentLogin: () => activeFlushLogin" in html assert "const contextIdentityFresh = !snapshot.context.error && !contextFreshness?.stale &&" in html assert "!contextFreshness?.degraded && !contextFreshness?.revalidating;" in html assert "activeFlushLogin = contextIdentityFresh ? String(snapshot.context.user?.login || '').trim() : '';" in html assert "issueOutbox.flush(activeFlushLogin)" in html assert "authoredOutbox.flush(activeFlushLogin)" in html assert "activeFlushLogin = '';" in html assert 'class="draft-copy"' in html assert "navigator.clipboard.writeText(item.copy_text)" in html assert "item.quarantined" in html