import json import subprocess from pathlib import Path SOURCE = Path(__file__).parents[1] / "frontend" / "release-receipt.js" def run_node(body: str) -> dict: script = f"const createReleaseReceipt=require({json.dumps(str(SOURCE))});\n" + body completed = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) return json.loads(completed.stdout) def test_receipt_survives_reload_for_confirmed_account_only_and_tracks_exact_commit(): output = run_node(r""" const values=new Map(); const storage={getItem:key=>values.has(key)?values.get(key):null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; let login='timmy'; const calls=[]; const first=createReleaseReceipt({storage,getLogin:()=>login,fetchJson:async path=>{calls.push(path);return {commit_sha:'merge456',ci_state:'pending',checks:[{name:'browser',state:'pending',url:''}],release:null};}}); const captured=first.capture({repository:'stackchain/api',number:7,key:'stackchain/api#7'}, {merge_commit_sha:'merge456'}); const restored=createReleaseReceipt({storage,getLogin:()=>login,fetchJson:first.fetchJson}).restore(); restored && first.refresh().then(status=>{ login='alex'; const other=createReleaseReceipt({storage,getLogin:()=>login,fetchJson:first.fetchJson}).restore(); process.stdout.write(JSON.stringify({captured,restored,status,other,calls,keys:[...values.keys()]})); }); """) assert output["captured"]["commit_sha"] == "merge456" assert output["restored"][0]["repository"] == "stackchain/api" assert output["status"][0]["label"] == "Checks running" assert output["status"][0]["checks"] == ["browser"] assert output["other"] == [] assert output["calls"] == ["api/v1/repos/stackchain/api/release-receipt/merge456"] assert output["keys"] == ["stackchain.release-receipt.v1:timmy"] def test_release_receipt_deletes_the_exact_captured_source_branch_without_stopping_release_tracking(): output = run_node(r""" const values=new Map(), calls=[]; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const receipt=createReleaseReceipt({storage,getLogin:()=> 'timmy',confirmAction:()=>true,fetchJson:async(path,options={})=>{ calls.push({path,method:options.method||'GET',body:options.body||null}); if ((options.method||'GET') === 'DELETE') return {number:7,deleted:true,source_branch:'timmy/feature',source_head_sha:'abc1234'}; return {commit_sha:'merge456',ci_state:'pending',checks:[],release:null}; }}); const captured=receipt.capture( {repository:'stackchain/api',number:7,key:'stackchain/api#7'}, {merge_commit_sha:'merge456',source_branch:'timmy/feature',source_head_sha:'abc1234',source_repository:'stackchain/api'} ); (async()=>{ await receipt.deleteBranch('stackchain/api','merge456'); await receipt.refresh(); process.stdout.write(JSON.stringify({captured,calls,restored:receipt.restore()})); })(); """) assert output["captured"]["source_branch"] == "timmy/feature" assert output["calls"][0] == { "path": "api/v1/repos/stackchain/api/pulls/7/source-branch", "method": "DELETE", "body": json.dumps({ "source_branch": "timmy/feature", "expected_head_sha": "abc1234", }, separators=(",", ":")), } restored = output["restored"][0] assert restored["cleanup"] == {"state": "deleted", "message": "Source branch deleted."} assert restored["status"]["label"] == "Checks running" def test_receipt_distinguishes_failed_checks_waiting_for_release_and_exact_release(): output = run_node(r""" const values=new Map(); const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const responses=[ {commit_sha:'merge456',ci_state:'failure',checks:[{name:'unit',state:'success'},{name:'browser',state:'failure'}],release:null}, {commit_sha:'merge456',ci_state:'success',checks:[{name:'browser',state:'success'}],release:null}, {commit_sha:'merge456',ci_state:'success',checks:[{name:'browser',state:'success'}],release:{tag:'rc-42',url:'https://forge.example/git/x/releases/tag/rc-42',assets:[{name:'manifest.json',url:'https://forge.example/manifest'}]}}, ]; const receipt=createReleaseReceipt({storage,getLogin:()=> 'timmy',fetchJson:async()=>responses.shift()}); receipt.capture({repository:'stackchain/api',number:7,key:'stackchain/api#7'}, {merge_commit_sha:'merge456'}); (async()=>{ const failed=await receipt.refresh(); const waiting=await receipt.refresh(); const released=await receipt.refresh(); receipt.dismiss(); process.stdout.write(JSON.stringify({failed,waiting,released,restored:receipt.restore()})); })(); """) assert output["failed"][0]["label"] == "Checks failed" assert output["failed"][0]["checks"] == ["browser"] assert output["waiting"][0]["label"] == "Checks passed · waiting for release" assert output["released"][0]["label"] == "Released · rc-42" assert output["released"][0]["release"]["assets"][0]["name"] == "manifest.json" assert output["restored"] == [] def test_watchlist_preserves_distinct_merges_deduplicates_and_dismisses_one(): output = run_node(r""" const values=new Map(); const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const receipt=createReleaseReceipt({storage,getLogin:()=> 'timmy',fetchJson:async()=>{throw new Error('unused')}}); receipt.capture({repository:'stackchain/api',number:7,key:'stackchain/api#7'}, {merge_commit_sha:'merge456'}); receipt.capture({repository:'stackchain/web',number:8,key:'stackchain/web#8'}, {merge_commit_sha:'merge789'}); receipt.capture({repository:'stackchain/api',number:7,key:'stackchain/api#7'}, {merge_commit_sha:'merge456'}); const before=receipt.restore(); receipt.dismiss('stackchain/api', 'merge456'); const after=receipt.restore(); process.stdout.write(JSON.stringify({before,after,stored:JSON.parse([...values.values()][0])})); """) assert [item["commit_sha"] for item in output["before"]] == ["merge456", "merge789"] assert [item["commit_sha"] for item in output["after"]] == ["merge789"] assert output["stored"]["version"] == 2 assert len(output["stored"]["entries"]) == 1 def test_watchlist_refreshes_every_exact_commit_in_one_single_flight(): output = run_node(r""" const values=new Map(); const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const calls=[]; const receipt=createReleaseReceipt({storage,getLogin:()=> 'timmy',fetchJson:async path=>{ calls.push(path); await new Promise(resolve=>setTimeout(resolve, 5)); const sha=path.split('/').pop(); return sha === 'merge456' ? {commit_sha:sha,ci_state:'failure',checks:[{name:'browser',state:'failure'}],release:null} : {commit_sha:sha,ci_state:'success',checks:[],release:{tag:'rc-9',url:'https://forge.example/rc-9'}}; }}); receipt.capture({repository:'stackchain/api',number:7,key:'stackchain/api#7'}, {merge_commit_sha:'merge456'}); receipt.capture({repository:'stackchain/web',number:8,key:'stackchain/web#8'}, {merge_commit_sha:'merge789'}); (async()=>{ const [first,second]=await Promise.all([receipt.refresh(),receipt.refresh()]); process.stdout.write(JSON.stringify({first,second,calls,restored:receipt.restore()})); })(); """) assert output["calls"] == [ "api/v1/repos/stackchain/api/release-receipt/merge456", "api/v1/repos/stackchain/web/release-receipt/merge789", ] assert [status["label"] for status in output["first"]] == ["Checks failed", "Released · rc-9"] assert output["second"] == output["first"] assert [item["status"]["commit_sha"] for item in output["restored"]] == ["merge456", "merge789"] def test_watchlist_renders_failed_merges_first_and_dismisses_individually(): output = run_node(r""" function node(tag='div') { return {tag,children:[],hidden:false,textContent:'',append(...xs){this.children.push(...xs)},replaceChildren(...xs){this.children=[...xs]},setAttribute(k,v){this[k]=v},addEventListener(k,fn){this[k]=fn}}; } global.document={createElement:tag=>node(tag)}; const values=new Map(); const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const launcher=node('button'), listNode=node(); const receipt=createReleaseReceipt({storage,getLogin:()=> 'timmy',launcher,listNode,fetchJson:async path=>{ const sha=path.split('/').pop(); return sha === 'failed' ? {commit_sha:sha,ci_state:'failure',checks:[{name:'browser',state:'failure'}]} : {commit_sha:sha,ci_state:'pending',checks:[{name:'unit',state:'pending'}]}; }}); receipt.capture({repository:'stackchain/api',number:1,key:'stackchain/api#1'}, {merge_commit_sha:'pending'}); receipt.capture({repository:'stackchain/web',number:2,key:'stackchain/web#2'}, {merge_commit_sha:'failed'}); (async()=>{ await receipt.refresh(); const before=listNode.children.map(row=>({key:row.children[0].children[0].textContent,state:row.children[0].children[1].textContent,label:row.children[1]['aria-label']})); const beforeLauncher=launcher.textContent; listNode.children[0].children[1].click(); process.stdout.write(JSON.stringify({before,launcher:beforeLauncher,after:receipt.restore()})); })(); """) assert output["before"] == [ {"key": "stackchain/web#2", "state": "Checks failed", "label": "Dismiss stackchain/web#2 from release tracking"}, {"key": "stackchain/api#1", "state": "Checks running", "label": "Dismiss stackchain/api#1 from release tracking"}, ] assert output["launcher"] == "1 release failure · 2 tracked" assert [item["commit_sha"] for item in output["after"]] == ["pending"] def test_watchlist_renders_confirmed_touch_cleanup_for_the_exact_source_branch(): output = run_node(r""" function node(tag='div') { return {tag,children:[],hidden:false,textContent:'',disabled:false,append(...xs){this.children.push(...xs)},replaceChildren(...xs){this.children=[...xs]},setAttribute(k,v){this[k]=v},addEventListener(k,fn){this[k]=fn}}; } global.document={createElement:tag=>node(tag)}; const values=new Map(), calls=[], listNode=node(); const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const receipt=createReleaseReceipt({storage,getLogin:()=> 'timmy',listNode,confirmAction:()=>true,fetchJson:async(path,options={})=>{calls.push({path,method:options.method});return {deleted:true};}}); receipt.capture({repository:'stackchain/api',number:7,key:'stackchain/api#7'}, {merge_commit_sha:'merge456',source_branch:'timmy/feature',source_head_sha:'abc1234',source_repository:'stackchain/api'}); const row=listNode.children[0], copy=row.children[0], actions=row.children[1], cleanup=actions.children[0]; (async()=>{ await cleanup.click(); process.stdout.write(JSON.stringify({branch:copy.children[2].textContent,cleanupText:cleanup.textContent,minLabel:cleanup['aria-label'],calls,after:receipt.restore()[0].cleanup})); })(); """) assert output["branch"] == "Branch timmy/feature · abc1234" assert output["cleanupText"] == "Delete source branch" assert output["minLabel"] == "Delete merged source branch timmy/feature at abc1234" assert output["calls"][0]["method"] == "DELETE" assert output["after"]["state"] == "deleted" def test_watchlist_polls_pending_merges_only_while_foregrounded(): output = run_node(r""" const values=new Map(), scheduled=[], calls=[]; const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const documentRef={hidden:false,addEventListener(type,fn){this[type]=fn}}; const windowRef={addEventListener(type,fn){this[type]=fn}}; const receipt=createReleaseReceipt({storage,getLogin:()=> 'timmy',documentRef,windowRef,pollMs:25000, setTimer:(fn,ms)=>{scheduled.push({fn,ms});return scheduled.length},clearTimer:()=>{}, fetchJson:async path=>{calls.push(path);const sha=path.split('/').pop();return {commit_sha:sha,ci_state:'success',checks:[],release:{tag:'rc-'+sha,url:'https://forge.example/'+sha}};}}); receipt.bind(); receipt.capture({repository:'stackchain/api',number:1,key:'stackchain/api#1'}, {merge_commit_sha:'one'}); receipt.capture({repository:'stackchain/web',number:2,key:'stackchain/web#2'}, {merge_commit_sha:'two'}); (async()=>{ const first=scheduled.at(-1); await first.fn(); documentRef.hidden=true; documentRef.visibilitychange(); process.stdout.write(JSON.stringify({delay:first.ms,calls,scheduled:scheduled.length,restored:receipt.restore()})); })(); """) assert output["delay"] == 25000 assert output["calls"] == [ "api/v1/repos/stackchain/api/release-receipt/one", "api/v1/repos/stackchain/web/release-receipt/two", ] assert output["scheduled"] == 2 assert all(item["status"]["release"] for item in output["restored"]) def test_watchlist_keeps_refreshing_other_merges_when_one_status_request_fails(): output = run_node(r""" const values=new Map(); const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const receipt=createReleaseReceipt({storage,getLogin:()=> 'timmy',fetchJson:async path=>{ const sha=path.split('/').pop(); if (sha === 'offline') throw new Error('Network unavailable'); return {commit_sha:sha,ci_state:'success',checks:[],release:{tag:'rc-ok',url:'https://forge.example/rc-ok'}}; }}); receipt.capture({repository:'stackchain/api',number:1,key:'stackchain/api#1'}, {merge_commit_sha:'offline'}); receipt.capture({repository:'stackchain/web',number:2,key:'stackchain/web#2'}, {merge_commit_sha:'released'}); (async()=>{ const statuses=await receipt.refresh(); process.stdout.write(JSON.stringify({statuses,entries:receipt.restore()})); })(); """) assert output["statuses"][0]["label"] == "Status unavailable" assert output["statuses"][1]["label"] == "Released · rc-ok" assert output["entries"][0]["status"]["error"] == "Network unavailable" assert output["entries"][1]["status"]["release"]["url"] == "https://forge.example/rc-ok" def test_watchlist_exposes_release_link_on_the_matching_merge_row(): output = run_node(r""" function node(tag='div') { return {tag,children:[],hidden:false,textContent:'',append(...xs){this.children.push(...xs)},replaceChildren(...xs){this.children=[...xs]},setAttribute(k,v){this[k]=v},addEventListener(){}}; } global.document={createElement:tag=>node(tag)}; const values=new Map(), listNode=node(); const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const receipt=createReleaseReceipt({storage,getLogin:()=> 'timmy',listNode,fetchJson:async()=>({commit_sha:'released',ci_state:'success',checks:[],release:{tag:'rc-12',url:'https://forge.example/rc-12'}})}); receipt.capture({repository:'stackchain/web',number:2,key:'stackchain/web#2'}, {merge_commit_sha:'released'}); (async()=>{ await receipt.refresh(); const link=listNode.children[0].children[0].children[2]; process.stdout.write(JSON.stringify({tag:link.tag,href:link.href,text:link.textContent})); })(); """) assert output == {"tag": "a", "href": "https://forge.example/rc-12", "text": "Open release rc-12"} def test_mobile_release_receipt_is_wired_into_the_merge_flow_and_phone_safe(): root = Path(__file__).parents[1] html = (root / "frontend" / "index.html").read_text() css = (root / "frontend" / "dashboard.css").read_text() dashboard = (root / "frontend" / "dashboard.js").read_text() assert '' in html assert 'id="release-receipt-launcher"' in html assert 'id="release-receipt-sheet"' in html assert 'id="release-watchlist"' in html assert 'releaseReceipt.capture(merging, mergeResult)' in dashboard assert "listNode:qs('#release-watchlist')" in dashboard assert "min-height:44px" in css[css.index(".release-receipt-sheet"):] assert "overflow-x:hidden" in css[css.index(".release-receipt-sheet"):] assert ".release-watchlist-item" in css assert ".release-watchlist-actions" in css cleanup_css = css[css.index(".release-watchlist-actions"):] assert "min-height:44px" in cleanup_css assert "grid-template-columns:1fr" in cleanup_css