feat: snooze mobile deadline reminders (Closes #1102)
This commit is contained in:
1 parent
fdf6ae3ddd
commit
2bd20130b6
17 files changed
+333
-44
No files matched your search
@@ -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-v120" in worker
|
||||
assert "stackchain-dashboard-shell-v121" in worker
|
||||
@@ -55,6 +55,110 @@ async def test_deadline_reminder_sends_one_private_local_day_digest_and_deduplic
|
||||
assert "private/repo" not in json.dumps(sent)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_snoozed_deadline_revalidates_and_sends_once_after_expiry(tmp_path):
|
||||
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
||||
store.upsert("device-a", {
|
||||
"endpoint": "https://push.example/device-a",
|
||||
"keys": {"p256dh": "public-key", "auth": "auth-secret"},
|
||||
})
|
||||
store.set_deadline_preferences(
|
||||
"device-a", enabled=True, timezone="UTC", reminder_hour=9
|
||||
)
|
||||
store.mark_deadline_reminder_delivered("device-a", "2026-08-13")
|
||||
snoozed_at = datetime(2026, 8, 13, 10, 0, tzinfo=timezone.utc)
|
||||
store.snooze_deadline_reminder(
|
||||
"device-a", now=snoozed_at.timestamp(), delay_seconds=3_600
|
||||
)
|
||||
snapshots = 0
|
||||
sent = []
|
||||
|
||||
async def assigned():
|
||||
nonlocal snapshots
|
||||
snapshots += 1
|
||||
return {"complete": True, "items": [{"id": 1, "due_date": "2026-08-14"}]}
|
||||
|
||||
async def send(_subscription, payload):
|
||||
sent.append(json.loads(payload))
|
||||
|
||||
config = PushConfiguration("public", "private", "mailto:ops@example.com")
|
||||
assert await dispatch_deadline_reminders(
|
||||
store, config, assigned, send,
|
||||
now=datetime(2026, 8, 13, 10, 59, tzinfo=timezone.utc),
|
||||
) == 0
|
||||
assert snapshots == 0
|
||||
|
||||
assert await dispatch_deadline_reminders(
|
||||
store, config, assigned, send,
|
||||
now=datetime(2026, 8, 13, 11, 0, tzinfo=timezone.utc),
|
||||
) == 1
|
||||
assert snapshots == 1
|
||||
assert sent[0]["deadline_count"] == 1
|
||||
assert sent[0]["tag"] == "stackchain-deadline-digest-2026-08-13"
|
||||
assert store.deadline_reminder_devices()[0].snoozed_until is None
|
||||
assert await dispatch_deadline_reminders(
|
||||
store, config, assigned, send,
|
||||
now=datetime(2026, 8, 13, 11, 1, tzinfo=timezone.utc),
|
||||
) == 0
|
||||
assert snapshots == 1
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_expired_deadline_snooze_clears_when_no_deadlines_remain(tmp_path):
|
||||
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
||||
store.upsert("device-a", {
|
||||
"endpoint": "https://push.example/device-a",
|
||||
"keys": {"p256dh": "public-key", "auth": "auth-secret"},
|
||||
})
|
||||
store.set_deadline_preferences(
|
||||
"device-a", enabled=True, timezone="UTC", reminder_hour=9
|
||||
)
|
||||
store.mark_deadline_reminder_delivered("device-a", "2026-08-13")
|
||||
store.snooze_deadline_reminder(
|
||||
"device-a",
|
||||
now=datetime(2026, 8, 13, 10, 0, tzinfo=timezone.utc).timestamp(),
|
||||
)
|
||||
|
||||
async def resolved():
|
||||
return {"complete": True, "items": []}
|
||||
|
||||
assert await dispatch_deadline_reminders(
|
||||
store,
|
||||
PushConfiguration("public", "private", "mailto:ops@example.com"),
|
||||
resolved,
|
||||
now=datetime(2026, 8, 13, 11, 0, tzinfo=timezone.utc),
|
||||
) == 0
|
||||
assert store.deadline_reminder_devices()[0].snoozed_until is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_expired_deadline_snooze_clears_when_deadlines_move_beyond_horizon(tmp_path):
|
||||
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
||||
store.upsert("device-a", {
|
||||
"endpoint": "https://push.example/device-a",
|
||||
"keys": {"p256dh": "public-key", "auth": "auth-secret"},
|
||||
})
|
||||
store.set_deadline_preferences(
|
||||
"device-a", enabled=True, timezone="UTC", reminder_hour=9, reminder_days=2
|
||||
)
|
||||
store.mark_deadline_reminder_delivered("device-a", "2026-08-13")
|
||||
store.snooze_deadline_reminder(
|
||||
"device-a",
|
||||
now=datetime(2026, 8, 13, 10, 0, tzinfo=timezone.utc).timestamp(),
|
||||
)
|
||||
|
||||
async def replanned():
|
||||
return {"complete": True, "items": [{"id": 1, "due_date": "2026-08-20"}]}
|
||||
|
||||
assert await dispatch_deadline_reminders(
|
||||
store,
|
||||
PushConfiguration("public", "private", "mailto:ops@example.com"),
|
||||
replanned,
|
||||
now=datetime(2026, 8, 13, 11, 0, tzinfo=timezone.utc),
|
||||
) == 0
|
||||
assert store.deadline_reminder_devices()[0].snoozed_until is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_deadline_reminder_counts_calendar_days_per_device_timezone(tmp_path):
|
||||
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
||||
@@ -240,6 +344,35 @@ def test_deadline_preferences_persist_on_the_existing_device_subscription(tmp_pa
|
||||
}
|
||||
|
||||
|
||||
def test_deadline_snooze_is_device_bound_and_requires_enabled_reminders(tmp_path):
|
||||
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
||||
for session_id in ("enabled", "disabled"):
|
||||
store.upsert(session_id, {
|
||||
"endpoint": f"https://push.example/{session_id}",
|
||||
"keys": {"p256dh": "public-key", "auth": "auth-secret"},
|
||||
})
|
||||
store.set_deadline_preferences(
|
||||
"enabled", enabled=True, timezone="UTC", reminder_hour=9
|
||||
)
|
||||
store.set_deadline_preferences(
|
||||
"disabled", enabled=False, timezone="UTC", reminder_hour=9
|
||||
)
|
||||
|
||||
assert store.snooze_deadline_reminder(
|
||||
"enabled", now=1_765_000_000, delay_seconds=3_600
|
||||
) is True
|
||||
assert store.snooze_deadline_reminder(
|
||||
"disabled", now=1_765_000_000, delay_seconds=3_600
|
||||
) is False
|
||||
assert store.snooze_deadline_reminder(
|
||||
"missing", now=1_765_000_000, delay_seconds=3_600
|
||||
) is False
|
||||
|
||||
devices = {device.session_id: device for device in store.deadline_reminder_devices()}
|
||||
assert devices["enabled"].snoozed_until == 1_765_003_600
|
||||
assert "disabled" not in devices
|
||||
|
||||
|
||||
def test_existing_deadline_preferences_migrate_to_two_day_horizon(tmp_path):
|
||||
database = tmp_path / "push.sqlite3"
|
||||
import sqlite3
|
||||
|
||||
@@ -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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" in source
|
||||
assert "BASE + 'static/later-sync.js'" in source
|
||||
@@ -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-v120" in worker
|
||||
assert "stackchain-dashboard-shell-v121" in worker
|
||||
@@ -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-v120" in worker
|
||||
assert "stackchain-dashboard-shell-v121" in worker
|
||||
|
||||
|
||||
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
||||
|
||||
@@ -265,7 +265,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
|
||||
assert "promptStorage:localStorage" in dashboard
|
||||
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
||||
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
||||
assert "stackchain-dashboard-shell-v120" in worker
|
||||
assert "stackchain-dashboard-shell-v121" in worker
|
||||
assert ".device-setup-panel" in css
|
||||
assert ".device-readiness-card" in css
|
||||
assert "overflow-x:hidden" in css
|
||||
|
||||
@@ -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-v120" in worker
|
||||
assert "stackchain-dashboard-shell-v121" in worker
|
||||
assert "BASE + 'static/mobile-insights.js'" in worker
|
||||
@@ -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-v120" in service_worker
|
||||
assert "stackchain-dashboard-shell-v121" in service_worker
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
||||
@@ -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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" 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
|
||||
@@ -1090,6 +1090,35 @@ async def test_authenticated_device_can_enable_deadline_reminders(tmp_path, monk
|
||||
assert store.deadline_preferences("session-a")["reminder_days"] == 7
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_authenticated_device_can_snooze_its_enabled_deadline_reminder(tmp_path, monkeypatch):
|
||||
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
||||
for session_id in ("session-a", "session-b"):
|
||||
store.upsert(session_id, {
|
||||
"endpoint": f"https://push.example/{session_id}",
|
||||
"keys": {"p256dh": "public-key", "auth": "auth-secret"},
|
||||
})
|
||||
store.set_deadline_preferences(
|
||||
session_id, enabled=True, timezone="UTC", reminder_hour=9
|
||||
)
|
||||
monkeypatch.setattr(main, "_push_subscription_store", store)
|
||||
monkeypatch.setattr(main.time, "time", lambda: 1_765_000_000)
|
||||
|
||||
async def management_id(_session):
|
||||
return "session-a"
|
||||
|
||||
monkeypatch.setattr(main.dashboard_auth, "session_management_id", management_id)
|
||||
request = SimpleNamespace(state=SimpleNamespace(dashboard_session=object()))
|
||||
|
||||
assert await main.snooze_deadline_reminder(request) == {
|
||||
"snoozed": True,
|
||||
"snoozed_until": 1_765_003_600,
|
||||
}
|
||||
devices = {device.session_id: device for device in store.deadline_reminder_devices()}
|
||||
assert devices["session-a"].snoozed_until == 1_765_003_600
|
||||
assert devices["session-b"].snoozed_until is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_subscription_rejects_an_unsafe_endpoint_before_persistence(tmp_path, monkeypatch):
|
||||
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
||||
|
||||
@@ -168,13 +168,13 @@ async function dispatchPush(payload) {{
|
||||
def test_offline_activation_migration_rolls_the_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" in source
|
||||
|
||||
|
||||
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" 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 +183,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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" 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 +192,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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" in source
|
||||
assert "BASE + 'static/issue-evidence-review.js'" in source
|
||||
assert "BASE + 'static/issue-attachment.js'" in source
|
||||
|
||||
@@ -200,14 +200,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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" 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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" in source
|
||||
assert "BASE + 'static/today-completion.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
@@ -215,7 +215,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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" in source
|
||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
@@ -223,7 +223,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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" 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 +233,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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" 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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" 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 +249,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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" 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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" 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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
@@ -850,7 +850,7 @@ def test_update_digest_push_opens_unread_inbox_without_item_actions_or_private_c
|
||||
assert "must-not-render" not in json.dumps(result["notifications"])
|
||||
|
||||
|
||||
def test_deadline_digest_push_offers_protect_today_and_agenda_without_rendering_private_copy():
|
||||
def test_deadline_digest_push_offers_protect_today_and_snooze_without_rendering_private_copy():
|
||||
result = run_worker_scenario(
|
||||
"""
|
||||
await dispatchPush({
|
||||
@@ -870,7 +870,7 @@ def test_deadline_digest_push_offers_protect_today_and_agenda_without_rendering_
|
||||
"tag": "stackchain-deadline-digest-2026-08-13",
|
||||
"actions": [
|
||||
{"action": "protect-today", "title": "Protect Today"},
|
||||
{"action": "open-agenda", "title": "Open Agenda"},
|
||||
{"action": "snooze-deadline", "title": "Remind in 1 hour"},
|
||||
],
|
||||
"data": {
|
||||
"route": "#/my-work/agenda",
|
||||
@@ -884,19 +884,62 @@ def test_deadline_digest_push_offers_protect_today_and_agenda_without_rendering_
|
||||
assert "must-not-render" not in json.dumps(result["notifications"])
|
||||
|
||||
|
||||
def test_deadline_digest_open_agenda_action_preserves_browsing_flow():
|
||||
def test_deadline_digest_snooze_action_records_device_bound_reminder_without_opening_app():
|
||||
result = run_worker_scenario(
|
||||
"""
|
||||
await dispatchPush({
|
||||
tag:'stackchain-deadline-digest-2026-08-13', route:'#/my-work/agenda',
|
||||
protect_route:'#/my-work/agenda/protect-today', deadline_count:1,
|
||||
});
|
||||
await dispatchNotificationClick('#/my-work/agenda', 'open-agenda', null, 'stackchain-deadline-digest-2026-08-13');
|
||||
const calls = [];
|
||||
context.fetch = async (request, options = {}) => {
|
||||
const url = String(request.url || request);
|
||||
const headers = new Headers(options.headers || {});
|
||||
calls.push({url, method:String(options.method || 'GET'), csrf:headers.get('X-CSRF-Token')});
|
||||
if (url.endsWith('/api/v1/session')) {
|
||||
return new Response(JSON.stringify({csrf_token:'session-proof'}), {
|
||||
status:200, headers:{'Content-Type':'application/json'},
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({snoozed:true}), {
|
||||
status:200, headers:{'Content-Type':'application/json'},
|
||||
});
|
||||
};
|
||||
await dispatchNotificationClick('#/my-work/agenda', 'snooze-deadline', null, 'stackchain-deadline-digest-2026-08-13');
|
||||
process.stdout.write(JSON.stringify({state,calls}));
|
||||
"""
|
||||
)
|
||||
|
||||
assert result["calls"] == [
|
||||
{
|
||||
"url": "https://forge.example/dashboard/api/v1/session",
|
||||
"method": "GET",
|
||||
"csrf": None,
|
||||
},
|
||||
{
|
||||
"url": "https://forge.example/dashboard/api/v1/push-subscription/deadlines/snooze",
|
||||
"method": "PATCH",
|
||||
"csrf": "session-proof",
|
||||
},
|
||||
]
|
||||
assert result["state"]["notificationClosed"] is True
|
||||
assert result["state"]["opened"] == []
|
||||
|
||||
|
||||
def test_deadline_digest_failed_snooze_opens_agenda_instead_of_losing_reminder():
|
||||
result = run_worker_scenario(
|
||||
"""
|
||||
context.fetch = async () => { throw new Error('offline'); };
|
||||
await dispatchNotificationClick(
|
||||
'#/my-work/agenda', 'snooze-deadline', null,
|
||||
'stackchain-deadline-digest-2026-08-13'
|
||||
);
|
||||
process.stdout.write(JSON.stringify(state));
|
||||
"""
|
||||
)
|
||||
|
||||
assert result["opened"] == ["https://forge.example/dashboard/#/my-work/agenda"]
|
||||
assert result["notificationClosed"] is True
|
||||
|
||||
|
||||
def test_push_mark_read_action_confirms_authenticated_mutation_without_opening_app():
|
||||
@@ -1094,7 +1137,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-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" in source
|
||||
assert "BASE + 'static/queue-today.js'" in source
|
||||
|
||||
|
||||
|
||||
@@ -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-v120';" in service_worker
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v121';" in service_worker
|
||||
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ sync.enqueueConfiguration(120, {{'issue:r:1:':60}});
|
||||
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v120" in source
|
||||
assert "stackchain-dashboard-shell-v121" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user