import json import subprocess from pathlib import Path ROOT = Path(__file__).parents[1] MODULE = ROOT / "frontend" / "create-pull-sheet.js" def run_node(scenario: str) -> dict: script = f""" const createPullSheet = require({json.dumps(str(MODULE))}); {scenario} """ result = subprocess.run(["node", "-e", script], text=True, capture_output=True, cwd=ROOT) assert result.returncode == 0, result.stderr return json.loads(result.stdout) def test_create_pull_controller_loads_branches_and_defaults_base(): result = run_node(""" const calls=[]; const controller=createPullSheet.createController({ request:async (url) => { calls.push(url); return {default_branch:'main',branches:[ {name:'feature/mobile',sha:'abc1234'}, {name:'main',sha:'def5678'} ]}; }, storage:{getItem:()=>null,setItem:()=>{}}, login:'timmy' }); controller.selectRepository('stackchain/api').then(value => { process.stdout.write(JSON.stringify({value,calls,state:controller.state()})); }).catch(error=>{console.error(error);process.exit(1)}); """) assert result["calls"] == ["api/v1/repos/stackchain/api/pull-creation-options"] assert result["state"]["base"] == "main" assert result["state"]["head"] == "feature/mobile" assert result["state"]["expected_head_sha"] == "abc1234" def test_create_pull_controller_submits_idempotently_and_keeps_draft_on_failure(): result = run_node(""" const calls=[]; const saved=[]; let fail=true; const controller=createPullSheet.createController({ request:async (url, options) => { calls.push([url,options]); if (!options) return {default_branch:'main',branches:[{name:'topic',sha:'abc1234'},{name:'main',sha:'def5678'}]}; if (fail) { fail=false; throw new Error('offline'); } return {number:9,repository:'acme/app',head:{ref:'topic',sha:'abc1234'},base:{ref:'main'},draft:true}; }, storage:{getItem:()=>null,setItem:(key,value)=>saved.push([key,JSON.parse(value)])}, login:'timmy', createId:()=> 'stable-id' }); (async()=>{ await controller.selectRepository('acme/app'); controller.update({title:'Ship it',body:'Context'}); let error=''; try { await controller.submit(); } catch (caught) { error=caught.message; } const retained=controller.state(); const confirmed=await controller.submit(); process.stdout.write(JSON.stringify({calls,error,retained,confirmed,saved})); })().catch(error=>{console.error(error);process.exit(1)}); """) assert result["error"] == "offline" assert result["retained"]["title"] == "Ship it" posts = [call for call in result["calls"] if call[1]] assert len(posts) == 2 assert posts[0][1]["headers"]["Idempotency-Key"] == "stable-id" assert posts[1][1]["headers"]["Idempotency-Key"] == "stable-id" assert posts[0][1]["body"]["expected_head_sha"] == "abc1234" assert result["confirmed"]["number"] == 9 def test_create_pull_controller_restores_only_the_current_accounts_draft(): result = run_node(""" const values={ 'stackchain.create-pull.v1.timmy':JSON.stringify({repository:'acme/app',head:'topic',base:'main',title:'Saved title',body:'Saved body',draft:false}), 'stackchain.create-pull.v1.alexander':JSON.stringify({repository:'private/repo',title:'Other account'}) }; const controller=createPullSheet.createController({ request:async()=>({}), storage:{getItem:key=>values[key] || null,setItem:()=>{}}, login:'Timmy' }); process.stdout.write(JSON.stringify(controller.state())); """) assert result == { "repository": "acme/app", "branches": [], "head": "topic", "base": "main", "expected_head_sha": "", "title": "Saved title", "body": "Saved body", "draft": False, } def test_mobile_pull_creation_sheet_is_packaged_and_touch_safe(): html = (ROOT / "frontend" / "index.html").read_text() css = (ROOT / "frontend" / "dashboard.css").read_text() bundle = (ROOT / "src" / "frontend_bundle.py").read_text() assert 'id="create-pull-sheet" role="dialog" aria-modal="true"' in html assert 'id="create-pull-form"' in html assert 'id="create-pull-repository"' in html assert 'id="create-pull-head"' in html assert 'id="create-pull-base"' in html assert 'id="submit-create-pull"' in html assert 'static/create-pull-sheet.js' in bundle assert ".create-pull-panel button" in css assert "min-height:44px" in css.replace(" ", "") def test_create_pull_binding_renders_branch_receipt_and_created_pull(): result = run_node(""" const rendered=[]; const statuses=[]; const opened=[]; const controller={ selectRepository:async repository=>({repository,branches:[{name:'topic',sha:'abc1234'},{name:'main',sha:'def5678'}],head:'topic',base:'main',expected_head_sha:'abc1234'}), update:values=>values, submit:async()=>({number:12,repository:'acme/app',existing:false}) }; const binding=createPullSheet.createBinding({controller, render:state=>rendered.push(state), status:value=>statuses.push(value), onCreated:value=>opened.push(value) }); (async()=>{ await binding.repositoryChanged('acme/app'); await binding.submit(); process.stdout.write(JSON.stringify({rendered,statuses,opened})); })().catch(error=>{console.error(error);process.exit(1)}); """) assert result["rendered"][0]["expected_head_sha"] == "abc1234" assert result["statuses"][-1] == "Pull request #12 created." assert result["opened"][0]["number"] == 12 def test_dashboard_wires_create_pull_inside_lazy_issue_capture_chunk(): dashboard = (ROOT / "frontend" / "dashboard.js").read_text() module = MODULE.read_text() assert "cp.b();" not in dashboard assert "else cp.b();" in module