From 2cbeba30fd5b1e2bdba5ab62b84a50f58a43f296 Mon Sep 17 00:00:00 2001 From: timmy Date: Sun, 9 Aug 2026 22:00:14 +0000 Subject: [PATCH] feat: comment and continue Today work (#429) --- README.md | 5 +- frontend/comment-next.js | 48 +++++ frontend/dashboard.css | 2 + frontend/dashboard.js | 77 ++++++++ frontend/index.html | 11 +- frontend/my-work.js | 2 +- frontend/service-worker.js | 3 +- tests/test_comment_next.py | 212 ++++++++++++++++++++++ tests/test_later_sync.py | 2 +- tests/test_markdown_renderer.py | 2 +- tests/test_mobile_composer_integration.py | 2 +- tests/test_plan_today.py | 2 +- tests/test_service_worker.py | 19 +- tests/test_today_sync.py | 2 +- 14 files changed, 370 insertions(+), 19 deletions(-) create mode 100644 frontend/comment-next.js create mode 100644 tests/test_comment_next.py diff --git a/README.md b/README.md index 4c0b97b..2c3225b 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,10 @@ another worker replays a confirmed result instead of posting duplicate content. five-item Today plan syncs across the operator's devices. Starting a Today work session also stores an account-bound checkpoint on the current device. After a reload or installed-app restart, **Resume Today** reopens the saved item (or the next surviving item if work changed); -finishing or choosing **End session** clears only the checkpoint and leaves the Today plan +**Comment & next** on that current issue or pull request posts the handoff online or admits it +to durable account-bound delivery, then removes the item only from Today and opens the next +one without closing or merging it. Delivery or local-admission failure preserves both the draft +and checkpoint. Finishing or choosing **End session** clears only the checkpoint and leaves the Today plan unchanged. Another or unconfirmed account cannot see or resume it. Server revisions prevent delayed responses from replacing a newer plan; same-account browser tabs exchange fresh snapshots, and reconnecting or returning to the dashboard refreshes server truth after replaying queued diff --git a/frontend/comment-next.js b/frontend/comment-next.js new file mode 100644 index 0000000..34474a0 --- /dev/null +++ b/frontend/comment-next.js @@ -0,0 +1,48 @@ +function createCommentNext({ post, queue, canQueue, accept = () => undefined, complete }) { + let inFlight = null; + + function submit(item, body, operationId = '') { + if (inFlight) return inFlight; + inFlight = (async () => { + try { + let comment; + try { + comment = await post(item, body); + } catch (error) { + if (!canQueue(error)) throw error; + const admission = await queue({ + kind: item.kind === 'pull' ? 'pull-comment' : 'issue-comment', + repository: item.repository, + number: item.number, + body, + operationId: typeof operationId === 'function' ? operationId() : operationId, + }); + if (!admission || (!admission.item && admission.durable !== true)) { + throw new Error('Comment was not saved for delivery.'); + } + accept(item, { delivery: admission.background ? 'queued' : 'saved' }); + return { + accepted: true, + delivery: admission.background ? 'queued' : 'saved', + background: Boolean(admission.background), + completed: Boolean(complete(item)), + }; + } + accept(item, { delivery: 'posted', comment }); + return { + accepted: true, + delivery: 'posted', + comment, + completed: Boolean(complete(item)), + }; + } finally { + inFlight = null; + } + })(); + return inFlight; + } + + return { submit, busy: () => Boolean(inFlight) }; +} + +if (typeof module !== 'undefined' && module.exports) module.exports = createCommentNext; diff --git a/frontend/dashboard.css b/frontend/dashboard.css index ef00529..11cc06e 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -226,6 +226,8 @@ textarea { resize: vertical; min-height: 120px; } .issue-comment { padding:10px 0; border-bottom:1px solid #1b2d45; } .issue-comment-composer { display:grid; gap:8px; margin-top:16px; } .issue-comment-composer button { min-height:44px; width:100%; } +.comment-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; } +.comment-actions button { min-height:44px; width:100%; } .issue-label-editor { max-width:100%; margin:14px 0; padding:12px; border:1px solid #2a496e; border-radius:12px; } .issue-label-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(180px,100%),1fr)); gap:8px; max-width:100%; } .issue-label-option { min-height:44px; max-width:100%; display:flex; align-items:center; gap:10px; padding:8px; border:1px solid #2a496e; border-radius:10px; overflow-wrap:anywhere; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 218dc83..0fc64f2 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -782,6 +782,44 @@ warm: warmTodayOffline, announce: message => { qs('#my-work-action-status').textContent = message; }, }); + function setCommentNextVisibility(kind) { + const item = kind === 'issue' ? selectedIssue : selectedPull; + qs('#send-' + kind + '-comment-next').hidden = !item || !workSession.checkpointed(item); + } + const issueCommentNext = createCommentNext({ + post: async (item, body) => { + const comment = await issueController.comment(item, body); + if (selectedIssue === item && issueConversation) renderIssueConversation(issueConversation.append(comment)); + return comment; + }, + queue: message => authoredOutbox.enqueueDurably(message), + canQueue: canQueueMessage, + accept: item => { + issueController.saveDraft(item, ''); + if (selectedIssue === item) qs('#issue-comment').value = ''; + }, + complete: item => completeTodayItem(item, { + successMessage: 'Comment saved. Next Today item opened.', + failureMessage: 'Comment saved, but Today still needs completion.', + }), + }); + const pullCommentNext = createCommentNext({ + post: async (item, body) => { + const comment = await pullController.comment(item, body); + if (selectedPull === item && pullConversation) renderPullConversation(pullConversation.append(comment)); + return comment; + }, + queue: message => authoredOutbox.enqueueDurably(message), + canQueue: canQueueMessage, + accept: item => { + pullController.saveDraft(item, ''); + if (selectedPull === item) qs('#pull-comment').value = ''; + }, + complete: item => completeTodayItem(item, { + successMessage: 'Comment saved. Next Today item opened.', + failureMessage: 'Comment saved, but Today still needs completion.', + }), + }); const closeOfflineIssue = createOfflineIssueClose({ enqueueDurably: message => authoredOutbox.enqueueDurably(message), completeToday: (item, options) => completeTodayItem(item, options), @@ -1732,6 +1770,7 @@ qs('#retry-issue-load').hidden = true; qs('#open-issue-gitea').href = item.url || '#'; qs('#send-issue-comment').disabled = false; + setCommentNextVisibility('issue'); qs('#edit-issue-content').disabled = true; qs('#issue-edit-form').hidden = true; qs('#issue-edit-status').textContent = ''; @@ -1894,6 +1933,7 @@ qs('#merge-pull').textContent = workSession.active() ? 'Merge & next' : 'Merge'; qs('#retry-pull-load').hidden = true; qs('#open-pull-gitea').href = item.url || '#'; + setCommentNextVisibility('pull'); qs('#close-pull-sheet').focus(); try { const detail = offlineDetail || await pullController.load(item); @@ -3351,6 +3391,43 @@ qs('#issue-milestone').disabled = false; } }); + async function submitCommentAndNext(kind) { + const item = kind === 'issue' ? selectedIssue : selectedPull; + if (!item || !workSession.checkpointed(item)) return; + const textarea = qs('#' + kind + '-comment'); + const status = qs('#' + kind + '-comment-status'); + const body = textarea.value.trim(); + if (!body) { + status.textContent = 'Write a comment before posting.'; + textarea.focus(); + return; + } + const postButton = qs('#send-' + kind + '-comment'); + const nextButton = qs('#send-' + kind + '-comment-next'); + const controller = kind === 'issue' ? issueCommentNext : pullCommentNext; + const operationId = () => localStorage.getItem('stackchain.' + kind + '-comment.v1:' + + item.repository + '#' + item.number + ':operation'); + postButton.disabled = true; + nextButton.disabled = true; + status.textContent = 'Posting comment and opening next…'; + try { + const result = await controller.submit(item, body, operationId); + const stillOpen = kind === 'issue' ? selectedIssue === item : selectedPull === item; + if (!stillOpen) return; + if (!result.completed) status.textContent = 'Comment saved, but Today still needs completion.'; + else if (result.delivery === 'posted') status.textContent = 'Comment posted.'; + else if (result.background) status.textContent = 'Queued for sync when the connection returns.'; + else status.textContent = 'Saved for next launch; background delivery unavailable.'; + } catch (error) { + status.textContent = error.message + ' Your draft and Today position are safe; retry.'; + textarea.focus(); + } finally { + postButton.disabled = false; + nextButton.disabled = false; + } + } + qs('#send-issue-comment-next').addEventListener('click', () => submitCommentAndNext('issue')); + qs('#send-pull-comment-next').addEventListener('click', () => submitCommentAndNext('pull')); qs('#send-issue-comment').addEventListener('click', async () => { if (!selectedIssue) return; const body = qs('#issue-comment').value.trim(); diff --git a/frontend/index.html b/frontend/index.html index 585ad8d..055163d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -269,7 +269,10 @@

Add comment

- +
+ + +
@@ -470,7 +473,10 @@

Add comment

- +
+ + +
@@ -602,6 +608,7 @@ + diff --git a/frontend/my-work.js b/frontend/my-work.js index a4f4e14..884ef1a 100644 --- a/frontend/my-work.js +++ b/frontend/my-work.js @@ -563,7 +563,7 @@ function createWorkSession({ return { active: () => running, - checkpointed: () => running && durable, + checkpointed: item => running && durable && (!item || workIdentity(item) === currentIdentity), end: () => finish(), resumable: () => Boolean(checkpoint?.read()), reopen() { diff --git a/frontend/service-worker.js b/frontend/service-worker.js index 6203cdd..3b62a60 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-v71'; +const CACHE = 'stackchain-dashboard-shell-v72'; 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; @@ -29,6 +29,7 @@ const SHELL = [ BASE + 'static/card-planning.js', BASE + 'static/today-work.js', BASE + 'static/today-completion.js', + BASE + 'static/comment-next.js', BASE + 'static/plan-today.js', BASE + 'static/plan-today-preview.js', BASE + 'static/today-sync.js', diff --git a/tests/test_comment_next.py b/tests/test_comment_next.py new file mode 100644 index 0000000..5fe14e2 --- /dev/null +++ b/tests/test_comment_next.py @@ -0,0 +1,212 @@ +import json +import subprocess +from pathlib import Path + +import pytest + +from tests.dashboard_bundle import dashboard + + +COMMENT_NEXT = Path(__file__).parents[1] / "frontend" / "comment-next.js" +MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js" + + +def run_node(script): + return subprocess.run( + ["node", "-e", script], check=True, capture_output=True, text=True + ).stdout + + +def test_comment_and_next_posts_then_completes_current_today_item_once(): + script = f""" +const createCommentNext = require({json.dumps(str(COMMENT_NEXT))}); +const calls = []; +let finishPost; +const controller = createCommentNext({{ + post: (item, body) => {{ calls.push(`post:${{item.number}}:${{body}}`); return new Promise(resolve => {{ finishPost = resolve; }}); }}, + queue: () => {{ throw new Error('must not queue'); }}, + canQueue: () => false, + complete: item => {{ calls.push(`complete:${{item.number}}`); return true; }}, +}}); +const item = {{kind:'issue', repository:'stackchain/dashboard', number:429}}; +const first = controller.submit(item, 'Handoff ready'); +const second = controller.submit(item, 'Handoff ready'); +if (first !== second) throw new Error('submission was not single-flight'); +finishPost({{id:7, body:'Handoff ready'}}); +(async () => {{ + const result = await first; + process.stdout.write(JSON.stringify({{result, calls, busy:controller.busy()}})); +}})().catch(error => {{ console.error(error); process.exit(1); }}); +""" + + assert json.loads(run_node(script)) == { + "result": { + "accepted": True, + "delivery": "posted", + "comment": {"id": 7, "body": "Handoff ready"}, + "completed": True, + }, + "calls": ["post:429:Handoff ready", "complete:429"], + "busy": False, + } + + +def test_comment_and_next_advances_after_durable_offline_admission(): + script = f""" +const createCommentNext = require({json.dumps(str(COMMENT_NEXT))}); +const calls = []; +const retryable = new Error('offline'); retryable.status = 503; +const controller = createCommentNext({{ + post: () => Promise.reject(retryable), + canQueue: error => error.status === 503, + queue: message => {{ calls.push(message); return Promise.resolve({{durable:true, background:false}}); }}, + complete: item => {{ calls.push(`complete:${{item.number}}`); return true; }}, +}}); +(async () => {{ + const result = await controller.submit({{kind:'pull', repository:'stackchain/dashboard', number:12}}, 'Please review', 'op-12'); + process.stdout.write(JSON.stringify({{result, calls}})); +}})().catch(error => {{ console.error(error); process.exit(1); }}); +""" + + assert json.loads(run_node(script)) == { + "result": { + "accepted": True, + "delivery": "saved", + "background": False, + "completed": True, + }, + "calls": [ + { + "kind": "pull-comment", + "repository": "stackchain/dashboard", + "number": 12, + "body": "Please review", + "operationId": "op-12", + }, + "complete:12", + ], + } + + +def test_comment_and_next_reads_retry_identity_after_the_failed_post(): + script = f""" +const createCommentNext = require({json.dumps(str(COMMENT_NEXT))}); +let operationId = ''; +let queued; +const controller = createCommentNext({{ + post: () => {{ operationId = 'post-attempt-id'; return Promise.reject(Object.assign(new Error('timeout'), {{status:503}})); }}, + canQueue: () => true, + queue: message => {{ queued = message; return Promise.resolve({{durable:true}}); }}, + complete: () => true, +}}); +(async () => {{ + const result = await controller.submit( + {{kind:'issue', repository:'r', number:3}}, 'Status', () => operationId + ); + process.stdout.write(JSON.stringify({{result, queued}})); +}})().catch(error => {{ console.error(error); process.exit(1); }}); +""" + + output = json.loads(run_node(script)) + assert output["result"]["accepted"] is True + assert output["result"]["delivery"] == "saved" + assert output["queued"]["operationId"] == "post-attempt-id" + + +def test_comment_and_next_clears_the_completed_draft_before_opening_next_item(): + script = f""" +const createCommentNext = require({json.dumps(str(COMMENT_NEXT))}); +const calls = []; +const controller = createCommentNext({{ + post: () => Promise.resolve({{id:1}}), queue: () => null, canQueue: () => false, + accept: item => calls.push(`clear:${{item.number}}`), + complete: item => {{ calls.push(`complete:${{item.number}}`); return true; }}, +}}); +(async () => {{ + await controller.submit({{kind:'issue',repository:'r',number:8}}, 'Done'); + process.stdout.write(JSON.stringify(calls)); +}})().catch(error => {{ console.error(error); process.exit(1); }}); +""" + + assert json.loads(run_node(script)) == ["clear:8", "complete:8"] + + +def test_comment_and_next_keeps_today_position_when_delivery_or_completion_fails(): + script = f""" +const createCommentNext = require({json.dumps(str(COMMENT_NEXT))}); +const failures = []; +const permanent = createCommentNext({{ + post: () => Promise.reject(Object.assign(new Error('forbidden'), {{status:403}})), + canQueue: () => false, + queue: () => Promise.resolve({{durable:true}}), + complete: () => {{ failures.push('advanced'); return true; }}, +}}); +const notDurable = createCommentNext({{ + post: () => Promise.reject(Object.assign(new Error('offline'), {{status:503}})), + canQueue: () => true, + queue: () => Promise.resolve({{durable:false, background:false}}), + complete: () => {{ failures.push('advanced'); return true; }}, +}}); +const cannotComplete = createCommentNext({{ + post: () => Promise.resolve({{id:9}}), canQueue: () => false, queue: () => null, + complete: () => false, +}}); +(async () => {{ + for (const [name, controller] of [['permanent', permanent], ['notDurable', notDurable]]) {{ + try {{ await controller.submit({{kind:'issue',repository:'r',number:1}}, 'draft'); }} + catch (error) {{ failures.push(`${{name}}:${{error.message}}`); }} + }} + const partial = await cannotComplete.submit({{kind:'issue',repository:'r',number:1}}, 'draft'); + process.stdout.write(JSON.stringify({{failures, partial}})); +}})().catch(error => {{ console.error(error); process.exit(1); }}); +""" + + assert json.loads(run_node(script)) == { + "failures": ["permanent:forbidden", "notDurable:Comment was not saved for delivery."], + "partial": { + "accepted": True, + "delivery": "posted", + "comment": {"id": 9}, + "completed": False, + }, + } + + +def test_today_checkpoint_only_matches_the_current_session_item(): + script = f""" +const buildMyWork = require({json.dumps(str(MY_WORK))}); +const items = [ + {{kind:'issue', repository:'r', number:1}}, + {{kind:'pull', repository:'r', number:2}}, +]; +const session = buildMyWork.createWorkSession({{ + getItems: () => items, getFilter: () => 'all', checkpointEnabled: () => true, + checkpoint: {{save:()=>undefined, clear:()=>undefined, read:()=>null}}, + onOpen:()=>undefined, onProgress:()=>undefined, onFinish:()=>undefined, +}}); +session.start(); +process.stdout.write(JSON.stringify({{ + current: session.checkpointed(items[0]), + other: session.checkpointed(items[1]), + generic: session.checkpointed(), +}})); +""" + + assert json.loads(run_node(script)) == { + "current": True, + "other": False, + "generic": True, + } + + +@pytest.mark.anyio +async def test_mobile_composers_offer_comment_and_next_only_for_today_checkpoint(): + html = await dashboard() + + assert 'id="send-issue-comment-next"' in html + assert 'id="send-pull-comment-next"' in html + assert html.count('>Comment & next') == 2 + assert "setCommentNextVisibility('issue')" in html + assert "setCommentNextVisibility('pull')" in html + assert '.comment-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html + assert '.comment-actions button { min-height:44px;' in html diff --git a/tests/test_later_sync.py b/tests/test_later_sync.py index 399c9c0..eda15ed 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-v71" in source + assert "stackchain-dashboard-shell-v72" 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 0131153..75f3e5a 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-v71" in worker + assert "stackchain-dashboard-shell-v72" in worker diff --git a/tests/test_mobile_composer_integration.py b/tests/test_mobile_composer_integration.py index 013bb5a..9bd16ca 100644 --- a/tests/test_mobile_composer_integration.py +++ b/tests/test_mobile_composer_integration.py @@ -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-v71" in worker + assert "stackchain-dashboard-shell-v72" in worker diff --git a/tests/test_plan_today.py b/tests/test_plan_today.py index 45ec1f5..1463dd4 100644 --- a/tests/test_plan_today.py +++ b/tests/test_plan_today.py @@ -168,6 +168,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-v71" in source + assert "stackchain-dashboard-shell-v72" 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 3f14e4b..df17d18 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-v71" in source + assert "stackchain-dashboard-shell-v72" 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,7 +131,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell(): def test_offline_review_next_ships_today_completion_atomically(): source = WORKER.read_text() - assert "stackchain-dashboard-shell-v71" in source + assert "stackchain-dashboard-shell-v72" in source assert "BASE + 'static/today-completion.js'" in source assert "BASE + 'static/dashboard.js'" in source @@ -139,7 +139,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-v71" in source + assert "stackchain-dashboard-shell-v72" in source assert "BASE + 'static/create-issue-sheet.js'" in source assert "BASE + 'static/dashboard.js'" in source @@ -147,14 +147,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-v71" in source + assert "stackchain-dashboard-shell-v72" 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-v71" in source + assert "stackchain-dashboard-shell-v72" in source assert "BASE + 'static/dashboard.css'" in source assert "BASE + 'static/dashboard.js'" in source assert "BASE + 'static/install-app.js'" in source @@ -163,21 +163,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-v71" in source + assert "stackchain-dashboard-shell-v72" 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-v71" in source + assert "stackchain-dashboard-shell-v72" 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-v71" in source + assert "stackchain-dashboard-shell-v72" in source assert "BASE + 'static/update-ownership.js'" in source @@ -358,7 +358,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-v71" in source + assert "stackchain-dashboard-shell-v72" in source assert "BASE + 'static/queue-today.js'" in source @@ -397,6 +397,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell(): "/dashboard/static/card-planning.js", "/dashboard/static/today-work.js", "/dashboard/static/today-completion.js", + "/dashboard/static/comment-next.js", "/dashboard/static/plan-today.js", "/dashboard/static/plan-today-preview.js", "/dashboard/static/today-sync.js", diff --git a/tests/test_today_sync.py b/tests/test_today_sync.py index 13473d0..78e2039 100644 --- a/tests/test_today_sync.py +++ b/tests/test_today_sync.py @@ -86,7 +86,7 @@ sync.enqueue('add', 'issue:r:1:'); 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-v71" in source + assert "stackchain-dashboard-shell-v72" in source assert "BASE + 'static/today-sync.js'" in source -- 2.43.0