feat: review durable Human Gate decision history (Closes #1441)
This commit is contained in:
1 parent
efa01cf47f
commit
a5d05d7590
22 files changed
+316
-34
No files matched your search
@@ -154,3 +154,84 @@ def test_release_artifact_reopens_human_gates_with_one_fresh_mobile_snapshot(
|
||||
fake.shutdown()
|
||||
fake.server_close()
|
||||
fake_thread.join(timeout=5)
|
||||
|
||||
|
||||
def test_release_artifact_reviews_live_human_gate_history_and_receipt_on_phone(tmp_path: Path):
|
||||
archives = sorted((ROOT / "dist").glob("stackchain-dashboard-*.tar.gz"))
|
||||
assert len(archives) == 1
|
||||
fake = FakeGiteaServer(("127.0.0.1", 0))
|
||||
fake_thread = threading.Thread(target=fake.serve_forever, daemon=True)
|
||||
fake_thread.start()
|
||||
browser_errors: list[str] = []
|
||||
history_requests: list[str] = []
|
||||
held = {
|
||||
"id": "held", "title": "Held candidate", "project": "stackchain/stackchain-dashboard",
|
||||
"candidate_hash": "bbb222", "state": "held", "revision": 2, "priority": 5,
|
||||
"created_at": 100, "updated_at": 200, "reason": "Needs mobile evidence",
|
||||
"override_reason": "", "checklist": {}, "receipt_id": "receipt-2",
|
||||
"checks": [], "artifacts": [], "links": [], "provenance": {},
|
||||
"history": [{"action": "held", "at": 200, "receipt_id": "receipt-2"}],
|
||||
}
|
||||
released = {
|
||||
**held, "id": "released", "title": "Released candidate", "candidate_hash": "aaa111",
|
||||
"state": "released", "updated_at": 150, "reason": "", "receipt_id": "receipt-1",
|
||||
}
|
||||
try:
|
||||
with release_server(
|
||||
archives[0], tmp_path, f"http://127.0.0.1:{fake.server_port}"
|
||||
) as origin, sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(args=["--ignore-certificate-errors"])
|
||||
context = browser.new_context(
|
||||
viewport={"width": 390, "height": 844}, ignore_https_errors=True
|
||||
)
|
||||
page = context.new_page()
|
||||
page.on("pageerror", lambda error: browser_errors.append(error.stack or str(error)))
|
||||
|
||||
def gates_route(route):
|
||||
history_requests.append(route.request.url)
|
||||
if route.request.url.endswith("state=all"):
|
||||
payload = {"pending_count": 0, "items": [held, released]}
|
||||
elif route.request.url.endswith("/held"):
|
||||
payload = held
|
||||
else:
|
||||
payload = {"pending_count": 0, "items": []}
|
||||
route.fulfill(status=200, content_type="application/json", body=json.dumps(payload))
|
||||
|
||||
page.route("**/api/v1/human-gates**", gates_route)
|
||||
page.route("**/api/v1/human-gates/**", gates_route)
|
||||
page.route("**/api/v1/human-gate-receipts/receipt-2", lambda route: route.fulfill(
|
||||
status=200, content_type="application/json", body=json.dumps({
|
||||
"receipt_id": "receipt-2", "gate_id": "held", "candidate_hash": "bbb222",
|
||||
"state": "held", "decided_at": 200, "reason": "Needs mobile evidence",
|
||||
"override_reason": "", "checklist": {}, "unmet_required_checks": [],
|
||||
})
|
||||
))
|
||||
page.goto(origin + "/", wait_until="networkidle")
|
||||
page.locator('input[name="device_label"]').fill("Human Gate history phone")
|
||||
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
|
||||
page.locator("#submit-sign-in").click()
|
||||
page.wait_for_url(origin + "/", wait_until="networkidle")
|
||||
page.evaluate("document.querySelector('#open-human-gates').click()")
|
||||
expect(page.locator("#human-gates")).to_be_visible()
|
||||
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('[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)
|
||||
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 not browser_errors
|
||||
browser.close()
|
||||
finally:
|
||||
fake.shutdown()
|
||||
fake.server_close()
|
||||
fake_thread.join(timeout=5)
|
||||
@@ -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-v147" in worker
|
||||
assert "stackchain-dashboard-shell-v148" in worker
|
||||
@@ -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-v147" in service_worker
|
||||
assert "stackchain-dashboard-shell-v148" in service_worker
|
||||
|
||||
|
||||
def test_prepare_today_lazily_refreshes_and_directly_reviews_following():
|
||||
|
||||
@@ -69,6 +69,38 @@ async def test_decision_requires_revision_and_returns_durable_receipt(gate_api):
|
||||
assert all(response.headers["cache-control"] == "no-store" for response in (decided, repeated, receipt, stale))
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_decision_history_is_newest_first_receipt_linked_and_principal_bound(gate_api):
|
||||
first = gate_api.intake("1:timmy", CANDIDATE, idempotency_key="history-first")
|
||||
gate_api.decide(
|
||||
"1:timmy", first["id"], expected_revision=1, decision="release", reason="",
|
||||
override_reason="", checklist=CHECKLIST, idempotency_key="history-release",
|
||||
)
|
||||
second_candidate = {**CANDIDATE, "candidate_hash": "def456", "title": "New candidate"}
|
||||
second = gate_api.intake("1:timmy", second_candidate, idempotency_key="history-second")
|
||||
second_receipt = gate_api.decide(
|
||||
"1:timmy", second["id"], expected_revision=1, decision="hold",
|
||||
reason="Needs another mobile pass", override_reason="", checklist={},
|
||||
idempotency_key="history-hold",
|
||||
)
|
||||
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
listing = await client.get("/api/v1/human-gates?state=all")
|
||||
receipt = await client.get(
|
||||
f"/api/v1/human-gate-receipts/{second_receipt['receipt_id']}"
|
||||
)
|
||||
|
||||
assert listing.status_code == 200
|
||||
assert listing.headers["cache-control"] == "no-store"
|
||||
assert [item["state"] for item in listing.json()["items"]] == ["held", "released"]
|
||||
assert listing.json()["items"][0]["receipt_id"] == second_receipt["receipt_id"]
|
||||
assert receipt.json()["reason"] == "Needs another mobile pass"
|
||||
assert gate_api.list("2:timmy", state="all")["items"] == []
|
||||
with pytest.raises(LookupError):
|
||||
gate_api.receipt("2:timmy", second_receipt["receipt_id"])
|
||||
|
||||
|
||||
@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")
|
||||
|
||||
@@ -136,6 +136,60 @@ const gates=createHumanGates({storage,getLogin:()=> 'timmy',isOnline:()=>true,no
|
||||
assert "Inbox zero" in output["zero"]["html"]
|
||||
|
||||
|
||||
def test_live_history_lists_decisions_and_opens_the_durable_receipt_without_caching():
|
||||
output = run_node(r"""
|
||||
const writes=[]; const requests=[];
|
||||
const nodes={
|
||||
count:{},list:{innerHTML:''},status:{textContent:''},panel:{},detail:{innerHTML:''},
|
||||
pendingTab:{setAttribute(){},disabled:false},historyTab:{setAttribute(){},disabled:false},
|
||||
};
|
||||
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'},
|
||||
]};
|
||||
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({
|
||||
storage:{getItem:()=>null,setItem:(key,value)=>writes.push({key,value})},
|
||||
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/held') return detail;
|
||||
if(path==='api/v1/human-gate-receipts/receipt-2') return receipt;
|
||||
return pending;
|
||||
},
|
||||
});
|
||||
(async()=>{
|
||||
await gates.load(); const writesAfterPending=writes.length;
|
||||
const decisions=await gates.showHistory(); const historyHtml=nodes.list.innerHTML;
|
||||
await gates.selectHistory('held');
|
||||
process.stdout.write(JSON.stringify({
|
||||
writesAfterPending,writesAfterHistory:writes.length,requests,decisions,
|
||||
historyHtml,detailHtml:nodes.detail.innerHTML,status:nodes.status.textContent,
|
||||
}));
|
||||
})();
|
||||
""")
|
||||
assert output["writesAfterPending"] == 1
|
||||
assert output["writesAfterHistory"] == 1
|
||||
assert output["requests"] == [
|
||||
"api/v1/human-gates",
|
||||
"api/v1/human-gates?state=all",
|
||||
"api/v1/human-gates/held",
|
||||
"api/v1/human-gate-receipts/receipt-2",
|
||||
]
|
||||
assert [item["id"] for item in output["decisions"]] == ["held", "released"]
|
||||
assert "Waiting" not in output["historyHtml"]
|
||||
assert "Held" in output["historyHtml"]
|
||||
assert "Released" in output["historyHtml"]
|
||||
assert "Needs mobile evidence" in output["detailHtml"]
|
||||
assert "receipt-2" in output["detailHtml"]
|
||||
assert "data-gate-decision" not in output["detailHtml"]
|
||||
assert output["status"] == "2 past Human Gate decisions."
|
||||
|
||||
|
||||
def test_queue_changes_publish_mobile_counts_and_authoritative_decision_completion():
|
||||
output = run_node(r"""
|
||||
const changes=[];
|
||||
@@ -332,7 +386,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-v147" in worker
|
||||
assert "stackchain-dashboard-shell-v148" in worker
|
||||
|
||||
|
||||
def test_unmet_required_check_is_named_inline_and_focuses_the_override_reason():
|
||||
@@ -569,7 +623,27 @@ 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-v147" in WORKER.read_text()
|
||||
assert "stackchain-dashboard-shell-v148" in WORKER.read_text()
|
||||
|
||||
|
||||
def test_human_gate_history_tabs_are_touch_sized_wired_and_invalidate_the_shell():
|
||||
index = INDEX.read_text()
|
||||
dashboard = DASHBOARD.read_text()
|
||||
progressive = PROGRESSIVE.read_text()
|
||||
css = CSS.read_text()
|
||||
|
||||
assert 'class="human-gate-views" role="group" aria-label="Human Gate view"' in index
|
||||
assert 'id="human-gates-pending"' in index
|
||||
assert 'id="human-gates-history"' in index
|
||||
assert "pendingTab:qs('#human-gates-pending')" in dashboard
|
||||
assert "historyTab:qs('#human-gates-history')" in dashboard
|
||||
assert "humanGates.showHistory()" in dashboard
|
||||
assert "card.addEventListener('click', () => selectHistory" in MODULE.read_text()
|
||||
assert "pendingTab:query('#human-gates-pending')" in progressive
|
||||
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()
|
||||
|
||||
|
||||
def test_deep_link_opens_human_gates_without_waiting_for_optional_workspace():
|
||||
|
||||
@@ -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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in worker
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in worker
|
||||
assert "stackchain-dashboard-shell-v148" in worker
|
||||
|
||||
|
||||
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
||||
|
||||
@@ -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-v147" in worker
|
||||
assert "stackchain-dashboard-shell-v148" in worker
|
||||
assert ".device-setup-panel" in css
|
||||
assert ".device-readiness-card" in css
|
||||
assert "overflow-x:hidden" in css
|
||||
|
||||
@@ -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-v147" in worker
|
||||
assert "stackchain-dashboard-shell-v148" in worker
|
||||
assert "BASE + 'static/mobile-insights.js'" in worker
|
||||
@@ -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-v147" in service_worker
|
||||
assert "stackchain-dashboard-shell-v148" in service_worker
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
||||
@@ -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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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
|
||||
@@ -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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" in source
|
||||
|
||||
|
||||
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" 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-v147';" in service_worker
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v148';" in service_worker
|
||||
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||
|
||||
|
||||
|
||||
@@ -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-v147" in source
|
||||
assert "stackchain-dashboard-shell-v148" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user