import json import subprocess from pathlib import Path ROOT = Path(__file__).parents[1] TODAY_SUMMARY = ROOT / "frontend" / "today-summary.js" def run_node(script: str): result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr return json.loads(result.stdout) def test_summary_begins_with_exact_worked_and_tomorrow_items_but_keeps_time_private(): script = f""" const createSummary = require({json.dumps(str(TODAY_SUMMARY))}); 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), }}; const summary = createSummary({{storage,getLogin:()=> ' Timmy '}}); const snapshot = summary.begin( [{{identity:'issue:acme/mobile:41:',title:'Ship mobile capture',context:'acme/mobile#41',actual_minutes:42}}], [{{identity:'issue:acme/mobile:42:',title:'Review release',context:'acme/mobile#42'}}], ); process.stdout.write(JSON.stringify({{ snapshot, text:summary.text(), keys:[...values.keys()], }})); """ output = run_node(script) assert output["snapshot"]["worked"] == [{ "identity": "issue:acme/mobile:41:", "title": "Ship mobile capture", "context": "acme/mobile#41", "actual_minutes": 42, "include": True, }] assert output["snapshot"]["tomorrow"] == [{ "identity": "issue:acme/mobile:42:", "title": "Review release", "context": "acme/mobile#42", "include": True, }] assert output["snapshot"]["include_actuals"] is False assert output["text"] == "Today\n- Ship mobile capture\n\nTomorrow\n- Review release" assert output["keys"] == ["stackchain.today-summary-draft.v1.timmy"] def test_summary_privacy_choices_and_note_survive_reload_for_the_same_account(): script = f""" const createSummary = require({json.dumps(str(TODAY_SUMMARY))}); 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), }}; const options = {{storage,getLogin:()=> 'timmy'}}; const summary = createSummary(options); summary.begin( [{{identity:'issue:acme/mobile:41:',title:'Ship mobile capture',context:'acme/mobile#41',actual_minutes:42}}], [{{identity:'issue:acme/mobile:42:',title:'Review release',context:'acme/mobile#42'}}], ); summary.includeActuals(true); summary.setNote(' Waiting on review '); summary.choose('tomorrow', 'issue:acme/mobile:42:', false); const restored = createSummary(options); process.stdout.write(JSON.stringify({{ restored:restored.restore(), snapshot:restored.snapshot(), text:restored.text(), }})); """ output = run_node(script) assert output["restored"] is True assert output["snapshot"]["include_actuals"] is True assert output["snapshot"]["note"] == "Waiting on review" assert output["snapshot"]["tomorrow"][0]["include"] is False assert output["text"] == "Today\n- Ship mobile capture (42m)\n\nNote\nWaiting on review" def test_summary_shares_natively_or_once_to_clipboard_and_keeps_canceled_drafts(): script = f""" const createSummary = require({json.dumps(str(TODAY_SUMMARY))}); 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), }}; const row = [{{identity:'issue:acme/mobile:41:',title:'Ship mobile capture',context:'acme/mobile#41',actual_minutes:42}}]; const canceled = createSummary({{storage,getLogin:()=> 'timmy',share:async()=>{{const error=new Error('cancel');error.name='AbortError';throw error;}}}}); canceled.begin(row, []); const canceledResult = await canceled.shareSummary(); const keptAfterCancel = values.has('stackchain.today-summary-draft.v1.timmy'); const copies = []; const fallback = createSummary({{storage,getLogin:()=> 'timmy',copy:async text=>copies.push(text)}}); fallback.restore(); const copiedResult = await fallback.shareSummary(); process.stdout.write(JSON.stringify({{ canceledResult, keptAfterCancel, copiedResult, copies, keptAfterCopy:values.has('stackchain.today-summary-draft.v1.timmy'), }})); """ output = run_node(f"(async()=>{{{script}}})().catch(error=>{{console.error(error);process.exit(1);}})") assert output["canceledResult"] == {"status": "canceled"} assert output["keptAfterCancel"] is True assert output["copiedResult"] == {"status": "copied"} assert output["copies"] == ["Today\n- Ship mobile capture"] assert output["keptAfterCopy"] is False def test_summary_validates_an_exact_gitea_thread_before_durable_posting(): script = f""" const createSummary = require({json.dumps(str(TODAY_SUMMARY))}); 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), }}; const resolved=[]; const admitted=[]; const summary=createSummary({{ storage,getLogin:()=> 'timmy',createOperationId:()=> 'summary-op-1', resolveTarget:async target=>{{resolved.push(target);return{{...target,kind:'issue',title:'Daily status',state:'open'}};}}, enqueueDurably:async message=>{{admitted.push(message);return{{item:message,durability:'background'}};}}, }}); summary.begin([{{identity:'issue:acme/mobile:41:',title:'Ship mobile capture',actual_minutes:42}}], []); const parsed=summary.setDestination(' StackChain/Status #42 '); const target=await summary.validateDestination(); const result=await summary.postSummary(); process.stdout.write(JSON.stringify({{ parsed,target,result,resolved,admitted,snapshot:summary.snapshot(), kept:values.has('stackchain.today-summary-draft.v1.timmy'), }})); """ output = run_node(f"(async()=>{{{script}}})().catch(error=>{{console.error(error);process.exit(1);}})") assert output["parsed"] == { "repository": "stackchain/status", "number": 42, "label": "stackchain/status#42" } assert output["resolved"] == [{"repository": "stackchain/status", "number": 42}] assert output["target"] == { "repository": "stackchain/status", "number": 42, "kind": "issue", "title": "Daily status", "state": "open", } assert output["admitted"] == [{ "kind": "search-reply", "repository": "stackchain/status", "number": 42, "targetKind": "issue", "body": "Today\n- Ship mobile capture", "operationId": "summary-op-1", }] assert output["result"]["status"] == "queued" assert output["result"]["durability"] == "background" assert output["snapshot"] is None assert output["kept"] is False def test_summary_keeps_reviewed_destination_and_operation_when_admission_fails(): script = f""" const createSummary = require({json.dumps(str(TODAY_SUMMARY))}); 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), }}; const options={{ storage,getLogin:()=> 'timmy',createOperationId:()=> 'stable-op', resolveTarget:async target=>({{...target,kind:'pull',title:'Release candidate',state:'open'}}), enqueueDurably:async()=>{{throw new Error('storage unavailable');}}, }}; const summary=createSummary(options); summary.begin([{{identity:'pull:acme/mobile:9:',title:'Review release',actual_minutes:18}}], []); summary.setDestination('acme/mobile#9'); await summary.validateDestination(); let error=''; try {{ await summary.postSummary(); }} catch (caught) {{ error=caught.message; }} const restored=createSummary(options); process.stdout.write(JSON.stringify({{ error,restored:restored.restore(),snapshot:restored.snapshot(),text:restored.text(), }})); """ output = run_node(f"(async()=>{{{script}}})().catch(error=>{{console.error(error);process.exit(1);}})") assert output["error"] == "storage unavailable" assert output["restored"] is True assert output["snapshot"]["operation_id"] == "stable-op" assert output["snapshot"]["target"]["kind"] == "pull" assert output["text"] == "Today\n- Review release" def test_summary_coalesces_concurrent_post_taps_into_one_durable_admission(): script = f""" const createSummary = require({json.dumps(str(TODAY_SUMMARY))}); let release; const gate=new Promise(resolve=>release=resolve); const admitted=[]; const summary=createSummary({{ getLogin:()=> 'timmy',createOperationId:()=> 'one-operation', resolveTarget:async target=>({{...target,kind:'issue',title:'Status',state:'open'}}), enqueueDurably:async message=>{{admitted.push(message);await gate;return{{item:message,durability:'background'}};}}, }}); summary.begin([{{identity:'issue:acme/mobile:41:',title:'Ship mobile capture',actual_minutes:42}}], []); summary.setDestination('acme/mobile#41'); await summary.validateDestination(); const first=summary.postSummary(); const second=summary.postSummary(); release(); const results=await Promise.all([first,second]); process.stdout.write(JSON.stringify({{admitted,results}})); """ output = run_node(f"(async()=>{{{script}}})().catch(error=>{{console.error(error);process.exit(1);}})") assert len(output["admitted"]) == 1 assert output["results"][0] == output["results"][1] def test_dashboard_connects_recap_and_wrap_up_to_a_mobile_summary_review_sheet(): from src import main html = main.FRONTEND_BUILD.dashboard_html css = (ROOT / "frontend" / "dashboard.css").read_text() dashboard = (ROOT / "frontend" / "dashboard.js").read_text() recap = (ROOT / "frontend" / "today-recap.js").read_text() wrap_up = (ROOT / "frontend" / "today-wrap-up.js").read_text() assert 'id="today-summary-sheet" role="dialog"' in html assert 'id="today-summary-worked"' in html assert 'id="today-summary-tomorrow"' in html assert 'id="today-summary-include-actuals"' in html assert 'id="share-today-summary"' in html assert 'id="today-summary-destination"' in html assert 'id="validate-today-summary-destination"' in html assert 'id="post-today-summary"' in html assert 'id="today-summary-target"' in html assert "static/today-summary.js" in main.FRONTEND_BUILD.page_sources assert "setupTodaySummary({" in dashboard assert "enqueueDurably:message => authoredOutbox.enqueueDurably(message)" in dashboard assert "fetchJson:api" in dashboard assert "/preview?kind=issue" in (ROOT / "frontend" / "today-summary.js").read_text() assert "todaySummaryView.open(workedItems, tomorrowItems)" in dashboard assert "openWrapUp(handoff.actual_minutes, workedItems)" in recap assert "onComplete(result, actualMinutes, workedItems, tomorrowItems)" in wrap_up assert ".today-summary-actions button { min-height:44px;" in css assert ".today-summary-panel" in css and "overflow-x:hidden" in css assert ".today-summary-destination" in css and "overflow-wrap:anywhere" in css assert "tests/e2e/test_mobile_today_summary_release.py" in ( ROOT / ".gitea" / "workflows" / "ci.yml" ).read_text() def test_summary_uses_the_human_label_from_recap_feedback_rows(): script = f""" const createSummary = require({json.dumps(str(TODAY_SUMMARY))}); const summary = createSummary(); summary.begin([{{ identity:'issue:acme/mobile:41:', label:'Ship mobile capture', context:'acme/mobile#41', actual_minutes:42, }}], []); process.stdout.write(JSON.stringify({{text:summary.text()}})); """ assert run_node(script)["text"] == "Today\n- Ship mobile capture" def test_summary_draft_is_not_visible_after_the_confirmed_account_changes(): script = f""" const createSummary = require({json.dumps(str(TODAY_SUMMARY))}); 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 summary=createSummary({{storage,getLogin:()=>login}}); summary.begin([{{identity:'issue:acme/mobile:41:',title:'Private work',actual_minutes:42}}], []); login='alexander'; process.stdout.write(JSON.stringify({{snapshot:summary.snapshot(),text:summary.text()}})); """ assert run_node(script) == {"snapshot": None, "text": ""}