From 15d7bc92349fe5472af89f96f81ca44142395877 Mon Sep 17 00:00:00 2001 From: timmy Date: Wed, 26 Aug 2026 08:55:08 +0000 Subject: [PATCH] feat: share progressive live snapshot (Closes #1425) --- frontend/index.html | 1 + frontend/progressive-human-gates.js | 8 ++- frontend/progressive-live-snapshot.js | 40 +++++++++++++ frontend/progressive-my-work.js | 11 +++- frontend/service-worker.js | 3 +- tests/test_comment_next.py | 2 +- tests/test_following_frontend.py | 2 +- tests/test_human_gates_frontend.py | 62 ++++++++++++++++++++- 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_progressive_my_work.py | 68 +++++++++++++++++++++++ tests/test_service_worker.py | 40 +++++++------ tests/test_today_readiness.py | 2 +- tests/test_today_sync.py | 2 +- 19 files changed, 221 insertions(+), 34 deletions(-) create mode 100644 frontend/progressive-live-snapshot.js diff --git a/frontend/index.html b/frontend/index.html index dd047c9..bfba1d9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2323,6 +2323,7 @@ + diff --git a/frontend/progressive-human-gates.js b/frontend/progressive-human-gates.js index 607e58c..82e0f42 100644 --- a/frontend/progressive-human-gates.js +++ b/frontend/progressive-human-gates.js @@ -10,8 +10,10 @@ function createProgressiveHumanGates(options = {}) { if (!response.ok) throw new Error(payload.error || payload.detail?.message || payload.detail || 'Review request failed.'); return payload; }); + const liveSnapshot = options.liveSnapshot || null; const getIdentity = options.getIdentity || (async () => { - const response = await fetchJson('api/v1/live', {headers:{Accept:'application/json'}}); + const response = liveSnapshot ? await liveSnapshot.acquire() : + await fetchJson('api/v1/live', {headers:{Accept:'application/json'}}); const user = response?.context?.user || {}; const login = String(user.login || '').trim(); return {login, accountKey:login && user.id ? String(user.id) + ':' + login : login}; @@ -91,7 +93,9 @@ function createProgressiveHumanGates(options = {}) { } if (typeof window !== 'undefined' && typeof document !== 'undefined') { - window.stackchainProgressiveHumanGates = createProgressiveHumanGates({document}); + window.stackchainProgressiveHumanGates = createProgressiveHumanGates({ + document, liveSnapshot:window.stackchainProgressiveLiveSnapshot, + }); void window.stackchainProgressiveHumanGates.start().catch(() => {}); } if (typeof module !== 'undefined' && module.exports) { diff --git a/frontend/progressive-live-snapshot.js b/frontend/progressive-live-snapshot.js new file mode 100644 index 0000000..b327309 --- /dev/null +++ b/frontend/progressive-live-snapshot.js @@ -0,0 +1,40 @@ +function createProgressiveLiveSnapshot({fetchSnapshot}) { + let value = null; + let flight = null; + + const acquire = () => { + if (value) return Promise.resolve(value); + if (flight) return flight; + flight = Promise.resolve().then(() => fetchSnapshot()).then(snapshot => { + value = snapshot; + flight = null; + return snapshot; + }, error => { + flight = null; + throw error; + }); + return flight; + }; + + return { + acquire, + snapshot:() => value, + pending:() => flight, + identity() { + const user = value?.context?.user || {}; + const login = String(user.login || '').trim(); + return {login, accountKey:login && user.id ? String(user.id) + ':' + login : login}; + }, + }; +} + +if (typeof window !== 'undefined') { + window.stackchainProgressiveLiveSnapshot = createProgressiveLiveSnapshot({ + fetchSnapshot:async () => { + const response = await fetch('api/v1/live', {headers:{Accept:'application/json'}}); + if (!response.ok) throw new Error('HTTP ' + response.status); + return response.json(); + }, + }); +} +if (typeof module !== 'undefined' && module.exports) module.exports = createProgressiveLiveSnapshot; diff --git a/frontend/progressive-my-work.js b/frontend/progressive-my-work.js index ef376fc..a0b5867 100644 --- a/frontend/progressive-my-work.js +++ b/frontend/progressive-my-work.js @@ -1,5 +1,6 @@ function createProgressiveMyWork({ - document, fetchSnapshot, pollerOptions = {}, lifecycleTarget = globalThis, + document, fetchSnapshot, liveSnapshot: snapshotBroker = null, + pollerOptions = {}, lifecycleTarget = globalThis, }) { const list = document.querySelector('#my-work-list'); const status = document.querySelector('#my-work-status'); @@ -25,6 +26,9 @@ function createProgressiveMyWork({ const deferredQueues = { today:'Today', agenda:'Agenda', later:'Later', draft:'Drafts', }; + const fetchProgressiveSnapshot = (revisions = {}, options = {}) => + snapshotBroker && Object.keys(revisions || {}).length === 0 ? + snapshotBroker.acquire() : fetchSnapshot(revisions, options); const escapeHtml = value => String(value || '').replace(/[&<>"']/g, character => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''', @@ -128,7 +132,7 @@ function createProgressiveMyWork({ if (typeof createContextPoller === 'function') { poller = createContextPoller({ ...pollerOptions, - fetchContext: fetchSnapshot, + fetchContext: fetchProgressiveSnapshot, onSnapshot: applySnapshot, onError: () => { if (status && !stopped) status.textContent = 'Assigned work is reconnecting…'; @@ -175,7 +179,7 @@ function createProgressiveMyWork({ )); return Boolean(await request); } - const request = Promise.resolve().then(() => fetchSnapshot()); + const request = Promise.resolve().then(() => fetchProgressiveSnapshot()); liveSnapshotPromise = request.then(snapshot => ( snapshot && typeof snapshot === 'object' && Object.prototype.hasOwnProperty.call(snapshot, 'context') ? snapshot : null @@ -208,6 +212,7 @@ if (typeof window !== 'undefined' && typeof document !== 'undefined') { window.stackchainProgressiveMyWork = createProgressiveMyWork({ document, lifecycleTarget: window, + liveSnapshot:window.stackchainProgressiveLiveSnapshot, fetchSnapshot: async (revisions = {}, { signal } = {}) => { const query = createContextPoller.buildRevisionQuery(revisions); const response = await fetch('api/v1/live' + (query ? '?' + query : ''), { diff --git a/frontend/service-worker.js b/frontend/service-worker.js index f3f3fdb..67f6c2f 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-v141'; +const CACHE = 'stackchain-dashboard-shell-v142'; 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; @@ -188,6 +188,7 @@ const SHELL = [ BASE + 'static/offline-work.js', BASE + 'static/offline-today.js', BASE + 'static/my-work.js', + BASE + 'static/progressive-live-snapshot.js', BASE + 'static/progressive-my-work.js', BASE + 'static/progressive-capture.js', BASE + 'static/agenda-replan.js', diff --git a/tests/test_comment_next.py b/tests/test_comment_next.py index 0e06056..b4c4166 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-v141" in worker + assert "stackchain-dashboard-shell-v142" in worker diff --git a/tests/test_following_frontend.py b/tests/test_following_frontend.py index ed94120..b796837 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-v141" in service_worker + assert "stackchain-dashboard-shell-v142" 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 def152c..551fce6 100644 --- a/tests/test_human_gates_frontend.py +++ b/tests/test_human_gates_frontend.py @@ -8,6 +8,9 @@ PROGRESSIVE = Path(__file__).parents[1] / "frontend" / "progressive-human-gates. INDEX = Path(__file__).parents[1] / "frontend" / "index.html" DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js" WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js" +LIVE_SNAPSHOT = Path(__file__).parents[1] / "frontend" / "progressive-live-snapshot.js" +PROGRESSIVE_MY_WORK = Path(__file__).parents[1] / "frontend" / "progressive-my-work.js" +MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js" def run_node(body): @@ -16,6 +19,63 @@ def run_node(body): return json.loads(result.stdout) +def test_progressive_my_work_and_human_gates_share_one_cold_live_snapshot(): + script = f""" +const createProgressiveLiveSnapshot=require({json.dumps(str(LIVE_SNAPSHOT))}); +globalThis.buildMyWork=require({json.dumps(str(MY_WORK))}); +const createProgressiveMyWork=require({json.dumps(str(PROGRESSIVE_MY_WORK))}); +const createProgressiveHumanGates=require({json.dumps(str(PROGRESSIVE))}); +let liveCalls=0, directMyWorkCalls=0, resolveLive; +const broker=createProgressiveLiveSnapshot({{fetchSnapshot:()=>{{ + liveCalls += 1; + return new Promise(resolve=>{{resolveLive=resolve;}}); +}}}}); +const workDocument={{ + hidden:false, + querySelector:selector=>selector==='#my-work-list'?{{innerHTML:''}}:selector==='#my-work-status'?{{textContent:''}}:null, + querySelectorAll:()=>[], +}}; +const work=createProgressiveMyWork({{ + document:workDocument, liveSnapshot:broker, + fetchSnapshot:async()=>{{directMyWorkCalls += 1; return {{}};}}, +}}); +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(){{}}}}, '#close-human-gates':{{addEventListener(){{}}}}, +}}; +const gates=createProgressiveHumanGates({{ + document:{{querySelector:selector=>nodes[selector]||null}}, + location:{{hash:'#/my-work/human-gates'}}, history:{{replaceState(){{}}}}, + storage:{{getItem:()=>null,setItem(){{}}}}, isOnline:()=>true, + liveSnapshot:broker, + fetchJson:async path=>({{pending_count:1,items:[{{id:'g1',title:'Shared identity',candidate_hash:'abc',revision:1,checks:[]}}]}}), +}}); +(async()=>{{ + const workStart=work.start(); const gateStart=gates.start(); + await Promise.resolve(); await Promise.resolve(); + const callsWhilePending=liveCalls; + resolveLive({{context:{{user:{{id:7,login:'timmy'}},issues:[],pull_requests:[]}},events:[],notifications:[]}}); + await Promise.all([workStart,gateStart]); + process.stdout.write(JSON.stringify({{ + callsWhilePending,liveCalls,directMyWorkCalls, + login:work.login(),gateStarted:gates.handoff().started, + }})); +}})().catch(error=>{{console.error(error);process.exit(1);}}); +""" + result = subprocess.run(["node", "-e", script], capture_output=True, text=True) + + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout) == { + "callsWhilePending": 1, + "liveCalls": 1, + "directMyWorkCalls": 0, + "login": "timmy", + "gateStarted": True, + } + + def test_queue_loads_pending_count_uses_account_cache_and_renders_inbox_zero(): output = run_node(r""" const values=new Map(); const storage={getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}; @@ -202,7 +262,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-v141" in WORKER.read_text() + assert "stackchain-dashboard-shell-v142" in WORKER.read_text() def test_deep_link_opens_human_gates_without_waiting_for_optional_workspace(): diff --git a/tests/test_later_sync.py b/tests/test_later_sync.py index f728da0..21079d4 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-v141" in source + assert "stackchain-dashboard-shell-v142" 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 e14d59f..b7f4cea 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-v141" in worker + assert "stackchain-dashboard-shell-v142" in worker diff --git a/tests/test_mobile_composer_integration.py b/tests/test_mobile_composer_integration.py index 4218e74..83d38fb 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-v141" in worker + assert "stackchain-dashboard-shell-v142" 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 cf591ac..b18a7e8 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-v141" in worker + assert "stackchain-dashboard-shell-v142" 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 ac69eb8..5da50f4 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-v141" in worker + assert "stackchain-dashboard-shell-v142" 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 30d45fc..249f030 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-v141" in service_worker + assert "stackchain-dashboard-shell-v142" in service_worker @pytest.mark.anyio diff --git a/tests/test_plan_today.py b/tests/test_plan_today.py index ca3a1ae..3ae26f4 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-v141" in source + assert "stackchain-dashboard-shell-v142" 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_progressive_my_work.py b/tests/test_progressive_my_work.py index 1143dd2..bed0a84 100644 --- a/tests/test_progressive_my_work.py +++ b/tests/test_progressive_my_work.py @@ -4,12 +4,80 @@ from pathlib import Path MODULE = Path(__file__).parents[1] / "frontend" / "progressive-my-work.js" +LIVE_SNAPSHOT = Path(__file__).parents[1] / "frontend" / "progressive-live-snapshot.js" MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js" DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js" INDEX = Path(__file__).parents[1] / "frontend" / "index.html" CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css" +def test_progressive_live_snapshot_coalesces_concurrent_cold_launch_consumers(): + harness = f""" +const createProgressiveLiveSnapshot=require({json.dumps(str(LIVE_SNAPSHOT))}); +let calls=0, resolveRequest; +const broker=createProgressiveLiveSnapshot({{fetchSnapshot:()=>{{ + calls += 1; + return new Promise(resolve=>{{resolveRequest=resolve;}}); +}}}}); +(async()=>{{ + const myWork=broker.acquire(); + const humanGates=broker.acquire(); + await Promise.resolve(); + const callsWhilePending=calls; + const snapshot={{context:{{user:{{id:7,login:'timmy'}}}},events:[],notifications:[]}}; + resolveRequest(snapshot); + const [first,second]=await Promise.all([myWork,humanGates]); + const cached=await broker.acquire(); + process.stdout.write(JSON.stringify({{ + callsWhilePending,calls,same:first===second && second===cached, + identity:broker.identity(),snapshot:broker.snapshot(), + }})); +}})().catch(error=>{{console.error(error);process.exit(1);}}); +""" + result = subprocess.run(["node", "-e", harness], capture_output=True, text=True) + + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout) == { + "callsWhilePending": 1, + "calls": 1, + "same": True, + "identity": {"login": "timmy", "accountKey": "7:timmy"}, + "snapshot": { + "context": {"user": {"id": 7, "login": "timmy"}}, + "events": [], + "notifications": [], + }, + } + + +def test_progressive_live_snapshot_retries_after_a_failed_shared_flight(): + harness = f""" +const createProgressiveLiveSnapshot=require({json.dumps(str(LIVE_SNAPSHOT))}); +let calls=0; +const snapshot={{context:{{user:{{login:'timmy'}}}},events:[],notifications:[]}}; +const broker=createProgressiveLiveSnapshot({{fetchSnapshot:async()=>{{ + calls += 1; + if(calls===1) throw new Error('temporary outage'); + return snapshot; +}}}}); +(async()=>{{ + const first=await Promise.allSettled([broker.acquire(),broker.acquire()]); + const recovered=await broker.acquire(); + process.stdout.write(JSON.stringify({{ + calls,first:first.map(result=>result.status),recovered:recovered===snapshot, + }})); +}})().catch(error=>{{console.error(error);process.exit(1);}}); +""" + result = subprocess.run(["node", "-e", harness], capture_output=True, text=True) + + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout) == { + "calls": 2, + "first": ["rejected", "rejected"], + "recovered": True, + } + + def test_progressive_my_work_renders_and_filters_assigned_work_before_full_workspace(): harness = f""" const fs=require('fs'); const vm=require('vm'); diff --git a/tests/test_service_worker.py b/tests/test_service_worker.py index bbadafa..5921394 100644 --- a/tests/test_service_worker.py +++ b/tests/test_service_worker.py @@ -186,10 +186,17 @@ async function dispatchPush(payload) {{ return json.loads(completed.stdout) +def test_shared_progressive_snapshot_broker_rolls_the_offline_shell(): + source = WORKER.read_text() + + assert "stackchain-dashboard-shell-v142" 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-v141" in source + assert "stackchain-dashboard-shell-v142" in source assert "BASE + 'static/week-plan.js'" in source assert "BASE + 'static/dashboard.css'" in source @@ -197,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-v141" in source + assert "stackchain-dashboard-shell-v142" in source def test_per_day_week_conflict_ui_rolls_the_offline_shell(): source = WORKER.read_text() - assert "stackchain-dashboard-shell-v141" in source + assert "stackchain-dashboard-shell-v142" 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-v141" in source + assert "stackchain-dashboard-shell-v142" in source assert "BASE + 'static/my-work.js'" in source assert "BASE + 'static/dashboard.js'" in source assert "BASE + 'static/dashboard.css'" in source @@ -219,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-v141" in source + assert "stackchain-dashboard-shell-v142" 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 @@ -228,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-v141" in source + assert "stackchain-dashboard-shell-v142" in source assert "BASE + 'static/issue-evidence-review.js'" in source assert "BASE + 'static/issue-attachment.js'" in source @@ -236,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-v141" in source + assert "stackchain-dashboard-shell-v142" 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-v141" in source + assert "stackchain-dashboard-shell-v142" in source assert "BASE + 'static/today-completion.js'" in source assert "BASE + 'static/dashboard.js'" in source @@ -251,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-v141" in source + assert "stackchain-dashboard-shell-v142" in source assert "BASE + 'static/create-issue-sheet.js'" in source assert "BASE + 'static/dashboard.js'" in source @@ -259,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-v141" in source + assert "stackchain-dashboard-shell-v142" 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 @@ -269,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-v141" in source + assert "stackchain-dashboard-shell-v142" 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-v141" in source + assert "stackchain-dashboard-shell-v142" in source assert "BASE + 'static/dashboard.css'" in source assert "BASE + 'static/dashboard.js'" in source assert "BASE + 'static/install-app.js'" in source @@ -285,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-v141" in source + assert "stackchain-dashboard-shell-v142" 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-v141" in source + assert "stackchain-dashboard-shell-v142" 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-v141" in source + assert "stackchain-dashboard-shell-v142" in source assert "BASE + 'static/update-ownership.js'" in source @@ -1354,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-v141" in source + assert "stackchain-dashboard-shell-v142" in source assert "BASE + 'static/queue-today.js'" in source @@ -1410,6 +1417,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell(): "/dashboard/static/offline-work.js", "/dashboard/static/offline-today.js", "/dashboard/static/my-work.js", + "/dashboard/static/progressive-live-snapshot.js", "/dashboard/static/progressive-my-work.js", "/dashboard/static/progressive-capture.js", "/dashboard/static/agenda-replan.js", diff --git a/tests/test_today_readiness.py b/tests/test_today_readiness.py index e2f35fe..8c0b7ed 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-v141';" in service_worker + assert "const CACHE = 'stackchain-dashboard-shell-v142';" 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 7808f89..f946712 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-v141" in source + assert "stackchain-dashboard-shell-v142" in source assert "BASE + 'static/today-sync.js'" in source