feat: review wrap-up commitments in Today (Closes #992)
CI / lint (pull_request) Successful in 2m56s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 1m50s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-16 23:15:00 +00:00
1 parent 40623f939c
commit 468676d2f4
27 files changed
+537 -53

No files matched your search

@@ -0,0 +1,65 @@
from __future__ import annotations
import os
import threading
from pathlib import Path
import pytest
if os.getenv("STACKCHAIN_RUN_RELEASE_E2E") != "1":
pytest.skip("packaged next-day Today handoff runs only in its gated CI job", allow_module_level=True)
pytest.importorskip("playwright.sync_api")
from playwright.sync_api import expect, sync_playwright
from fake_gitea import FakeGiteaServer
from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, release_server
def test_release_artifact_reviews_wrap_up_commitments_on_a_phone(tmp_path: Path):
archives = sorted((ROOT / "dist").glob("stackchain-dashboard-*.tar.gz"))
assert len(archives) == 1
fake = FakeGiteaServer(("127.0.0.1", 0))
thread = threading.Thread(target=fake.serve_forever, daemon=True)
thread.start()
errors: list[str] = []
try:
with release_server(archives[0], tmp_path, f"http://127.0.0.1:{fake.server_port}") as origin, sync_playwright() as playwright:
browser = playwright.chromium.launch(args=["--ignore-certificate-errors"])
page = browser.new_page(viewport={"width": 390, "height": 844})
page.on("pageerror", lambda error: errors.append(error.stack or str(error)))
page.goto(origin + "/", wait_until="networkidle")
page.locator('input[name="device_label"]').fill("Tomorrow review phone")
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
page.locator("#submit-sign-in").click()
page.wait_for_url(origin + "/", wait_until="networkidle")
page.evaluate("""localStorage.setItem('stackchain.today-handoff.v1.timmy', JSON.stringify([
'issue:acme/mobile:41:', 'issue:acme/mobile:42:'
]))""")
page.reload(wait_until="networkidle")
dialog = page.locator("#today-handoff-dialog")
expect(dialog).to_be_visible()
expect(page.locator("#today-handoff-items .today-handoff-item")).to_have_count(2)
for control in dialog.locator("button, input").all():
bounds = control.bounding_box()
assert bounds and bounds["height"] >= 24
buttons = dialog.locator("button")
for control in buttons.all():
bounds = control.bounding_box()
assert bounds and bounds["height"] >= 44
page.locator('[data-today-handoff="issue:acme/mobile:42:"]').uncheck()
page.locator("#confirm-today-handoff").click()
expect(dialog).to_be_hidden()
assert page.evaluate("JSON.parse(localStorage.getItem('stackchain.today-work.v1.timmy'))") == [
"issue:acme/mobile:41:"
]
assert page.evaluate("JSON.parse(localStorage.getItem('stackchain.today-handoff.v1.timmy'))") == [
"issue:acme/mobile:42:"
]
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
assert errors == []
browser.close()
finally:
fake.shutdown()
fake.server_close()
thread.join(timeout=5)
+2 -1
View File
@@ -60,6 +60,7 @@ def test_release_promotion_waits_for_artifact_mobile_offline_journey():
"tests/e2e/test_mobile_search_preview_navigation.py "
"tests/e2e/test_mobile_find_work_release.py "
"tests/e2e/test_mobile_today_handoff_release.py "
"tests/e2e/test_mobile_today_wrap_up_release.py -q"
"tests/e2e/test_mobile_today_wrap_up_release.py "
"tests/e2e/test_mobile_wrap_up_handoff_release.py -q"
) in browser
assert "needs: [lint, build-release, browser-journey]" in release
+1 -1
View File
@@ -303,4 +303,4 @@ async def test_unread_update_offers_reply_mark_read_and_next_independent_of_toda
assert '.update-reply-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
assert '.update-reply-actions button { min-height:44px;' in html
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v114" in worker
assert "stackchain-dashboard-shell-v115" in worker
+24
View File
@@ -65,6 +65,30 @@ def test_deferrals_are_durable_revisioned_idempotent_and_account_scoped(tmp_path
) == {"revision": 2, "records": {}}
def test_next_day_today_handoff_is_preserved_without_changing_legacy_deferrals(tmp_path):
store = LaterStore(tmp_path / "later.sqlite3")
legacy = store.apply(
"timmy", "legacy", "defer", "issue:r:1:",
wake_at="2026-08-17T09:00:00.000Z",
)
handoff = store.apply(
"timmy", "wrap-up", "defer", "issue:r:2:",
wake_at="2026-08-17T09:00:00.000Z",
handoff="today",
)
assert legacy["records"]["issue:r:1:"] == "2026-08-17T09:00:00.000Z"
assert handoff["records"] == {
"issue:r:1:": "2026-08-17T09:00:00.000Z",
"issue:r:2:": {
"wake_at": "2026-08-17T09:00:00.000Z",
"handoff": "today",
},
}
assert LaterStore(store.path).get("timmy") == handoff
def test_initialized_later_reads_remain_available_during_a_planning_write(tmp_path):
path = tmp_path / "later.sqlite3"
store = LaterStore(path, timeout=0.05)
+92 -4
View File
@@ -97,6 +97,35 @@ sync.enqueue('defer','issue:r:2:','2026-08-10T09:00:00.000Z');
assert result["pending"] == []
def test_today_handoff_intent_replays_through_account_sync():
script = f"""
const createLaterSync=require({json.dumps(str(LATER_SYNC))});
const values=new Map(); const requests=[];
const sync=createLaterSync({{
storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
getLogin:()=> 'timmy',createOperationId:()=> 'wrap-up-op',
fetchJson:async(_url,options={{}})=>{{
const body=JSON.parse(options.body); requests.push(body);
return {{revision:1,records:{{[body.operations[0].item_id]:{{wake_at:body.operations[0].wake_at,handoff:body.operations[0].handoff}}}},accepted_operation_ids:['wrap-up-op'],duplicate_operation_ids:[],rejected_operations:[]}};
}},onRemoteRecords:records=>{{globalThis.records=records}},onStatus:()=>{{}},
}});
sync.enqueue('defer','issue:r:2:','2026-08-17T09:00:00.000Z','today');
(async()=>{{await sync.flush();process.stdout.write(JSON.stringify({{requests,records:globalThis.records}}));}})();
"""
result = run_node(script)
assert result["requests"] == [{"operations": [{
"operation_id": "wrap-up-op",
"action": "defer",
"item_id": "issue:r:2:",
"wake_at": "2026-08-17T09:00:00.000Z",
"handoff": "today",
"base_revision": 0,
}]}]
assert result["records"] == {"issue:r:2:": {
"wake_at": "2026-08-17T09:00:00.000Z", "handoff": "today",
}}
def test_later_queue_expires_ancient_edits_before_replay():
script = f"""
const createLaterSync=require({json.dumps(str(LATER_SYNC))});
@@ -294,6 +323,26 @@ sync.startLifecycle({{window:{{addEventListener:(n,h)=>handlers[n]=h}},document:
}
def test_unsynced_wrap_up_handoff_migrates_after_browser_restart():
script = f"""
const createLaterSync=require({json.dumps(str(LATER_SYNC))});
const values=new Map();const requests=[];
const sync=createLaterSync({{
storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
getLogin:()=> 'timmy',createOperationId:()=> 'migrated-handoff',
fetchJson:async(_url,options={{}})=>{{const body=JSON.parse(options.body);requests.push(body);return {{revision:1,records:{{}},accepted_operation_ids:['migrated-handoff'],duplicate_operation_ids:[],rejected_operations:[]}}}},
onRemoteRecords:()=>{{}},onStatus:()=>{{}},
}});
sync.migrate({{'issue:r:1:':{{wake_at:'2026-08-17T09:00:00.000Z',handoff:'today'}}}});
(async()=>{{await sync.flush();process.stdout.write(JSON.stringify(requests));}})();
"""
assert run_node(script) == [{"operations": [{
"operation_id": "migrated-handoff", "action": "defer",
"item_id": "issue:r:1:", "wake_at": "2026-08-17T09:00:00.000Z",
"handoff": "today", "base_revision": 0,
}]}]
def test_later_work_emits_local_changes_adopts_remote_truth_and_retires_expiry():
script = f"""
const createLaterWork=require({json.dumps(str(LATER_WORK))});
@@ -324,6 +373,45 @@ process.stdout.write(JSON.stringify({{changes,expired,remote:remote.later,awake:
assert result["expired"] == [["issue:stackchain/api:17:"]]
def test_later_work_preserves_synced_today_handoff_until_it_wakes():
script = f"""
const createLaterWork=require({json.dumps(str(LATER_WORK))});
const values=new Map(); const changes=[]; const expired=[];
let clock=new Date('2026-08-16T20:00:00Z');
const item={{kind:'issue',repository:'stackchain/dashboard',number:992,title:'Commitment'}};
const work=createLaterWork({{
storage:{{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
getLogin:()=> 'timmy',now:()=>clock,setTimer:()=>1,clearTimer:()=>{{}},
onChange:(action,id,wakeAt,handoff)=>changes.push([action,id,wakeAt,handoff||null]),
onExpire:(ids,handoffs)=>expired.push([ids,handoffs]),
}});
work.defer(item,new Date('2026-08-17T09:00:00Z'),{{handoff:'today'}});
const stored=work.read();
clock=new Date('2026-08-17T09:00:01Z');
const awake=work.partition([item]);
process.stdout.write(JSON.stringify({{stored,changes,expired,awake:awake.active}}));
"""
result = run_node(script)
assert result["stored"] == {
"issue:stackchain/dashboard:992:": {
"wake_at": "2026-08-17T09:00:00.000Z",
"handoff": "today",
}
}
assert result["changes"] == [[
"defer", "issue:stackchain/dashboard:992:",
"2026-08-17T09:00:00.000Z", "today",
]]
assert result["expired"] == [[
["issue:stackchain/dashboard:992:"],
["issue:stackchain/dashboard:992:"],
]]
assert result["awake"] == [{
"kind": "issue", "repository": "stackchain/dashboard",
"number": 992, "title": "Commitment",
}]
@pytest.mark.anyio
async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
html = await dashboard()
@@ -332,9 +420,9 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
assert 'id="later-sync-status"' in html
assert "const laterSync = createLaterSync({" in html
assert "onRemoteRecords: records =>" in html
assert "onChange: (action, itemId, wakeAt) =>" in html
assert "onExpire: ids =>" in html
assert "laterSync.enqueue(action, itemId, wakeAt)" in html
assert "onChange: (action, itemId, wakeAt, handoff) =>" in html
assert "onExpire: (ids, handoffs) =>" in html
assert "laterSync.enqueue(action, itemId, wakeAt, handoff)" in html
assert "ids.map(id => laterSync.enqueue('restore', id)).every(Boolean)" in html
assert "laterSync.migrate(laterWork.read());" in html
assert "laterSync.flush();" in html
@@ -347,5 +435,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
def test_later_sync_ships_atomically_in_the_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/later-sync.js'" in source
+1 -1
View File
@@ -256,4 +256,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
assert ".markdown-content a { min-height:44px;" in css
assert "stackchain-dashboard-shell-v114" in worker
assert "stackchain-dashboard-shell-v115" in worker
+1 -1
View File
@@ -45,7 +45,7 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
assert "stackchain-dashboard-shell-v114" in worker
assert "stackchain-dashboard-shell-v115" in worker
def test_all_conversation_composers_offer_accessible_mobile_mentions():
+1 -1
View File
@@ -214,7 +214,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
assert "promptStorage:localStorage" in dashboard
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
assert "BASE + 'static/mobile-device-setup.js'" in worker
assert "stackchain-dashboard-shell-v114" in worker
assert "stackchain-dashboard-shell-v115" in worker
assert ".device-setup-panel" in css
assert ".device-readiness-card" in css
assert "overflow-x:hidden" in css
+1 -1
View File
@@ -283,4 +283,4 @@ async def test_dashboard_wires_thumb_safe_start_day_briefing_into_offline_mobile
assert ".mobile-start-day-finish { min-height:44px;" in html
assert "max-width:100%; overflow-wrap:anywhere;" in html
assert "BASE + 'static/mobile-start-day.js'" in service_worker
assert "stackchain-dashboard-shell-v114" in service_worker
assert "stackchain-dashboard-shell-v115" in service_worker
+1 -1
View File
@@ -410,7 +410,7 @@ async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(
def test_plan_today_controller_is_available_in_the_offline_shell():
source = SERVICE_WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/plan-today.js'" in source
assert "BASE + 'static/plan-today-readiness.js'" in source
assert "BASE + 'static/plan-today-preview.js'" in source
+15 -14
View File
@@ -165,13 +165,13 @@ async function dispatchPush(payload) {{
def test_offline_activation_migration_rolls_the_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
def test_resumable_today_session_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/my-work.js'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@@ -180,7 +180,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/authored-outbox.js'" in source
assert "BASE + 'static/background-issue-sync.js'" in source
@@ -189,7 +189,7 @@ def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/issue-evidence-review.js'" in source
assert "BASE + 'static/issue-attachment.js'" in source
@@ -197,14 +197,14 @@ def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/dashboard.js'" in source
def test_offline_review_next_ships_today_completion_atomically():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/today-completion.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -212,7 +212,7 @@ def test_offline_review_next_ships_today_completion_atomically():
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -220,7 +220,7 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/issue-sheet.js'" in source
assert "BASE + 'static/checklist-conflict.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -230,14 +230,14 @@ def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/later-picker.js'" in source
def test_navigation_deadline_ships_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@@ -246,21 +246,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
def test_today_convergence_ships_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/today-sync.js'" in source
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/mobile-search-viewport.js'" in source
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/update-ownership.js'" in source
@@ -930,7 +930,7 @@ def test_one_session_bound_csrf_proof_is_reused_for_a_background_drain():
def test_queue_today_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/queue-today.js'" in source
@@ -989,6 +989,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/today-timer.js",
"/dashboard/static/today-recap.js",
"/dashboard/static/today-wrap-up.js",
"/dashboard/static/today-handoff.js",
"/dashboard/static/today-completion.js",
"/dashboard/static/today-readiness.js",
"/dashboard/static/comment-next.js",
+84
View File
@@ -0,0 +1,84 @@
import json
import subprocess
from pathlib import Path
HANDOFF = Path(__file__).parents[1] / "frontend" / "today-handoff.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_due_wrap_up_commitments_are_reviewed_in_order_and_added_durably():
script = f"""
const createHandoff=require({json.dumps(str(HANDOFF))});
const values=new Map();
const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const items=[
{{kind:'issue',repository:'r',number:2,title:'Second'}},
{{kind:'issue',repository:'r',number:1,title:'First'}},
];
const identity=item=>'issue:'+item.repository+':'+item.number+':';
const today=[]; const operations=[];
const controller=createHandoff({{
storage,getLogin:()=> 'timmy',identity,
todayWork:{{contains:item=>today.includes(identity(item)),add:item=>{{if(today.length>=1)return 'full';today.push(identity(item));return 'added';}}}},
todaySync:{{enqueue:(action,id)=>{{operations.push([action,id]);return true;}},flush:async()=>true}},
}});
controller.capture(['issue:r:1:','issue:r:2:','issue:r:1:']);
const review=controller.review(items);
controller.choose('issue:r:2:',false);
const first=await controller.commit(items);
controller.choose('issue:r:2:',true);
const blocked=await controller.commit(items);
process.stdout.write(JSON.stringify({{review,first,blocked,today,operations,pending:controller.pending()}}));
"""
result = run_node(f"(async()=>{{{script}}})().catch(error=>{{console.error(error);process.exit(1);}})")
assert [row["identity"] for row in result["review"]] == ["issue:r:1:", "issue:r:2:"]
assert all(row["selected"] for row in result["review"])
assert result["first"] == {"scheduled": 1, "blocked": 0, "remaining": 1}
assert result["blocked"] == {"scheduled": 0, "blocked": 1, "remaining": 1}
assert result["today"] == ["issue:r:1:"]
assert result["operations"] == [["add", "issue:r:1:"]]
assert result["pending"] == ["issue:r:2:"]
def test_handoff_pending_state_is_account_scoped_and_capture_is_idempotent():
script = f"""
const createHandoff=require({json.dumps(str(HANDOFF))});
const values=new Map();let login='timmy';
const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const options={{storage,getLogin:()=>login,identity:item=>item.id,todayWork:{{contains:()=>false,add:()=> 'added'}},todaySync:{{enqueue:()=>true,flush:async()=>true}}}};
const controller=createHandoff(options);
controller.capture(['one','two']);controller.capture(['two','one']);
const timmy=controller.pending();login='alexander';const isolated=controller.pending();
login='timmy';const resumed=createHandoff(options).pending();
process.stdout.write(JSON.stringify({{timmy,isolated,resumed}}));
"""
assert run_node(f"(async()=>{{{script}}})()") == {
"timmy": ["one", "two"], "isolated": [], "resumed": ["one", "two"],
}
def test_dashboard_packages_accessible_mobile_commitment_review_and_account_sync():
from src import main
html = main.FRONTEND_BUILD.dashboard_html
dashboard = (HANDOFF.parents[0] / "dashboard.js").read_text()
css = (HANDOFF.parents[0] / "dashboard.css").read_text()
service_worker = (HANDOFF.parents[0] / "service-worker.js").read_text()
assert 'id="today-handoff-dialog"' in html
assert 'aria-labelledby="today-handoff-title"' in html
assert 'id="today-handoff-items"' in html
assert 'id="confirm-today-handoff"' in html
assert '<script src="static/today-handoff.js"></script>' in (HANDOFF.parents[0] / "index.html").read_text()
assert "createTodayHandoff({" in dashboard
assert "laterSync.enqueue(action, itemId, wakeAt, handoff)" in dashboard
assert "todayHandoff.capture(handoffs)" in dashboard
assert ".today-handoff-actions button { min-height:44px;" in css
assert "BASE + 'static/today-handoff.js'" in service_worker
assert "static/today-handoff.js" in main.FRONTEND_BUILD.page_sources
+1 -1
View File
@@ -221,7 +221,7 @@ async def test_today_blocker_opens_existing_preview_and_preserves_readiness_gate
def test_readiness_runtime_is_available_in_offline_shell():
service_worker = SERVICE_WORKER.read_text()
assert "const CACHE = 'stackchain-dashboard-shell-v114';" in service_worker
assert "const CACHE = 'stackchain-dashboard-shell-v115';" in service_worker
assert "BASE + 'static/today-readiness.js'" in service_worker
+1 -1
View File
@@ -127,7 +127,7 @@ sync.enqueueConfiguration(120, {{'issue:r:1:':60}});
def test_inflight_today_drain_ships_in_a_new_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v114" in source
assert "stackchain-dashboard-shell-v115" in source
assert "BASE + 'static/today-sync.js'" in source
+2 -2
View File
@@ -38,7 +38,7 @@ const todayWork = {{
const laterWork = {{
identity:todayWork.identity,
presetUntil:preset => new Date('2026-08-17T09:00:00-04:00'),
defer:(item, until) => {{ later[todayWork.identity(item)] = until.toISOString(); laterOps.push(todayWork.identity(item)); return 'deferred'; }},
defer:(item, until, options) => {{ later[todayWork.identity(item)] = until.toISOString(); laterOps.push([todayWork.identity(item), options]); return 'deferred'; }},
}};
const todaySync = {{enqueue:(action,id) => {{todayOps.push([action,id]); return true;}}, flush:()=>Promise.resolve(true)}};
const wrap = createWrapUp({{todayWork,laterWork,todaySync}});
@@ -52,7 +52,7 @@ process.stdout.write(JSON.stringify({{result,today,later,todayOps,laterOps}}));
assert output["today"] == ["issue:stackchain/dashboard:1:", "pull:stackchain/dashboard:3:"]
assert output["later"] == {"issue:stackchain/dashboard:2:": "2026-08-17T13:00:00.000Z"}
assert output["todayOps"] == [["remove", "issue:stackchain/dashboard:2:"]]
assert output["laterOps"] == ["issue:stackchain/dashboard:2:"]
assert output["laterOps"] == [["issue:stackchain/dashboard:2:", {"handoff": "today"}]]
def test_dashboard_packages_a_mobile_wrap_up_dialog_and_opens_it_after_recap_save():