feat: hand off Today lock-screen actions privately (Closes #1172)
CI / lint (pull_request) Successful in 3m4s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 4m2s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-20 08:02:45 +00:00
1 parent f56ca6373c
commit 988f6646c2
18 files changed
+228 -68

No files matched your search

+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-v125" in worker
assert "stackchain-dashboard-shell-v126" in worker
+5
View File
@@ -364,6 +364,7 @@ process.stdout.write(JSON.stringify(state));
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
"stackchain-today-action-mailbox-v1",
]
assert result["remaining"] == ["gitea.preference"]
assert result["replaced"] == [
@@ -391,6 +392,7 @@ process.stdout.write(JSON.stringify(state));
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
"stackchain-today-action-mailbox-v1",
]
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
assert result["replaced"] == ["/dashboard/login?reason=session-expired"]
@@ -523,6 +525,7 @@ process.stdout.write(JSON.stringify(state));
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
"stackchain-today-action-mailbox-v1",
]
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
@@ -547,6 +550,7 @@ process.stdout.write(JSON.stringify(state));
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
"stackchain-today-action-mailbox-v1",
]
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
@@ -644,6 +648,7 @@ process.stdout.write(JSON.stringify(state));
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
"stackchain-today-action-mailbox-v1",
]
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
assert result["assigned"] == "/dashboard/login"
+1 -1
View File
@@ -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-v125" in source
assert "stackchain-dashboard-shell-v126" 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-v125" in worker
assert "stackchain-dashboard-shell-v126" 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-v125" in worker
assert "stackchain-dashboard-shell-v126" in worker
def test_all_conversation_composers_offer_accessible_mobile_mentions():
+1 -1
View File
@@ -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-v125" in worker
assert "stackchain-dashboard-shell-v126" in worker
assert ".device-setup-panel" in css
assert ".device-readiness-card" in css
assert "overflow-x:hidden" in css
+1 -1
View File
@@ -243,5 +243,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-v125" in worker
assert "stackchain-dashboard-shell-v126" in worker
assert "BASE + 'static/mobile-insights.js'" in worker
+1 -1
View File
@@ -358,7 +358,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-v125" in service_worker
assert "stackchain-dashboard-shell-v126" in service_worker
@pytest.mark.anyio
+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-v125" in source
assert "stackchain-dashboard-shell-v126" 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
+1
View File
@@ -33,6 +33,7 @@ process.stdout.write(JSON.stringify(databases));
"stackchain-voice-transcripts-v1",
"stackchain-search-reply-drafts-v1",
"stackchain-conversation-reply-drafts-v1",
"stackchain-today-action-mailbox-v1",
}
+1
View File
@@ -50,6 +50,7 @@ const clear = createPrivateDeviceDataPurger({{
"stackchain-voice-transcripts-v1",
"stackchain-search-reply-drafts-v1",
"stackchain-conversation-reply-drafts-v1",
"stackchain-today-action-mailbox-v1",
]
assert state["caches"] == ["stackchain-dashboard-shell-v37"]
assert state["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
+101 -22
View File
@@ -14,7 +14,7 @@ def run_worker_scenario(scenario: str) -> dict:
const fs = require('fs');
const vm = require('vm');
const listeners = {{}};
const state = {{ added: [], addAttempts: [], individuallyAdded: [], failedAdds: [], deleted: [], deletedDatabases: [], claimed: false, skipped: false, fetches: [], puts: [], migrated: [], activationOrder: [], oldCachedAssets: {{}}, sharedRecords: {{}}, failSharedPut: false, backgroundFlushes: 0, backgroundResumes: 0, outboxPurges: 0, outboxLifecycle: [], notifications: [], closedNotifications: 0, focused: [], opened: [], failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }};
const state = {{ added: [], addAttempts: [], individuallyAdded: [], failedAdds: [], deleted: [], deletedDatabases: [], claimed: false, skipped: false, fetches: [], puts: [], migrated: [], activationOrder: [], oldCachedAssets: {{}}, sharedRecords: {{}}, todayCommands: {{}}, failTodayCommandPut: false, failSharedPut: false, backgroundFlushes: 0, backgroundResumes: 0, outboxPurges: 0, outboxLifecycle: [], notifications: [], closedNotifications: 0, focused: [], opened: [], failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }};
const storedResponses = new Map();
storedResponses.set(
'https://forge.example/dashboard/__offline-session-lease',
@@ -66,12 +66,24 @@ const context = {{
get: async id => state.sharedRecords[id] || null,
delete: async id => {{ delete state.sharedRecords[id]; }},
}},
__STACKCHAIN_TODAY_ACTION_STORE: {{
put: async (clientId, command) => {{
if (state.failTodayCommandPut) throw new Error('quota');
state.todayCommands[clientId] = command;
}},
claim: async (clientId, now) => {{
const command = state.todayCommands[clientId] || null;
delete state.todayCommands[clientId];
return command && command.expiresAt > now ? command : null;
}},
purge: async () => {{ state.todayCommands = {{}}; }},
}},
addEventListener: (name, handler) => {{ listeners[name] = handler; }},
skipWaiting: async () => {{ state.skipped = true; }},
clients: {{
claim: async () => {{ state.claimed = true; }},
matchAll: async () => state.clientList || [],
openWindow: async url => {{ state.opened.push(url); }},
openWindow: async url => {{ state.opened.push(url); return state.openedClient || {{id:'opened-client',url}}; }},
}},
registration: {{
showNotification: async (title, options) => state.notifications.push({{title,options}}),
@@ -133,9 +145,9 @@ async function dispatchSync(tag) {{
listeners.sync({{ tag, waitUntil: promise => {{ pending = promise; }} }});
if (pending) await pending;
}}
async function dispatchMessage(data, ports = []) {{
async function dispatchMessage(data, ports = [], source = null) {{
let pending;
listeners.message({{ data, ports, waitUntil: promise => {{ pending = promise; }} }});
listeners.message({{ data, ports, source, waitUntil: promise => {{ pending = promise; }} }});
if (pending) await pending;
}}
async function dispatchNotificationClick(route, action = '', notificationId = null, tag = null, url = null, actionToken = '') {{
@@ -165,16 +177,16 @@ async function dispatchPush(payload) {{
return json.loads(completed.stdout)
def test_offline_activation_migration_rolls_the_shell_cache():
def test_private_today_action_mailbox_rolls_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v125" in source
assert "stackchain-dashboard-shell-v126" in source
def test_resumable_today_session_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v125" in source
assert "stackchain-dashboard-shell-v126" in source
assert "BASE + 'static/my-work.js'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@@ -183,7 +195,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-v125" in source
assert "stackchain-dashboard-shell-v126" 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
@@ -192,7 +204,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-v125" in source
assert "stackchain-dashboard-shell-v126" in source
assert "BASE + 'static/issue-evidence-review.js'" in source
assert "BASE + 'static/issue-attachment.js'" in source
@@ -200,14 +212,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-v125" in source
assert "stackchain-dashboard-shell-v126" 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-v125" in source
assert "stackchain-dashboard-shell-v126" in source
assert "BASE + 'static/today-completion.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -215,7 +227,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-v125" in source
assert "stackchain-dashboard-shell-v126" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -223,7 +235,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-v125" in source
assert "stackchain-dashboard-shell-v126" 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
@@ -233,14 +245,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-v125" in source
assert "stackchain-dashboard-shell-v126" 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-v125" in source
assert "stackchain-dashboard-shell-v126" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@@ -249,21 +261,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-v125" in source
assert "stackchain-dashboard-shell-v126" 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-v125" in source
assert "stackchain-dashboard-shell-v126" 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-v125" in source
assert "stackchain-dashboard-shell-v126" in source
assert "BASE + 'static/update-ownership.js'" in source
@@ -451,6 +463,7 @@ def test_revoked_background_session_purges_worker_data_and_notifies_dashboard_cl
"stackchain-voice-transcripts-v1",
"stackchain-search-reply-drafts-v1",
"stackchain-conversation-reply-drafts-v1",
"stackchain-today-action-mailbox-v1",
]
assert result["state"]["deleted"] == ["stackchain-dashboard-old"]
assert result["state"]["clientMessages"] == [
@@ -610,17 +623,81 @@ def test_today_lock_screen_action_updates_an_open_dashboard_without_navigation()
assert result["opened"] == []
def test_today_lock_screen_action_opens_a_validated_one_shot_route_when_closed():
def test_today_lock_screen_action_opens_clean_route_and_privately_hands_off_once_when_closed():
result = run_worker_scenario(
"""
await dispatchNotificationClick('#/my-work/today','resume-today',null,'stackchain-today-session');
state.openedClient={id:'cold-client',url:'https://forge.example/dashboard/#/my-work/today'};
await dispatchNotificationClick('#/my-work/today','resume-today',null,'stackchain-today-session',null,'opaque-token-1234567890');
const messages=[];
const source={id:'cold-client',url:'https://forge.example/dashboard/#/my-work/today',postMessage:message=>messages.push(message)};
await dispatchMessage({type:'stackchain-claim-today-action'}, [], source);
await dispatchMessage({type:'stackchain-claim-today-action'}, [], source);
state.claimMessages=messages;
process.stdout.write(JSON.stringify(state));
"""
)
assert result["opened"] == [
"https://forge.example/dashboard/?today_timer_action=resume#/my-work/today"
"https://forge.example/dashboard/#/my-work/today"
]
assert "today_timer_action" not in result["opened"][0]
assert "today_action_token" not in result["opened"][0]
assert result["claimMessages"] == [{
"type": "stackchain-today-timer-action",
"action": "resume",
"actionToken": "opaque-token-1234567890",
}]
assert result["todayCommands"] == {}
def test_today_lock_screen_private_handoff_rejects_wrong_client_and_expires_closed():
result = run_worker_scenario(
"""
state.openedClient={id:'intended-client',url:'https://forge.example/dashboard/#/my-work/today'};
await dispatchNotificationClick('#/my-work/today','finish-today',null,'stackchain-today-session',null,'opaque-token-1234567890');
const wrong=[];
await dispatchMessage({type:'stackchain-claim-today-action'}, [], {id:'other-client',url:'https://forge.example/dashboard/#/my-work/today',postMessage:message=>wrong.push(message)});
state.todayCommands['intended-client'].expiresAt=0;
const expired=[];
await dispatchMessage({type:'stackchain-claim-today-action'}, [], {id:'intended-client',url:'https://forge.example/dashboard/#/my-work/today',postMessage:message=>expired.push(message)});
process.stdout.write(JSON.stringify({wrong,expired,commands:state.todayCommands}));
"""
)
assert result == {"wrong": [], "expired": [], "commands": {}}
def test_today_action_mailbox_persistence_failure_opens_clean_route_without_mutation():
result = run_worker_scenario(
"""
state.failTodayCommandPut=true;
state.openedClient={id:'cold-client',url:'https://forge.example/dashboard/#/my-work/today'};
const outcome=await dispatchNotificationClick('#/my-work/today','finish-today',null,'stackchain-today-session',null,'opaque-token-1234567890')
.then(()=> 'safe', error=>error.message);
process.stdout.write(JSON.stringify({outcome,opened:state.opened,commands:state.todayCommands}));
"""
)
assert result == {
"outcome": "safe",
"opened": ["https://forge.example/dashboard/#/my-work/today"],
"commands": {},
}
def test_private_data_purge_removes_pending_today_action_handoffs():
result = run_worker_scenario(
"""
state.openedClient={id:'cold-client',url:'https://forge.example/dashboard/#/my-work/today'};
await dispatchNotificationClick('#/my-work/today','finish-today',null,'stackchain-today-session',null,'opaque-token-1234567890');
await dispatchMessage({type:'stackchain-purge-outbox'}, [{postMessage:()=>{}}]);
const messages=[];
await dispatchMessage({type:'stackchain-claim-today-action'}, [], {id:'cold-client',url:'https://forge.example/dashboard/#/my-work/today',postMessage:message=>messages.push(message)});
process.stdout.write(JSON.stringify({messages,commands:state.todayCommands}));
"""
)
assert result == {"messages": [], "commands": {}}
def test_finish_today_lock_screen_action_forwards_only_its_opaque_token():
@@ -707,6 +784,7 @@ def test_device_purge_message_stops_worker_outbox_and_acknowledges_completion():
"stackchain-voice-transcripts-v1",
"stackchain-search-reply-drafts-v1",
"stackchain-conversation-reply-drafts-v1",
"stackchain-today-action-mailbox-v1",
]
assert result["replies"] == [{"ok": True}]
@@ -1167,7 +1245,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-v125" in source
assert "stackchain-dashboard-shell-v126" in source
assert "BASE + 'static/queue-today.js'" in source
@@ -1465,6 +1543,7 @@ def test_expired_offline_lease_purges_private_worker_data_and_refuses_cached_she
"stackchain-voice-transcripts-v1",
"stackchain-search-reply-drafts-v1",
"stackchain-conversation-reply-drafts-v1",
"stackchain-today-action-mailbox-v1",
]
assert result["state"]["deleted"] == ["stackchain-dashboard-old"]
assert "private cached dashboard" not in result["body"]
+11 -15
View File
@@ -74,21 +74,19 @@ def test_disabling_or_ending_session_removes_notification():
]
def test_valid_notification_action_is_consumed_once_and_removed_from_url():
def test_cold_launch_requests_one_private_action_claim_without_url_capabilities():
result = run_scenario(r"""
values.set('stackchain.today-lock-screen.v1.timmy','1');
locationRef.href='https://forge.example/dashboard/?today_timer_action=pause#/my-work/today';
const consumed = await lockScreen.consumeLaunchAction();
const second = await lockScreen.consumeLaunchAction();
listeners['sw-message']({data:{type:'stackchain-today-timer-action',action:'resume'}});
process.stdout.write(JSON.stringify({consumed,second,actions,href:locationRef.href}));
process.stdout.write(JSON.stringify({consumed,actions,href:locationRef.href,messages}));
""")
assert result == {
"consumed": True,
"second": False,
"actions": [["pause", None], ["resume", None]],
"actions": [["resume", None]],
"href": "https://forge.example/dashboard/#/my-work/today",
"messages": [{"type": "stackchain-claim-today-action"}],
}
@@ -133,20 +131,18 @@ def test_break_sync_carries_deadline_and_resume_is_bound_to_that_exact_break():
assert "private/repo" not in json.dumps(result["messages"])
def test_cold_launch_finish_action_is_removed_from_history_before_completion():
def test_private_cold_launch_finish_action_is_bound_to_the_active_identity():
result = run_scenario(r"""
await lockScreen.enable();
await lockScreen.sync({identity:'issue:private/repo:42:',running:true}, true);
locationRef.href='https://forge.example/dashboard/?today_timer_action=complete&today_action_token=opaque-token-1234567890#/my-work/today';
const consumed = await lockScreen.consumeLaunchAction();
process.stdout.write(JSON.stringify({consumed,actions,href:locationRef.href}));
await lockScreen.consumeLaunchAction();
await listeners['sw-message']({data:{type:'stackchain-today-timer-action',action:'complete',actionToken:'opaque-token-1234567890'}});
process.stdout.write(JSON.stringify({actions,href:locationRef.href,messages}));
""")
assert result == {
"consumed": True,
"actions": [["complete", "issue:private/repo:42:"]],
"href": "https://forge.example/dashboard/#/my-work/today",
}
assert result["actions"] == [["complete", "issue:private/repo:42:"]]
assert result["href"] == "https://forge.example/dashboard/#/my-work/today"
assert result["messages"][-1] == {"type": "stackchain-claim-today-action"}
def test_permission_denial_is_truthful_and_does_not_persist_opt_in():
+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-v125';" in service_worker
assert "const CACHE = 'stackchain-dashboard-shell-v126';" in service_worker
assert "BASE + 'static/today-readiness.js'" in service_worker
+1 -1
View File
@@ -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-v125" in source
assert "stackchain-dashboard-shell-v126" in source
assert "BASE + 'static/today-sync.js'" in source