import json import subprocess from pathlib import Path ASSIGN_AND_START = Path(__file__).parents[1] / "frontend" / "assign-and-start.js" 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_repeated_assign_and_start_taps_claim_once_then_start_confirmed_issue(): script = f""" const fs=require('fs'); if (!fs.existsSync({json.dumps(str(ASSIGN_AND_START))})) {{ process.stdout.write(JSON.stringify({{available:false}})); }} else {{ const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))}); const calls=[]; let release; const claimResult=new Promise(resolve=>{{release=resolve;}}); const flow=createAssignAndStart({{ available:()=>true, claim:item=>{{calls.push('claim:'+item.number);return claimResult;}}, start:item=>{{calls.push('start:'+item.number);return 'started';}}, recover:item=>calls.push('recover:'+item.number), announce:message=>calls.push('announce:'+message), }}); const issue={{repository:'stackchain/dashboard',number:411}}; const first=flow.run(issue); const second=flow.run(issue); release({{...issue,assignees:['timmy']}}); Promise.all([first,second]).then(results=>process.stdout.write(JSON.stringify({{available:true,calls,results,same:first===second}}))); }} """ assert run_node(script) == { "available": True, "calls": [ "claim:411", "start:411", "announce:Assigned, added to Today, and ready to work.", ], "results": ["started", "started"], "same": True, } def test_full_today_blocks_before_assignment_and_explains_recovery(): script = f""" const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))}); const calls=[]; const flow=createAssignAndStart({{ available:()=>false, claim:()=>{{calls.push('claim');return Promise.resolve({{}});}}, start:()=>calls.push('start'), recover:()=>calls.push('recover'), announce:message=>calls.push('announce:'+message), }}); flow.run({{repository:'stackchain/dashboard',number:411}}).then(result=> process.stdout.write(JSON.stringify({{result,calls}})) ); """ assert run_node(script) == { "result": "full", "calls": ["announce:Today is full—remove an item before assigning this issue."], } def test_confirmed_assignment_recovers_in_issue_detail_when_today_start_fails(): script = f""" const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))}); const calls=[]; const issue={{repository:'stackchain/dashboard',number:411,assignees:['timmy']}}; const flow=createAssignAndStart({{ available:()=>true, claim:()=>Promise.resolve(issue), start:()=> 'sync-unavailable', recover:item=>calls.push('recover:'+item.number), announce:message=>calls.push('announce:'+message), }}); flow.run(issue).then(result=>process.stdout.write(JSON.stringify({{result,calls}}))); """ assert run_node(script) == { "result": "recovery", "calls": [ "announce:Assigned to you, but Today could not start. The issue is open so you can recover.", "recover:411", ], } def test_failed_assignment_does_not_start_today_and_can_be_retried(): script = f""" const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))}); const calls=[];let attempt=0; const issue={{repository:'stackchain/dashboard',number:411}}; const flow=createAssignAndStart({{ available:()=>true, claim:()=>{{attempt+=1;calls.push('claim:'+attempt);return attempt===1 ? Promise.reject(new Error('conflict')) : Promise.resolve(issue);}}, start:()=>{{calls.push('start');return 'started';}}, recover:()=>calls.push('recover'), announce:message=>calls.push('announce:'+message), }}); flow.run(issue).catch(error=>{{ calls.push('error:'+error.message); return flow.run(issue); }}).then(result=>process.stdout.write(JSON.stringify({{result,calls}}))); """ assert run_node(script) == { "result": "started", "calls": [ "claim:1", "error:conflict", "claim:2", "start", "announce:Assigned, added to Today, and ready to work.", ], } def test_already_owned_search_result_starts_without_claiming(): script = f""" const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))}); const calls=[]; const issue={{repository:'stackchain/dashboard',number:413,assigned_to_me:true}}; const flow=createAssignAndStart({{ available:()=>true, claim:()=>{{calls.push('claim');return Promise.resolve(issue);}}, start:item=>{{calls.push('start:'+item.number);return 'started';}}, recover:item=>calls.push('recover:'+item.number), announce:message=>calls.push('announce:'+message), }}); flow.run(issue, {{alreadyOwned:true}}).then(result=> process.stdout.write(JSON.stringify({{result,calls}})) ); """ assert run_node(script) == { "result": "started", "calls": [ "start:413", "announce:Added to Today and ready to work.", ], } def test_find_work_renders_phone_safe_assign_and_start_and_wires_offline_shell(): dashboard = DASHBOARD.read_text() css = CSS.read_text() worker = WORKER.read_text() assert "data-claim-start-index" in dashboard assert ">Assign & start" in dashboard assert "const assignAndStart = createAssignAndStart({" in dashboard assert "assignAndStart.run(item)" in dashboard assert "createAndStart.available" in dashboard assert "createAndStart.complete" in dashboard assert ".find-work-claim-actions" in css assert "grid-template-columns:repeat(2,minmax(0,1fr))" in css assert "BASE + 'static/assign-and-start.js'" in worker