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_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_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 '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 ".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