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 @@
@@ -470,7 +473,10 @@
@@ -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
Add comment
- +