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_interrupted_batch_resumes_confirmed_assignment_without_claiming_twice(): script = f""" const createBatchFindWork=require({json.dumps(str(BATCH_FIND_WORK))}); const values={{}}; const storage={{ getItem:key=>values[key] || null, setItem:(key,value)=>{{values[key]=value;}}, removeItem:key=>{{delete values[key];}}, }}; const calls=[]; const item={{repository:'stackchain/dashboard',number:679,title:'Recover me'}}; const first=createBatchFindWork({{ capacity:()=>1, storage, owner:()=> 'timmy', claim:value=>{{calls.push('claim');return Promise.resolve({{...value,assignees:['timmy']}});}}, queue:()=>{{calls.push('queue-failed');return 'sync-unavailable';}}, }}); first.run([item]).then(firstResult=>{{ const second=createBatchFindWork({{ capacity:()=>1, storage, owner:()=> 'timmy', claim:()=>{{calls.push('duplicate-claim');return Promise.resolve(item);}}, queue:value=>{{calls.push('queue-resumed:'+value.assignees[0]);return 'queued';}}, }}); return second.resume().then(resumed=>process.stdout.write(JSON.stringify({{ firstResult, resumed, calls, keys:Object.keys(values), }}))); }}); """ assert run_node(script) == { "firstResult": { "status": "complete", "selected": 1, "available": 1, "queued": [], "failed": [{ "key": "stackchain/dashboard#679", "reason": "assigned but Today sync is unavailable", "assigned": True, }], }, "resumed": { "status": "complete", "selected": 1, "available": 1, "queued": ["stackchain/dashboard#679"], "failed": [], }, "calls": ["claim", "queue-failed", "queue-resumed:timmy"], "keys": [], } def test_resumed_batch_skips_queued_items_and_continues_in_original_order(): script = f""" const createBatchFindWork=require({json.dumps(str(BATCH_FIND_WORK))}); const key='stackchain.find-work-batch.v1.timmy'; const values={{[key]:JSON.stringify({{ owner:'timmy', available:2, items:[ {{item:{{repository:'stackchain/dashboard',number:678}},state:'queued'}}, {{item:{{repository:'stackchain/dashboard',number:679}},state:'pending'}}, ], }})}}; const calls=[]; const flow=createBatchFindWork({{ capacity:()=>2, owner:()=> 'timmy', storage:{{getItem:key=>values[key]||null,setItem:(key,value)=>values[key]=value,removeItem:key=>delete values[key]}}, claim:item=>{{calls.push('claim:'+item.number);return Promise.resolve(item);}}, queue:item=>{{calls.push('queue:'+item.number);return 'queued';}}, }}); flow.resume().then(result=>process.stdout.write(JSON.stringify({{result,calls}}))); """ assert run_node(script) == { "result": { "status": "complete", "selected": 2, "available": 2, "queued": ["stackchain/dashboard#678", "stackchain/dashboard#679"], "failed": [], }, "calls": ["claim:679", "queue:679"], } def test_time_budget_blocks_claims_until_every_estimate_fits(): script = f""" const createBatchFindWork=require({json.dumps(str(BATCH_FIND_WORK))}); const calls=[]; const flow=createBatchFindWork({{ capacity:()=>3, timeBudget:()=>({{capacity_minutes:90,planned_minutes:30}}), claim:item=>{{calls.push('claim:'+item.number);return Promise.resolve(item);}}, queue:item=>{{calls.push('queue:'+item.number);return 'queued';}}, }}); const items=[701,702].map(number=>({{repository:'stackchain/dashboard',number}})); Promise.all([ flow.run(items, {{'stackchain/dashboard#701':30}}), flow.run(items, {{'stackchain/dashboard#701':30,'stackchain/dashboard#702':45}}), ]).then(results=>process.stdout.write(JSON.stringify({{results,calls}}))); """ assert run_node(script) == { "results": [ { "status": "estimates-required", "selected": 2, "available": 3, "queued": [], "failed": [], "remaining_minutes": 60, "requested_minutes": 30, "invalid": ["stackchain/dashboard#702"], }, { "status": "over-budget", "selected": 2, "available": 3, "queued": [], "failed": [], "remaining_minutes": 60, "requested_minutes": 75, "over_minutes": 15, }, ], "calls": [], } def test_time_budget_persists_estimates_only_for_successfully_queued_claims(): script = f""" const createBatchFindWork=require({json.dumps(str(BATCH_FIND_WORK))}); const calls=[]; const flow=createBatchFindWork({{ capacity:()=>3, timeBudget:()=>({{capacity_minutes:120,planned_minutes:30}}), claim:item=>item.number===702 ? Promise.reject(new Error('already claimed')) : Promise.resolve(item), queue:item=>'queued', persistEstimate:(item,minutes)=>calls.push(item.number+':'+minutes), }}); flow.run([701,702].map(number=>({{repository:'stackchain/dashboard',number}})),{{ 'stackchain/dashboard#701':30, 'stackchain/dashboard#702':45, }}).then(result=>process.stdout.write(JSON.stringify({{result,calls}}))); """ assert run_node(script) == { "result": { "status": "complete", "selected": 2, "available": 3, "queued": ["stackchain/dashboard#701"], "failed": [{"key": "stackchain/dashboard#702", "reason": "already claimed"}], }, "calls": ["701:30"], } def test_mobile_find_work_has_preclaim_time_review_controls(): html = HTML.read_text() dashboard = DASHBOARD.read_text() css = CSS.read_text() assert 'id="find-work-estimate-review"' in html assert 'id="find-work-estimate-list"' in html assert 'id="confirm-find-work-estimates"' in html assert 'inputmode="numeric"' in dashboard assert "timeBudget:" in dashboard assert "persistEstimate:" in dashboard assert ".find-work-estimate-review" in css assert ".find-work-estimate-review input" in css and "min-height:44px" in css 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.includes('page=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_fill_today_selects_ranked_matches_up_to_remaining_capacity(): script = f""" const createFindWork=require({json.dumps(str(PICK_WORK))}); const states=[]; const controller=createFindWork({{ fetchJson:()=>Promise.resolve({{items:[ {{id:1,repository:'stackchain/dashboard',number:701}}, {{id:2,repository:'stackchain/dashboard',number:702}}, {{id:3,repository:'stackchain/dashboard',number:703}}, {{id:4,repository:'stackchain/dashboard',number:704}}, ],page:1,total:4,has_more:false}}), onItems:()=>{{}}, onPagination:()=>{{}}, onStatus:()=>{{}}, onSelection:state=>states.push(state), }}); controller.load().then(()=>{{ controller.startSelection(); controller.toggleSelection(controller.items()[1]); const outcome=controller.fillSelection(3); process.stdout.write(JSON.stringify({{ outcome, selected:controller.selectedItems().map(item=>item.number), lastState:states.at(-1), }})); }}); """ assert run_node(script) == { "outcome": {"selected": 3, "added": 2, "limit": 3}, "selected": [702, 701, 703], "lastState": { "active": True, "count": 3, "ids": [ "stackchain/dashboard#702", "stackchain/dashboard#701", "stackchain/dashboard#703", ], }, } def test_fill_today_with_no_capacity_does_not_change_selection(): script = f""" const createFindWork=require({json.dumps(str(PICK_WORK))}); const controller=createFindWork({{ fetchJson:()=>Promise.resolve({{items:[{{id:1,repository:'stackchain/dashboard',number:701}}]}}), onItems:()=>{{}}, onPagination:()=>{{}}, onStatus:()=>{{}}, onSelection:()=>{{}}, }}); controller.load().then(()=>{{ const outcome=controller.fillSelection(0); process.stdout.write(JSON.stringify({{outcome,selection:controller.selection()}})); }}); """ assert run_node(script) == { "outcome": {"selected": 0, "added": 0, "limit": 0}, "selection": {"active": False, "count": 0, "ids": []}, } 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_mobile_find_work_exposes_touch_accessible_batch_recovery(): html = HTML.read_text() dashboard = DASHBOARD.read_text() css = CSS.read_text() assert 'id="resume-find-work-batch"' in html assert 'aria-describedby="find-work-status"' in html assert "mountRecovery(" in BATCH_FIND_WORK.read_text() assert "typeof localStorage === 'undefined'" in BATCH_FIND_WORK.read_text() assert ".find-work-batch-recovery" in css and "min-height:44px" in css def test_mobile_find_work_exposes_fill_today_action_with_capacity_handoff(): html = HTML.read_text() dashboard = DASHBOARD.read_text() css = CSS.read_text() assert 'id="fill-find-work-today"' in html assert 'aria-describedby="find-work-selection-status"' in html assert "findWorkController.fillSelection(remainingSlots)" in dashboard assert "openFindWorkEstimateReview(findWorkController.selectedItems())" in dashboard assert ".fill-find-work-today" in css and "min-height:44px" in css 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&facets=true&q=api", "api/v1/available-issues?page=1&facets=true&q=dashboard", "api/v1/available-issues?page=1&facets=true&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 def test_find_work_facets_serialize_filters_and_preserve_selection(): script = f""" const createFindWork=require({json.dumps(str(PICK_WORK))}); const calls=[]; const controller=createFindWork({{ fetchJson:path=>{{ calls.push(path); return Promise.resolve({{items:[{{id:1,repository:'stackchain/api',number:1}}],page:1,total:1,has_more:false,facets:{{repositories:['stackchain/api'],labels:['critical']}}}}); }}, onItems:()=>{{}}, onPagination:()=>{{}}, onStatus:()=>{{}}, onSelection:()=>{{}}, onFacets:()=>{{}}, }}); controller.reset({{items:[{{id:9,repository:'stackchain/old',number:9}}],page:1,total:1,has_more:false}}); controller.startSelection(); controller.toggleSelection(controller.items()[0]); controller.setFilters({{repositories:['stackchain/api'],labels:['critical']}}).then(()=>process.stdout.write(JSON.stringify({{ calls, filters:controller.filters(), selected:controller.selectedItems().map(item=>item.number) }}))); """ assert run_node(script) == { "calls": ["api/v1/available-issues?page=1&facets=true&repository=stackchain%2Fapi&label=critical"], "filters": {"repositories": ["stackchain/api"], "labels": ["critical"]}, "selected": [9], } def test_mobile_find_work_exposes_accessible_touch_sized_facets(): html = HTML.read_text() dashboard = DASHBOARD.read_text() css = CSS.read_text() assert 'id="find-work-filters"' in html assert 'id="find-work-repository-filters"' in html assert 'id="find-work-label-filters"' in html assert 'id="clear-find-work-filters"' in html assert 'aria-label="Filter available work"' in html assert "setFilters(next)" in PICK_WORK.read_text() assert ".find-work-filters" in css assert ".find-work-filter-option" in css and "min-height:44px" in css