import json import subprocess from pathlib import Path CREATE_AND_START = Path(__file__).parents[1] / "frontend" / "create-and-start.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_confirmed_issue_persists_and_syncs_estimate_before_starting_once(): script = f""" const fs = require('fs'); if (!fs.existsSync({json.dumps(str(CREATE_AND_START))})) {{ process.stdout.write(JSON.stringify({{available:false}})); }} else {{ const createCreateAndStart = require({json.dumps(str(CREATE_AND_START))}); const calls = []; const issue = {{kind:'issue', repository:'stackchain/dashboard', number:387}}; const flow = createCreateAndStart({{ todayWork: {{ limit:5, read:()=>[], identity:item=>'issue:'+item.repository+':'+item.number+':', add:()=>{{calls.push('add');return 'added';}}, planning:()=>({{capacity_minutes:120,estimates:{{}}}}), replacePlanning:plan=>{{calls.push('plan:'+JSON.stringify(plan));return true;}}, }}, todaySync: {{ enqueue:(action,id)=>{{calls.push(action+':'+id);return true;}}, enqueueConfiguration:(capacity,estimates)=>{{calls.push('configure:'+capacity+':'+JSON.stringify(estimates));return true;}}, flush:()=>calls.push('flush') }}, refresh:()=>calls.push('refresh'), warm:()=>calls.push('warm'), start:item=>calls.push('start:'+item.number), announce:message=>calls.push('announce:'+message), }}); const result = flow.complete(issue, 45); flow.complete(issue, 45); process.stdout.write(JSON.stringify({{available:true,result,calls}})); }} """ assert run_node(script) == { "available": True, "result": "started", "calls": [ "add", 'plan:{"capacity_minutes":120,"estimates":{"issue:stackchain/dashboard:387:":45}}', "add:issue:stackchain/dashboard:387:", 'configure:120:{"issue:stackchain/dashboard:387:":45}', "refresh", "flush", "warm", "start:387", "announce:Created, added to Today, and ready to work.", ], } def test_mobile_capture_wires_distinct_create_and_start_intent_into_the_offline_shell(): html = HTML.read_text() dashboard = DASHBOARD.read_text() css = CSS.read_text() worker = WORKER.read_text() assert '' in html assert '' in html assert "const createAndStart = createCreateAndStart({" in dashboard assert "event.submitter.id === 'create-and-start-issue'" in dashboard assert "review.intent === 'create-and-start'" in dashboard assert 'id="create-issue-estimate"' in html assert 'min="5" max="1440" step="5"' in html assert "createAndStart.capacity(qs('#create-issue-estimate').value)" in dashboard assert "estimateMinutes: startCapacity.minutes" in dashboard assert "createAndStart.complete(created, completion.estimateMinutes)" in dashboard assert ".create-issue-estimate" in css assert ".create-issue-actions" in css and "grid-template-columns" in css assert "BASE + 'static/create-and-start.js'" in worker def test_create_and_start_intent_is_durable_and_has_a_one_tap_relaunch_continuation(): dashboard = DASHBOARD.read_text() assert "completionIntent: 'create-and-start'" in dashboard assert "result.completions || []" in dashboard assert "(result.completions || []).forEach(completion =>" in dashboard assert "issueOutbox.pendingCompletions(activeFlushLogin)" in dashboard assert "issueOutbox.completeIntent(completion.id, activeFlushLogin)" in dashboard assert 'class="draft-continue"' in dashboard assert "Continue created work" in dashboard def test_create_and_start_stops_before_session_when_today_sync_cannot_be_queued(): script = f""" const createCreateAndStart = require({json.dumps(str(CREATE_AND_START))}); const calls = []; const issue = {{kind:'issue', repository:'stackchain/dashboard', number:388}}; const flow = createCreateAndStart({{ todayWork: {{limit:5, read:()=>[], identity:()=> 'issue:stackchain/dashboard:388:', add:()=> 'added'}}, todaySync: {{enqueue:()=>false, flush:()=>calls.push('flush')}}, refresh:()=>calls.push('refresh'), warm:()=>calls.push('warm'), start:()=>calls.push('start'), announce:message=>calls.push(message), }}); process.stdout.write(JSON.stringify({{result:flow.complete(issue), calls}})); """ assert run_node(script) == { "result": "sync-unavailable", "calls": ["Created and saved to Today on this device, but account sync is unavailable."], } def test_create_and_start_can_resume_after_today_sync_admission_recovers(): script = f""" const createCreateAndStart = require({json.dumps(str(CREATE_AND_START))}); const calls=[];let admitted=false;let added=false; const issue={{kind:'issue',repository:'stackchain/dashboard',number:389}}; const flow=createCreateAndStart({{ todayWork:{{limit:5,read:()=>[],identity:()=> 'issue:stackchain/dashboard:389:',add:()=>{{ const outcome=added?'exists':'added';added=true;return outcome; }}}}, todaySync:{{enqueue:()=>admitted,flush:()=>calls.push('flush')}}, refresh:()=>calls.push('refresh'),warm:()=>calls.push('warm'),start:()=>calls.push('start'), announce:message=>calls.push(message), }}); const first=flow.complete(issue); admitted=true; const second=flow.complete(issue); process.stdout.write(JSON.stringify({{first,second,calls}})); """ output = run_node(script) assert output["first"] == "sync-unavailable" assert output["second"] == "started" assert output["calls"][-5:] == [ "refresh", "flush", "warm", "start", "Created, added to Today, and ready to work.", ] def test_create_and_start_reports_validation_and_remaining_today_capacity(): script = f""" const createCreateAndStart=require({json.dumps(str(CREATE_AND_START))}); const flow=createCreateAndStart({{ todayWork:{{limit:5,read:()=>['first','second'],planning:()=>({{ capacity_minutes:120,estimates:{{first:30,second:40}} }})}}, }}); process.stdout.write(JSON.stringify({{ empty:flow.capacity(''),tooSmall:flow.capacity('4'),fits:flow.capacity('45'),over:flow.capacity('55') }})); """ assert run_node(script) == { "empty": {"valid": False, "reason": "required"}, "tooSmall": {"valid": False, "reason": "range"}, "fits": {"valid": True, "minutes": 45, "capacityMinutes": 120, "plannedMinutes": 70, "remainingMinutes": 50, "projectedMinutes": 5, "fits": True}, "over": {"valid": True, "minutes": 55, "capacityMinutes": 120, "plannedMinutes": 70, "remainingMinutes": 50, "projectedMinutes": -5, "fits": False}, } def test_create_and_start_does_not_start_when_estimate_cannot_be_saved(): script = f""" const createCreateAndStart=require({json.dumps(str(CREATE_AND_START))});const calls=[]; const flow=createCreateAndStart({{ todayWork:{{limit:5,read:()=>[],identity:()=> 'issue:o/r:849:',add:()=> 'added', planning:()=>({{capacity_minutes:60,estimates:{{}}}}),replacePlanning:()=>false}}, todaySync:{{enqueue:()=>true,enqueueConfiguration:()=>true,flush:()=>calls.push('flush')}}, refresh:()=>calls.push('refresh'),warm:()=>calls.push('warm'),start:()=>calls.push('start'), announce:message=>calls.push(message), }}); process.stdout.write(JSON.stringify({{result:flow.complete({{repository:'o/r',number:849}},30),calls}})); """ assert run_node(script) == { "result": "estimate-unavailable", "calls": ["Created and added to Today, but its estimate could not be saved on this device."], } def test_create_and_start_does_not_start_when_estimate_sync_cannot_be_queued(): script = f""" const createCreateAndStart=require({json.dumps(str(CREATE_AND_START))});const calls=[]; const flow=createCreateAndStart({{ todayWork:{{limit:5,read:()=>[],identity:()=> 'issue:o/r:849:',add:()=> 'added', planning:()=>({{capacity_minutes:60,estimates:{{}}}}),replacePlanning:()=>true}}, todaySync:{{enqueue:()=>true,enqueueConfiguration:()=>false,flush:()=>calls.push('flush')}}, refresh:()=>calls.push('refresh'),warm:()=>calls.push('warm'),start:()=>calls.push('start'), announce:message=>calls.push(message), }}); process.stdout.write(JSON.stringify({{result:flow.complete({{repository:'o/r',number:849}},30),calls}})); """ assert run_node(script) == { "result": "sync-unavailable", "calls": ["Created and estimated on this device, but Today plan sync is unavailable."], }