feat: paginate mobile Human Gate history (Closes #1444)
This commit is contained in:
1 parent
57c7373079
commit
0f1cac0e25
21 files changed
+325
-51
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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():
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user