diff --git a/frontend/authored-outbox.js b/frontend/authored-outbox.js index 7f38cd4..65ed834 100644 --- a/frontend/authored-outbox.js +++ b/frontend/authored-outbox.js @@ -241,7 +241,7 @@ function createAuthoredOutbox({ storage, fetchJson, coordinator, backgroundSync, let blocked = 0; currentLogin = String(currentLogin || '').trim(); for (const item of read()) { - if (item.status === 'attention') continue; + if (item.status === 'attention' || item.kind === 'issue-close') continue; if (!currentLogin || item.ownerLogin !== currentLogin) { blocked += 1; continue; } const outcome = await sendItem(item, currentLogin); if (outcome.result) confirmed.push(outcome.result); diff --git a/frontend/background-issue-sync.js b/frontend/background-issue-sync.js index 5b8e09b..039ce66 100644 --- a/frontend/background-issue-sync.js +++ b/frontend/background-issue-sync.js @@ -97,6 +97,7 @@ function createIssueSyncStore({ const timestamp = Number(now()); const items = await records.getAll(); const item = items.find(candidate => candidate.ownerLogin === ownerLogin && + candidate.kind !== 'issue-close' && (candidate.status === 'queued' || candidate.status === 'sending') && (candidate.status !== 'sending' || Number(candidate.claimUntil) <= timestamp)); if (!item) return null; @@ -114,6 +115,7 @@ function createIssueSyncStore({ const eligible = (await records.getAll()).filter(candidate => candidate.recordType !== 'receipt-preference' && candidate.ownerLogin === ownerLogin && + candidate.kind !== 'issue-close' && (candidate.status === 'queued' || candidate.status === 'sending') && (candidate.status !== 'sending' || Number(candidate.claimUntil) <= timestamp)); const lanes = { diff --git a/frontend/dashboard.js b/frontend/dashboard.js index dba01e9..9a65365 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -1662,9 +1662,12 @@ '' + '' + '' : + item.kind === 'authored-outbox' && closureOutbox ? + '' + + '' + + '' : item.kind === 'authored-outbox' && !reviewOutbox ? - '' + + '' + '' + '' : reviewOutbox ? @@ -1676,7 +1679,8 @@ const state = (isOutbox || isUnfiled) ? '' + (item.quarantined ? 'Identity protected' : (isUnfiled ? 'Needs filing' : item.status === 'completion' ? 'Created · ready to start' : - item.status === 'attention' ? 'Needs attention' : item.status === 'sending' ? 'Sending' : 'Queued for sync')) + '' + + item.status === 'attention' ? 'Needs attention' : item.status === 'sending' ? 'Sending' : + item.status === 'authorization' ? 'Awaiting authorization' : 'Queued for sync')) + '' + (item.ownership ? '
' + escapeHtml(item.ownership) + '
' : '') : ''; const attempt = item.last_attempt_error ? 'Last attempt ' + escapeHtml(fmt(item.last_attempt_at)) + ' · ' + escapeHtml(item.last_attempt_error) + '' : ''; @@ -1691,7 +1695,8 @@ '

Delivery center

' + '

Waiting ' + deliveryCenter.counts.waiting + ' · ' + 'Sending ' + deliveryCenter.counts.sending + ' · ' + - 'Needs attention ' + deliveryCenter.counts.attention + '

' + + 'Needs attention ' + deliveryCenter.counts.attention + ' · ' + + 'Authorize ' + deliveryCenter.counts.authorization + '

' + ''; const deliveryCards = deliveryCenter.deliveries.length ? deliveryCenter.deliveries.map(renderDraftCard).join('') : @@ -1769,6 +1774,18 @@ else applyOutboxResult(await issueOutbox.retry(item.outbox_id, activeFlushLogin)); }); }); + list.querySelectorAll('.draft-authorize').forEach(button => { + button.addEventListener('click', async () => { + const item = lastDrafts[Number(button.dataset.draftIndex)]; + if (!item?.outbox_id || !activeFlushLogin) return; + button.disabled = true; + qs('#my-work-action-status').textContent = 'Fresh authorization required for this exact issue.'; + const result = await authoredOutbox.retry(item.outbox_id, activeFlushLogin); + applyAuthoredOutboxResult(result); + qs('#my-work-action-status').textContent = result.confirmed?.length ? + 'Issue closed and queued intent cleared.' : 'Issue closure was not confirmed. The queued intent is still safe.'; + }); + }); list.querySelectorAll('.draft-copy').forEach(button => { button.addEventListener('click', async () => { const item = lastDrafts[Number(button.dataset.draftIndex)]; diff --git a/frontend/drafts.js b/frontend/drafts.js index a84bd8c..643512e 100644 --- a/frontend/drafts.js +++ b/frontend/drafts.js @@ -160,10 +160,12 @@ function createDraftInbox({ storage, getCurrentLogin = () => '', now = () => Dat outbox_id: item.id, outbox_kind: item.kind, kind: 'authored-outbox', - status: item.status === 'attention' ? 'attention' : (item.status === 'sending' ? 'sending' : 'queued'), + status: item.status === 'attention' ? 'attention' : (item.status === 'sending' ? 'sending' : + (isClosure ? 'authorization' : 'queued')), label: item.deliveryState === 'uncertain' ? 'Verify delivery' : (item.status === 'attention' ? (isClosure ? 'Issue closure needs attention' : 'Needs attention') : - (isReview ? 'Queued review' : (isClosure ? 'Queued issue closure' : 'Queued message'))), + (isReview ? 'Queued review' : (isClosure ? 'Awaiting authorization' : 'Queued message'))), + authorization_required: isClosure, delivery_state: item.deliveryState, repository: isUpdate ? '' : item.repository, title: target, @@ -237,12 +239,13 @@ function createDraftInbox({ storage, getCurrentLogin = () => '', now = () => Dat const deliveries = items.filter(item => item.kind === 'issue-outbox' || item.kind === 'authored-outbox'); const drafts = items.filter(item => item.kind !== 'issue-outbox' && item.kind !== 'authored-outbox'); const counts = deliveries.reduce((summary, item) => { - const state = item.status === 'sending' ? 'sending' : (item.status === 'attention' ? 'attention' : 'waiting'); + const state = item.status === 'sending' ? 'sending' : (item.status === 'attention' ? 'attention' : + (item.status === 'authorization' ? 'authorization' : 'waiting')); summary[state] += 1; return summary; - }, { waiting:0, sending:0, attention:0 }); + }, { waiting:0, sending:0, attention:0, authorization:0 }); const retryable = deliveries.filter(item => - item.status === 'queued' && !item.quarantined && item.delivery_state !== 'uncertain' + item.status === 'queued' && !item.authorization_required && !item.quarantined && item.delivery_state !== 'uncertain' ); return { drafts, deliveries, counts, retryable }; } diff --git a/frontend/service-worker.js b/frontend/service-worker.js index e64eeb9..b4c4ae7 100644 --- a/frontend/service-worker.js +++ b/frontend/service-worker.js @@ -1,6 +1,6 @@ const BASE = new URL('./', self.location.href).pathname; importScripts(BASE + 'static/background-issue-sync.js'); -const CACHE = 'stackchain-dashboard-shell-v81'; +const CACHE = 'stackchain-dashboard-shell-v82'; 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; diff --git a/tests/test_authored_outbox.py b/tests/test_authored_outbox.py index d9f780e..8ac2581 100644 --- a/tests/test_authored_outbox.py +++ b/tests/test_authored_outbox.py @@ -150,7 +150,7 @@ if (queued) {{ assert output["progress"] is None -def test_authored_outbox_persists_and_delivers_idempotent_issue_closure(): +def test_authored_outbox_requires_explicit_retry_to_authorize_issue_closure(): script = f""" const createAuthoredOutbox = require({json.dumps(str(OUTBOX))}); const values = new Map(); const calls = []; @@ -162,13 +162,20 @@ const outbox = createAuthoredOutbox({{ const queued = outbox.enqueue({{ kind:'issue-close',repository:'stackchain/dashboard',number:27,operationId:'close-op' }}); -outbox.flush('timmy').then(result => process.stdout.write(JSON.stringify({{ - queued,calls,result,remaining:outbox.list() -}}))); +(async()=>{{ + const automatic = await outbox.flush('timmy'); + const awaitingAuthorization = outbox.list(); + const explicit = await outbox.retry(queued.id, 'timmy'); + process.stdout.write(JSON.stringify({{ + queued,automatic,awaitingAuthorization,explicit,calls,remaining:outbox.list() + }})); +}})(); """ output = run_node(script) assert output["queued"]["kind"] == "issue-close" + assert output["automatic"]["confirmed"] == [] + assert output["awaitingAuthorization"][0]["status"] == "queued" assert output["calls"] == [{ "url": "api/v1/repos/stackchain/dashboard/issues/27/close", "options": { @@ -176,10 +183,50 @@ outbox.flush('timmy').then(result => process.stdout.write(JSON.stringify({{ "headers": {"Accept": "application/json", "Idempotency-Key": "close-op"}, }, }] - assert output["result"]["confirmed"] == [{"number": 27, "state": "closed"}] + assert output["explicit"]["confirmed"] == [{"number": 27, "state": "closed"}] assert output["remaining"] == [] +def test_queued_closure_stays_durable_through_background_skip_then_closes_from_explicit_retry(): + sync = Path(__file__).parents[1] / "frontend" / "background-issue-sync.js" + script = f""" +const createAuthoredOutbox=require({json.dumps(str(OUTBOX))}); +const createBackgroundIssueSync=require({json.dumps(str(sync))}); +const values=new Map();const records=new Map();let tail=Promise.resolve();const calls=[]; +const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}; +const transaction=work=>{{const run=tail.then(()=>work({{ + get:async id=>records.get(id),getAll:async()=>[...records.values()].map(value=>({{...value}})), + put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id), +}}));tail=run.catch(()=>{{}});return run;}}; +const fetchJson=async(url,options={{}})=>{{ + calls.push({{url,key:options.headers?.['Idempotency-Key'] || ''}}); + return url==='api/v1/background-identity'?{{login:'timmy'}}:{{number:27,state:'closed'}}; +}}; +(async()=>{{ + const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}}); + const background=createBackgroundIssueSync({{store,fetchJson}});background.requestSync=async()=>{{}}; + const outbox=createAuthoredOutbox({{storage,fetchJson,getOwnerLogin:()=>'timmy',backgroundSync:background}}); + const admitted=await outbox.enqueueDurably({{kind:'issue-close',repository:'stackchain/dashboard',number:27,operationId:'close-op'}}); + const automatic=await background.flush(); + outbox.reconcileBackground(await background.snapshot()); + const foregroundFlush=await outbox.flush('timmy'); + const explicit=await outbox.retry(admitted.item.id,'timmy'); + process.stdout.write(JSON.stringify({{automatic,foregroundFlush,explicit,calls,remaining:outbox.list(),snapshot:await background.snapshot()}})); +}})(); +""" + output = run_node(script) + + assert output["automatic"]["confirmed"] == [] + assert output["foregroundFlush"]["confirmed"] == [] + assert output["calls"] == [ + {"url": "api/v1/background-identity", "key": ""}, + {"url": "api/v1/repos/stackchain/dashboard/issues/27/close", "key": "close-op"}, + ] + assert output["explicit"]["confirmed"] == [{"number": 27, "state": "closed"}] + assert output["remaining"] == [] + assert output["snapshot"][0]["status"] == "sent" + + def test_authored_outbox_classifies_failures_and_continues_past_attention_items(): script = f""" const createAuthoredOutbox = require({json.dumps(str(OUTBOX))}); diff --git a/tests/test_background_issue_sync.py b/tests/test_background_issue_sync.py index 5ccbd6b..db7d51f 100644 --- a/tests/test_background_issue_sync.py +++ b/tests/test_background_issue_sync.py @@ -372,34 +372,44 @@ createBackgroundIssueSync({{store,fetchJson}}).flush().then(result=>process.stdo }] -def test_closed_app_sync_delivers_issue_closure_and_returns_actionable_receipt(): - authored = { - "id": "close-op", "operationId": "close-op", "ownerLogin": "timmy", "status": "queued", - "kind": "issue-close", "repository": "stackchain/dashboard", "number": 27, "body": "", - } +def test_closed_app_sync_leaves_issue_closure_awaiting_foreground_authorization(): + records = [ + { + "id": "close-op", "operationId": "close-op", "ownerLogin": "timmy", "status": "queued", + "kind": "issue-close", "repository": "stackchain/dashboard", "number": 27, "body": "", + }, + { + "id": "comment-op", "operationId": "comment-op", "ownerLogin": "timmy", "status": "queued", + "kind": "issue-comment", "repository": "stackchain/dashboard", "number": 27, "body": "Still deliver", + }, + ] script = f""" const createBackgroundIssueSync = require({json.dumps(str(SYNC))}); -let queued = {json.dumps(authored)}; const calls=[]; -const store = {{ - claimNext:async owner=>queued?.ownerLogin===owner?(queued=null,{json.dumps(authored)}):null, - complete:async()=>{{}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0, +const records=new Map({json.dumps([[item["id"], item] for item in records])});let tail=Promise.resolve();const calls=[]; +const transaction=work=>{{const run=tail.then(()=>work({{ + get:async id=>records.get(id),getAll:async()=>[...records.values()].map(value=>({{...value}})), + put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id), +}}));tail=run.catch(()=>{{}});return run;}}; +const fetchJson=async(url,options={{}})=>{{ + calls.push({{url,options}}); + return url==='api/v1/background-identity'?{{login:'timmy'}}:{{id:45}}; }}; -const fetchJson=async(url,options={{}})=>{{calls.push({{url,options}});return url==='api/v1/background-identity'?{{login:'timmy'}}:{{number:27,state:'closed'}};}}; -createBackgroundIssueSync({{store,fetchJson}}).flush().then(result=>process.stdout.write(JSON.stringify({{calls,result}}))); +(async()=>{{ + const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}}); + const result=await createBackgroundIssueSync({{store,fetchJson}}).flush(); + process.stdout.write(JSON.stringify({{calls,result,snapshot:await store.snapshot()}})); +}})(); """ output = run_node(script) - mutation = output["calls"][1] - assert mutation["url"] == "api/v1/repos/stackchain/dashboard/issues/27/close" - assert mutation["options"]["method"] == "PATCH" - assert mutation["options"]["headers"] == { - "Accept": "application/json", "Idempotency-Key": "close-op" - } - assert output["result"]["confirmed"] == [{"number": 27, "state": "closed"}] - assert output["result"]["receipts"] == [{ - "id": "close-op", "status": "confirmed", "kind": "message", - "route": "#/my-work/issue/stackchain/dashboard/27", - }] + mutations = output["calls"][1:] + assert [call["url"] for call in mutations] == [ + "api/v1/repos/stackchain/dashboard/issues/27/comments" + ] + assert output["result"]["confirmed"] == [{"id": 45}] + by_id = {item["id"]: item for item in output["snapshot"]} + assert by_id["close-op"]["status"] == "queued" + assert by_id["comment-op"]["status"] == "sent" def test_reconciling_one_outbox_lane_preserves_the_other_lane(): diff --git a/tests/test_comment_next.py b/tests/test_comment_next.py index ddcbf5d..cea3bac 100644 --- a/tests/test_comment_next.py +++ b/tests/test_comment_next.py @@ -305,4 +305,4 @@ async def test_current_today_update_offers_reply_and_next_without_marking_read() 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-v81" in worker + assert "stackchain-dashboard-shell-v82" in worker diff --git a/tests/test_drafts.py b/tests/test_drafts.py index 950bd9e..8d8f744 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -142,25 +142,32 @@ process.stdout.write(JSON.stringify(item)); assert output["quarantined"] is False -def test_draft_inbox_exposes_queued_issue_closure_for_retry_or_discard(): +def test_draft_inbox_exposes_queued_issue_closure_as_awaiting_authorization(): script = f""" const createDraftInbox = require({json.dumps(str(DRAFTS))}); const values=new Map([['stackchain.authored-outbox.v1',JSON.stringify({{version:2,items:[{{ id:'close-1',kind:'issue-close',repository:'stackchain/dashboard',number:27,body:'', - ownerLogin:'timmy',status:'attention',error:'Closure rejected',queuedAt:200 + ownerLogin:'timmy',status:'queued',queuedAt:200 }}]}})]]); const storage={{get length(){{return values.size}},key:i=>Array.from(values.keys())[i]||null,getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; -const item=createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}}).list()[0]; -process.stdout.write(JSON.stringify(item)); +const inbox=createDraftInbox({{storage,getCurrentLogin:()=>'timmy'}}); +const item=inbox.list()[0]; +process.stdout.write(JSON.stringify({{item,partition:inbox.partition()}})); """ output = run_node(script) + item = output["item"] - assert output["label"] == "Issue closure needs attention" - assert output["outbox_kind"] == "issue-close" - assert output["title"] == "stackchain/dashboard#27" - assert output["preview"] == "Closure rejected" - assert output["route"] == {"kind": "issue", "repository": "stackchain/dashboard", "number": 27} - assert output["quarantined"] is False + assert item["label"] == "Awaiting authorization" + assert item["status"] == "authorization" + assert item["authorization_required"] is True + assert item["outbox_kind"] == "issue-close" + assert item["title"] == "stackchain/dashboard#27" + assert item["route"] == {"kind": "issue", "repository": "stackchain/dashboard", "number": 27} + assert item["quarantined"] is False + assert output["partition"]["counts"] == { + "waiting": 0, "sending": 0, "attention": 0, "authorization": 1 + } + assert output["partition"]["retryable"] == [] def test_draft_inbox_exposes_created_issue_waiting_for_create_and_start_continuation(): @@ -248,7 +255,7 @@ process.stdout.write(JSON.stringify(inbox.partition())); assert len(output["drafts"]) == 1 assert len(output["deliveries"]) == 5 - assert output["counts"] == {"waiting": 2, "sending": 1, "attention": 2} + assert output["counts"] == {"waiting": 2, "sending": 1, "attention": 2, "authorization": 0} assert [item["outbox_id"] for item in output["retryable"]] == ["waiting"] waiting = next(item for item in output["deliveries"] if item["outbox_id"] == "waiting") assert waiting["last_attempt_at"] == 90 @@ -270,7 +277,7 @@ async def test_mobile_dashboard_exposes_touch_safe_draft_recovery_lane(): assert "captureDraft.repository && !issueCaptureRepositories.includes(captureDraft.repository)" in html assert "Verified not posted — retry" in html assert "const closureOutbox = item.outbox_kind === 'issue-close';" in html - assert "closureOutbox ? 'Open issue' : 'Open message'" in html + assert "item.kind === 'authored-outbox' && closureOutbox" in html assert "payload.detail?.message" in html assert "error.code = payload.detail?.code" in html @@ -284,6 +291,7 @@ async def test_mobile_dashboard_renders_delivery_center_separately_and_retries_w assert 'Waiting ' in html assert 'Sending ' in html assert 'Needs attention ' in html + assert 'Authorize ' in html assert "const deliveryCenter = draftInbox.partition(lastDrafts);" in html assert "await Promise.all([issueOutbox.flush(activeFlushLogin), authoredOutbox.flush(activeFlushLogin)])" in html assert "deliveryCenter.retryable.length" in html @@ -293,6 +301,17 @@ async def test_mobile_dashboard_renders_delivery_center_separately_and_retries_w assert '.delivery-center button { min-height:44px;' in html +@pytest.mark.anyio +async def test_mobile_dashboard_requires_an_explicit_authorize_and_close_gesture(): + html = await dashboard() + + assert 'class="draft-authorize"' in html + assert '>Authorize & close' in html + assert "list.querySelectorAll('.draft-authorize')" in html + assert "authoredOutbox.retry(item.outbox_id, activeFlushLogin)" in html + assert "item.status === 'authorization' ? 'Awaiting authorization'" in html + + @pytest.mark.anyio async def test_dashboard_only_flushes_account_bound_outboxes_after_a_fresh_identity_snapshot(): html = await dashboard() diff --git a/tests/test_later_sync.py b/tests/test_later_sync.py index ea2b107..9195cc1 100644 --- a/tests/test_later_sync.py +++ b/tests/test_later_sync.py @@ -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-v81" in source + assert "stackchain-dashboard-shell-v82" in source assert "BASE + 'static/later-sync.js'" in source diff --git a/tests/test_markdown_renderer.py b/tests/test_markdown_renderer.py index 56348f9..c9952c1 100644 --- a/tests/test_markdown_renderer.py +++ b/tests/test_markdown_renderer.py @@ -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-v81" in worker + assert "stackchain-dashboard-shell-v82" in worker diff --git a/tests/test_mobile_composer_integration.py b/tests/test_mobile_composer_integration.py index e369697..84866eb 100644 --- a/tests/test_mobile_composer_integration.py +++ b/tests/test_mobile_composer_integration.py @@ -35,7 +35,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-v81" in worker + assert "stackchain-dashboard-shell-v82" in worker def test_all_conversation_composers_offer_accessible_mobile_mentions(): diff --git a/tests/test_plan_today.py b/tests/test_plan_today.py index ca6294f..a4a738c 100644 --- a/tests/test_plan_today.py +++ b/tests/test_plan_today.py @@ -292,6 +292,6 @@ 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-v81" in source + assert "stackchain-dashboard-shell-v82" in source assert "BASE + 'static/plan-today.js'" in source assert "BASE + 'static/plan-today-preview.js'" in source diff --git a/tests/test_service_worker.py b/tests/test_service_worker.py index 09aa29e..d9958f8 100644 --- a/tests/test_service_worker.py +++ b/tests/test_service_worker.py @@ -122,7 +122,7 @@ async function dispatchNotificationClick(route) {{ def test_resumable_today_session_ships_in_a_new_offline_shell(): source = WORKER.read_text() - assert "stackchain-dashboard-shell-v81" in source + assert "stackchain-dashboard-shell-v82" in source assert "BASE + 'static/my-work.js'" in source assert "BASE + 'static/dashboard.js'" in source assert "BASE + 'static/dashboard.css'" in source @@ -131,14 +131,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-v81" in source + assert "stackchain-dashboard-shell-v82" 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-v81" in source + assert "stackchain-dashboard-shell-v82" in source assert "BASE + 'static/today-completion.js'" in source assert "BASE + 'static/dashboard.js'" in source @@ -146,7 +146,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-v81" in source + assert "stackchain-dashboard-shell-v82" in source assert "BASE + 'static/create-issue-sheet.js'" in source assert "BASE + 'static/dashboard.js'" in source @@ -154,14 +154,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-v81" in source + assert "stackchain-dashboard-shell-v82" 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-v81" in source + assert "stackchain-dashboard-shell-v82" in source assert "BASE + 'static/dashboard.css'" in source assert "BASE + 'static/dashboard.js'" in source assert "BASE + 'static/install-app.js'" in source @@ -170,21 +170,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-v81" in source + assert "stackchain-dashboard-shell-v82" 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-v81" in source + assert "stackchain-dashboard-shell-v82" 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-v81" in source + assert "stackchain-dashboard-shell-v82" in source assert "BASE + 'static/update-ownership.js'" in source @@ -385,7 +385,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-v81" in source + assert "stackchain-dashboard-shell-v82" in source assert "BASE + 'static/queue-today.js'" in source diff --git a/tests/test_today_readiness.py b/tests/test_today_readiness.py index d904cdd..13d56d6 100644 --- a/tests/test_today_readiness.py +++ b/tests/test_today_readiness.py @@ -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-v81';" in service_worker + assert "const CACHE = 'stackchain-dashboard-shell-v82';" in service_worker assert "BASE + 'static/today-readiness.js'" in service_worker diff --git a/tests/test_today_sync.py b/tests/test_today_sync.py index 106c78d..432643c 100644 --- a/tests/test_today_sync.py +++ b/tests/test_today_sync.py @@ -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-v81" in source + assert "stackchain-dashboard-shell-v82" in source assert "BASE + 'static/today-sync.js'" in source