feat: paginate mobile Human Gate history (Closes #1444)
CI / lint (pull_request) Successful in 3m53s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 7m21s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-26 21:10:10 +00:00
1 parent 57c7373079
commit 0f1cac0e25
21 files changed
+325 -51

No files matched your search

+20 -4
View File
@@ -176,6 +176,11 @@ def test_release_artifact_reviews_live_human_gate_history_and_receipt_on_phone(t
**held, "id": "released", "title": "Released candidate", "candidate_hash": "aaa111",
"state": "released", "updated_at": 150, "reason": "", "receipt_id": "receipt-1",
}
superseded = {
**held, "id": "superseded", "title": "Superseded candidate",
"candidate_hash": "old000", "state": "superseded", "updated_at": 100,
"reason": "", "receipt_id": None,
}
try:
with release_server(
archives[0], tmp_path, f"http://127.0.0.1:{fake.server_port}"
@@ -189,8 +194,10 @@ def test_release_artifact_reviews_live_human_gate_history_and_receipt_on_phone(t
def gates_route(route):
history_requests.append(route.request.url)
if route.request.url.endswith("state=all"):
payload = {"pending_count": 0, "items": [held, released]}
if "state=history" in route.request.url and "cursor=older-page" in route.request.url:
payload = {"pending_count": 0, "items": [superseded], "next_cursor": None}
elif "state=history" in route.request.url:
payload = {"pending_count": 0, "items": [held, released], "next_cursor": "older-page"}
elif route.request.url.endswith("/held"):
payload = held
else:
@@ -216,19 +223,28 @@ def test_release_artifact_reviews_live_human_gate_history_and_receipt_on_phone(t
cached_before = page.evaluate("Object.keys(localStorage).sort()")
page.locator("#human-gates-history").click()
expect(page.locator("#human-gates-status")).to_have_text("2 past Human Gate decisions.")
expect(page.locator("#human-gates-status")).to_contain_text("Older decisions are available")
expect(page.locator('[data-human-gate-history-id="held"]')).to_contain_text("Held")
expect(page.locator('[data-human-gate-history-id="released"]')).to_contain_text("Released")
page.locator('[data-human-gate-history-id="held"]').click()
expect(page.locator("#human-gate-detail")).to_contain_text("Needs mobile evidence")
expect(page.locator("#human-gate-detail")).to_contain_text("receipt-2")
expect(page.locator("#human-gate-detail [data-gate-decision]")).to_have_count(0)
more = page.get_by_role("button", name="Load older decisions")
more_bounds = more.bounding_box()
assert more_bounds and more_bounds["height"] >= 44
more.click()
expect(page.locator('[data-human-gate-history-id="superseded"]')).to_contain_text("Superseded")
expect(page.locator("#human-gate-detail")).to_contain_text("receipt-2")
expect(page.locator("#human-gates-status")).to_have_text("All 3 Human Gate decisions loaded.")
expect(page.locator("[data-human-gate-history-more]")).to_have_count(0)
assert page.evaluate("Object.keys(localStorage).sort()") == cached_before
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
for selector in ("#human-gates-pending", "#human-gates-history"):
bounds = page.locator(selector).bounding_box()
assert bounds and bounds["height"] >= 44
assert any(url.endswith("state=all") for url in history_requests)
assert any("state=history&limit=20" in url for url in history_requests)
assert any("cursor=older-page" in url for url in history_requests)
assert not browser_errors
browser.close()
finally:
+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-v148" in worker
assert "stackchain-dashboard-shell-v149" in worker
+1 -1
View File
@@ -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-v148" in service_worker
assert "stackchain-dashboard-shell-v149" in service_worker
def test_prepare_today_lazily_refreshes_and_directly_reviews_following():
+34
View File
@@ -101,6 +101,40 @@ async def test_decision_history_is_newest_first_receipt_linked_and_principal_bou
gate_api.receipt("2:timmy", second_receipt["receipt_id"])
@pytest.mark.anyio
async def test_decision_history_api_follows_an_opaque_continuation_cursor(gate_api):
for index in range(3):
candidate = {
**CANDIDATE,
"project": f"history/{index}",
"candidate_hash": f"history-{index}",
}
gate = gate_api.intake("1:timmy", candidate, idempotency_key=f"api-history-{index}")
gate_api.decide(
"1:timmy", gate["id"], expected_revision=1, decision="release",
reason="", override_reason="", checklist=CHECKLIST,
idempotency_key=f"api-decision-{index}",
)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
first = await client.get("/api/v1/human-gates?state=history&limit=2")
second = await client.get(
"/api/v1/human-gates",
params={"state": "history", "limit": 2, "cursor": first.json()["next_cursor"]},
)
malformed = await client.get(
"/api/v1/human-gates?state=history&cursor=not-a-cursor"
)
assert first.status_code == 200
assert len(first.json()["items"]) == 2
assert second.status_code == 200
assert len(second.json()["items"]) == 1
assert second.json()["next_cursor"] is None
assert malformed.status_code == 422
@pytest.mark.anyio
async def test_decision_requires_fresh_authorization_bound_to_the_exact_gate(monkeypatch, gate_api):
gate = gate_api.intake("1:timmy", CANDIDATE, idempotency_key="run-authorized")
+38
View File
@@ -64,6 +64,44 @@ def test_pending_queue_orders_highest_priority_then_oldest(tmp_path):
assert [item["id"] for item in store.list("timmy")["items"]] == [oldest_high["id"], newest_high["id"], low["id"]]
def test_decision_history_pages_are_bounded_stable_and_not_hidden_by_pending_work(tmp_path):
tick = iter(range(1000)).__next__
store = HumanGateStore(tmp_path / "gates.sqlite3", clock=tick)
decided = []
for index in range(3):
gate = store.intake(
"timmy",
{**candidate(f"decided-{index}"), "project": f"decided/{index}"},
idempotency_key=f"decided-{index}",
)
store.decide(
"timmy", gate["id"], expected_revision=1, decision="release",
reason="", override_reason="", checklist=checklist(),
idempotency_key=f"decision-{index}",
)
decided.append(gate["id"])
for index in range(105):
store.intake(
"timmy",
{**candidate(f"pending-{index}"), "project": f"pending/{index}"},
idempotency_key=f"pending-{index}",
)
first = store.list("timmy", state="history", limit=2)
second = store.list("timmy", state="history", limit=2, cursor=first["next_cursor"])
assert first["pending_count"] == 105
assert [item["id"] for item in first["items"]] == list(reversed(decided[1:]))
assert first["next_cursor"]
assert [item["id"] for item in second["items"]] == [decided[0]]
assert second["next_cursor"] is None
assert store.list("timmy", state="history", limit=2, cursor=first["next_cursor"]) == second
with pytest.raises(GateValidationError, match="cursor"):
store.list("alex", state="history", cursor=first["next_cursor"])
with pytest.raises(GateValidationError, match="cursor"):
store.list("timmy", state="history", cursor="not-a-cursor")
def test_decision_checks_revision_rules_and_returns_durable_idempotent_receipt(tmp_path):
path = tmp_path / "gates.sqlite3"
store = HumanGateStore(path, clock=iter([100, 101]).__next__)
+92 -7
View File
@@ -145,10 +145,9 @@ const nodes={
};
const pending={pending_count:1,items:[{id:'pending',title:'Waiting',candidate_hash:'aaa',state:'pending',revision:1,checks:[]}]};
const history={pending_count:1,items:[
{id:'pending',title:'Waiting',candidate_hash:'aaa',state:'pending',revision:1},
{id:'held',title:'Held candidate',candidate_hash:'bbb',state:'held',updated_at:200,reason:'Needs mobile evidence',receipt_id:'receipt-2'},
{id:'released',title:'Released candidate',candidate_hash:'ccc',state:'released',updated_at:100,receipt_id:'receipt-1'},
]};
],next_cursor:null};
const detail={id:'held',title:'Held candidate',project:'stackchain/dashboard',candidate_hash:'bbb',state:'held',updated_at:200,reason:'Needs mobile evidence',receipt_id:'receipt-2',history:[{action:'held',at:200}]};
const receipt={receipt_id:'receipt-2',gate_id:'held',candidate_hash:'bbb',state:'held',decided_at:200,reason:'Needs mobile evidence',override_reason:'',checklist:{}};
const gates=createHumanGates({
@@ -156,7 +155,7 @@ const gates=createHumanGates({
getLogin:()=> 'timmy',getAccountKey:()=> '7:timmy',isOnline:()=>true,nodes,location:{hash:''},
fetchJson:async path=>{
requests.push(path);
if(path==='api/v1/human-gates?state=all') return history;
if(path==='api/v1/human-gates?state=history&limit=20') return history;
if(path==='api/v1/human-gates/held') return detail;
if(path==='api/v1/human-gate-receipts/receipt-2') return receipt;
return pending;
@@ -176,7 +175,7 @@ const gates=createHumanGates({
assert output["writesAfterHistory"] == 1
assert output["requests"] == [
"api/v1/human-gates",
"api/v1/human-gates?state=all",
"api/v1/human-gates?state=history&limit=20",
"api/v1/human-gates/held",
"api/v1/human-gate-receipts/receipt-2",
]
@@ -190,6 +189,91 @@ const gates=createHumanGates({
assert output["status"] == "2 past Human Gate decisions."
def test_mobile_history_loads_older_decisions_without_losing_the_open_receipt():
output = run_node(r"""
const requests=[];
const nodes={
count:{},list:{innerHTML:'',querySelectorAll:()=>[]},status:{textContent:''},panel:{},detail:{innerHTML:''},
pendingTab:{setAttribute(){}},historyTab:{setAttribute(){}},
};
const first={pending_count:0,items:[
{id:'new',title:'Newest decision',candidate_hash:'aaa',state:'released',updated_at:300,receipt_id:'receipt-new'},
{id:'middle',title:'Middle decision',candidate_hash:'bbb',state:'held',updated_at:200,receipt_id:'receipt-middle'},
],next_cursor:'page-two'};
const second={pending_count:0,items:[
{id:'old',title:'Oldest decision',candidate_hash:'ccc',state:'released',updated_at:100,receipt_id:'receipt-old'},
],next_cursor:null};
const detail={id:'new',title:'Newest decision',project:'stackchain/dashboard',candidate_hash:'aaa',state:'released',receipt_id:'receipt-new'};
const receipt={receipt_id:'receipt-new',gate_id:'new',candidate_hash:'aaa',state:'released',decided_at:300,checklist:{}};
const gates=createHumanGates({
storage:{getItem:()=>null,setItem(){throw new Error('history must not be cached')}},
getLogin:()=> 'timmy',getAccountKey:()=> '7:timmy',isOnline:()=>true,nodes,location:{hash:''},
fetchJson:async path=>{
requests.push(path);
if(path==='api/v1/human-gates?state=history&limit=20') return first;
if(path.includes('cursor=page-two')) return second;
if(path==='api/v1/human-gates/new') return detail;
if(path==='api/v1/human-gate-receipts/receipt-new') return receipt;
throw new Error('unexpected '+path);
},
});
(async()=>{
await gates.showHistory();
await gates.selectHistory('new');
const selectedHtml=nodes.detail.innerHTML;
await gates.loadMoreHistory();
process.stdout.write(JSON.stringify({requests,listHtml:nodes.list.innerHTML,selectedHtml,detailHtml:nodes.detail.innerHTML,status:nodes.status.textContent}));
})();
""")
assert output["requests"] == [
"api/v1/human-gates?state=history&limit=20",
"api/v1/human-gates/new",
"api/v1/human-gate-receipts/receipt-new",
"api/v1/human-gates?state=history&limit=20&cursor=page-two",
]
assert all(title in output["listHtml"] for title in ("Newest decision", "Middle decision", "Oldest decision"))
assert "Load older decisions" not in output["listHtml"]
assert output["detailHtml"] == output["selectedHtml"]
assert output["status"] == "All 3 Human Gate decisions loaded."
def test_failed_older_history_page_preserves_loaded_decisions_and_retries_in_place():
output = run_node(r"""
let attempts=0;
const nodes={
count:{},list:{innerHTML:'',querySelectorAll:()=>[],querySelector:()=>null},status:{textContent:''},panel:{},detail:{innerHTML:'receipt remains'},
pendingTab:{setAttribute(){}},historyTab:{setAttribute(){}},
};
const gates=createHumanGates({
storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',isOnline:()=>true,nodes,location:{hash:''},
fetchJson:async path=>{
if(!path.includes('cursor=')) return {pending_count:0,items:[{id:'new',title:'Newest',candidate_hash:'aaa',state:'released',updated_at:300}],next_cursor:'older'};
attempts += 1;
if(attempts===1) throw new Error('History service timed out');
return {pending_count:0,items:[{id:'old',title:'Oldest',candidate_hash:'bbb',state:'held',updated_at:100}],next_cursor:null};
},
});
(async()=>{
await gates.showHistory();
nodes.detail.innerHTML='receipt remains';
let message='';
try{await gates.loadMoreHistory()}catch(error){message=error.message}
const failed={message,html:nodes.list.innerHTML,status:nodes.status.textContent,detail:nodes.detail.innerHTML};
await gates.loadMoreHistory();
process.stdout.write(JSON.stringify({failed,attempts,html:nodes.list.innerHTML,status:nodes.status.textContent}));
})();
""")
assert output["failed"]["message"] == "History service timed out"
assert "Newest" in output["failed"]["html"]
assert "Oldest" not in output["failed"]["html"]
assert "Retry older decisions" in output["failed"]["html"]
assert output["failed"]["status"] == "History service timed out Loaded decisions are still available."
assert output["failed"]["detail"] == "receipt remains"
assert output["attempts"] == 2
assert "Oldest" in output["html"]
assert output["status"] == "All 2 Human Gate decisions loaded."
def test_queue_changes_publish_mobile_counts_and_authoritative_decision_completion():
output = run_node(r"""
const changes=[];
@@ -386,7 +470,7 @@ def test_mobile_decision_tray_is_safe_area_aware_touch_sized_and_does_not_cover_
def test_human_gate_decision_tray_frontend_assets_invalidate_the_installed_shell_cache():
worker = WORKER.read_text()
assert "stackchain-dashboard-shell-v148" in worker
assert "stackchain-dashboard-shell-v149" in worker
def test_unmet_required_check_is_named_inline_and_focuses_the_override_reason():
@@ -623,7 +707,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-v148" in WORKER.read_text()
assert "stackchain-dashboard-shell-v149" in WORKER.read_text()
def test_human_gate_history_tabs_are_touch_sized_wired_and_invalidate_the_shell():
@@ -643,7 +727,8 @@ def test_human_gate_history_tabs_are_touch_sized_wired_and_invalidate_the_shell(
assert "historyTab:query('#human-gates-history')" in progressive
assert ".human-gate-views button{min-height:44px" in css
assert ".human-gate-history-card time" in css
assert "stackchain-dashboard-shell-v148" in WORKER.read_text()
assert ".human-gate-history-more{min-height:44px;width:100%" in css
assert "stackchain-dashboard-shell-v149" in WORKER.read_text()
def test_deep_link_opens_human_gates_without_waiting_for_optional_workspace():
+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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in worker
assert "stackchain-dashboard-shell-v149" 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-v148" in worker
assert "stackchain-dashboard-shell-v149" 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-v148" in worker
assert "stackchain-dashboard-shell-v149" in worker
assert ".device-setup-panel" in css
assert ".device-readiness-card" in css
assert "overflow-x:hidden" in css
+1 -1
View File
@@ -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-v148" in worker
assert "stackchain-dashboard-shell-v149" in worker
assert "BASE + 'static/mobile-insights.js'" in worker
+1 -1
View File
@@ -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-v148" in service_worker
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" 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
+17 -17
View File
@@ -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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" in source
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148" in source
assert "stackchain-dashboard-shell-v149" in source
assert "BASE + 'static/update-ownership.js'" in source
@@ -1390,7 +1390,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-v148" in source
assert "stackchain-dashboard-shell-v149" 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-v148';" in service_worker
assert "const CACHE = 'stackchain-dashboard-shell-v149';" 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-v148" in source
assert "stackchain-dashboard-shell-v149" in source
assert "BASE + 'static/today-sync.js'" in source