import json import os import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard @pytest.mark.anyio async def test_empty_all_work_queue_offers_find_and_create_actions(): markup = (Path(__file__).resolve().parents[1] / "frontend" / "index.html").read_text() source = await dashboard() assert 'id="empty-work-start"' in markup assert 'Ready for something new?' in markup assert 'id="empty-work-find"' in markup assert 'id="empty-work-create"' in markup assert "qs('#empty-work-find').addEventListener('click'" in source assert "qs('#find-work').click()" in source assert "qs('#empty-work-create').addEventListener('click'" in source assert "qs('#new-issue').click()" in source assert "emptyWorkStart.hidden = !showEmptyStart" in source MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js" NOTIFICATION_UNDO = Path(__file__).parents[1] / "frontend" / "notification-undo.js" TODAY_TIMER = Path(__file__).parents[1] / "frontend" / "today-timer.js" LATER_WORK = Path(__file__).parents[1] / "frontend" / "later-work.js" DETAIL_DEFER = Path(__file__).parents[1] / "frontend" / "detail-defer.js" LATER_PICKER = Path(__file__).parents[1] / "frontend" / "later-picker.js" LATER_AND_START = Path(__file__).parents[1] / "frontend" / "later-and-start.js" REVIEW_SHEET = Path(__file__).parents[1] / "frontend" / "review-sheet.js" ISSUE_SHEET = Path(__file__).parents[1] / "frontend" / "issue-sheet.js" CREATE_ISSUE_SHEET = Path(__file__).parents[1] / "frontend" / "create-issue-sheet.js" PULL_SHEET = Path(__file__).parents[1] / "frontend" / "pull-sheet.js" CONVERSATION = Path(__file__).parents[1] / "frontend" / "conversation.js" COMMENT_ACTIONS = Path(__file__).parents[1] / "frontend" / "comment-actions.js" PICK_WORK = Path(__file__).parents[1] / "frontend" / "pick-work.js" WORK_ROUTE = Path(__file__).parents[1] / "frontend" / "work-route.js" UPDATE_OWNERSHIP = Path(__file__).parents[1] / "frontend" / "update-ownership.js" def test_protect_today_route_is_an_explicit_agenda_action(): script = f""" const routes = require({json.dumps(str(WORK_ROUTE))}); process.stdout.write(JSON.stringify(routes.parse('#/my-work/agenda/protect-today'))); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == { "kind": "queue", "filter": "agenda", "action": "protect-today" } CARD_PLANNING = Path(__file__).parents[1] / "frontend" / "card-planning.js" TODAY_WORK = Path(__file__).parents[1] / "frontend" / "today-work.js" WORK_SELECTION = Path(__file__).parents[1] / "frontend" / "work-selection.js" def test_work_selection_selects_matching_items_in_order_with_an_explicit_cap(): script = f""" const createWorkSelection = require({json.dumps(str(WORK_SELECTION))}); const selection = createWorkSelection({{limit:3}}); selection.start(); selection.select({{kind:'issue', repository:'stackchain/api', number:1}}); const result = selection.selectMany([ {{kind:'issue', repository:'stackchain/api', number:1}}, {{kind:'pull', repository:'stackchain/web', number:2}}, {{kind:'issue', repository:'stackchain/app', number:3}}, {{kind:'issue', repository:'stackchain/overflow', number:4}}, ]); process.stdout.write(JSON.stringify({{result, snapshot:selection.snapshot()}})); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) output = json.loads(result.stdout) assert output == { "result": {"status": "limit", "added": 2, "count": 3, "limit": 3}, "snapshot": { "active": True, "count": 3, "ids": [ "issue:stackchain/api:1:", "pull:stackchain/web:2:", "issue:stackchain/app:3:", ], }, } def test_work_selection_clears_matches_without_leaving_selection_mode(): script = f""" const createWorkSelection = require({json.dumps(str(WORK_SELECTION))}); const selection = createWorkSelection(); selection.start(); selection.selectMany([ {{kind:'issue', repository:'stackchain/api', number:1}}, {{kind:'pull', repository:'stackchain/web', number:2}}, ]); process.stdout.write(JSON.stringify(selection.clear())); """ result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) assert json.loads(result.stdout) == {"active": True, "count": 0, "ids": []} @pytest.mark.anyio async def test_mobile_batch_planning_can_select_and_clear_active_queue_matches(): html = await dashboard() assert 'id="select-matching-work"' in html assert 'id="clear-work-selection"' in html assert "workSelection.selectMany(visible)" in html assert "workSelection.clear()" in html assert "matches selected" in html assert ".selection-scope-actions button { min-height:44px;" in html assert "max-width:100%" in html @pytest.mark.anyio async def test_issue_sheet_formats_due_date_as_a_calendar_day_without_local_timestamp_conversion(): source = await dashboard() assert "formatCalendarDueDate(detail.due_date)" in source assert "new Date(detail.due_date).toLocaleDateString()" not in source def test_queue_finder_matches_repository_number_and_title_without_reordering(): script = f""" const work = require({json.dumps(str(MY_WORK))}); const items = [ {{repository:'stackchain/api', key:'stackchain/api#42', number:42, title:'Retry failed deploy'}}, {{repository:'stackchain/web', key:'stackchain/web#7', number:7, title:'Polish mobile queue'}}, {{repository:'other/repo', key:'other/repo#42', number:42, title:'Unrelated task'}}, ]; process.stdout.write(JSON.stringify({{ repo:work.findQueueItems(items, 'STACKCHAIN/API').map(item => item.key), number:work.findQueueItems(items, '#42').map(item => item.key), title:work.findQueueItems(items, 'mobile queue').map(item => item.key), clear:work.findQueueItems(items, ' ').map(item => item.key), }})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "repo": ["stackchain/api#42"], "number": ["stackchain/api#42", "other/repo#42"], "title": ["stackchain/web#7"], "clear": ["stackchain/api#42", "stackchain/web#7", "other/repo#42"], } def test_mobile_agenda_groups_assigned_deadlines_in_local_seven_day_horizon(): script = f""" const work = require({json.dumps(str(MY_WORK))}); const now = new Date('2026-08-12T12:00:00'); const items = [ {{kind:'issue', key:'o/r#5', repository:'o/r', number:5, is_assigned:true, due_date:'2026-08-11T18:00:00'}}, {{kind:'issue', key:'o/r#3', repository:'o/r', number:3, is_assigned:true, due_date:'2026-08-12T17:00:00'}}, {{kind:'issue', key:'a/r#9', repository:'a/r', number:9, is_assigned:true, due_date:'2026-08-13T09:00:00'}}, {{kind:'issue', key:'a/r#2', repository:'a/r', number:2, is_assigned:true, due_date:'2026-08-18T09:00:00'}}, {{kind:'issue', key:'a/r#1', repository:'a/r', number:1, is_assigned:true, due_date:'2026-08-20T09:00:00'}}, {{kind:'issue', key:'a/r#7', repository:'a/r', number:7, is_assigned:false, due_date:'2026-08-13T08:00:00'}}, {{kind:'pull', key:'a/r#8', repository:'a/r', number:8, is_assigned:true, due_date:'2026-08-13T08:00:00'}}, {{kind:'issue', key:'a/r#6', repository:'a/r', number:6, is_assigned:true}}, ]; process.stdout.write(JSON.stringify(work.agendaMyWork(items, now).map(item => [item.key, item.agenda_group]))); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == [ ["o/r#5", "Overdue"], ["o/r#3", "Today"], ["a/r#9", "Tomorrow"], ["a/r#2", "Next 7 days"], ] def test_mobile_agenda_sorts_equal_deadlines_by_repository_and_number(): script = f""" const work = require({json.dumps(str(MY_WORK))}); const items = [ {{kind:'issue', key:'z/r#1', repository:'z/r', number:1, is_assigned:true, due_date:'2026-08-14T09:00:00'}}, {{kind:'issue', key:'a/r#10', repository:'a/r', number:10, is_assigned:true, due_date:'2026-08-14T09:00:00'}}, {{kind:'issue', key:'a/r#2', repository:'a/r', number:2, is_assigned:true, due_date:'2026-08-14T09:00:00'}}, ]; process.stdout.write(JSON.stringify(work.agendaMyWork(items, new Date('2026-08-12T12:00:00')).map(item => item.key))); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == ["a/r#2", "a/r#10", "z/r#1"] def test_mobile_agenda_runs_as_an_ordered_non_durable_work_session(): script = f""" const buildMyWork = require({json.dumps(str(MY_WORK))}); const values = new Map([['stackchain.today-session.v1', 'preserve-today']]); const checkpoint = {{ read:() => null, save:() => {{ throw new Error('Agenda must not save Today'); }}, clear:() => {{ throw new Error('Agenda must not clear Today'); }}, }}; const active = [ {{kind:'issue',repository:'o/r',number:3,title:'Tomorrow',is_assigned:true,due_date:'2026-08-14'}}, {{kind:'issue',repository:'o/r',number:1,title:'Overdue',is_assigned:true,due_date:'2026-08-12'}}, {{kind:'issue',repository:'o/r',number:2,title:'Today',is_assigned:true,due_date:'2026-08-13'}}, {{kind:'issue',repository:'o/r',number:4,title:'No deadline',is_assigned:true}}, ]; const opened = []; let finished = 0; const session = buildMyWork.createWorkSession({{ getItems:() => buildMyWork.agendaMyWork(active, new Date('2026-08-13T12:00:00')), getFilter:() => 'all', checkpoint, checkpointEnabled:() => false, onOpen:item => opened.push(item.title), onProgress:() => {{}}, onFinish:() => {{ finished += 1; }}, }}); const started = session.start(); session.next(); session.next(); const completed = session.next(); process.stdout.write(JSON.stringify({{ started, completed, opened, finished, active:session.active(), today:values.get('stackchain.today-session.v1'), }})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "started": True, "completed": False, "opened": ["Overdue", "Today", "Tomorrow"], "finished": 1, "active": False, "today": "preserve-today", } @pytest.mark.anyio async def test_mobile_agenda_launcher_starts_the_rendered_queue_and_finishes_in_agenda(): html = await dashboard() assert "selectedWorkFilter === 'agenda' ? agendaMyWork(activeMyWork)" in html assert "selectedWorkFilter === 'agenda' ? 'all'" in html assert "startWorkSession.textContent = selectedWorkFilter === 'agenda' ? 'Start Agenda' : 'Start work'" in html assert "const sessionItems = workSession.items()" in html assert "qs('#my-work-action-status').textContent = 'Agenda complete.'" in html assert "checkpointEnabled: () => selectedWorkFilter === 'today'" in html @pytest.mark.parametrize("timezone_name", ["Asia/Tokyo", "America/Los_Angeles"]) def test_mobile_agenda_preserves_the_gitea_calendar_day_in_every_timezone(timezone_name): script = f""" const work = require({json.dumps(str(MY_WORK))}); const now = new Date(2026, 7, 13, 12, 0, 0); const built = work({{ user:{{login:'timmy'}}, notifications:[], pull_requests:[], issues:[{{number:1,title:'Ship',repository:'o/r',labels:[],assignees:['timmy'],due_date:'2026-08-13T23:59:59Z'}}], }}, now); process.stdout.write(JSON.stringify({{ dueLabel:built[0].due_label, agenda:work.agendaMyWork(built, now).map(item => [item.key, item.agenda_group]), }})); """ environment = {**os.environ, "TZ": timezone_name} result = subprocess.run( ["node", "-e", script], capture_output=True, text=True, env=environment ) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "dueLabel": "Due today", "agenda": [["o/r#1", "Today"]], } def test_agenda_pager_loads_every_issue_page_single_flight_and_retries_failed_page(): script = f""" const work = require({json.dumps(str(MY_WORK))}); const calls = []; let failPage = 3; let items = [{{id:1, title:'first'}}]; let pagination = {{issue:{{page:1,total:4,has_more:true}}}}; const pager = work.createWorkPager({{ load: async (stream, page) => {{ calls.push(page); await new Promise(resolve => setTimeout(resolve, 5)); if (page === failPage) throw new Error('offline'); return {{page,total:4,has_more:page < 4,items:[{{id:page,title:'page '+page}}]}}; }}, onItems: (_stream, next) => {{ items = next; }}, onPagination: next => {{ pagination = next; }}, onStatus: () => {{}}, }}); pager.reset(pagination); async function run() {{ const first = pager.loadAll('issue', () => items); const duplicate = pager.loadAll('issue', () => items); const failed = await first; const samePromise = first === duplicate; failPage = 0; const retried = await pager.loadAll('issue', () => items); process.stdout.write(JSON.stringify({{ failed, samePromise, retried, calls, ids:items.map(item => item.id), pagination, }})); }} run(); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "failed": False, "samePromise": True, "retried": True, "calls": [2, 3, 3, 4], "ids": [1, 2, 3, 4], "pagination": {"issue": {"page": 4, "total": 4, "has_more": False}}, } @pytest.mark.anyio async def test_agenda_activation_checks_all_issue_pages_before_showing_empty_state(): html = await dashboard() assert "workPager.loadAll('issue'" in html assert "Checking all assigned deadlines…" in html assert "Agenda check paused. Retry to check older assigned deadlines." in html assert "selectedWorkFilter === 'agenda' && workPagination.issue?.has_more" in html @pytest.mark.anyio async def test_mobile_queue_finder_is_labeled_thumb_safe_and_offers_older_search(): html = await dashboard() assert '