import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard SESSION = Path(__file__).resolve().parents[1] / "frontend" / "update-triage-session.js" def run_session(script): source = f"const createSession = require({json.dumps(str(SESSION))});\n" + script result = subprocess.run(["node", "-e", source], capture_output=True, text=True) assert result.returncode == 0, result.stderr return json.loads(result.stdout) def test_update_triage_persists_account_bound_pass_and_resumes_by_identity(): result = run_session(""" const values = new Map(); let login = 'timmy'; let items = [1,2,3].map(id => ({notification_id:id,title:'Update '+id})); const opened = [], progress = []; const options = { storage:{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}, getLogin:()=>login, getItems:()=>items, onOpen:item=>opened.push(item.notification_id), onProgress:value=>progress.push(value), onFinish:()=>opened.push('done'), }; let session = createSession(options); session.start(); session.keepUnreadAndNext(); const saved = JSON.parse(values.get('stackchain.update-triage.v1')); session = createSession(options); const resumable = session.resumable(); session.resume(); login = 'other'; const isolated = session.resumable(); process.stdout.write(JSON.stringify({opened, progress, saved, resumable, isolated})); """) assert result["opened"] == [1, 2, 2] assert result["progress"] == [ {"index": 1, "total": 3}, {"index": 2, "total": 3}, {"index": 2, "total": 3}, ] assert result["saved"] == { "version": 2, "login": "timmy", "identities": ["1", "2", "3"], "current": "2", "completed": ["1"], "kept": ["1"], } assert result["resumable"] is True assert result["isolated"] is False def test_update_triage_reconciles_removed_items_and_does_not_admit_new_arrivals(): result = run_session(""" const values = new Map(); let items = [1,2,3].map(notification_id => ({notification_id})); const opened = [], progress = []; const session = createSession({ storage:{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}, getLogin:()=> 'timmy', getItems:()=>items, onOpen:item=>opened.push(item.notification_id), onProgress:value=>progress.push(value), onFinish:()=>opened.push('done'), }); session.start(); items = [1,3,4].map(notification_id => ({notification_id})); session.completeAndNext(); session.completeAndNext(); process.stdout.write(JSON.stringify({opened, progress, stored:values.get('stackchain.update-triage.v1') || null})); """) assert result == { "opened": [1, 3, "done"], "progress": [{"index": 1, "total": 3}, {"index": 3, "total": 3}], "stored": None, } def test_update_triage_exposes_only_the_next_surviving_snapshot_item(): result = run_session(""" const values = new Map(); let items = [1,2,3].map(notification_id => ({notification_id})); const session = createSession({ storage:{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}, getLogin:()=> 'timmy', getItems:()=>items, onOpen:()=>{}, onProgress:()=>{}, onFinish:()=>{}, }); session.start(); const first = session.next()?.notification_id; items = [1,3,4].map(notification_id => ({notification_id})); const afterRemoval = session.next()?.notification_id; session.keepUnreadAndNext(); const afterAdvance = session.next(); process.stdout.write(JSON.stringify({first, afterRemoval, afterAdvance:afterAdvance || null})); """) assert result == {"first": 2, "afterRemoval": 3, "afterAdvance": None} def test_update_triage_reports_truthful_outcomes_and_reopens_only_surviving_kept_items(): result = run_session(""" const values = new Map(); let items = [1,2,3].map(notification_id => ({notification_id})); const opened = [], outcomes = []; const session = createSession({ storage:{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}, getLogin:()=> 'timmy', getItems:()=>items, onOpen:item=>opened.push(item.notification_id), onProgress:()=>{}, onFinish:value=>outcomes.push(value), }); session.start(); session.keepUnreadAndNext(); session.completeAndNext(); items = [1,3].map(notification_id => ({notification_id})); session.keepUnreadAndNext(); const reviewed = session.reviewKept(); process.stdout.write(JSON.stringify({opened, outcomes, reviewed})); """) assert result == { "opened": [1, 2, 3, 1], "outcomes": [{"reviewed": 3, "resolved": 1, "kept": 2, "keptIdentities": ["1", "3"]}], "reviewed": True, } def test_update_triage_reconciles_kept_outcomes_before_reporting_cleared(): result = run_session(""" const values = new Map(); let items = [1,2].map(notification_id => ({notification_id})); const outcomes = []; const session = createSession({ storage:{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}, getLogin:()=> 'timmy', getItems:()=>items, onOpen:()=>{}, onProgress:()=>{}, onFinish:value=>outcomes.push(value), }); session.start(); session.keepUnreadAndNext(); items = [2].map(notification_id => ({notification_id})); session.completeAndNext(); process.stdout.write(JSON.stringify(outcomes[0])); """) assert result == {"reviewed": 2, "resolved": 2, "kept": 0, "keptIdentities": []} @pytest.mark.anyio async def test_dashboard_wires_resumable_updates_triage_mobile_flow(): html = await dashboard() assert '' in html assert 'id="update-triage-progress"' in html assert 'id="keep-update-unread" type="button" aria-label="Keep unread and open next update">Keep unread' in html assert "openUpdates: openUpdateTriage" in html assert "updateTriage.acceptCompleted()" in html assert "updateTriage.keepUnreadAndNext()" in html assert "updateTriage.reconcile()" in html assert 'id="mobile-update-outcome"' in html assert 'id="review-kept-updates"' in html assert "updateTriage.reviewKept(keptUpdateIdentities)" in html assert "showUpdateTriageOutcome(outcome)" in html assert ".update-triage-progress" in html