import json import subprocess from pathlib import Path ROOT = Path(__file__).parents[1] PROTECT_TODAY = ROOT / "frontend" / "protect-today.js" def run_node(script: str) -> dict: result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) return json.loads(result.stdout) def test_protect_today_prioritizes_and_deduplicates_urgent_deadlines_before_existing_plan(): script = f""" const protectToday = require({json.dumps(str(PROTECT_TODAY))}); const issue = (number, group, due) => ({{ kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number, is_assigned:true, state:'open', agenda_group:group, due_date:due, }}); const urgentLater = issue(3, 'Today', '2026-08-13'); const urgentFirst = issue(2, 'Overdue', '2026-08-12'); const duplicate = issue(2, 'Overdue', '2026-08-12'); const existingUrgent = issue(1, 'Today', '2026-08-13'); const existingNormal = issue(8, null, null); const result = protectToday.propose({{ agenda:[urgentLater, duplicate, urgentFirst, issue(4, 'Tomorrow', '2026-08-14')], today:[existingNormal, existingUrgent], identity:item => item.repository + '#' + item.number, limit:5, }}); process.stdout.write(JSON.stringify({{ selected:result.selected.map(item => item.number), protected:result.protected.map(item => item.number), displaced:result.displaced.map(item => item.number), summary:result.summary, }})); """ assert run_node(script) == { "selected": [2, 1, 3, 8], "protected": [2, 1, 3], "displaced": [], "summary": "3 urgent items protected · 0 displaced", } def test_protect_today_reports_existing_work_displaced_by_five_item_limit(): script = f""" const protectToday = require({json.dumps(str(PROTECT_TODAY))}); const issue = (number, group = null) => ({{ kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number, is_assigned:true, state:'open', agenda_group:group, due_date:group === 'Overdue' ? '2026-08-12' : group === 'Today' ? '2026-08-13' : null, }}); const result = protectToday.propose({{ agenda:[issue(10, 'Overdue'), issue(11, 'Today'), issue(12, 'Today')], today:[issue(1), issue(2), issue(3), issue(4), issue(5)], identity:item => item.repository + '#' + item.number, limit:5, }}); process.stdout.write(JSON.stringify({{ selected:result.selected.map(item => item.number), displaced:result.displaced.map(item => item.number), summary:result.summary, }})); """ assert run_node(script) == { "selected": [10, 11, 12, 1, 2], "displaced": [3, 4, 5], "summary": "3 urgent items protected · 3 displaced for review", } def test_protect_today_excludes_closed_unassigned_and_nonurgent_items(): script = f""" const protectToday = require({json.dumps(str(PROTECT_TODAY))}); const items = [ {{kind:'issue',repository:'r',number:1,is_assigned:true,state:'closed',agenda_group:'Overdue',due_date:'2026-08-01'}}, {{kind:'issue',repository:'r',number:2,is_assigned:false,state:'open',agenda_group:'Today',due_date:'2026-08-13'}}, {{kind:'issue',repository:'r',number:3,is_assigned:true,state:'open',agenda_group:'Tomorrow',due_date:'2026-08-14'}}, {{kind:'pull',repository:'r',number:4,is_assigned:true,state:'open',agenda_group:'Overdue',due_date:'2026-08-01'}}, ]; const result = protectToday.propose({{agenda:items,today:[],identity:item => item.repository + '#' + item.number}}); process.stdout.write(JSON.stringify(result)); """ assert run_node(script) == { "selected": [], "protected": [], "displaced": [], "summary": "No overdue or due-today work needs protection.", } def test_dashboard_renders_mobile_protect_today_entrypoint_and_preview_context(): html = (ROOT / "frontend" / "index.html").read_text() dashboard = (ROOT / "frontend" / "dashboard.js").read_text() css = (ROOT / "frontend" / "dashboard.css").read_text() bundle = (ROOT / "src" / "frontend_bundle.py").read_text() assert 'id="protect-today"' in html assert 'id="protect-today-status"' in html assert "protectToday.propose" in dashboard assert "pendingProtectToday" in dashboard assert "Protect Today" in dashboard assert ".protect-today" in css assert "min-height:44px" in css.replace(" ", "") assert '"static/protect-today.js"' in bundle