Merge pull request 'Complete delegated mobile filing with a shareable issue receipt' (#867) from timmy/866-issue-filing-receipt into main
All checks were successful
CI / lint (push) Successful in 1m48s
CI / build-release (push) Successful in 5s
CI / browser-journey (push) Successful in 50s
CI / release-candidate (push) Successful in 5s

This commit is contained in:
timmy 2026-08-15 03:10:27 +00:00
commit 7a31ea5cba
18 changed files with 300 additions and 24 deletions

View File

@ -360,6 +360,9 @@ function createBackgroundIssueSync({
const resource = item.kind === 'pull-comment' ? 'pull' : 'issue';
return { id: item.id, status, kind: 'message', route: '#/my-work/' + resource + '/' + repository + '/' + encodeURIComponent(item.number) };
}
if (typeof delivered.url === 'string' && /^https?:\/\//.test(delivered.url)) {
return { id: item.id, status, kind: 'issue', url: delivered.url };
}
return { id: item.id, status, kind: 'issue', route: '#/my-work/issue/' + repository + '/' + encodeURIComponent(delivered.number) };
}

View File

@ -12,6 +12,13 @@ header { position: sticky; top: 0; z-index: 20; padding: 12px 16px; display:flex
.live-data-status-header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
.live-data-status-header h2, .live-data-status-header p { margin-top:0; }
.live-data-status-header button, .live-data-status-actions button { min-height:44px; }
.issue-filing-receipt { position:fixed; inset:0; z-index:108; display:flex; align-items:flex-end; justify-content:center; background:rgba(5,12,21,.82); backdrop-filter:blur(4px); }
.issue-filing-receipt[hidden] { display:none; }
.issue-filing-receipt-panel { box-sizing:border-box; width:min(560px,100%); max-height:100dvh; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #4ade80; border-radius:18px 18px 0 0; background:#0b1526; overflow-wrap:anywhere; }
.issue-filing-receipt-panel h2, .issue-filing-receipt-panel h3 { margin:.25rem 0; }
.issue-filing-receipt-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:16px; }
.issue-filing-receipt-actions button, .issue-filing-receipt-actions .button-link { box-sizing:border-box; display:flex; align-items:center; justify-content:center; min-width:0; min-height:44px; width:100%; text-align:center; }
@media (max-width:359px) { .issue-filing-receipt-actions { grid-template-columns:1fr; } }
.live-data-status-feeds { display:grid; gap:8px; margin:14px 0; }
.live-data-status-feed { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; padding:12px; border:1px solid #2a496e; border-radius:12px; background:#0f2237; }
.live-data-status-feed strong, .live-data-status-feed span { overflow-wrap:anywhere; }

View File

@ -3962,6 +3962,14 @@
}
let suppressCreateDraftOnHistoryClose = false;
const issueFilingReceipt = createIssueFilingReceipt({
root:qs('#issue-filing-receipt'), heading:qs('#issue-filing-receipt-heading'),
key:qs('#issue-filing-receipt-key'), title:qs('#issue-filing-receipt-title'),
ownership:qs('#issue-filing-receipt-ownership'), openLink:qs('#issue-filing-receipt-open'),
shareButton:qs('#issue-filing-receipt-share'), fileAnotherButton:qs('#issue-filing-receipt-another'),
doneButton:qs('#issue-filing-receipt-done'), status:qs('#issue-filing-receipt-status'),
navigator, onFileAnother:() => qs('#new-issue').click(),
});
function closeCreateIssueSheet(navigate = true, preserveDraft = true) {
if (navigate && taskOverlayHistory.current() === 'new') {
suppressCreateDraftOnHistoryClose = !preserveDraft;
@ -4009,6 +4017,9 @@
item.kind === 'issue' && item.repository === confirmed.repository && item.number === confirmed.number
);
const completionHandled = startedCompletions.has(confirmed.repository + '#' + confirmed.number);
if (!(confirmed.assignees || []).includes(activeFlushLogin)) {
issueFilingReceipt.show(confirmed, qs('#new-issue'));
}
if (startCreated && !(result.completions || []).length && created) {
const outcome = createAndStart.complete(created);
if (outcome !== 'started' && openCreated) openRoutedWork(created, qs('#new-issue'));

View File

@ -424,6 +424,23 @@
<button id="add-plan-preview" type="button">Add to Today &amp; back</button>
</div>
<div class="issue-filing-receipt" id="issue-filing-receipt" role="dialog" aria-modal="true" aria-labelledby="issue-filing-receipt-heading" hidden>
<section class="issue-filing-receipt-panel">
<div class="small">Issue created</div>
<h2 id="issue-filing-receipt-heading" tabindex="-1">Ready to hand off</h2>
<div id="issue-filing-receipt-key" class="small"></div>
<h3 id="issue-filing-receipt-title"></h3>
<p id="issue-filing-receipt-ownership"></p>
<p id="issue-filing-receipt-status" class="small" aria-live="polite"></p>
<div class="issue-filing-receipt-actions">
<a id="issue-filing-receipt-open" class="button-link" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
<button id="issue-filing-receipt-share" type="button">Share link</button>
<button id="issue-filing-receipt-another" type="button">File another</button>
<button id="issue-filing-receipt-done" type="button">Done</button>
</div>
</section>
</div>
<div id="cmd-palette" role="dialog" aria-label="Command palette">
<div class="cmd-palette-header">
<strong>Search work</strong>
@ -1301,6 +1318,7 @@
<script src="static/mobile-composer-viewport.js"></script>
<script src="static/mention-composer.js"></script>
<script src="static/push-notifications.js"></script>
<script src="static/issue-filing-receipt.js"></script>
<script src="static/dashboard.js"></script>
</body>
</html>

View File

@ -0,0 +1,77 @@
(function (root, factory) {
const api = factory();
if (typeof module === 'object' && module.exports) module.exports = api;
else root.createIssueFilingReceipt = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
'use strict';
function createIssueFilingReceipt({
root, heading, key, title, ownership, openLink, shareButton,
fileAnotherButton, doneButton, status, navigator = {}, clipboard = navigator.clipboard,
onFileAnother = function () {},
}) {
let active = null;
let restoreFocus = null;
function close() {
root.hidden = true;
active = null;
const target = restoreFocus;
restoreFocus = null;
target?.focus?.();
}
async function share() {
if (!active) return;
const payload = {
title: active.repository + '#' + active.number + ' · ' + active.title,
text: active.title,
url: active.url,
};
if (typeof navigator.share === 'function') {
try {
await navigator.share(payload);
return;
} catch (error) {
if (error?.name === 'AbortError') return;
}
}
if (typeof clipboard?.writeText !== 'function') {
status.textContent = 'Issue link is ready in Open in Gitea.';
return;
}
await clipboard.writeText(active.url);
status.textContent = 'Issue link copied.';
}
function show(issue, returnTarget) {
active = issue;
restoreFocus = returnTarget || null;
key.textContent = issue.repository + '#' + issue.number;
title.textContent = issue.title;
ownership.textContent = issue.assignees?.length
? 'Assigned to @' + issue.assignees.join(', @') + '.'
: 'Created with no owner.';
openLink.href = issue.url;
status.textContent = '';
root.hidden = false;
heading.focus();
}
shareButton.addEventListener('click', share);
root.addEventListener('keydown', event => {
if (event.key !== 'Escape' || root.hidden) return;
event.preventDefault();
close();
});
doneButton.addEventListener('click', close);
fileAnotherButton.addEventListener('click', () => {
close();
onFileAnother();
});
return { show, close, share };
}
return createIssueFilingReceipt;
});

View File

@ -1,6 +1,6 @@
const BASE = new URL('./', self.location.href).pathname;
importScripts(BASE + 'static/background-issue-sync.js');
const CACHE = 'stackchain-dashboard-shell-v101';
const CACHE = 'stackchain-dashboard-shell-v102';
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;
@ -100,6 +100,7 @@ const SHELL = [
BASE + 'static/mobile-composer-viewport.js',
BASE + 'static/mention-composer.js',
BASE + 'static/push-notifications.js',
BASE + 'static/issue-filing-receipt.js',
BASE + 'static/background-issue-sync.js',
];
const OPTIONAL_FEATURES = [
@ -332,7 +333,7 @@ async function flushAndNotify() {
body: needsAuthorization ? 'Tap to authorize it in the Delivery center.' :
needsAttention ? 'Tap to review it in Drafts.' : 'Tap to open it in Stackchain.',
tag: 'stackchain-delivery-' + receipt.id,
data: { route: receipt.route },
data: receipt.url ? { url: receipt.url } : { route: receipt.route },
});
}
}
@ -452,8 +453,29 @@ async function openWorkRoute(route) {
return client.focus();
}
async function openCanonicalIssueUrl(rawUrl) {
let target;
try { target = new URL(rawUrl); } catch (_error) { return; }
if (
target.origin !== self.location.origin
|| !/^\/git\/[^/]+\/[^/]+\/issues\/\d+$/.test(target.pathname)
|| target.search || target.hash
) return;
const windows = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
const client = windows.find(candidate => candidate.url.startsWith(self.location.origin + BASE));
if (!client) return self.clients.openWindow(target.href);
if (client.navigate) await client.navigate(target.href);
return client.focus();
}
self.addEventListener('notificationclick', event => {
const route = String(event.notification.data?.route || '');
const issueUrl = String(event.notification.data?.url || '');
if (issueUrl) {
event.notification.close();
event.waitUntil(openCanonicalIssueUrl(issueUrl));
return;
}
if (event.action === 'protect-today') {
const protectRoute = String(event.notification.data?.protectRoute || route);
if (

View File

@ -31,12 +31,12 @@ FEATURE_SOURCES = {
"security-center": ("static/security-center.js",),
"today-timer": (
"static/today-completion.js", "static/work-detail-position.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/search-preview.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
"static/today-rollover.js", "static/later-work.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js",
"static/today-rollover.js", "static/later-work.js", "static/later-picker.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js",
"static/assign-and-start.js", "static/queue-today.js", "static/create-and-start.js",
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
"static/today-work.js", "static/pick-work.js", "static/batch-find-work.js",
"static/search-batch-plan.js", "static/issue-evidence-review.js", "static/issue-evidence-editor.js",
"static/issue-attachment.js", "static/issue-filing-review.js",
"static/issue-attachment.js", "static/issue-filing-review.js", "static/issue-filing-receipt.js",
),
}
CACHE_DECLARATION = re.compile(

View File

@ -78,6 +78,24 @@ const fetchJson=async(url,options={{}})=>{{calls.push({{url,body:options.body||'
}
def test_closed_app_issue_receipt_uses_the_confirmed_canonical_gitea_url():
script = f"""
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
let item={{id:'unowned-link',operationId:'unowned-link',ownerLogin:'timmy',status:'queued',repository:'o/r',title:'Backlog capture',body:'',labelIds:[],unassigned:true}};
const store={{claimNext:async()=>item,complete:async()=>{{item=null;}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0}};
const fetchJson=async url=>url==='api/v1/background-identity'?{{login:'timmy'}}:{{repository:'o/r',number:19,title:'Backlog capture',assignees:[],url:'https://forge.example/git/o/r/issues/19'}};
(async()=>{{const result=await createBackgroundIssueSync({{store,fetchJson}}).flush();process.stdout.write(JSON.stringify(result.receipts[0]));}})();
"""
receipt = run_node(script)
assert receipt == {
"id": "unowned-link",
"status": "confirmed",
"kind": "issue",
"url": "https://forge.example/git/o/r/issues/19",
}
def test_closed_app_sync_delivers_desired_blocker_state():
script = f"""
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});

View File

@ -303,4 +303,4 @@ async def test_unread_update_offers_reply_mark_read_and_next_independent_of_toda
assert '.update-reply-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
assert '.update-reply-actions button { min-height:44px;' in html
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v101" in worker
assert "stackchain-dashboard-shell-v102" in worker

View File

@ -0,0 +1,89 @@
import json
import subprocess
from pathlib import Path
RECEIPT = Path(__file__).parents[1] / "frontend" / "issue-filing-receipt.js"
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
def run_node(script: str) -> dict:
result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
return json.loads(result.stdout)
def test_confirmed_delegated_issue_opens_an_accessible_shareable_receipt():
script = f"""
const createReceipt = require({json.dumps(str(RECEIPT))});
const element = () => ({{hidden:true,textContent:'',href:'',focused:false,focus(){{this.focused=true;}},addEventListener(name, fn){{this[name]=fn;}}}});
const elements = {{root:element(),heading:element(),key:element(),title:element(),ownership:element(),openLink:element(),shareButton:element(),fileAnotherButton:element(),doneButton:element(),status:element()}};
const controller = createReceipt({{...elements,navigator:{{share:async payload=>{{elements.shared=payload;}}}}}});
controller.show({{repository:'stackchain/dashboard',number:866,title:'Finish the filing journey',assignees:['alex'],url:'https://forge.example/git/stackchain/dashboard/issues/866'}}, elements.doneButton);
(async()=>{{await elements.shareButton.click();process.stdout.write(JSON.stringify({{
hidden:elements.root.hidden,key:elements.key.textContent,title:elements.title.textContent,
ownership:elements.ownership.textContent,href:elements.openLink.href,focused:elements.heading.focused,
shared:elements.shared,
}}));}})();
"""
output = run_node(script)
assert output == {
"hidden": False,
"key": "stackchain/dashboard#866",
"title": "Finish the filing journey",
"ownership": "Assigned to @alex.",
"href": "https://forge.example/git/stackchain/dashboard/issues/866",
"focused": True,
"shared": {
"title": "stackchain/dashboard#866 · Finish the filing journey",
"text": "Finish the filing journey",
"url": "https://forge.example/git/stackchain/dashboard/issues/866",
},
}
def test_unassigned_receipt_copies_link_and_escape_restores_launch_focus():
script = f"""
const createReceipt = require({json.dumps(str(RECEIPT))});
const element = () => ({{hidden:true,textContent:'',href:'',focusCount:0,focus(){{this.focusCount++;}},addEventListener(name, fn){{this[name]=fn;}}}});
const elements = {{root:element(),heading:element(),key:element(),title:element(),ownership:element(),openLink:element(),shareButton:element(),fileAnotherButton:element(),doneButton:element(),status:element()}};
const copied=[];const launcher=element();
const controller=createReceipt({{...elements,navigator:{{}},clipboard:{{writeText:async value=>copied.push(value)}}}});
controller.show({{repository:'stackchain/dashboard',number:867,title:'Unowned follow-up',assignees:[],url:'https://forge.example/git/stackchain/dashboard/issues/867'}},launcher);
(async()=>{{await elements.shareButton.click();elements.root.keydown({{key:'Escape',preventDefault(){{elements.prevented=true;}}}});process.stdout.write(JSON.stringify({{
ownership:elements.ownership.textContent,copied,status:elements.status.textContent,hidden:elements.root.hidden,
restored:launcher.focusCount,prevented:elements.prevented,
}}));}})();
"""
output = run_node(script)
assert output == {
"ownership": "Created with no owner.",
"copied": ["https://forge.example/git/stackchain/dashboard/issues/867"],
"status": "Issue link copied.",
"hidden": True,
"restored": 1,
"prevented": True,
}
def test_mobile_shell_wires_confirmed_non_my_work_issues_to_the_receipt():
index = INDEX.read_text()
css = CSS.read_text()
dashboard = DASHBOARD.read_text()
assert 'id="issue-filing-receipt" role="dialog" aria-modal="true"' in index
assert 'id="issue-filing-receipt-heading"' in index
assert 'id="issue-filing-receipt-open"' in index
assert 'id="issue-filing-receipt-share"' in index
assert 'id="issue-filing-receipt-another"' in index
assert 'id="issue-filing-receipt-done"' in index
assert '<script src="static/issue-filing-receipt.js"></script>' in index
assert '.issue-filing-receipt-panel' in css
assert 'padding-bottom:calc(18px + env(safe-area-inset-bottom))' in css
assert '.issue-filing-receipt-actions' in css
assert 'min-height:44px' in css
assert 'createIssueFilingReceipt({' in dashboard
assert "issueFilingReceipt.show(confirmed, qs('#new-issue'))" in dashboard

View File

@ -347,5 +347,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
def test_later_sync_ships_atomically_in_the_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/later-sync.js'" in source

View File

@ -137,4 +137,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
assert ".markdown-content a { min-height:44px;" in css
assert "stackchain-dashboard-shell-v101" in worker
assert "stackchain-dashboard-shell-v102" in worker

View File

@ -45,7 +45,7 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
assert "stackchain-dashboard-shell-v101" in worker
assert "stackchain-dashboard-shell-v102" in worker
def test_all_conversation_composers_offer_accessible_mobile_mentions():

View File

@ -214,7 +214,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
assert "promptStorage:localStorage" in dashboard
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
assert "BASE + 'static/mobile-device-setup.js'" in worker
assert "stackchain-dashboard-shell-v101" in worker
assert "stackchain-dashboard-shell-v102" in worker
assert ".device-setup-panel" in css
assert ".device-readiness-card" in css
assert "overflow-x:hidden" in css

View File

@ -410,7 +410,7 @@ async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(
def test_plan_today_controller_is_available_in_the_offline_shell():
source = SERVICE_WORKER.read_text()
assert "stackchain-dashboard-shell-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/plan-today.js'" in source
assert "BASE + 'static/plan-today-readiness.js'" in source
assert "BASE + 'static/plan-today-preview.js'" in source

View File

@ -120,11 +120,11 @@ async function dispatchMessage(data, ports = []) {{
listeners.message({{ data, ports, waitUntil: promise => {{ pending = promise; }} }});
if (pending) await pending;
}}
async function dispatchNotificationClick(route, action = '', notificationId = null, tag = null) {{
async function dispatchNotificationClick(route, action = '', notificationId = null, tag = null, url = null) {{
let pending;
listeners.notificationclick({{
action,
notification: {{tag: tag || ('stackchain-update-' + notificationId), data: {{route, notificationId}}, close: () => {{ state.notificationClosed = true; }}}},
notification: {{tag: tag || ('stackchain-update-' + notificationId), data: {{route, notificationId, ...(url ? {{url}} : {{}})}}, close: () => {{ state.notificationClosed = true; }}}},
waitUntil: promise => {{ pending = promise; }},
}});
if (pending) await pending;
@ -150,7 +150,7 @@ async function dispatchPush(payload) {{
def test_resumable_today_session_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/my-work.js'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@ -159,14 +159,14 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v101" in source
assert "stackchain-dashboard-shell-v102" 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-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/today-completion.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@ -174,7 +174,7 @@ def test_offline_review_next_ships_today_completion_atomically():
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@ -182,14 +182,14 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v101" in source
assert "stackchain-dashboard-shell-v102" 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-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@ -198,21 +198,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
def test_today_convergence_ships_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v101" in source
assert "stackchain-dashboard-shell-v102" 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-v101" in source
assert "stackchain-dashboard-shell-v102" 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-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/update-ownership.js'" in source
@ -460,6 +460,36 @@ def test_opted_in_background_sync_notifies_privately_and_receipt_tap_focuses_rou
assert result["notificationClosed"] is True
def test_created_issue_receipt_tap_opens_the_confirmed_canonical_gitea_url():
result = run_worker_scenario(
"""
const issueUrl = 'https://forge.example/git/stackchain/api/issues/44';
state.receiptLogin = 'timmy';
state.flushResult = {login:'timmy', receipts:[
{id:'capture-1',status:'confirmed',kind:'issue',url:issueUrl},
]};
state.clientList = [{url:'https://forge.example/dashboard/', navigate:async function(url){ this.url=url; }, focus:async function(){ state.focused.push(this.url); }}];
await dispatchSync('stackchain-issue-outbox-v1');
await dispatchNotificationClick('', '', null, 'stackchain-delivery-capture-1', issueUrl);
process.stdout.write(JSON.stringify(state));
"""
)
assert result["notifications"] == [{
"title": "Queued issue created",
"options": {
"body": "Tap to open it in Stackchain.",
"tag": "stackchain-delivery-capture-1",
"data": {"url": "https://forge.example/git/stackchain/api/issues/44"},
},
}]
assert result["focused"] == [
"https://forge.example/git/stackchain/api/issues/44"
]
assert result["opened"] == []
assert result["notificationClosed"] is True
def test_inbound_push_renders_generic_update_and_tap_opens_existing_workflow():
result = run_worker_scenario(
"""
@ -764,7 +794,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-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/queue-today.js'" in source
@ -873,6 +903,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/mobile-composer-viewport.js",
"/dashboard/static/mention-composer.js",
"/dashboard/static/push-notifications.js",
"/dashboard/static/issue-filing-receipt.js",
"/dashboard/static/background-issue-sync.js",
}

View File

@ -221,7 +221,7 @@ async def test_today_blocker_opens_existing_preview_and_preserves_readiness_gate
def test_readiness_runtime_is_available_in_offline_shell():
service_worker = SERVICE_WORKER.read_text()
assert "const CACHE = 'stackchain-dashboard-shell-v101';" in service_worker
assert "const CACHE = 'stackchain-dashboard-shell-v102';" in service_worker
assert "BASE + 'static/today-readiness.js'" in service_worker

View File

@ -127,7 +127,7 @@ sync.enqueueConfiguration(120, {{'issue:r:1:':60}});
def test_inflight_today_drain_ships_in_a_new_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v101" in source
assert "stackchain-dashboard-shell-v102" in source
assert "BASE + 'static/today-sync.js'" in source