import json import os import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard 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" 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 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"] @pytest.mark.anyio async def test_mobile_queue_finder_is_labeled_thumb_safe_and_offers_older_search(): html = await dashboard() assert '