import json import subprocess from pathlib import Path BATCH_FIND_WORK = Path(__file__).parents[1] / "frontend" / "batch-find-work.js" PICK_WORK = Path(__file__).parents[1] / "frontend" / "pick-work.js" HTML = Path(__file__).parents[1] / "frontend" / "index.html" DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js" CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css" WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js" def run_node(script): return json.loads(subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ).stdout) def test_batch_preflights_today_capacity_before_claiming_any_issue(): script = f""" const createBatchFindWork=require({json.dumps(str(BATCH_FIND_WORK))}); const calls=[]; const flow=createBatchFindWork({{ capacity:()=>1, claim:item=>{{calls.push('claim:'+item.number);return Promise.resolve(item);}}, queue:item=>{{calls.push('queue:'+item.number);return 'queued';}}, onProgress:progress=>calls.push('progress:'+progress.status), }}); flow.run([ {{repository:'stackchain/dashboard',number:671}}, {{repository:'stackchain/dashboard',number:672}}, ]).then(result=>process.stdout.write(JSON.stringify({{result,calls}}))); """ assert run_node(script) == { "result": { "status": "full", "selected": 2, "available": 1, "queued": [], "failed": [], }, "calls": ["progress:full"], } def test_batch_claims_in_order_continues_after_conflict_and_reports_truthfully(): script = f""" const createBatchFindWork=require({json.dumps(str(BATCH_FIND_WORK))}); const calls=[]; const flow=createBatchFindWork({{ capacity:()=>3, claim:item=>{{ calls.push('claim:'+item.number); return item.number===672 ? Promise.reject(new Error('already claimed')) : Promise.resolve({{...item,assignees:['timmy']}}); }}, queue:item=>{{calls.push('queue:'+item.number);return 'queued';}}, onProgress:progress=>calls.push('progress:'+progress.status+':'+progress.processed), }}); flow.run([671,672,673].map(number=>({{repository:'stackchain/dashboard',number}}))) .then(result=>process.stdout.write(JSON.stringify({{result,calls}}))); """ assert run_node(script) == { "result": { "status": "complete", "selected": 3, "available": 3, "queued": ["stackchain/dashboard#671", "stackchain/dashboard#673"], "failed": [{"key": "stackchain/dashboard#672", "reason": "already claimed"}], }, "calls": [ "claim:671", "queue:671", "progress:running:1", "claim:672", "progress:running:2", "claim:673", "queue:673", "progress:running:3", "progress:complete:3", ], } def test_batch_does_not_reclaim_confirmed_issue_when_today_sync_needs_recovery(): script = f""" const createBatchFindWork=require({json.dumps(str(BATCH_FIND_WORK))}); const calls=[]; const flow=createBatchFindWork({{ capacity:()=>1, claim:item=>{{calls.push('claim');return Promise.resolve({{...item,assignees:['timmy']}});}}, queue:item=>{{calls.push('queue');return 'sync-unavailable';}}, }}); flow.run([{{repository:'stackchain/dashboard',number:671}}]) .then(result=>process.stdout.write(JSON.stringify({{result,calls}}))); """ assert run_node(script) == { "result": { "status": "complete", "selected": 1, "available": 1, "queued": [], "failed": [{ "key": "stackchain/dashboard#671", "reason": "assigned but Today sync is unavailable", "assigned": True, }], }, "calls": ["claim", "queue"], } def test_find_work_selection_survives_loaded_pages_and_removes_confirmed_claims(): script = f""" const createFindWork=require({json.dumps(str(PICK_WORK))}); const pages={{ 1:{{items:[{{id:1,repository:'stackchain/dashboard',number:671}}],page:1,total:2,has_more:true}}, 2:{{items:[{{id:2,repository:'stackchain/dashboard',number:672}}],page:2,total:2,has_more:false}}, }}; const controller=createFindWork({{ fetchJson:path=>path.includes('/claim') ? Promise.resolve({{repository:'stackchain/dashboard',number:671,assignees:['timmy']}}) : Promise.resolve(pages[path.endsWith('=2') ? 2 : 1]), onItems:()=>{{}}, onPagination:()=>{{}}, onStatus:()=>{{}}, onSelection:()=>{{}}, }}); controller.load().then(()=>{{ controller.startSelection(); controller.toggleSelection(controller.items()[0]); return controller.loadMore(); }}).then(()=>{{ controller.toggleSelection(controller.items()[1]); return controller.claim(controller.items()[0]); }}).then(()=>process.stdout.write(JSON.stringify({{ selected:controller.selectedItems().map(item=>item.number), selecting:controller.selection().active, }}))); """ assert run_node(script) == {"selected": [672], "selecting": True} def test_mobile_find_work_exposes_accessible_batch_controls_and_offline_asset(): html = HTML.read_text() dashboard = DASHBOARD.read_text() css = CSS.read_text() worker = WORKER.read_text() assert 'id="select-find-work"' in html assert 'id="batch-find-work-actions"' in html assert 'id="claim-selected-work"' in html assert 'aria-live="polite"' in html assert "createBatchFindWork({" in dashboard assert "findWorkController.startSelection()" in dashboard assert "findWorkController.toggleSelection(item)" in dashboard assert ".find-work-batch-actions" in css assert "position:sticky" in css assert "env(safe-area-inset-bottom)" in css assert "BASE + 'static/batch-find-work.js'" in worker assert '' in html def test_find_work_search_ignores_stale_response_and_preserves_cross_query_selection(): script = f""" const createFindWork=require({json.dumps(str(PICK_WORK))}); const pending=[]; const rendered=[]; const fetchJson=path=>new Promise(resolve=>pending.push({{path,resolve}})); const controller=createFindWork({{ fetchJson, onItems:items=>rendered.push(items.map(item=>item.number)), onPagination:()=>{{}}, onStatus:()=>{{}}, onSelection:()=>{{}}, }}); const first=controller.search('api'); const second=controller.search('dashboard'); pending[1].resolve({{items:[{{id:2,repository:'stackchain/dashboard',number:673}}],page:1,total:1,has_more:false}}); second.then(()=>{{ controller.startSelection(); controller.toggleSelection(controller.items()[0]); const third=controller.search('worker'); pending[2].resolve({{items:[{{id:3,repository:'stackchain/worker',number:674}}],page:1,total:1,has_more:false}}); return third; }}).then(()=>{{ controller.toggleSelection(controller.items()[0]); pending[0].resolve({{items:[{{id:1,repository:'stackchain/api',number:1}}],page:1,total:1,has_more:false}}); return first; }}).then(()=>process.stdout.write(JSON.stringify({{ calls:pending.map(entry=>entry.path), rendered, items:controller.items().map(item=>item.number), selected:controller.selectedItems().map(item=>item.number), }}))); """ assert run_node(script) == { "calls": [ "api/v1/available-issues?page=1&q=api", "api/v1/available-issues?page=1&q=dashboard", "api/v1/available-issues?page=1&q=worker", ], "rendered": [[673], [674]], "items": [674], "selected": [673, 674], } def test_mobile_find_work_search_has_clear_live_results_and_touch_targets(): html = HTML.read_text() dashboard = DASHBOARD.read_text() css = CSS.read_text() assert 'id="find-work-search"' in html assert 'id="clear-find-work-search"' in html assert 'id="find-work-match-status"' in html assert 'aria-live="polite"' in html assert "findWorkController.search(" in dashboard assert "#find-work-search" in dashboard assert ".find-work-search" in css assert ".find-work-search button" in css and "min-height:44px" in css