diff --git a/frontend/context-poller.js b/frontend/context-poller.js index c3f6b72..c75ce83 100644 --- a/frontend/context-poller.js +++ b/frontend/context-poller.js @@ -44,9 +44,14 @@ function createContextPoller({ const freshness = snapshot && snapshot.freshness; const sections = freshness && freshness.sections; const sectionValues = sections && Object.values(sections); + const degradedRetrySeconds = (sectionValues || []) + .filter(section => section.degraded) + .map(section => Number(section.retry_in_seconds ?? freshness.retry_in_seconds)) + .filter(seconds => Number.isFinite(seconds) && seconds > 0); + const failedSectionDelay = degradedRetrySeconds.length ? + Math.min(...degradedRetrySeconds) * 1000 : null; if (sectionValues && sectionValues.length && sectionValues.every(section => section.degraded)) { - const retrySeconds = Number(freshness.retry_in_seconds); - if (Number.isFinite(retrySeconds) && retrySeconds > 0) return retrySeconds * 1000; + return failedSectionDelay || intervalMs; } const freshForSeconds = Number(freshness && freshness.fresh_for_seconds); const healthyDeadlines = (sectionValues || []) @@ -55,10 +60,10 @@ function createContextPoller({ .filter(seconds => Number.isFinite(seconds)); if (Number.isFinite(freshForSeconds) && freshForSeconds > 0 && healthyDeadlines.length) { const earliestDeadline = Math.min(...healthyDeadlines); - if (earliestDeadline <= 0 && sectionValues.some(section => !section.degraded && section.revalidating)) { - return intervalMs; - } - return Math.max(0, earliestDeadline) * 1000; + const healthyDelay = earliestDeadline <= 0 && + sectionValues.some(section => !section.degraded && section.revalidating) ? + intervalMs : Math.max(0, earliestDeadline) * 1000; + return failedSectionDelay === null ? healthyDelay : Math.min(healthyDelay, failedSectionDelay); } return intervalMs; } diff --git a/frontend/progressive-human-gates.js b/frontend/progressive-human-gates.js index 82e0f42..7a4c14d 100644 --- a/frontend/progressive-human-gates.js +++ b/frontend/progressive-human-gates.js @@ -12,7 +12,7 @@ function createProgressiveHumanGates(options = {}) { }); const liveSnapshot = options.liveSnapshot || null; const getIdentity = options.getIdentity || (async () => { - const response = liveSnapshot ? await liveSnapshot.acquire() : + const response = liveSnapshot ? await liveSnapshot.acquire({requireIdentity:true}) : await fetchJson('api/v1/live', {headers:{Accept:'application/json'}}); const user = response?.context?.user || {}; const login = String(user.login || '').trim(); diff --git a/frontend/progressive-live-snapshot.js b/frontend/progressive-live-snapshot.js index b327309..51d7d7d 100644 --- a/frontend/progressive-live-snapshot.js +++ b/frontend/progressive-live-snapshot.js @@ -2,9 +2,18 @@ function createProgressiveLiveSnapshot({fetchSnapshot}) { let value = null; let flight = null; - const acquire = () => { - if (value) return Promise.resolve(value); - if (flight) return flight; + const hasIdentity = snapshot => { + const user = snapshot?.context?.user || {}; + return Boolean(String(user.login || '').trim()); + }; + const requireUsable = (snapshot, options) => { + if (!options?.requireIdentity || hasIdentity(snapshot)) return snapshot; + if (value === snapshot) value = null; + throw new Error('Authenticated account identity is unavailable.'); + }; + const acquire = (options = {}) => { + if (value) return Promise.resolve().then(() => requireUsable(value, options)); + if (flight) return flight.then(snapshot => requireUsable(snapshot, options)); flight = Promise.resolve().then(() => fetchSnapshot()).then(snapshot => { value = snapshot; flight = null; @@ -13,7 +22,7 @@ function createProgressiveLiveSnapshot({fetchSnapshot}) { flight = null; throw error; }); - return flight; + return flight.then(snapshot => requireUsable(snapshot, options)); }; return { diff --git a/frontend/progressive-my-work.js b/frontend/progressive-my-work.js index 6ea364d..ab65e59 100644 --- a/frontend/progressive-my-work.js +++ b/frontend/progressive-my-work.js @@ -24,6 +24,7 @@ function createProgressiveMyWork({ let poller = null; let detailTrigger = null; let openWork = null; + let contextUnavailable = false; const deferredQueues = { today:'Today', agenda:'Agenda', later:'Later', draft:'Drafts', }; @@ -89,7 +90,8 @@ function createProgressiveMyWork({ const render = () => { if (stopped || !list) return; const visible = visibleItems(); - list.innerHTML = deferredQueues[active] ? + list.innerHTML = contextUnavailable && !items.length ? + '
Assigned work is reconnecting…
' : deferredQueues[active] ? '
' + deferredQueues[active] + ' is still loading…
' : visible.length ? visible.map((item, index) => { const title = escapeHtml(item.title || item.key || 'Untitled work'); const context = escapeHtml(item.key || ''); @@ -128,6 +130,17 @@ function createProgressiveMyWork({ if (stopped) return false; const transferable = snapshot && typeof snapshot === 'object' && Object.prototype.hasOwnProperty.call(snapshot, 'context'); + const contextDegraded = transferable && ( + !snapshot.context || snapshot.freshness?.sections?.context?.degraded + ); + if (contextDegraded) { + contextUnavailable = true; + render(); + notify(); + if (status) status.textContent = 'Assigned work is reconnecting…'; + return false; + } + contextUnavailable = false; if (transferable) liveSnapshot = snapshot; const context = snapshot?.context || snapshot || {}; confirmedLogin = String(context.user?.login || '').trim(); diff --git a/frontend/service-worker.js b/frontend/service-worker.js index 8fd3e4d..ca3f8c1 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-v143'; +const CACHE = 'stackchain-dashboard-shell-v144'; 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/test_comment_next.py b/tests/test_comment_next.py index 058ab45..eb0bb27 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-v143" in worker + assert "stackchain-dashboard-shell-v144" in worker diff --git a/tests/test_context_polling.py b/tests/test_context_polling.py index 11ff4bd..03e2ba8 100644 --- a/tests/test_context_polling.py +++ b/tests/test_context_polling.py @@ -202,6 +202,40 @@ def test_dashboard_adopts_progressive_snapshot_or_falls_back_to_immediate_load() assert "if (!adoptedProgressiveSnapshot) await load();" in source +def test_context_poller_uses_failed_section_retry_before_healthy_freshness_deadline(): + script = f""" +const createContextPoller = require({json.dumps(str(POLLER))}); +const delays = []; +const poller = createContextPoller({{ + fetchContext: () => Promise.resolve({{ + context: null, events: [], notifications: [], + freshness: {{ + fresh_for_seconds: 8, + retry_in_seconds: 5, + sections: {{ + context: {{ degraded: true, retry_in_seconds: 5, age_seconds: 12 }}, + events: {{ degraded: false, age_seconds: 0 }}, + notifications: {{ degraded: false, age_seconds: 0 }}, + }}, + }}, + }}), + onSnapshot: () => {{}}, + onError: error => {{ throw error; }}, + setTimer: (_callback, delay) => {{ delays.push(delay); return delays.length; }}, + clearTimer: () => {{}}, + setDeadlineTimer: () => 1, + clearDeadlineTimer: () => {{}}, + intervalMs: 8000, +}}); +(async () => {{ + await poller.refresh(); + process.stdout.write(JSON.stringify({{delays}})); +}})(); +""" + + assert run_node(script) == {"delays": [5000]} + + def test_context_poller_waits_for_server_cooldown_when_every_section_is_degraded(): script = f""" const createContextPoller = require({json.dumps(str(POLLER))}); diff --git a/tests/test_following_frontend.py b/tests/test_following_frontend.py index 8894d53..6bd6e85 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-v143" in service_worker + assert "stackchain-dashboard-shell-v144" 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 d212956..b91a6ab 100644 --- a/tests/test_human_gates_frontend.py +++ b/tests/test_human_gates_frontend.py @@ -76,6 +76,50 @@ const gates=createProgressiveHumanGates({{ } +def test_progressive_human_gates_retries_partial_snapshot_identity_without_hydration(): + script = f""" +const createProgressiveLiveSnapshot=require({json.dumps(str(LIVE_SNAPSHOT))}); +const createProgressiveHumanGates=require({json.dumps(str(PROGRESSIVE))}); +let liveCalls=0, gateCalls=0; +const responses=[ + {{context:null,events:[],notifications:[]}}, + {{context:{{user:{{id:7,login:'timmy'}}}},events:[],notifications:[]}}, +]; +const broker=createProgressiveLiveSnapshot({{fetchSnapshot:async()=>{{liveCalls+=1;return responses.shift();}}}}); +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()=>{{gateCalls+=1;return {{pending_count:1,items:[{{id:'g1',title:'Recovered',candidate_hash:'abc',revision:1,checks:[]}}]}};}}, +}}); +(async()=>{{ + let firstError=''; + try{{await gates.start();}}catch(error){{firstError=error.message;}} + const recovered=await gates.start(); + process.stdout.write(JSON.stringify({{ + firstError,recovered,liveCalls,gateCalls,started: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) == { + "firstError": "Authenticated account identity is unavailable.", + "recovered": True, + "liveCalls": 2, + "gateCalls": 2, + "started": 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)}; @@ -262,7 +306,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-v143" in WORKER.read_text() + assert "stackchain-dashboard-shell-v144" 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 a9808e4..e62baf2 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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 9255b3c..9e08eba 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-v143" in worker + assert "stackchain-dashboard-shell-v144" in worker diff --git a/tests/test_mobile_composer_integration.py b/tests/test_mobile_composer_integration.py index 8e90622..a3c94d3 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-v143" in worker + assert "stackchain-dashboard-shell-v144" 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 ba18263..e71fbb5 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-v143" in worker + assert "stackchain-dashboard-shell-v144" 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 f829b16..eb4c7e2 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-v143" in worker + assert "stackchain-dashboard-shell-v144" 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 aee85c3..c3333b4 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-v143" in service_worker + assert "stackchain-dashboard-shell-v144" in service_worker @pytest.mark.anyio diff --git a/tests/test_plan_today.py b/tests/test_plan_today.py index f8fd3e9..c101b9b 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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 bed0a84..cfe26f5 100644 --- a/tests/test_progressive_my_work.py +++ b/tests/test_progressive_my_work.py @@ -50,6 +50,50 @@ const broker=createProgressiveLiveSnapshot({{fetchSnapshot:()=>{{ } +def test_progressive_live_snapshot_retries_identityless_success_for_identity_consumers(): + harness = f""" +const createProgressiveLiveSnapshot=require({json.dumps(str(LIVE_SNAPSHOT))}); +let calls=0; +const responses=[ + {{context:null,events:[],notifications:[{{id:1}}]}}, + {{context:{{user:{{id:7,login:'timmy'}}}},events:[],notifications:[]}}, +]; +const broker=createProgressiveLiveSnapshot({{fetchSnapshot:async()=>{{ + calls += 1; + return responses.shift(); +}}}}); +(async()=>{{ + const partial=await broker.acquire(); + const first=await Promise.allSettled([ + broker.acquire({{requireIdentity:true}}), + broker.acquire({{requireIdentity:true}}), + ]); + const retries=[ + broker.acquire({{requireIdentity:true}}), + broker.acquire({{requireIdentity:true}}), + ]; + const recovered=await Promise.all(retries); + process.stdout.write(JSON.stringify({{ + calls, + partialNotifications:partial.notifications.length, + first:first.map(result=>result.status), + same:recovered[0]===recovered[1], + identity:broker.identity(), + }})); +}})().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, + "partialNotifications": 1, + "first": ["rejected", "rejected"], + "same": True, + "identity": {"login": "timmy", "accountKey": "7:timmy"}, + } + + def test_progressive_live_snapshot_retries_after_a_failed_shared_flight(): harness = f""" const createProgressiveLiveSnapshot=require({json.dumps(str(LIVE_SNAPSHOT))}); @@ -78,6 +122,43 @@ const broker=createProgressiveLiveSnapshot({{fetchSnapshot:async()=>{{ } +def test_progressive_my_work_reports_reconnecting_for_degraded_context_instead_of_inbox_zero(): + harness = f""" +const fs=require('fs'); const vm=require('vm'); +const list={{innerHTML:''}}; const status={{textContent:''}}; +const document={{ + hidden:false, + querySelector:s=>s==='#my-work-list'?list:s==='#my-work-status'?status:null, + querySelectorAll:()=>[], +}}; +const context={{module:{{exports:{{}}}},exports:{{}},console,URL,document}}; vm.createContext(context); +vm.runInContext(fs.readFileSync({json.dumps(str(MY_WORK))},'utf8'),context); +context.buildMyWork=context.module.exports; context.module={{exports:{{}}}}; +vm.runInContext(fs.readFileSync({json.dumps(str(MODULE))},'utf8'),context); +const flow=context.module.exports({{document,fetchSnapshot:async()=>({{ + context:null,events:[],notifications:[{{id:1}}], + freshness:{{retry_in_seconds:5,sections:{{ + context:{{degraded:true,retry_in_seconds:5}}, + events:{{degraded:false,age_seconds:1}},notifications:{{degraded:false,age_seconds:1}}, + }}}}, +}})}}); +(async()=>{{ + const started=await flow.start(); + process.stdout.write(JSON.stringify({{started,status:status.textContent,html:list.innerHTML,counts:flow.counts()}})); +}})().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) == { + "started": False, + "status": "Assigned work is reconnecting…", + "html": '
Assigned work is reconnecting…
', + "counts": {"all": 0, "filed": 0, "authored": 0, "attention": 0, + "update": 0, "review": 0}, + } + + 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 e6fe7c8..58bf7b4 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" in source def test_per_day_week_conflict_ui_rolls_the_offline_shell(): source = WORKER.read_text() - assert "stackchain-dashboard-shell-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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-v143" in source + assert "stackchain-dashboard-shell-v144" 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 d038d66..25623c3 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-v143';" in service_worker + assert "const CACHE = 'stackchain-dashboard-shell-v144';" 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 df09e48..78a5748 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-v143" in source + assert "stackchain-dashboard-shell-v144" in source assert "BASE + 'static/today-sync.js'" in source