import json import subprocess from pathlib import Path ROOT = Path(__file__).parents[1] TODAY_PROGRESS = ROOT / "frontend" / "today-progress.js" def run_node(script: str) -> dict: result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr return json.loads(result.stdout) def test_progress_drafts_are_account_and_item_scoped_bounded_and_durable(): script = f""" const createProgress = require({json.dumps(str(TODAY_PROGRESS))}); 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 progress = createProgress({{storage, getLogin:() => login, admit:async () => ({{}}), maxLength:20}}); const issue = {{identity:'issue:stackchain/dashboard:12:', kind:'issue', repository:'stackchain/dashboard', number:12}}; const pull = {{identity:'pull:stackchain/dashboard:13:', kind:'pull', repository:'stackchain/dashboard', number:13}}; const saved = progress.save(issue.identity, ' Investigated latency '); progress.save(pull.identity, 'Reviewing tests'); const restored = createProgress({{storage, getLogin:() => login, admit:async () => ({{}}), maxLength:20}}); login = 'alexander'; const otherAccount = restored.load(issue.identity); login = 'timmy'; process.stdout.write(JSON.stringify({{ saved, issue:restored.load(issue.identity), pull:restored.load(pull.identity), otherAccount, tooLong:restored.save(issue.identity, 'x'.repeat(21)), keys:[...values.keys()], }})); """ output = run_node(script) assert output["saved"] is True assert output["issue"] == "Investigated latency" assert output["pull"] == "Reviewing tests" assert output["otherAccount"] == "" assert output["tooLong"] is False assert output["keys"] == ["stackchain.today-progress.v1.timmy"] def test_post_admits_exact_comment_once_and_clears_only_after_durable_admission(): script = f""" const createProgress = require({json.dumps(str(TODAY_PROGRESS))}); 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=[]; let fail=true; const progress=createProgress({{ storage, getLogin:()=> 'timmy', makeId:()=> 'progress-op-1', admit:async message => {{ calls.push(message); if (fail) throw new Error('offline store unavailable'); return {{background:true,item:{{id:message.operationId}}}}; }}, }}); const target={{identity:'issue:stackchain/dashboard:12:',kind:'issue',repository:'stackchain/dashboard',number:12}}; progress.save(target.identity, 'Shipped the first slice'); let error=''; try {{ await progress.post(target); }} catch (caught) {{ error=caught.message; }} const retained=progress.load(target.identity); fail=false; const admitted=await progress.post(target); process.stdout.write(JSON.stringify({{error,retained,after:progress.load(target.identity),calls,admitted}})); """ output = run_node("(async()=>{" + script + "})().catch(error=>{console.error(error);process.exit(1)})") assert output["error"] == "offline store unavailable" assert output["retained"] == "Shipped the first slice" assert output["after"] == "" assert len(output["calls"]) == 2 assert output["calls"][0] == output["calls"][1] == { "kind": "issue-comment", "repository": "stackchain/dashboard", "number": 12, "body": "Shipped the first slice", "operationId": "progress-op-1", } assert output["admitted"]["background"] is True def test_photo_only_progress_is_durably_admitted_and_retained_until_success(): script = f""" const createProgress = require({json.dumps(str(TODAY_PROGRESS))}); 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 photo = {{filename:'result.jpg',contentType:'image/jpeg',blob:{{size:42}},operationId:'photo-op-1'}}; const calls=[]; let fail=true; const progress=createProgress({{ storage, getLogin:()=> 'timmy', makeId:()=> 'progress-photo-op-1', admit:async message => {{ calls.push(message); if (fail) throw new Error('photo store unavailable'); return {{background:true}}; }}, }}); const target={{identity:'issue:stackchain/dashboard:12:',kind:'issue',repository:'stackchain/dashboard',number:12}}; let error=''; try {{ await progress.post(target, '', [photo]); }} catch (caught) {{ error=caught.message; }} const retained=progress.load(target.identity); fail=false; await progress.post(target, undefined, [photo]); process.stdout.write(JSON.stringify({{error,retained,after:progress.load(target.identity),calls}})); """ output = run_node("(async()=>{" + script + "})().catch(error=>{console.error(error);process.exit(1)})") assert output["error"] == "photo store unavailable" assert output["retained"] == "" assert output["after"] == "" assert len(output["calls"]) == 2 assert output["calls"][0] == output["calls"][1] == { "kind": "issue-comment", "repository": "stackchain/dashboard", "number": 12, "body": "", "operationId": "progress-photo-op-1", "attachments": [{ "filename": "result.jpg", "contentType": "image/jpeg", "blob": {"size": 42}, "operationId": "photo-op-1", }], } def test_photo_payload_changes_rotate_the_progress_operation_identity(): script = f""" const createProgress = require({json.dumps(str(TODAY_PROGRESS))}); const values = new Map(); const ids = ['progress-1', 'progress-2', 'progress-3']; const storage = {{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}}; const progress=createProgress({{storage,getLogin:()=> 'timmy',makeId:()=>ids.shift(),admit:async()=>({{}})}}); const identity='issue:stackchain/dashboard:12:'; const before={{filename:'proof.jpg',contentType:'image/jpeg',note:'Before',operationId:'photo-1'}}; const replacement={{filename:'proof.jpg',contentType:'image/jpeg',note:'After',operationId:'photo-2'}}; progress.save(identity, 'Repair complete', [before]); const first=JSON.parse(values.values().next().value).drafts[identity].operation_id; progress.save(identity, 'Repair complete', [before]); const unchanged=JSON.parse(values.values().next().value).drafts[identity].operation_id; progress.save(identity, 'Repair complete', [replacement]); const replaced=JSON.parse(values.values().next().value).drafts[identity].operation_id; replacement.note='Redacted replacement'; progress.save(identity, 'Repair complete', [replacement]); const recaptioned=JSON.parse(values.values().next().value).drafts[identity].operation_id; process.stdout.write(JSON.stringify({{first,unchanged,replaced,recaptioned}})); """ output = run_node(script) assert output == { "first": "progress-1", "unchanged": "progress-1", "replaced": "progress-2", "recaptioned": "progress-3", } def test_photo_cleanup_failure_resumes_cleanup_without_readmitting_delivery(): script = f""" const createProgress = require({json.dumps(str(TODAY_PROGRESS))}); 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=[]; let cleanupFails=true; let cleanupCalls=0; const options={{storage,getLogin:()=> 'timmy',makeId:()=> 'stable-progress-op',admit:async message=>{{calls.push(message);return {{background:true}};}}}}; let progress=createProgress(options); const target={{identity:'pull:stackchain/dashboard:22:',kind:'pull',repository:'stackchain/dashboard',number:22}}; const photo={{filename:'proof.webp',contentType:'image/webp',blob:{{size:9}},operationId:'photo-op'}}; const cleanup=async()=>{{cleanupCalls++;if(cleanupFails)throw new Error('draft cleanup blocked')}}; let error=''; let deliveryAdmitted=false; try {{await progress.post(target,'',[photo],cleanup);}} catch(caught){{error=caught.message;deliveryAdmitted=caught.deliveryAdmitted===true;}} progress=createProgress(options); cleanupFails=false; const recovered=await progress.post(target,'edited text must not be admitted',[{{...photo,operationId:'replacement'}}],cleanup); process.stdout.write(JSON.stringify({{error,deliveryAdmitted,calls,cleanupCalls,recovered,after:progress.load(target.identity)}})); """ output = run_node("(async()=>{" + script + "})().catch(error=>{console.error(error);process.exit(1)})") assert output["error"] == "Progress update is already queued; photo cleanup is pending." assert output["deliveryAdmitted"] is True assert len(output["calls"]) == 1 assert output["cleanupCalls"] == 2 assert output["recovered"]["alreadyAdmitted"] is True assert output["after"] == "" def test_progress_rejects_inactive_or_unsupported_targets_without_mutation(): script = f""" const createProgress = require({json.dumps(str(TODAY_PROGRESS))}); const values=new Map(); let admitted=0; const storage={{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}}; const progress=createProgress({{storage,getLogin:()=> 'timmy',admit:async()=>{{admitted++;}}}}); const results=[]; for (const target of [null, {{identity:'review:r:1:',kind:'review',repository:'r',number:1}}, {{identity:'issue:r:0:',kind:'issue',repository:'r',number:0}}]) {{ try {{ await progress.post(target, 'note'); results.push('accepted'); }} catch (error) {{ results.push(error.message); }} }} process.stdout.write(JSON.stringify({{results,admitted,keys:[...values.keys()]}})); """ output = run_node("(async()=>{" + script + "})().catch(error=>{console.error(error);process.exit(1)})") assert output["admitted"] == 0 assert output["keys"] == [] assert output["results"] == [ "An active Today issue or pull request is required.", "An active Today issue or pull request is required.", "An active Today issue or pull request is required.", ] def test_progress_sheet_scopes_voice_to_the_open_item_and_aborts_on_close(): script = f""" const {{createView}}=require({json.dumps(str(TODAY_PROGRESS))}); class Element {{ constructor() {{ this.hidden=false;this.value='';this.textContent='';this.open=false;this.listeners={{}}; }} addEventListener(name,callback) {{ this.listeners[name]=callback; }} click() {{ return this.listeners.click?.(); }} showModal() {{ this.open=true; }} close() {{ this.open=false; }} focus() {{}} }} const selectors=['#today-progress-sheet','#today-progress-body','#today-progress-status','[data-mobile-today-update]', '#today-progress-target','#cancel-today-progress','#save-today-progress','#post-today-progress']; const elements=Object.fromEntries(selectors.map(selector=>[selector,new Element()])); const target={{identity:'issue:stackchain/dashboard:1058:',kind:'issue',repository:'stackchain/dashboard',number:1058,label:'#1058',title:'Voice progress'}}; const calls=[]; const voice={{open:async identity=>calls.push(['open',identity]),cancel:()=>calls.push(['cancel'])}}; const activity={{open:async value=>calls.push(['activity-open',value.identity]),close:()=>calls.push(['activity-close'])}}; const view=createView({{ progress:{{load:()=> 'Saved text',save:()=>true}},currentTarget:()=>target,qs:selector=>elements[selector],voice,activity, photos:{{open:async()=>calls.push(['photos'])}}, }}); (async()=>{{ await elements['[data-mobile-today-update]'].click(); const opened={{body:elements['#today-progress-body'].value,sheet:elements['#today-progress-sheet'].open,calls:[...calls]}}; await elements['#cancel-today-progress'].click(); process.stdout.write(JSON.stringify({{opened,closed:!elements['#today-progress-sheet'].open,calls}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ assert run_node(script) == { "opened": { "body": "Saved text", "sheet": True, "calls": [["open", "issue:stackchain/dashboard:1058:"], ["photos"], ["activity-open", "issue:stackchain/dashboard:1058:"]], }, "closed": True, "calls": [ ["open", "issue:stackchain/dashboard:1058:"], ["photos"], ["activity-open", "issue:stackchain/dashboard:1058:"], ["cancel"], ["activity-close"], ], } def test_recent_activity_loads_the_exact_issue_newest_page_before_rendering(): script = f""" const {{createActivity}}=require({json.dumps(str(TODAY_PROGRESS))}); const calls=[]; const paints=[]; const page={{comments:[{{id:2,body:'Latest',user:{{login:'alex'}},created_at:'2026-08-18T01:00:00Z'}}],page:3,older_page:2,total:41}}; const activity=createActivity({{ fetchJson:async path=>{{calls.push(path);return page;}}, createPager:({{loadPage}})=>({{reset:value=>value,loadOlder:()=>loadPage(2)}}), paint:value=>paints.push(value), setStatus:value=>calls.push('status:'+value), }}); const target={{identity:'issue:stackchain/dashboard:1060:',kind:'issue',repository:'stackchain/dashboard',number:1060}}; (async()=>{{await activity.open(target);process.stdout.write(JSON.stringify({{calls,paints}}));}})() .catch(error=>{{console.error(error);process.exit(1);}}); """ output = run_node(script) assert output["calls"] == [ "status:Loading recent activity…", "api/v1/repos/stackchain/dashboard/issues/1060/comments?limit=20", "status:", ] assert output["paints"] == [{ "comments": [{ "id": 2, "body": "Latest", "user": {"login": "alex"}, "created_at": "2026-08-18T01:00:00Z", }], "page": 3, "older_page": 2, "total": 41, }] def test_recent_activity_rejects_a_slow_response_after_close(): script = f""" const {{createActivity}}=require({json.dumps(str(TODAY_PROGRESS))}); let resolve; const paints=[]; const statuses=[]; const activity=createActivity({{ fetchJson:()=>new Promise(done=>{{resolve=done;}}), createPager:()=>({{reset:value=>value}}), paint:value=>paints.push(value), setStatus:value=>statuses.push(value), }}); const target={{identity:'pull:stackchain/dashboard:9:',kind:'pull',repository:'stackchain/dashboard',number:9}}; (async()=>{{const pending=activity.open(target);activity.close();resolve({{comments:[{{id:1}}]}});const result=await pending;process.stdout.write(JSON.stringify({{result,paints,statuses}}));}})() .catch(error=>{{console.error(error);process.exit(1);}}); """ assert run_node(script) == { "result": False, "paints": [], "statuses": ["Loading recent activity…", ""], } def test_recent_activity_retry_recovers_the_same_pull_after_failure(): script = f""" const {{createActivity}}=require({json.dumps(str(TODAY_PROGRESS))}); const calls=[]; const paints=[]; let fail=true; const activity=createActivity({{ fetchJson:async path=>{{calls.push(path);if(fail)throw new Error('offline');return {{comments:[],page:1,older_page:null,total:0}};}}, createPager:()=>({{reset:value=>value}}), paint:value=>paints.push(value), }}); const target={{identity:'pull:stackchain/dashboard:9:',kind:'pull',repository:'stackchain/dashboard',number:9}}; (async()=>{{const opened=await activity.open(target);fail=false;const retried=await activity.retry();process.stdout.write(JSON.stringify({{opened,retried,calls,paints}}));}})() .catch(error=>{{console.error(error);process.exit(1);}}); """ output = run_node(script) assert output["opened"] is False assert output["retried"] is True assert output["calls"] == [ "api/v1/repos/stackchain/dashboard/pulls/9/comments?limit=20", "api/v1/repos/stackchain/dashboard/pulls/9/comments?limit=20", ] assert output["paints"] == [{"comments": [], "page": 1, "older_page": None, "total": 0}] def test_recent_activity_load_older_paints_the_deduplicated_page(): script = f""" const {{createActivity}}=require({json.dumps(str(TODAY_PROGRESS))}); const calls=[]; const paints=[]; const activity=createActivity({{ fetchJson:async path=>{{calls.push(path);return {{comments:[],page:2,older_page:null,total:20}};}}, createPager:({{loadPage}})=>({{reset:value=>value,loadOlder:async()=>{{await loadPage(2);return {{comments:[{{id:1}}],page:2,older_page:null,total:21}};}}}}), paint:value=>paints.push(value), }}); const target={{identity:'issue:stackchain/dashboard:1060:',kind:'issue',repository:'stackchain/dashboard',number:1060}}; (async()=>{{await activity.open(target);const loaded=await activity.loadOlder();process.stdout.write(JSON.stringify({{loaded,calls,paints}}));}})() .catch(error=>{{console.error(error);process.exit(1);}}); """ output = run_node(script) assert output["loaded"] is True assert output["calls"][-1] == "api/v1/repos/stackchain/dashboard/issues/1060/comments?page=2&limit=20" assert output["paints"][-1]["comments"] == [{"id": 1}] def test_recent_activity_mount_renders_owned_actions_on_the_exact_comment_card(): script = f""" const {{mountActivity}}=require({json.dumps(str(TODAY_PROGRESS))}); const createPager=require({json.dumps(str(ROOT / 'frontend' / 'conversation.js'))}); class Element {{ constructor(){{this.innerHTML='';this.textContent='';this.hidden=false;this.listeners={{}};this.scrollHeight=0;this.scrollTop=0;}} addEventListener(name,callback){{this.listeners[name]=callback;}} }} const selectors=['#today-progress-activity-list','#today-progress-activity-count','#load-older-today-progress-activity', '#today-progress-activity-status','#retry-today-progress-activity']; const elements=Object.fromEntries(selectors.map(selector=>[selector,new Element()])); const actions={{actionHtml:comment=>comment.author==='timmy'?'':''}}; const activity=mountActivity(selector=>elements[selector],async()=>({{ comments:[{{id:17,author:'timmy',body:'Needs correction',created_at:'2026-08-18T00:00:00Z'}}],page:1,older_page:null,total:1, }}),{{getActions:async()=>actions,createPager,escape:value=>String(value),render:value=>String(value)}}); (async()=>{{ await activity.open({{identity:'issue:stackchain/dashboard:1062:',kind:'issue',repository:'stackchain/dashboard',number:1062}}); process.stdout.write(JSON.stringify({{html:elements['#today-progress-activity-list'].innerHTML,count:elements['#today-progress-activity-count'].textContent}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ output = run_node(script) assert 'class="today-progress-activity-item issue-comment"' in output["html"] assert 'data-comment-id="17"' in output["html"] assert 'data-comment-action="edit"' in output["html"] assert output["count"] == "1 of 1 messages" def test_recent_activity_edit_control_preserves_a_failed_correction_for_retry(): script = f""" const {{mountActivity}}=require({json.dumps(str(TODAY_PROGRESS))}); const createPager=require({json.dumps(str(ROOT / 'frontend' / 'conversation.js'))}); const createActions=require({json.dumps(str(ROOT / 'frontend' / 'comment-actions.js'))}); class Element {{constructor(){{this.innerHTML='';this.textContent='';this.hidden=false;this.listeners={{}};this.scrollHeight=0;this.scrollTop=0;}}addEventListener(n,c){{this.listeners[n]=c;}}}} const selectors=['#today-progress-activity-list','#today-progress-activity-count','#load-older-today-progress-activity','#today-progress-activity-status','#retry-today-progress-activity']; const elements=Object.fromEntries(selectors.map(selector=>[selector,new Element()])); let state={{comments:[{{id:17,author:'timmy',body:'Wrong'}}],page:1,older_page:null,total:1}}; let attempted=''; const actions=createActions({{ getLogin:()=> 'timmy', fetchJson:async(_path,options)=>{{attempted=JSON.parse(options.body).body;throw new Error('network unavailable');}}, }}); const activity=mountActivity(selector=>elements[selector],async()=>state,{{getActions:async()=>actions,createPager,escape:value=>String(value),render:value=>String(value),isOffline:()=>false}}); const textarea={{value:'Corrected transcript',focuses:0,focus(){{this.focuses++;}}}}; const save={{disabled:false,listeners:{{}},addEventListener(n,c){{this.listeners[n]=c;}}}}; const cancel={{listeners:{{}},addEventListener(n,c){{this.listeners[n]=c;}}}}; const card={{dataset:{{commentId:'17'}},innerHTML:'',querySelector:selector=>({{'.comment-edit-textarea':textarea,'[data-comment-edit-save]':save,'[data-comment-edit-cancel]':cancel}})[selector]}}; const button={{dataset:{{commentAction:'edit'}},closest:selector=>selector==='[data-comment-action]'?button:card}}; (async()=>{{ await activity.open({{identity:'issue:stackchain/dashboard:1062:',kind:'issue',repository:'stackchain/dashboard',number:1062}}); await elements['#today-progress-activity-list'].listeners.click({{target:button}}); await save.listeners.click({{currentTarget:save}}); process.stdout.write(JSON.stringify({{attempted,value:textarea.value,focuses:textarea.focuses,disabled:save.disabled,status:elements['#today-progress-activity-status'].textContent}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ output = run_node(script) assert output == { "attempted": "Corrected transcript", "value": "Corrected transcript", "focuses": 2, "disabled": False, "status": "network unavailable", } def test_mobile_progress_sheet_is_accessible_bundled_and_safe_area_aware(): html = (ROOT / "frontend" / "index.html").read_text() css = (ROOT / "frontend" / "dashboard.css").read_text() dashboard = (ROOT / "frontend" / "dashboard.js").read_text() bundle = (ROOT / "src" / "frontend_bundle.py").read_text() assert 'data-mobile-today-update' in html assert 'id="today-progress-sheet"' in html assert 'aria-labelledby="today-progress-title"' in html assert 'id="today-progress-body"' in html assert 'id="today-progress-activity"' in html assert 'id="today-progress-activity-list"' in html assert 'id="today-progress-activity-status"' in html assert 'id="retry-today-progress-activity"' in html assert 'id="load-older-today-progress-activity"' in html for control in ( "voice-today-progress", "start-voice-today-progress", "stop-voice-today-progress", "voice-today-progress-review", "voice-today-progress-transcript", "append-voice-today-progress", "replace-with-voice-today-progress", "discard-voice-today-progress", "voice-today-progress-status", ): assert f'id="{control}"' in html assert 'for="voice-today-progress-transcript"' in html assert 'data-draft-label="progress update"' in html assert 'id="take-today-progress-photo"' in html assert 'id="today-progress-attachment"' in html assert 'id="today-progress-attachment-preview"' in html assert 'maxlength="2000"' in html assert 'id="save-today-progress"' in html assert 'id="post-today-progress"' in html assert '' in html assert '"static/today-progress.js"' in bundle assert "workSession.target('continue')" in dashboard assert "authoredOutbox.enqueueDurably" in dashboard assert "createTodayProgressPhotos" in dashboard assert "scope:'today-progress'" in TODAY_PROGRESS.read_text() assert "lanes:{ today:" in TODAY_PROGRESS.read_text() assert "photos:todayProgressPhotos" in dashboard assert "['today-progress', '#today-progress-body']" in dashboard assert "voice:todayProgressVoice" in dashboard assert "mountTodayProgressActivity" in dashboard assert "activity:mountTodayProgressActivity" in dashboard assert "fetchReviewJson,actionHydrator" in dashboard assert ".today-progress-activity-list" in css assert ".today-progress-activity-actions button" in css assert "min-height:44px" in css assert "const controller = issueAttachment.mount" in TODAY_PROGRESS.read_text() assert "const checkpointAttachments = await photos?.serialize?.() || [];" in TODAY_PROGRESS.read_text() assert "progress.save(openedTarget.identity, body.value, checkpointAttachments)" in TODAY_PROGRESS.read_text() assert "error.deliveryAdmitted" in TODAY_PROGRESS.read_text() assert ".today-progress-panel" in css assert "env(safe-area-inset-bottom)" in css assert ".today-progress-actions button" in css and "min-height:44px" in css assert ".today-progress-panel .voice-conversation-controls button" in css