Comment and continue to the next Today item #430
|
|
@ -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
|
||||
|
|
|
|||
48
frontend/comment-next.js
Normal file
48
frontend/comment-next.js
Normal file
|
|
@ -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;
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -269,7 +269,10 @@
|
|||
<section class="issue-comment-composer" aria-labelledby="issue-comment-title">
|
||||
<h2 id="issue-comment-title">Add comment</h2>
|
||||
<textarea id="issue-comment" maxlength="10000" placeholder="Write a comment"></textarea>
|
||||
<button id="send-issue-comment" type="button">Post comment</button>
|
||||
<div class="comment-actions">
|
||||
<button id="send-issue-comment" type="button">Post comment</button>
|
||||
<button id="send-issue-comment-next" type="button" hidden>Comment & next</button>
|
||||
</div>
|
||||
<div id="issue-comment-status" class="small" aria-live="assertive"></div>
|
||||
</section>
|
||||
<details class="issue-planning" id="issue-planning">
|
||||
|
|
@ -470,7 +473,10 @@
|
|||
<section class="pull-comment-composer" aria-labelledby="pull-comment-title">
|
||||
<h2 id="pull-comment-title">Add comment</h2>
|
||||
<textarea id="pull-comment" maxlength="10000" placeholder="Write a comment"></textarea>
|
||||
<button id="send-pull-comment" type="button">Post comment</button>
|
||||
<div class="comment-actions">
|
||||
<button id="send-pull-comment" type="button">Post comment</button>
|
||||
<button id="send-pull-comment-next" type="button" hidden>Comment & next</button>
|
||||
</div>
|
||||
<div id="pull-comment-status" class="small" aria-live="assertive"></div>
|
||||
</section>
|
||||
<details class="pull-review" id="pull-review">
|
||||
|
|
@ -602,6 +608,7 @@
|
|||
<script src="static/card-planning.js"></script>
|
||||
<script src="static/today-work.js"></script>
|
||||
<script src="static/today-completion.js"></script>
|
||||
<script src="static/comment-next.js"></script>
|
||||
<script src="static/plan-today.js"></script>
|
||||
<script src="static/plan-today-preview.js"></script>
|
||||
<script src="static/today-sync.js"></script>
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
212
tests/test_comment_next.py
Normal file
212
tests/test_comment_next.py
Normal file
|
|
@ -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</button>') == 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user