feat: keep Today work ready offline (#349)
This commit is contained in:
1 parent
e11e55149c
commit
ac3d1f32d1
11 files changed
+267
-8
No files matched your search
@@ -137,4 +137,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-v40" in worker
|
||||
assert "stackchain-dashboard-shell-v41" in worker
|
||||
@@ -35,4 +35,4 @@ 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-v40" in worker
|
||||
assert "stackchain-dashboard-shell-v41" in worker
|
||||
@@ -0,0 +1,124 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.dashboard_bundle import dashboard
|
||||
|
||||
|
||||
OFFLINE_TODAY = Path(__file__).parents[1] / "frontend" / "offline-today.js"
|
||||
|
||||
|
||||
def run_scenario(scenario: str) -> dict:
|
||||
script = f"""
|
||||
const createOfflineToday = require({json.dumps(str(OFFLINE_TODAY))});
|
||||
(async () => {{
|
||||
{scenario}
|
||||
}})().catch(error => {{ console.error(error); process.exit(1); }});
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_warms_bounded_today_queue_with_two_concurrent_requests():
|
||||
result = run_scenario("""
|
||||
let active = 0;
|
||||
let peak = 0;
|
||||
const loaded = [];
|
||||
const saved = [];
|
||||
const items = Array.from({length:7}, (_, index) => ({
|
||||
kind:index % 2 ? 'pull' : 'issue', repository:'stackchain/dashboard', number:index + 1,
|
||||
updated_at:'2026-08-08T12:0' + index + ':00Z',
|
||||
}));
|
||||
const warmer = createOfflineToday({
|
||||
loadDetail: async item => {
|
||||
active += 1;
|
||||
peak = Math.max(peak, active);
|
||||
loaded.push(item.number);
|
||||
await new Promise(resolve => setTimeout(resolve, 5));
|
||||
active -= 1;
|
||||
return {title:'Work ' + item.number, body:'Context ' + item.number};
|
||||
},
|
||||
loadSavedDetail: () => null,
|
||||
saveDetail: (login, item, detail) => saved.push({login, number:item.number, detail}),
|
||||
});
|
||||
const status = await warmer.warm('timmy', items);
|
||||
process.stdout.write(JSON.stringify({peak, loaded, saved, status}));
|
||||
""")
|
||||
|
||||
assert result["peak"] == 2
|
||||
assert result["loaded"] == [1, 2, 3, 4, 5]
|
||||
assert [entry["number"] for entry in result["saved"]] == [1, 2, 3, 4, 5]
|
||||
assert result["saved"][0]["detail"]["source_updated_at"] == "2026-08-08T12:00:00Z"
|
||||
assert result["status"] == {"total": 5, "ready": 5, "failed": 0, "pending": 0}
|
||||
|
||||
|
||||
def test_refreshes_only_changed_revisions_and_keeps_prior_copy_on_failure():
|
||||
result = run_scenario("""
|
||||
const items = [
|
||||
{kind:'issue', repository:'stackchain/dashboard', number:1, updated_at:'new'},
|
||||
{kind:'pull', repository:'stackchain/dashboard', number:2, updated_at:'same'},
|
||||
];
|
||||
const old = new Map([
|
||||
['issue:stackchain/dashboard:1', {title:'Prior 1', source_updated_at:'old'}],
|
||||
['pull:stackchain/dashboard:2', {title:'Prior 2', source_updated_at:'same'}],
|
||||
]);
|
||||
const loaded = [];
|
||||
const warmer = createOfflineToday({
|
||||
loadDetail: async item => { loaded.push(item.number); throw new Error('temporary outage'); },
|
||||
loadSavedDetail: (_login, item) => old.get(item.kind + ':' + item.repository + ':' + item.number),
|
||||
saveDetail: () => { throw new Error('must not replace a prior copy after failure'); },
|
||||
});
|
||||
const status = await warmer.warm('timmy', items);
|
||||
process.stdout.write(JSON.stringify({loaded, status, failed:warmer.failedKeys()}));
|
||||
""")
|
||||
|
||||
assert result == {
|
||||
"loaded": [1],
|
||||
"status": {"total": 2, "ready": 2, "failed": 1, "pending": 0},
|
||||
"failed": ["issue:stackchain/dashboard:1"],
|
||||
}
|
||||
|
||||
|
||||
def test_retry_fetches_only_failed_items():
|
||||
result = run_scenario("""
|
||||
const items = [
|
||||
{kind:'issue', repository:'stackchain/dashboard', number:1, updated_at:'v2'},
|
||||
{kind:'issue', repository:'stackchain/dashboard', number:2, updated_at:'v1'},
|
||||
];
|
||||
let fail = true;
|
||||
const calls = [];
|
||||
const saved = new Map([['issue:stackchain/dashboard:2', {source_updated_at:'v1'}]]);
|
||||
const warmer = createOfflineToday({
|
||||
loadDetail: async item => {
|
||||
calls.push(item.number);
|
||||
if (fail) throw new Error('offline');
|
||||
return {title:'Fresh'};
|
||||
},
|
||||
loadSavedDetail: (_login, item) => saved.get(item.kind + ':' + item.repository + ':' + item.number),
|
||||
saveDetail: (_login, item, detail) => saved.set(item.kind + ':' + item.repository + ':' + item.number, detail),
|
||||
});
|
||||
await warmer.warm('timmy', items);
|
||||
fail = false;
|
||||
const status = await warmer.retry('timmy', items);
|
||||
process.stdout.write(JSON.stringify({calls, status}));
|
||||
""")
|
||||
|
||||
assert result["calls"] == [1, 1]
|
||||
assert result["status"] == {"total": 2, "ready": 2, "failed": 0, "pending": 0}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dashboard_warms_today_after_fresh_snapshot_and_exposes_mobile_retry():
|
||||
html = await dashboard()
|
||||
|
||||
assert '<script src="static/offline-today.js"></script>' in html
|
||||
assert 'id="offline-today-status"' in html
|
||||
assert 'id="retry-offline-today"' in html
|
||||
assert "offlineToday.warm(confirmedOwnerLogin, todayMyWork)" in html
|
||||
assert "offlineToday.retry(confirmedOwnerLogin, todayMyWork)" in html
|
||||
assert "Today offline: " in html
|
||||
assert ".offline-today-readiness button { min-height:44px;" in html
|
||||
@@ -97,7 +97,7 @@ async function dispatchNotificationClick(route) {{
|
||||
def test_share_target_sign_in_fix_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v40" in source
|
||||
assert "stackchain-dashboard-shell-v41" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
@@ -106,14 +106,14 @@ def test_share_target_sign_in_fix_ships_in_a_new_shell_cache():
|
||||
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v40" in source
|
||||
assert "stackchain-dashboard-shell-v41" 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-v40" in source
|
||||
assert "stackchain-dashboard-shell-v41" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
@@ -304,6 +304,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/issue-outbox.js",
|
||||
"/dashboard/static/authored-outbox.js",
|
||||
"/dashboard/static/offline-work.js",
|
||||
"/dashboard/static/offline-today.js",
|
||||
"/dashboard/static/my-work.js",
|
||||
"/dashboard/static/today-work.js",
|
||||
"/dashboard/static/update-ownership.js",
|
||||
|
||||
Reference in new issue
Block a user