Refresh Human Gates atomically on every reopen #1434
|
|
@ -13,6 +13,7 @@ function createHumanGates(options = {}) {
|
|||
let loadEpoch = 0;
|
||||
const decisionKeys = new Map();
|
||||
let decisionFlight = null;
|
||||
let openFlight = null;
|
||||
let onChange = options.onChange;
|
||||
|
||||
const escape = value => String(value ?? '').replace(/[&<>"']/g, character => ({
|
||||
|
|
@ -203,7 +204,19 @@ function createHumanGates(options = {}) {
|
|||
function open() {
|
||||
location.hash = '#/my-work/human-gates';
|
||||
if (nodes.panel) nodes.panel.hidden = false;
|
||||
return load().then(() => reviewNext());
|
||||
if (openFlight) return openFlight;
|
||||
const operation = (async () => {
|
||||
await load();
|
||||
reviewSnapshot = queue.items.slice();
|
||||
reviewIndex = 0;
|
||||
return reviewNext();
|
||||
})();
|
||||
openFlight = operation;
|
||||
const clearFlight = () => {
|
||||
if (openFlight === operation) openFlight = null;
|
||||
};
|
||||
operation.then(clearFlight, clearFlight);
|
||||
return operation;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -60,10 +60,7 @@ function createProgressiveHumanGates(options = {}) {
|
|||
|
||||
async function start(force = false) {
|
||||
if (!force && location.hash !== '#/my-work/human-gates') return false;
|
||||
if (started) {
|
||||
if (nodes.panel) nodes.panel.hidden = false;
|
||||
return true;
|
||||
}
|
||||
if (started) return controller.open().then(() => true);
|
||||
if (startFlight) return startFlight;
|
||||
startFlight = (async () => {
|
||||
const identity = await getIdentity();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/private-data-registry.js');
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v144';
|
||||
const CACHE = 'stackchain-dashboard-shell-v145';
|
||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||
|
|
|
|||
95
tests/e2e/test_human_gates_reopen_release.py
Normal file
95
tests/e2e/test_human_gates_reopen_release.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
if os.getenv("STACKCHAIN_RUN_RELEASE_E2E") != "1":
|
||||
pytest.skip("packaged Human Gates journey runs only in its gated CI job", allow_module_level=True)
|
||||
pytest.importorskip("playwright.sync_api")
|
||||
from playwright.sync_api import expect, sync_playwright
|
||||
|
||||
from fake_gitea import FakeGiteaServer
|
||||
from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, release_server
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("width", "height"), [(320, 568), (390, 844)])
|
||||
def test_release_artifact_reopens_human_gates_with_one_fresh_mobile_snapshot(
|
||||
tmp_path: Path, width: int, height: int
|
||||
):
|
||||
archives = sorted((ROOT / "dist").glob("stackchain-dashboard-*.tar.gz"))
|
||||
assert len(archives) == 1, "browser job must download exactly one assembled release archive"
|
||||
|
||||
fake = FakeGiteaServer(("127.0.0.1", 0))
|
||||
fake_thread = threading.Thread(target=fake.serve_forever, daemon=True)
|
||||
fake_thread.start()
|
||||
current = {"gate": "g1"}
|
||||
list_requests: list[str] = []
|
||||
browser_errors: list[str] = []
|
||||
|
||||
def gate(gate_id: str) -> dict:
|
||||
return {
|
||||
"id": gate_id,
|
||||
"title": "First candidate" if gate_id == "g1" else "Fresh candidate",
|
||||
"project": "stackchain/stackchain-dashboard",
|
||||
"candidate_hash": "a1" if gate_id == "g1" else "b2",
|
||||
"revision": 1,
|
||||
"priority": 5,
|
||||
"checks": [],
|
||||
"artifacts": [],
|
||||
"links": [],
|
||||
"provenance": {},
|
||||
"history": [],
|
||||
}
|
||||
|
||||
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": width, "height": height}, ignore_https_errors=True
|
||||
)
|
||||
page = context.new_page()
|
||||
page.on("pageerror", lambda error: browser_errors.append(error.stack or str(error)))
|
||||
|
||||
def human_gates_route(route):
|
||||
path = route.request.url.split("?", 1)[0]
|
||||
if path.endswith("/api/v1/human-gates"):
|
||||
list_requests.append(current["gate"])
|
||||
payload = {"pending_count": 1, "items": [gate(current["gate"])]}
|
||||
else:
|
||||
payload = gate(path.rsplit("/", 1)[-1])
|
||||
route.fulfill(status=200, content_type="application/json", body=json.dumps(payload))
|
||||
|
||||
page.route("**/api/v1/human-gates**", human_gates_route)
|
||||
page.goto(origin + "/", wait_until="networkidle")
|
||||
page.locator('input[name="device_label"]').fill("Human Gates release 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()
|
||||
expect(page.locator("#human-gate-detail")).to_contain_text("First candidate")
|
||||
page.evaluate("document.querySelector('#close-human-gates').click()")
|
||||
|
||||
before_reopen = len(list_requests)
|
||||
current["gate"] = "g2"
|
||||
page.evaluate("document.querySelector('#open-human-gates').click()")
|
||||
expect(page.locator("#human-gate-detail")).to_contain_text("Fresh candidate")
|
||||
expect(page.locator("#human-gates-list")).to_contain_text("Fresh candidate")
|
||||
assert len(list_requests) == before_reopen + 1
|
||||
assert page.evaluate("window.innerWidth") == width
|
||||
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||
bounds = page.locator('[data-human-gate-id="g2"]').bounding_box()
|
||||
assert bounds and bounds["height"] >= 44
|
||||
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-v144" in worker
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in service_worker
|
||||
assert "stackchain-dashboard-shell-v145" in service_worker
|
||||
|
||||
|
||||
def test_prepare_today_lazily_refreshes_and_directly_reviews_following():
|
||||
|
|
|
|||
|
|
@ -272,6 +272,92 @@ const gates=createHumanGates({storage:{getItem:()=>null,setItem(){}},getLogin:()
|
|||
assert [item["id"] for item in output["snapshot"]["items"]] == ["g2"]
|
||||
|
||||
|
||||
def test_concurrent_reopen_calls_share_one_fresh_list_request():
|
||||
output = run_node(r"""
|
||||
let listCalls=0, releaseList;
|
||||
const pending=new Promise(resolve=>releaseList=resolve);
|
||||
const item={id:'g1',title:'Candidate',candidate_hash:'a1',revision:1,checks:[]};
|
||||
const gates=createHumanGates({
|
||||
storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',isOnline:()=>true,
|
||||
nodes:{count:{},list:{},status:{},panel:{hidden:true},detail:{}},location:{hash:''},
|
||||
fetchJson:async path=>{
|
||||
if(path.endsWith('/g1')) return item;
|
||||
listCalls += 1;
|
||||
await pending;
|
||||
return {pending_count:1,items:[item]};
|
||||
},
|
||||
});
|
||||
(async()=>{
|
||||
const first=gates.open();
|
||||
const second=gates.open();
|
||||
releaseList();
|
||||
const reviewed=await Promise.all([first,second]);
|
||||
process.stdout.write(JSON.stringify({listCalls,ids:reviewed.map(item=>item.id)}));
|
||||
})();
|
||||
""")
|
||||
assert output == {"listCalls": 1, "ids": ["g1", "g1"]}
|
||||
|
||||
|
||||
def test_failed_reopen_clears_single_flight_and_can_retry():
|
||||
output = run_node(r"""
|
||||
let listCalls=0;
|
||||
const item={id:'g1',title:'Recovered',candidate_hash:'a1',revision:1,checks:[]};
|
||||
const gates=createHumanGates({
|
||||
storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',isOnline:()=>true,
|
||||
nodes:{count:{},list:{},status:{},panel:{hidden:true},detail:{}},location:{hash:''},
|
||||
fetchJson:async path=>{
|
||||
if(path.endsWith('/g1')) return item;
|
||||
listCalls += 1;
|
||||
if(listCalls === 1) throw new Error('offline');
|
||||
return {pending_count:1,items:[item]};
|
||||
},
|
||||
});
|
||||
(async()=>{
|
||||
let firstError='';
|
||||
try { await gates.open(); } catch(error) { firstError=error.message; }
|
||||
const recovered=await gates.open();
|
||||
process.stdout.write(JSON.stringify({firstError,listCalls,recovered:recovered.id}));
|
||||
})();
|
||||
""")
|
||||
assert output == {"firstError": "offline", "listCalls": 2, "recovered": "g1"}
|
||||
|
||||
|
||||
def test_reopen_refreshes_queue_and_starts_a_fresh_atomic_review_snapshot():
|
||||
output = run_node(r"""
|
||||
const first={id:'g1',title:'First candidate',project:'p/one',candidate_hash:'a1',revision:1,checks:[]};
|
||||
const second={id:'g2',title:'Second candidate',project:'p/two',candidate_hash:'b2',revision:1,checks:[]};
|
||||
let listCalls=0;
|
||||
const nodes={count:{},list:{innerHTML:''},status:{},panel:{hidden:true},detail:{innerHTML:''}};
|
||||
const gates=createHumanGates({
|
||||
storage:{getItem:()=>null,setItem(){}},getLogin:()=> 'timmy',isOnline:()=>true,nodes,location:{hash:''},
|
||||
fetchJson:async path=>{
|
||||
if(path.endsWith('/g1')) return first;
|
||||
if(path.endsWith('/g2')) return second;
|
||||
listCalls += 1;
|
||||
return {pending_count:1,items:[listCalls === 1 ? first : second]};
|
||||
},
|
||||
});
|
||||
(async()=>{
|
||||
await gates.open();
|
||||
await new Promise(resolve=>setTimeout(resolve,0));
|
||||
const before=gates.current().id;
|
||||
await gates.open();
|
||||
const selected=gates.select('g2');
|
||||
await new Promise(resolve=>setTimeout(resolve,0));
|
||||
process.stdout.write(JSON.stringify({
|
||||
before,listCalls,selected:selected.id,current:gates.current().id,
|
||||
listHtml:nodes.list.innerHTML,detailHtml:nodes.detail.innerHTML,
|
||||
}));
|
||||
})();
|
||||
""")
|
||||
assert output["before"] == "g1"
|
||||
assert output["listCalls"] == 2
|
||||
assert output["selected"] == "g2"
|
||||
assert output["current"] == "g2"
|
||||
assert "Second candidate" in output["listHtml"]
|
||||
assert "Second candidate" in output["detailHtml"]
|
||||
|
||||
|
||||
def test_selecting_a_queue_card_opens_that_exact_gate():
|
||||
output = run_node(r"""
|
||||
const details={
|
||||
|
|
@ -306,7 +392,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-v144" in WORKER.read_text()
|
||||
assert "stackchain-dashboard-shell-v145" in WORKER.read_text()
|
||||
|
||||
|
||||
def test_deep_link_opens_human_gates_without_waiting_for_optional_workspace():
|
||||
|
|
@ -346,6 +432,55 @@ const app=createProgressiveHumanGates({{
|
|||
}
|
||||
|
||||
|
||||
def test_progressive_reopen_refreshes_the_review_session_before_hydration():
|
||||
script = f"""
|
||||
const createProgressiveHumanGates=require({json.dumps(str(PROGRESSIVE))});
|
||||
const listeners={{}};
|
||||
const nodes={{
|
||||
'#human-gates-count':{{}}, '#human-gates-list':{{innerHTML:'',addEventListener(){{}}}},
|
||||
'#human-gates-status':{{}}, '#human-gates':{{hidden:true}},
|
||||
'#human-gate-detail':{{innerHTML:'',addEventListener(){{}},querySelectorAll:()=>[]}},
|
||||
'#open-human-gates':{{addEventListener:(name,fn)=>listeners.open=fn}},
|
||||
'#close-human-gates':{{addEventListener:(name,fn)=>listeners.close=fn}},
|
||||
}};
|
||||
let listCalls=0;
|
||||
const first={{id:'g1',title:'First',candidate_hash:'a1',revision:1,checks:[]}};
|
||||
const second={{id:'g2',title:'Second',candidate_hash:'b2',revision:1,checks:[]}};
|
||||
const app=createProgressiveHumanGates({{
|
||||
document:{{querySelector:selector=>nodes[selector]||null}},
|
||||
location:{{hash:'#/my-work/human-gates'}}, history:{{replaceState(){{}}}},
|
||||
storage:{{getItem:()=>null,setItem(){{}}}}, isOnline:()=>true,
|
||||
getIdentity:async()=>({{login:'timmy',accountKey:'7:timmy'}}),
|
||||
fetchJson:async path=>{{
|
||||
if(path.endsWith('/g1')) return first;
|
||||
if(path.endsWith('/g2')) return second;
|
||||
listCalls += 1;
|
||||
return {{pending_count:1,items:[listCalls === 1 ? first : second]}};
|
||||
}},
|
||||
}});
|
||||
(async()=>{{
|
||||
await app.start();
|
||||
listeners.close();
|
||||
await listeners.open();
|
||||
await new Promise(resolve=>setTimeout(resolve,0));
|
||||
const controller=app.handoff().controller;
|
||||
process.stdout.write(JSON.stringify({{
|
||||
listCalls,current:controller.current().id,hidden:nodes['#human-gates'].hidden,
|
||||
listHtml:nodes['#human-gates-list'].innerHTML,
|
||||
detailHasSecond:nodes['#human-gate-detail'].innerHTML.includes('Second'),
|
||||
}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||
"""
|
||||
result = subprocess.run(["node", "-e", script], check=True, text=True, capture_output=True)
|
||||
assert json.loads(result.stdout) == {
|
||||
"listCalls": 2,
|
||||
"current": "g2",
|
||||
"hidden": False,
|
||||
"listHtml": '<button class="human-gate-card" type="button" data-human-gate-id="g2"><strong>Second</strong><code>b2</code><span>Priority 0</span></button>',
|
||||
"detailHasSecond": True,
|
||||
}
|
||||
|
||||
|
||||
def test_dashboard_adopts_progressive_human_gates_without_a_second_list_load():
|
||||
dashboard = DASHBOARD.read_text()
|
||||
index = INDEX.read_text()
|
||||
|
|
|
|||
|
|
@ -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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in worker
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in worker
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in worker
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in worker
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in service_worker
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" in source
|
||||
|
||||
|
||||
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
|
|
@ -1361,7 +1361,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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" 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-v144';" in service_worker
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v145';" 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-v144" in source
|
||||
assert "stackchain-dashboard-shell-v145" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user