feat: follow pull requests from mobile Search (Closes #1307)
CI / lint (pull_request) Successful in 3m34s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 5m17s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-23 13:41:33 +00:00
1 parent 41c28cc70a
commit 7131792ded
23 files changed
+271 -83

No files matched your search

+25 -6
View File
@@ -13,25 +13,39 @@ ROOT = Path(__file__).parents[2]
FRONTEND = ROOT / "frontend"
def test_following_queue_is_phone_usable_at_narrow_viewport():
@pytest.mark.parametrize("viewport", [
{"width": 320, "height": 568},
{"width": 390, "height": 844},
])
def test_following_queue_is_phone_usable_at_narrow_viewport(viewport):
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 320, "height": 568})
page = browser.new_page(viewport=viewport)
page.set_content((FRONTEND / "index.html").read_text())
page.add_style_tag(path=FRONTEND / "dashboard.css")
page.add_script_tag(path=FRONTEND / "search-preview.js")
page.add_script_tag(path=FRONTEND / "following.js")
row = page.locator('[data-mobile-queue="following"]')
expect(row).to_have_count(1)
page.locator("#following-list").evaluate("""node => {
node.innerHTML = '<button class="following-card has-unseen-change" type="button"><span><em>New activity</em><strong>Changed issue with a long mobile title</strong><small>stackchain/api #42 · open · just now</small></span><span aria-hidden="true">›</span></button>';
expect(row).to_contain_text("Issues and pull requests you watch")
page.evaluate("""() => {
globalThis.fetch = async () => ({
ok:true,
json:async () => ({revision:1,items:[{
repository:'stackchain/api', kind:'pull', number:42,
title:'Changed pull request with a long mobile title', state:'open',
updated_at:'2026-08-23T05:00:00Z', has_unseen_change:true
}]})
});
globalThis.followingReleaseQueue = attachFollowing(() => {});
globalThis.followingReleaseQueue.open();
}""")
page.locator("#review-following").evaluate("node => node.hidden = false")
page.locator("#following-sheet").evaluate("node => node.showModal()")
expect(page.locator("#following-sheet")).to_be_visible()
expect(page.locator(".following-card")).to_be_visible()
expect(page.locator(".following-card")).to_contain_text("New activity")
expect(page.locator(".following-card")).to_contain_text("Pull request")
expect(page.locator("#review-following")).to_have_text("Review new activity")
assert page.locator("#review-following").bounding_box()["height"] >= 44
assert page.locator(".following-card").bounding_box()["height"] >= 44
@@ -52,6 +66,11 @@ def test_following_queue_is_phone_usable_at_narrow_viewport():
expect(retire).to_be_visible()
expect(retire).to_have_text("Stop watching & next")
assert retire.bounding_box()["height"] >= 44
page.evaluate("""() => renderSearchPreviewWatch(
{repository:'stackchain/api', number:42, kind:'pull', state:'open', watching:false},
{status:'ready'}, document.querySelector('#watch-search-result'))""")
expect(retire).to_have_text("Watch pull request")
assert retire.bounding_box()["height"] >= 44
overflow = page.evaluate("document.documentElement.scrollWidth > document.documentElement.clientWidth")
assert overflow is False
browser.close()
+6 -5
View File
@@ -864,7 +864,7 @@ def test_search_preview_preserves_authoritative_watch_and_offers_following_repai
const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))});
(async () => {{
const states = [];
const detail = {{repository:'stackchain/api',number:42,kind:'issue',state:'open',claimable:true,watching:false}};
const detail = {{repository:'stackchain/api',number:42,kind:'pull',state:'open',claimable:true,watching:false}};
const preview = createSearchPreview({{
fetchJson:async()=>detail,
watch:async()=>({{watching:true,following_synced:false,error:'Watching in Gitea, but Following could not sync. Retry this action.'}}),
@@ -886,7 +886,7 @@ process.stdout.write(JSON.stringify({{
assert json.loads(result.stdout) == {
"status": "watch-partial",
"watching": True,
"message": "Watching in Gitea, but Following could not sync. Tap Stop watching, then Watch issue to repair.",
"message": "Watching in Gitea, but Following could not sync. Tap Stop watching, then Watch pull request to repair.",
}
@@ -901,7 +901,7 @@ def test_mobile_search_preview_exposes_touch_safe_watch_action():
assert ".search-preview-actions button" in css and "min-height:44px" in css
def test_search_preview_watch_is_available_for_open_issues_and_closed_following_retirement():
def test_search_preview_watch_is_available_for_open_issues_pulls_and_closed_following_retirement():
script = f"""
require({json.dumps(str(SEARCH_PREVIEW))});
(async () => {{
@@ -936,7 +936,8 @@ process.stdout.write(JSON.stringify({{loaded,watching:hydrated.watching,button,e
assert result.returncode == 0, result.stderr
payload = json.loads(result.stdout)
assert payload["loaded"] == [
"api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
"api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue",
"api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=pull",
]
assert payload["watching"] is True
assert payload["button"] == {
@@ -944,7 +945,7 @@ process.stdout.write(JSON.stringify({{loaded,watching:hydrated.watching,button,e
"textContent": "Stop watching",
"disabled": False,
}
assert payload["excluded"] == [True, True]
assert payload["excluded"] == [True, False]
assert payload["closedFollowing"]["watching"] is True
assert payload["retire"] == {
"hidden": False,
+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-v134" in worker
assert "stackchain-dashboard-shell-v135" in worker
+44 -2
View File
@@ -52,6 +52,7 @@ async def test_confirmed_watch_updates_account_following_collection(monkeypatch,
assert following.headers["cache-control"] == "no-store"
assert following.json() == {"revision": 1, "degraded": False, "refresh_failures": 0, "items": [{
"repository": "stackchain/api",
"kind": "issue",
"number": 42,
"title": "Make mobile review useful",
"state": "open",
@@ -61,6 +62,47 @@ async def test_confirmed_watch_updates_account_following_collection(monkeypatch,
}]}
@pytest.mark.anyio
async def test_confirmed_pull_watch_round_trips_through_following(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"p" * 32)
detail = {
"repository": "stackchain/api", "kind": "pull", "number": 84,
"title": "Ship typed Following", "state": "open",
"updated_at": "2026-08-23T05:00:00Z",
"url": "https://forge.example/stackchain/api/pulls/84",
}
previews = []
async def preview(repository, kind, number):
previews.append((repository, kind, number))
return detail
async def set_subscription(repository, number, watching):
return {"watching": watching}
async def user():
return {"login": "timmy"}
monkeypatch.setattr(main, "_following_store", lambda: store)
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription)
monkeypatch.setattr(main, "current_user", user)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
watched = await client.put(
"/api/v1/repos/stackchain/api/issues/84/preview/subscription?kind=pull"
)
following = await client.get("/api/v1/following")
assert watched.status_code == 200
assert following.status_code == 200
assert following.json()["items"][0]["kind"] == "pull"
assert previews == [
("stackchain/api", "pull", 84),
("stackchain/api", "pull", 84),
]
@pytest.mark.anyio
async def test_full_following_collection_rejects_watch_before_gitea_mutation(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"b" * 32, limit=1)
@@ -228,7 +270,7 @@ async def test_following_refreshes_changed_items_and_preserves_failed_items(monk
async def test_following_acknowledges_only_the_exact_loaded_revision(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"f" * 32)
item = {
"repository": "stackchain/api", "number": 42, "title": "Changed",
"repository": "stackchain/api", "kind": "pull", "number": 42, "title": "Changed",
"state": "open", "updated_at": "2026-08-23T03:00:00Z",
"url": "https://forge.example/stackchain/api/issues/42",
}
@@ -243,7 +285,7 @@ async def test_following_acknowledges_only_the_exact_loaded_revision(monkeypatch
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.put(
"/api/v1/following/stackchain/api/issues/42/seen",
"/api/v1/following/stackchain/api/issues/42/seen?kind=pull",
json={"updated_at": "2026-08-23T04:00:00Z"},
)
+57 -1
View File
@@ -50,6 +50,61 @@ process.stdout.write(JSON.stringify(state));
}]
def test_following_opens_pull_requests_in_the_pull_workspace():
script = f"""
const createFollowing = require({json.dumps(str(MODULE))});
const opened=[];
const feature=createFollowing({{
fetchJson:async () => ({{revision:1,items:[{{
repository:'stackchain/api',kind:'pull',number:84,title:'Typed review',state:'open',
updated_at:'2026-08-23T05:00:00Z',url:'https://forge.example/pulls/84',has_unseen_change:true
}}]}}),
onOpen:async item => opened.push(item), onAcknowledge:async () => {{}},
}});
(async () => {{
await feature.load();
await feature.startReview();
process.stdout.write(JSON.stringify({{opened,session:feature.session()}}));
}})().catch(error => {{console.error(error);process.exit(1);}});
"""
result = json.loads(subprocess.run(
["node", "-e", script], text=True, capture_output=True, check=True
).stdout)
assert result["opened"][0]["kind"] == "pull"
assert result["opened"][0]["following"] is True
assert result["session"]["items"][0]["kind"] == "pull"
def test_following_typed_identity_prevents_issue_pull_collisions():
script = f"""
const createFollowing = require({json.dumps(str(MODULE))});
const acknowledged=[];
const items=['issue','pull'].map(kind => ({{
repository:'stackchain/api',kind,number:84,title:kind,state:'closed',
updated_at:'2026-08-23T05:00:00Z',has_unseen_change:true
}}));
const feature=createFollowing({{
fetchJson:async () => ({{revision:1,items}}),
onOpen:async () => {{}}, onAcknowledge:async item => acknowledged.push(item.kind),
}});
(async () => {{
await feature.load();
await feature.previewLoaded({{...items[1]}});
feature.retire(items[1]);
process.stdout.write(JSON.stringify({{acknowledged,items:feature.items()}}));
}})().catch(error => {{console.error(error);process.exit(1);}});
"""
result = json.loads(subprocess.run(
["node", "-e", script], text=True, capture_output=True, check=True
).stdout)
assert result["acknowledged"] == ["pull"]
assert [(item["kind"], item["has_unseen_change"]) for item in result["items"]] == [
("issue", True)
]
def test_following_opens_explicitly_but_never_becomes_work_recommendation():
launcher = ROOT / "frontend" / "mobile-queue-launcher.js"
script = f"""
@@ -292,12 +347,13 @@ def test_following_review_controls_are_wired_into_the_phone_preview_flow():
assert "query('#review-following').addEventListener('click'" in following
assert "getSession:() => followingQueue.session() || commandSearchState" in dashboard
assert "onOpened:item => followingQueue.previewLoaded(item)" in dashboard
assert "'/seen?kind=' + encodeURIComponent(item.kind)" in following
assert "afterUnwatch:item => followingQueue.retire(item)" in dashboard
assert "followingQueue.returnToFollowing()" in dashboard
assert "'Back to Following'" in dashboard
assert "if (searchPreviewReturnKind === 'following')" in dashboard
assert "e.key === 'Escape' && searchPreviewReturnKind === 'following'" in dashboard
assert "stackchain-dashboard-shell-v134" in service_worker
assert "stackchain-dashboard-shell-v135" in service_worker
def test_prepare_today_lazily_refreshes_and_directly_reviews_following():
+46 -1
View File
@@ -21,7 +21,10 @@ def test_confirmed_watch_is_account_scoped_idempotent_and_encrypted(tmp_path):
first = store.set_watching("Timmy", ITEM, True)
repeated = store.set_watching("timmy", ITEM, True)
assert first == repeated == {"revision": 1, "items": [{**ITEM, "has_unseen_change": False}]}
assert first == repeated == {
"revision": 1,
"items": [{**ITEM, "kind": "issue", "has_unseen_change": False}],
}
assert store.get("other") == {"revision": 0, "items": []}
stored = sqlite3.connect(path).execute(
"SELECT items FROM following_issues WHERE login = ?", ("timmy",)
@@ -72,3 +75,45 @@ def test_reconfirmed_watch_does_not_mark_an_unseen_change_as_seen(tmp_path):
snapshot = store.set_watching("timmy", changed, True)
assert snapshot["items"][0]["has_unseen_change"] is True
def test_following_preserves_kind_and_uses_it_in_item_identity(tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=KEY)
issue = {**ITEM, "kind": "issue"}
pull = {
**ITEM,
"kind": "pull",
"title": "Review the mobile flow",
"url": "https://forge.example/stackchain/api/pulls/42",
}
store.set_watching("timmy", issue, True)
snapshot = store.set_watching("timmy", pull, True)
assert [(item["kind"], item["number"]) for item in snapshot["items"]] == [
("pull", 42),
("issue", 42),
]
def test_following_reads_legacy_items_as_issues(tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=KEY)
store.set_watching("timmy", ITEM, True)
assert store.get("timmy")["items"][0]["kind"] == "issue"
def test_acknowledgement_only_marks_the_requested_item_kind_seen(tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=KEY)
for kind in ("issue", "pull"):
store.set_watching("timmy", {**ITEM, "kind": kind}, True)
store.refresh("timmy", [{**ITEM, "kind": kind, "updated_at": "2026-08-23T06:00:00Z"}])
snapshot = store.acknowledge(
"timmy", ITEM["repository"], ITEM["number"], "2026-08-23T06:00:00Z", kind="pull"
)
assert {item["kind"]: item["has_unseen_change"] for item in snapshot["items"]} == {
"issue": True,
"pull": False,
}
+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-v134" in source
assert "stackchain-dashboard-shell-v135" 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-v134" in worker
assert "stackchain-dashboard-shell-v135" 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-v134" in worker
assert "stackchain-dashboard-shell-v135" 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-v134" in worker
assert "stackchain-dashboard-shell-v135" 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-v134" in worker
assert "stackchain-dashboard-shell-v135" in worker
assert "BASE + 'static/mobile-insights.js'" in worker
+1 -1
View File
@@ -414,7 +414,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-v134" in service_worker
assert "stackchain-dashboard-shell-v135" in service_worker
@pytest.mark.anyio
+1 -1
View File
@@ -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-v134" in source
assert "stackchain-dashboard-shell-v135" 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
+16 -16
View File
@@ -186,7 +186,7 @@ async function dispatchPush(payload) {{
def test_week_unplan_undo_rolls_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v134" in source
assert "stackchain-dashboard-shell-v135" in source
assert "BASE + 'static/week-plan.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@@ -194,20 +194,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-v134" in source
assert "stackchain-dashboard-shell-v135" in source
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v134" in source
assert "stackchain-dashboard-shell-v135" 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-v134" in source
assert "stackchain-dashboard-shell-v135" in source
assert "BASE + 'static/my-work.js'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@@ -216,7 +216,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-v134" in source
assert "stackchain-dashboard-shell-v135" 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
@@ -225,7 +225,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-v134" in source
assert "stackchain-dashboard-shell-v135" in source
assert "BASE + 'static/issue-evidence-review.js'" in source
assert "BASE + 'static/issue-attachment.js'" in source
@@ -233,14 +233,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-v134" in source
assert "stackchain-dashboard-shell-v135" 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-v134" in source
assert "stackchain-dashboard-shell-v135" in source
assert "BASE + 'static/today-completion.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -248,7 +248,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-v134" in source
assert "stackchain-dashboard-shell-v135" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -256,7 +256,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-v134" in source
assert "stackchain-dashboard-shell-v135" 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
@@ -266,14 +266,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-v134" in source
assert "stackchain-dashboard-shell-v135" 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-v134" in source
assert "stackchain-dashboard-shell-v135" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@@ -282,21 +282,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-v134" in source
assert "stackchain-dashboard-shell-v135" 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-v134" in source
assert "stackchain-dashboard-shell-v135" 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-v134" in source
assert "stackchain-dashboard-shell-v135" in source
assert "BASE + 'static/update-ownership.js'" in source
@@ -1302,7 +1302,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-v134" in source
assert "stackchain-dashboard-shell-v135" in source
assert "BASE + 'static/queue-today.js'" in source
+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-v134';" in service_worker
assert "const CACHE = 'stackchain-dashboard-shell-v135';" 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-v134" in source
assert "stackchain-dashboard-shell-v135" in source
assert "BASE + 'static/today-sync.js'" in source