From 2e9d108c6f55db24b659c35f5afb3ad87a01a8c0 Mon Sep 17 00:00:00 2001 From: timmy Date: Wed, 26 Aug 2026 13:50:11 +0000 Subject: [PATCH] feat: refresh Human Gates review sessions (Closes #1433) --- frontend/human-gates.js | 15 +- frontend/progressive-human-gates.js | 5 +- frontend/service-worker.js | 2 +- tests/e2e/test_human_gates_reopen_release.py | 95 +++++++++++++ tests/test_comment_next.py | 2 +- tests/test_following_frontend.py | 2 +- tests/test_human_gates_frontend.py | 137 ++++++++++++++++++- tests/test_later_sync.py | 2 +- tests/test_markdown_renderer.py | 2 +- tests/test_mobile_composer_integration.py | 2 +- tests/test_mobile_device_setup.py | 2 +- tests/test_mobile_insights.py | 2 +- tests/test_mobile_start_day.py | 2 +- tests/test_plan_today.py | 2 +- tests/test_service_worker.py | 34 ++--- tests/test_today_readiness.py | 2 +- tests/test_today_sync.py | 2 +- 17 files changed, 275 insertions(+), 35 deletions(-) create mode 100644 tests/e2e/test_human_gates_reopen_release.py diff --git a/frontend/human-gates.js b/frontend/human-gates.js index 09ad515..5c532f9 100644 --- a/frontend/human-gates.js +++ b/frontend/human-gates.js @@ -13,6 +13,7 @@ function createHumanGates(options = {}) { let loadEpoch = 0; const decisionKeys = new Map(); let decisionFlight = null; + let openFlight = null; let onChange = options.onChange; const escape = value => String(value ?? '').replace(/[&<>"']/g, character => ({ @@ -203,7 +204,19 @@ function createHumanGates(options = {}) { function open() { location.hash = '#/my-work/human-gates'; if (nodes.panel) nodes.panel.hidden = false; - return load().then(() => reviewNext()); + if (openFlight) return openFlight; + const operation = (async () => { + await load(); + reviewSnapshot = queue.items.slice(); + reviewIndex = 0; + return reviewNext(); + })(); + openFlight = operation; + const clearFlight = () => { + if (openFlight === operation) openFlight = null; + }; + operation.then(clearFlight, clearFlight); + return operation; } return { diff --git a/frontend/progressive-human-gates.js b/frontend/progressive-human-gates.js index 7a4c14d..e0e0ff6 100644 --- a/frontend/progressive-human-gates.js +++ b/frontend/progressive-human-gates.js @@ -60,10 +60,7 @@ function createProgressiveHumanGates(options = {}) { async function start(force = false) { if (!force && location.hash !== '#/my-work/human-gates') return false; - if (started) { - if (nodes.panel) nodes.panel.hidden = false; - return true; - } + if (started) return controller.open().then(() => true); if (startFlight) return startFlight; startFlight = (async () => { const identity = await getIdentity(); diff --git a/frontend/service-worker.js b/frontend/service-worker.js index ca3f8c1..cb52202 100644 --- a/frontend/service-worker.js +++ b/frontend/service-worker.js @@ -1,7 +1,7 @@ const BASE = new URL('./', self.location.href).pathname; importScripts(BASE + 'static/private-data-registry.js'); importScripts(BASE + 'static/background-issue-sync.js'); -const CACHE = 'stackchain-dashboard-shell-v144'; +const CACHE = 'stackchain-dashboard-shell-v145'; const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href; const OUTAGE_STATUSES = new Set([500, 502, 503, 504]); const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000; diff --git a/tests/e2e/test_human_gates_reopen_release.py b/tests/e2e/test_human_gates_reopen_release.py new file mode 100644 index 0000000..f112450 --- /dev/null +++ b/tests/e2e/test_human_gates_reopen_release.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +import json +import os +import threading +from pathlib import Path + +import pytest + +if os.getenv("STACKCHAIN_RUN_RELEASE_E2E") != "1": + pytest.skip("packaged Human Gates journey 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 + + +@pytest.mark.parametrize(("width", "height"), [(320, 568), (390, 844)]) +def test_release_artifact_reopens_human_gates_with_one_fresh_mobile_snapshot( + tmp_path: Path, width: int, height: int +): + archives = sorted((ROOT / "dist").glob("stackchain-dashboard-*.tar.gz")) + assert len(archives) == 1, "browser job must download exactly one assembled release archive" + + fake = FakeGiteaServer(("127.0.0.1", 0)) + fake_thread = threading.Thread(target=fake.serve_forever, daemon=True) + fake_thread.start() + current = {"gate": "g1"} + list_requests: list[str] = [] + browser_errors: list[str] = [] + + def gate(gate_id: str) -> dict: + return { + "id": gate_id, + "title": "First candidate" if gate_id == "g1" else "Fresh candidate", + "project": "stackchain/stackchain-dashboard", + "candidate_hash": "a1" if gate_id == "g1" else "b2", + "revision": 1, + "priority": 5, + "checks": [], + "artifacts": [], + "links": [], + "provenance": {}, + "history": [], + } + + 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"]) + context = browser.new_context( + viewport={"width": width, "height": height}, ignore_https_errors=True + ) + page = context.new_page() + page.on("pageerror", lambda error: browser_errors.append(error.stack or str(error))) + + def human_gates_route(route): + path = route.request.url.split("?", 1)[0] + if path.endswith("/api/v1/human-gates"): + list_requests.append(current["gate"]) + payload = {"pending_count": 1, "items": [gate(current["gate"])]} + else: + payload = gate(path.rsplit("/", 1)[-1]) + route.fulfill(status=200, content_type="application/json", body=json.dumps(payload)) + + page.route("**/api/v1/human-gates**", human_gates_route) + page.goto(origin + "/", wait_until="networkidle") + page.locator('input[name="device_label"]').fill("Human Gates release 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("document.querySelector('#open-human-gates').click()") + expect(page.locator("#human-gates")).to_be_visible() + expect(page.locator("#human-gate-detail")).to_contain_text("First candidate") + page.evaluate("document.querySelector('#close-human-gates').click()") + + before_reopen = len(list_requests) + current["gate"] = "g2" + page.evaluate("document.querySelector('#open-human-gates').click()") + expect(page.locator("#human-gate-detail")).to_contain_text("Fresh candidate") + expect(page.locator("#human-gates-list")).to_contain_text("Fresh candidate") + assert len(list_requests) == before_reopen + 1 + assert page.evaluate("window.innerWidth") == width + assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") + bounds = page.locator('[data-human-gate-id="g2"]').bounding_box() + assert bounds and bounds["height"] >= 44 + assert not browser_errors + browser.close() + finally: + fake.shutdown() + fake.server_close() + fake_thread.join(timeout=5) diff --git a/tests/test_comment_next.py b/tests/test_comment_next.py index eb0bb27..128d4c7 100644 --- a/tests/test_comment_next.py +++ b/tests/test_comment_next.py @@ -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-v144" in worker + assert "stackchain-dashboard-shell-v145" in worker diff --git a/tests/test_following_frontend.py b/tests/test_following_frontend.py index 6bd6e85..bc91419 100644 --- a/tests/test_following_frontend.py +++ b/tests/test_following_frontend.py @@ -593,7 +593,7 @@ process.stdout.write(JSON.stringify({{ assert ".following-disposition-mode" in css assert "if (searchPreviewReturnKind === 'following')" in dashboard assert "e.key === 'Escape' && searchPreviewReturnKind === 'following'" in dashboard - assert "stackchain-dashboard-shell-v144" in service_worker + assert "stackchain-dashboard-shell-v145" in service_worker def test_prepare_today_lazily_refreshes_and_directly_reviews_following(): diff --git a/tests/test_human_gates_frontend.py b/tests/test_human_gates_frontend.py index b91a6ab..0ff8ad0 100644 --- a/tests/test_human_gates_frontend.py +++ b/tests/test_human_gates_frontend.py @@ -272,6 +272,92 @@ const gates=createHumanGates({storage:{getItem:()=>null,setItem(){}},getLogin:() assert [item["id"] for item in output["snapshot"]["items"]] == ["g2"] +def test_concurrent_reopen_calls_share_one_fresh_list_request(): + output = run_node(r""" +let listCalls=0, releaseList; +const pending=new Promise(resolve=>releaseList=resolve); +const item={id:'g1',title:'Candidate',candidate_hash:'a1',revision:1,checks:[]}; +const gates=createHumanGates({ + storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',isOnline:()=>true, + nodes:{count:{},list:{},status:{},panel:{hidden:true},detail:{}},location:{hash:''}, + fetchJson:async path=>{ + if(path.endsWith('/g1')) return item; + listCalls += 1; + await pending; + return {pending_count:1,items:[item]}; + }, +}); +(async()=>{ + const first=gates.open(); + const second=gates.open(); + releaseList(); + const reviewed=await Promise.all([first,second]); + process.stdout.write(JSON.stringify({listCalls,ids:reviewed.map(item=>item.id)})); +})(); +""") + assert output == {"listCalls": 1, "ids": ["g1", "g1"]} + + +def test_failed_reopen_clears_single_flight_and_can_retry(): + output = run_node(r""" +let listCalls=0; +const item={id:'g1',title:'Recovered',candidate_hash:'a1',revision:1,checks:[]}; +const gates=createHumanGates({ + storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',isOnline:()=>true, + nodes:{count:{},list:{},status:{},panel:{hidden:true},detail:{}},location:{hash:''}, + fetchJson:async path=>{ + if(path.endsWith('/g1')) return item; + listCalls += 1; + if(listCalls === 1) throw new Error('offline'); + return {pending_count:1,items:[item]}; + }, +}); +(async()=>{ + let firstError=''; + try { await gates.open(); } catch(error) { firstError=error.message; } + const recovered=await gates.open(); + process.stdout.write(JSON.stringify({firstError,listCalls,recovered:recovered.id})); +})(); +""") + assert output == {"firstError": "offline", "listCalls": 2, "recovered": "g1"} + + +def test_reopen_refreshes_queue_and_starts_a_fresh_atomic_review_snapshot(): + output = run_node(r""" +const first={id:'g1',title:'First candidate',project:'p/one',candidate_hash:'a1',revision:1,checks:[]}; +const second={id:'g2',title:'Second candidate',project:'p/two',candidate_hash:'b2',revision:1,checks:[]}; +let listCalls=0; +const nodes={count:{},list:{innerHTML:''},status:{},panel:{hidden:true},detail:{innerHTML:''}}; +const gates=createHumanGates({ + storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',isOnline:()=>true,nodes,location:{hash:''}, + fetchJson:async path=>{ + if(path.endsWith('/g1')) return first; + if(path.endsWith('/g2')) return second; + listCalls += 1; + return {pending_count:1,items:[listCalls === 1 ? first : second]}; + }, +}); +(async()=>{ + await gates.open(); + await new Promise(resolve=>setTimeout(resolve,0)); + const before=gates.current().id; + await gates.open(); + const selected=gates.select('g2'); + await new Promise(resolve=>setTimeout(resolve,0)); + process.stdout.write(JSON.stringify({ + before,listCalls,selected:selected.id,current:gates.current().id, + listHtml:nodes.list.innerHTML,detailHtml:nodes.detail.innerHTML, + })); +})(); +""") + assert output["before"] == "g1" + assert output["listCalls"] == 2 + assert output["selected"] == "g2" + assert output["current"] == "g2" + assert "Second candidate" in output["listHtml"] + assert "Second candidate" in output["detailHtml"] + + def test_selecting_a_queue_card_opens_that_exact_gate(): output = run_node(r""" const details={ @@ -306,7 +392,7 @@ def test_human_gate_mobile_shell_and_deep_route_are_wired(): assert "mobileStartDay.reconcile({authoritative:true, authoritativePhases:['gate']})" in dashboard assert "counts.gate = queueCounts.gate" in dashboard assert "gate:preparationItems.gate || []" in dashboard - assert "stackchain-dashboard-shell-v144" in WORKER.read_text() + assert "stackchain-dashboard-shell-v145" in WORKER.read_text() def test_deep_link_opens_human_gates_without_waiting_for_optional_workspace(): @@ -346,6 +432,55 @@ const app=createProgressiveHumanGates({{ } +def test_progressive_reopen_refreshes_the_review_session_before_hydration(): + script = f""" +const createProgressiveHumanGates=require({json.dumps(str(PROGRESSIVE))}); +const listeners={{}}; +const nodes={{ + '#human-gates-count':{{}}, '#human-gates-list':{{innerHTML:'',addEventListener(){{}}}}, + '#human-gates-status':{{}}, '#human-gates':{{hidden:true}}, + '#human-gate-detail':{{innerHTML:'',addEventListener(){{}},querySelectorAll:()=>[]}}, + '#open-human-gates':{{addEventListener:(name,fn)=>listeners.open=fn}}, + '#close-human-gates':{{addEventListener:(name,fn)=>listeners.close=fn}}, +}}; +let listCalls=0; +const first={{id:'g1',title:'First',candidate_hash:'a1',revision:1,checks:[]}}; +const second={{id:'g2',title:'Second',candidate_hash:'b2',revision:1,checks:[]}}; +const app=createProgressiveHumanGates({{ + document:{{querySelector:selector=>nodes[selector]||null}}, + location:{{hash:'#/my-work/human-gates'}}, history:{{replaceState(){{}}}}, + storage:{{getItem:()=>null,setItem(){{}}}}, isOnline:()=>true, + getIdentity:async()=>({{login:'timmy',accountKey:'7:timmy'}}), + fetchJson:async path=>{{ + if(path.endsWith('/g1')) return first; + if(path.endsWith('/g2')) return second; + listCalls += 1; + return {{pending_count:1,items:[listCalls === 1 ? first : second]}}; + }}, +}}); +(async()=>{{ + await app.start(); + listeners.close(); + await listeners.open(); + await new Promise(resolve=>setTimeout(resolve,0)); + const controller=app.handoff().controller; + process.stdout.write(JSON.stringify({{ + listCalls,current:controller.current().id,hidden:nodes['#human-gates'].hidden, + listHtml:nodes['#human-gates-list'].innerHTML, + detailHasSecond:nodes['#human-gate-detail'].innerHTML.includes('Second'), + }})); +}})().catch(error=>{{console.error(error);process.exit(1);}}); +""" + result = subprocess.run(["node", "-e", script], check=True, text=True, capture_output=True) + assert json.loads(result.stdout) == { + "listCalls": 2, + "current": "g2", + "hidden": False, + "listHtml": '', + "detailHasSecond": True, + } + + def test_dashboard_adopts_progressive_human_gates_without_a_second_list_load(): dashboard = DASHBOARD.read_text() index = INDEX.read_text() diff --git a/tests/test_later_sync.py b/tests/test_later_sync.py index e62baf2..3f49c6b 100644 --- a/tests/test_later_sync.py +++ b/tests/test_later_sync.py @@ -435,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-v144" in source + assert "stackchain-dashboard-shell-v145" in source assert "BASE + 'static/later-sync.js'" in source diff --git a/tests/test_markdown_renderer.py b/tests/test_markdown_renderer.py index 9e08eba..a23e79f 100644 --- a/tests/test_markdown_renderer.py +++ b/tests/test_markdown_renderer.py @@ -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-v144" in worker + assert "stackchain-dashboard-shell-v145" in worker diff --git a/tests/test_mobile_composer_integration.py b/tests/test_mobile_composer_integration.py index a3c94d3..5be6334 100644 --- a/tests/test_mobile_composer_integration.py +++ b/tests/test_mobile_composer_integration.py @@ -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-v144" in worker + assert "stackchain-dashboard-shell-v145" in worker def test_all_conversation_composers_offer_accessible_mobile_mentions(): diff --git a/tests/test_mobile_device_setup.py b/tests/test_mobile_device_setup.py index e71fbb5..65851ab 100644 --- a/tests/test_mobile_device_setup.py +++ b/tests/test_mobile_device_setup.py @@ -383,7 +383,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow(): assert "controller.recoverPermission('deadline')" in dashboard assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard assert "BASE + 'static/mobile-device-setup.js'" in worker - assert "stackchain-dashboard-shell-v144" in worker + assert "stackchain-dashboard-shell-v145" in worker assert ".device-setup-panel" in css assert ".device-readiness-card" in css assert "overflow-x:hidden" in css diff --git a/tests/test_mobile_insights.py b/tests/test_mobile_insights.py index eb4c7e2..51cedbd 100644 --- a/tests/test_mobile_insights.py +++ b/tests/test_mobile_insights.py @@ -274,5 +274,5 @@ async def test_mobile_home_progressively_discloses_secondary_panels_as_insights( def test_mobile_insights_rolls_into_the_offline_shell(): worker = (CONTROLLER.parent / "service-worker.js").read_text() - assert "stackchain-dashboard-shell-v144" in worker + assert "stackchain-dashboard-shell-v145" in worker assert "BASE + 'static/mobile-insights.js'" in worker diff --git a/tests/test_mobile_start_day.py b/tests/test_mobile_start_day.py index c3333b4..02ab8bc 100644 --- a/tests/test_mobile_start_day.py +++ b/tests/test_mobile_start_day.py @@ -469,7 +469,7 @@ 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-v144" in service_worker + assert "stackchain-dashboard-shell-v145" in service_worker @pytest.mark.anyio diff --git a/tests/test_plan_today.py b/tests/test_plan_today.py index c101b9b..61340b4 100644 --- a/tests/test_plan_today.py +++ b/tests/test_plan_today.py @@ -418,7 +418,7 @@ async def test_starting_saved_today_work_closes_a_concurrent_rollover_planner(): def test_plan_today_controller_is_available_in_the_offline_shell(): source = SERVICE_WORKER.read_text() - assert "stackchain-dashboard-shell-v144" in source + assert "stackchain-dashboard-shell-v145" 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 diff --git a/tests/test_service_worker.py b/tests/test_service_worker.py index 58bf7b4..d6a2345 100644 --- a/tests/test_service_worker.py +++ b/tests/test_service_worker.py @@ -189,14 +189,14 @@ async function dispatchPush(payload) {{ def test_shared_progressive_snapshot_broker_rolls_the_offline_shell(): source = WORKER.read_text() - assert "stackchain-dashboard-shell-v144" in source + assert "stackchain-dashboard-shell-v145" in source assert "BASE + 'static/progressive-live-snapshot.js'" in source def test_week_unplan_undo_rolls_the_offline_shell(): source = WORKER.read_text() - assert "stackchain-dashboard-shell-v144" in source + assert "stackchain-dashboard-shell-v145" in source assert "BASE + 'static/week-plan.js'" in source assert "BASE + 'static/dashboard.css'" in source @@ -204,20 +204,20 @@ def test_week_unplan_undo_rolls_the_offline_shell(): def test_private_today_action_mailbox_rolls_the_offline_shell(): source = WORKER.read_text() - assert "stackchain-dashboard-shell-v144" in source + assert "stackchain-dashboard-shell-v145" in source def test_per_day_week_conflict_ui_rolls_the_offline_shell(): source = WORKER.read_text() - assert "stackchain-dashboard-shell-v144" in source + assert "stackchain-dashboard-shell-v145" in source assert "BASE + 'static/week-plan.js'" in source def test_resumable_today_session_ships_in_a_new_offline_shell(): source = WORKER.read_text() - assert "stackchain-dashboard-shell-v144" in source + assert "stackchain-dashboard-shell-v145" in source assert "BASE + 'static/my-work.js'" in source assert "BASE + 'static/dashboard.js'" in source assert "BASE + 'static/dashboard.css'" in source @@ -226,7 +226,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-v144" in source + assert "stackchain-dashboard-shell-v145" 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 @@ -235,7 +235,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-v144" in source + assert "stackchain-dashboard-shell-v145" in source assert "BASE + 'static/issue-evidence-review.js'" in source assert "BASE + 'static/issue-attachment.js'" in source @@ -243,14 +243,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-v144" in source + assert "stackchain-dashboard-shell-v145" 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-v144" in source + assert "stackchain-dashboard-shell-v145" in source assert "BASE + 'static/today-completion.js'" in source assert "BASE + 'static/dashboard.js'" in source @@ -258,7 +258,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-v144" in source + assert "stackchain-dashboard-shell-v145" in source assert "BASE + 'static/create-issue-sheet.js'" in source assert "BASE + 'static/dashboard.js'" in source @@ -266,7 +266,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-v144" in source + assert "stackchain-dashboard-shell-v145" 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 @@ -276,14 +276,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-v144" in source + assert "stackchain-dashboard-shell-v145" 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-v144" in source + assert "stackchain-dashboard-shell-v145" in source assert "BASE + 'static/dashboard.css'" in source assert "BASE + 'static/dashboard.js'" in source assert "BASE + 'static/install-app.js'" in source @@ -292,21 +292,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-v144" in source + assert "stackchain-dashboard-shell-v145" 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-v144" in source + assert "stackchain-dashboard-shell-v145" 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-v144" in source + assert "stackchain-dashboard-shell-v145" in source assert "BASE + 'static/update-ownership.js'" in source @@ -1361,7 +1361,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-v144" in source + assert "stackchain-dashboard-shell-v145" in source assert "BASE + 'static/queue-today.js'" in source diff --git a/tests/test_today_readiness.py b/tests/test_today_readiness.py index 25623c3..5b3f564 100644 --- a/tests/test_today_readiness.py +++ b/tests/test_today_readiness.py @@ -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-v144';" in service_worker + assert "const CACHE = 'stackchain-dashboard-shell-v145';" in service_worker assert "BASE + 'static/today-readiness.js'" in service_worker diff --git a/tests/test_today_sync.py b/tests/test_today_sync.py index 78a5748..aca1272 100644 --- a/tests/test_today_sync.py +++ b/tests/test_today_sync.py @@ -343,7 +343,7 @@ listeners['stackchain:first-task-complete'](); 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-v144" in source + assert "stackchain-dashboard-shell-v145" in source assert "BASE + 'static/today-sync.js'" in source