feat: resume mobile Updates triage (Closes #747)
CI / lint (pull_request) Successful in 1m37s
CI / build-release (pull_request) Successful in 7s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-13 14:58:19 +00:00
1 parent 341a3283ea
commit 4f6cebae7c
18 files changed
+265 -26

No files matched your search

+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-v99" in worker
assert "stackchain-dashboard-shell-v100" in worker
+1 -1
View File
@@ -187,7 +187,7 @@ def test_legacy_cache_marker_is_normalized_out_of_build_identity(tmp_path):
worker = changed_frontend / "service-worker.js"
worker.write_text(
worker.read_text().replace(
"const CACHE = 'stackchain-dashboard-shell-v99';",
"const CACHE = 'stackchain-dashboard-shell-v100';",
"const CACHE = 'stackchain-dashboard-shell-v999';",
)
)
+1 -1
View File
@@ -347,5 +347,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-v99" in source
assert "stackchain-dashboard-shell-v100" in source
assert "BASE + 'static/later-sync.js'" in source
+1 -1
View File
@@ -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-v99" in worker
assert "stackchain-dashboard-shell-v100" 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-v99" in worker
assert "stackchain-dashboard-shell-v100" in worker
def test_all_conversation_composers_offer_accessible_mobile_mentions():
+1 -1
View File
@@ -214,7 +214,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
assert "promptStorage:localStorage" in dashboard
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
assert "BASE + 'static/mobile-device-setup.js'" in worker
assert "stackchain-dashboard-shell-v99" in worker
assert "stackchain-dashboard-shell-v100" in worker
assert ".device-setup-panel" in css
assert ".device-readiness-card" in css
assert "overflow-x:hidden" in css
+1 -1
View File
@@ -410,7 +410,7 @@ async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(
def test_plan_today_controller_is_available_in_the_offline_shell():
source = SERVICE_WORKER.read_text()
assert "stackchain-dashboard-shell-v99" in source
assert "stackchain-dashboard-shell-v100" 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
+11 -10
View File
@@ -150,7 +150,7 @@ async function dispatchPush(payload) {{
def test_resumable_today_session_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v99" in source
assert "stackchain-dashboard-shell-v100" in source
assert "BASE + 'static/my-work.js'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@@ -159,14 +159,14 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v99" in source
assert "stackchain-dashboard-shell-v100" 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-v99" in source
assert "stackchain-dashboard-shell-v100" in source
assert "BASE + 'static/today-completion.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -174,7 +174,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-v99" in source
assert "stackchain-dashboard-shell-v100" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -182,14 +182,14 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v99" in source
assert "stackchain-dashboard-shell-v100" 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-v99" in source
assert "stackchain-dashboard-shell-v100" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@@ -198,21 +198,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-v99" in source
assert "stackchain-dashboard-shell-v100" 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-v99" in source
assert "stackchain-dashboard-shell-v100" 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-v99" in source
assert "stackchain-dashboard-shell-v100" in source
assert "BASE + 'static/update-ownership.js'" in source
@@ -761,7 +761,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-v99" in source
assert "stackchain-dashboard-shell-v100" in source
assert "BASE + 'static/queue-today.js'" in source
@@ -846,6 +846,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/mobile-task-dock.js",
"/dashboard/static/mobile-work-entry.js",
"/dashboard/static/mobile-queue-launcher.js",
"/dashboard/static/update-triage-session.js",
"/dashboard/static/agenda-session-launcher.js",
"/dashboard/static/mobile-launch.js",
"/dashboard/static/mobile-app-shortcuts.js",
+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-v99';" in service_worker
assert "const CACHE = 'stackchain-dashboard-shell-v100';" in service_worker
assert "BASE + 'static/today-readiness.js'" in service_worker
+1 -1
View File
@@ -127,7 +127,7 @@ sync.enqueueConfiguration(120, {{'issue:r:1:':60}});
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-v99" in source
assert "stackchain-dashboard-shell-v100" in source
assert "BASE + 'static/today-sync.js'" in source
+95
View File
@@ -0,0 +1,95 @@
import json
import subprocess
from pathlib import Path
import pytest
from tests.dashboard_bundle import dashboard
SESSION = Path(__file__).resolve().parents[1] / "frontend" / "update-triage-session.js"
def run_session(script):
source = f"const createSession = require({json.dumps(str(SESSION))});\n" + script
result = subprocess.run(["node", "-e", source], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_update_triage_persists_account_bound_pass_and_resumes_by_identity():
result = run_session("""
const values = new Map();
let login = 'timmy';
let items = [1,2,3].map(id => ({notification_id:id,title:'Update '+id}));
const opened = [], progress = [];
const options = {
storage:{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)},
getLogin:()=>login, getItems:()=>items,
onOpen:item=>opened.push(item.notification_id), onProgress:value=>progress.push(value), onFinish:()=>opened.push('done'),
};
let session = createSession(options);
session.start();
session.keepUnreadAndNext();
const saved = JSON.parse(values.get('stackchain.update-triage.v1'));
session = createSession(options);
const resumable = session.resumable();
session.resume();
login = 'other';
const isolated = session.resumable();
process.stdout.write(JSON.stringify({opened, progress, saved, resumable, isolated}));
""")
assert result["opened"] == [1, 2, 2]
assert result["progress"] == [
{"index": 1, "total": 3},
{"index": 2, "total": 3},
{"index": 2, "total": 3},
]
assert result["saved"] == {
"version": 1,
"login": "timmy",
"identities": ["1", "2", "3"],
"current": "2",
"completed": ["1"],
}
assert result["resumable"] is True
assert result["isolated"] is False
def test_update_triage_reconciles_removed_items_and_does_not_admit_new_arrivals():
result = run_session("""
const values = new Map();
let items = [1,2,3].map(notification_id => ({notification_id}));
const opened = [], progress = [];
const session = createSession({
storage:{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)},
getLogin:()=> 'timmy', getItems:()=>items,
onOpen:item=>opened.push(item.notification_id), onProgress:value=>progress.push(value), onFinish:()=>opened.push('done'),
});
session.start();
items = [1,3,4].map(notification_id => ({notification_id}));
session.completeAndNext();
session.completeAndNext();
process.stdout.write(JSON.stringify({opened, progress, stored:values.get('stackchain.update-triage.v1') || null}));
""")
assert result == {
"opened": [1, 3, "done"],
"progress": [{"index": 1, "total": 3}, {"index": 3, "total": 3}],
"stored": None,
}
@pytest.mark.anyio
async def test_dashboard_wires_resumable_updates_triage_mobile_flow():
html = await dashboard()
assert '<script src="static/update-triage-session.js"></script>' in html
assert 'id="update-triage-progress"' in html
assert 'id="keep-update-unread" type="button">Keep unread &amp; next</button>' in html
assert "openUpdates: openUpdateTriage" in html
assert "updateTriage.acceptCompleted()" in html
assert "updateTriage.keepUnreadAndNext()" in html
assert "updateTriage.reconcile()" in html
assert ".update-triage-progress" in html