import json import subprocess from pathlib import Path ASSIGN_AND_START = Path(__file__).parents[1] / "frontend" / "assign-and-start.js" QUEUE_TODAY = Path(__file__).parents[1] / "frontend" / "queue-today.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_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_queue_today_claims_once_without_starting_the_active_session(): script = f""" const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))}); const calls=[]; let release; const claimResult=new Promise(resolve=>{{release=resolve;}}); const issue={{repository:'stackchain/dashboard',number:415}}; const flow=createAssignAndStart({{ available:()=>true, claim:item=>{{calls.push('claim:'+item.number);return claimResult;}}, start:item=>{{calls.push('start:'+item.number);return 'started';}}, queue:item=>{{calls.push('queue:'+item.number);return 'queued';}}, recover:item=>calls.push('recover:'+item.number), announce:message=>calls.push('announce:'+message), }}); const first=flow.run(issue, {{destination:'queue'}}); const second=flow.run(issue, {{destination:'queue'}}); release({{...issue,assignees:['timmy']}}); Promise.all([first,second]).then(results=>process.stdout.write(JSON.stringify({{ calls,results,same:first===second }}))); """ assert run_node(script) == { "calls": [ "claim:415", "queue:415", "announce:Assigned and queued in Today. Keep finding work when ready.", ], "results": ["queued", "queued"], "same": True, } def test_queue_today_persists_syncs_and_warms_without_a_session_side_effect(): script = f""" const fs=require('fs'); if (!fs.existsSync({json.dumps(str(QUEUE_TODAY))})) {{ process.stdout.write(JSON.stringify({{available:false}})); }} else {{ const createQueueToday=require({json.dumps(str(QUEUE_TODAY))}); const calls=[]; const issue={{kind:'issue',repository:'stackchain/dashboard',number:415}}; const queue=createQueueToday({{ todayWork:{{identity:()=> 'issue:stackchain/dashboard:415:',add:()=>{{calls.push('add');return 'added';}}}}, todaySync:{{enqueue:(action,id)=>{{calls.push(action+':'+id);return true;}},flush:()=>calls.push('flush')}}, refresh:()=>calls.push('refresh'), warm:()=>calls.push('warm'), }}); process.stdout.write(JSON.stringify({{available:true,result:queue(issue),calls}})); }} """ assert run_node(script) == { "available": True, "result": "queued", "calls": [ "add", "add:issue:stackchain/dashboard:415:", "refresh", "flush", "warm", ], } def test_queue_today_reports_assignment_when_local_planning_needs_recovery(): script = f""" const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))}); const calls=[]; const issue={{repository:'stackchain/dashboard',number:415,assignees:['timmy']}}; const flow=createAssignAndStart({{ available:()=>true, claim:()=>Promise.resolve(issue), start:()=> 'started', queue:()=> 'sync-unavailable', recover:item=>calls.push('recover:'+item.number), announce:message=>calls.push('announce:'+message), }}); flow.run(issue, {{destination:'queue'}}).then(result=> process.stdout.write(JSON.stringify({{result,calls}})) ); """ assert run_node(script) == { "result": "recovery", "calls": [ "announce:Assigned and saved in Today on this device, but account sync is unavailable. The issue is open so you can recover.", "recover:415", ], } def test_find_work_renders_phone_safe_assign_and_start_and_wires_offline_shell(): html = HTML.read_text() dashboard = DASHBOARD.read_text() css = CSS.read_text() worker = WORKER.read_text() assert "data-claim-start-index" in dashboard assert "data-claim-queue-index" in dashboard assert ">Start now" in dashboard assert ">Queue Today" in dashboard assert "const assignAndStart = createAssignAndStart({" in dashboard assert "assignAndStart.run(item)" in dashboard assert "assignAndStart.run(item, { destination:'queue' })" in dashboard assert "queue: confirmed =>" in dashboard assert "const queueToday = createQueueToday({" in dashboard assert "return queueToday(claimed)" 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 ".find-work-claim-actions [data-claim-start-index]" in css assert "grid-column:1 / -1" in css assert "BASE + 'static/assign-and-start.js'" in worker assert "BASE + 'static/queue-today.js'" in worker assert '' in html