1877 lines
72 KiB
Python
1877 lines
72 KiB
Python
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
WORKER = Path(__file__).resolve().parents[1] / "frontend" / "service-worker.js"
|
|
PRIVATE_DATA_REGISTRY = WORKER.parent / "private-data-registry.js"
|
|
|
|
|
|
def run_worker_scenario(scenario: str) -> dict:
|
|
harness = f"""
|
|
const fs = require('fs');
|
|
const vm = require('vm');
|
|
const listeners = {{}};
|
|
const state = {{ added: [], addAttempts: [], individuallyAdded: [], failedAdds: [], deleted: [], deletedDatabases: [], claimed: false, skipped: false, fetches: [], puts: [], migrated: [], activationOrder: [], oldCachedAssets: {{}}, sharedRecords: {{}}, todayCommands: {{}}, failTodayCommandPut: false, failSharedPut: false, backgroundFlushes: 0, backgroundResumes: 0, outboxPurges: 0, outboxLifecycle: [], notifications: [], closedNotifications: 0, focused: [], opened: [], appBadges: [], clearedAppBadges: 0, badgeEnabled: false, badgeCounts: {{updates:0, following:0}}, failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }};
|
|
const storedResponses = new Map();
|
|
storedResponses.set(
|
|
'https://forge.example/dashboard/__offline-session-lease',
|
|
new Response(JSON.stringify({{
|
|
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
|
idle_expires_at: Math.floor(Date.now() / 1000) + 900,
|
|
}})),
|
|
);
|
|
const cache = {{
|
|
addAll: async urls => {{ state.added = urls; }},
|
|
add: async url => {{
|
|
state.addAttempts.push(url);
|
|
if (state.failedAdds.includes(url)) throw new Error('optional asset unavailable');
|
|
state.individuallyAdded.push(url);
|
|
}},
|
|
match: async request => {{
|
|
const key = String(request.url || request);
|
|
if (storedResponses.has(key)) return storedResponses.get(key).clone();
|
|
return state.cachedBody === null ? null : new Response(state.cachedBody);
|
|
}},
|
|
put: async (request, response) => {{
|
|
const key = String(request.url || request);
|
|
state.puts.push(key);
|
|
state.migrated.push(key);
|
|
state.activationOrder.push('put:' + key);
|
|
storedResponses.set(new URL(key, 'https://forge.example').href, response.clone());
|
|
}},
|
|
}};
|
|
const oldCache = {{
|
|
match: async request => {{
|
|
const key = String(request.url || request);
|
|
const body = state.oldCachedAssets[key];
|
|
return body === undefined ? null : new Response(body);
|
|
}},
|
|
}};
|
|
const context = {{
|
|
URL, Request, Response, Headers, AbortController,
|
|
setTimeout, clearTimeout, console,
|
|
self: {{
|
|
location: {{ href: 'https://forge.example/dashboard/service-worker.js', origin: 'https://forge.example' }},
|
|
__STACKCHAIN_NAVIGATION_TIMEOUT_MS: 15,
|
|
__STACKCHAIN_PUSH_ACTION_TIMEOUT_MS: 15,
|
|
stackchainPrivateDatabases: require({json.dumps(str(PRIVATE_DATA_REGISTRY))}),
|
|
__STACKCHAIN_SHARED_ATTACHMENT_STORE: {{
|
|
put: async (id, value) => {{
|
|
if (state.failSharedPut) throw new Error('quota');
|
|
state.sharedRecords[id] = value;
|
|
}},
|
|
get: async id => state.sharedRecords[id] || null,
|
|
delete: async id => {{ delete state.sharedRecords[id]; }},
|
|
}},
|
|
__STACKCHAIN_TODAY_ACTION_STORE: {{
|
|
put: async (clientId, command) => {{
|
|
if (state.failTodayCommandPut) throw new Error('quota');
|
|
state.todayCommands[clientId] = command;
|
|
}},
|
|
claim: async (clientId, now) => {{
|
|
const command = state.todayCommands[clientId] || null;
|
|
delete state.todayCommands[clientId];
|
|
return command && command.expiresAt > now ? command : null;
|
|
}},
|
|
purge: async () => {{ state.todayCommands = {{}}; }},
|
|
}},
|
|
__STACKCHAIN_APP_BADGE_PREFERENCE: {{
|
|
get: async () => state.badgeEnabled,
|
|
set: async enabled => {{ state.badgeEnabled = enabled; }},
|
|
getCounts: async () => ({{...state.badgeCounts}}),
|
|
setCount: async (channel, count) => {{ state.badgeCounts[channel] = count; }},
|
|
clearCounts: async () => {{ state.badgeCounts = {{updates:0, following:0}}; }},
|
|
}},
|
|
addEventListener: (name, handler) => {{ listeners[name] = handler; }},
|
|
skipWaiting: async () => {{ state.skipped = true; }},
|
|
clients: {{
|
|
claim: async () => {{ state.claimed = true; }},
|
|
matchAll: async () => state.clientList || [],
|
|
openWindow: async url => {{ state.opened.push(url); return state.openedClient || {{id:'opened-client',url}}; }},
|
|
}},
|
|
registration: {{
|
|
showNotification: async (title, options) => state.notifications.push({{title,options}}),
|
|
getNotifications: async () => [{{close:()=>{{state.closedNotifications += 1;}}}}],
|
|
setAppBadge: async value => state.appBadges.push(value),
|
|
clearAppBadge: async () => {{ state.clearedAppBadges += 1; }},
|
|
}},
|
|
__issueSync: {{
|
|
flush: async () => {{ state.backgroundFlushes += 1; state.outboxLifecycle.push('flush'); return state.flushResult; }},
|
|
purge: async () => {{ state.outboxPurges += 1; state.outboxLifecycle.push('purge'); }},
|
|
resume: async () => {{ state.backgroundResumes += 1; state.outboxLifecycle.push('resume'); }},
|
|
getReceiptPreference: async login => state.receiptLogin === login,
|
|
}},
|
|
}},
|
|
importScripts: () => {{}},
|
|
indexedDB: {{deleteDatabase:name=>{{
|
|
state.deletedDatabases.push(name);
|
|
const request={{}};
|
|
queueMicrotask(()=>request.onsuccess?.());
|
|
return request;
|
|
}}}},
|
|
caches: {{
|
|
open: async key => key === 'stackchain-dashboard-old' ? oldCache : cache,
|
|
keys: async () => ['stackchain-dashboard-old', 'another-app-cache'],
|
|
delete: async key => {{ state.deleted.push(key); state.activationOrder.push('delete:' + key); return true; }},
|
|
match: async request => cache.match(request),
|
|
}},
|
|
fetch: async (request, options = {{}}) => {{
|
|
state.fetches.push(String(request.url || request));
|
|
if (state.failFetch) throw new Error('offline');
|
|
if (state.stallFetch) return new Promise((resolve, reject) => {{
|
|
options.signal?.addEventListener('abort', () => {{
|
|
state.fetchAborted = true;
|
|
reject(new Error('aborted'));
|
|
}}, {{ once: true }});
|
|
}});
|
|
if (state.lateFetch) return new Promise(resolve => {{
|
|
options.signal?.addEventListener('abort', () => {{ state.fetchAborted = true; }}, {{ once: true }});
|
|
setTimeout(() => resolve(new Response('late network')), 50);
|
|
}});
|
|
const response = new Response('network', {{ status: state.fetchStatus }});
|
|
Object.defineProperty(response, 'redirected', {{ value: state.fetchRedirected }});
|
|
return response;
|
|
}},
|
|
}};
|
|
vm.createContext(context);
|
|
vm.runInContext(fs.readFileSync({json.dumps(str(WORKER))}, 'utf8') + '\\nself.__testFetchJson = fetchJson; self.__testWithSessionCsrf = withSessionCsrf; self.__testOptionalFeatures = OPTIONAL_FEATURES;', context);
|
|
async function dispatch(name, request) {{
|
|
let pending;
|
|
let response;
|
|
listeners[name]({{
|
|
request,
|
|
waitUntil: promise => {{ pending = promise; }},
|
|
respondWith: promise => {{ response = promise; }},
|
|
}});
|
|
if (pending) await pending;
|
|
return response ? await response : null;
|
|
}}
|
|
async function dispatchSync(tag) {{
|
|
let pending;
|
|
listeners.sync({{ tag, waitUntil: promise => {{ pending = promise; }} }});
|
|
if (pending) await pending;
|
|
}}
|
|
async function dispatchMessage(data, ports = [], source = null) {{
|
|
let pending;
|
|
listeners.message({{ data, ports, source, waitUntil: promise => {{ pending = promise; }} }});
|
|
if (pending) await pending;
|
|
}}
|
|
async function dispatchNotificationClick(route, action = '', notificationId = null, tag = null, url = null, actionToken = '') {{
|
|
let pending;
|
|
listeners.notificationclick({{
|
|
action,
|
|
notification: {{tag: tag || ('stackchain-update-' + notificationId), data: {{route, notificationId, ...(url ? {{url}} : {{}}), ...(actionToken ? {{actionToken}} : {{}})}}, close: () => {{ state.notificationClosed = true; }}}},
|
|
waitUntil: promise => {{ pending = promise; }},
|
|
}});
|
|
if (pending) await pending;
|
|
}}
|
|
async function dispatchPush(payload) {{
|
|
let pending;
|
|
listeners.push({{
|
|
data: {{json: () => payload}},
|
|
waitUntil: promise => {{ pending = promise; }},
|
|
}});
|
|
if (pending) await pending;
|
|
}}
|
|
(async () => {{
|
|
{scenario}
|
|
}})().catch(error => {{ console.error(error); process.exit(1); }});
|
|
"""
|
|
completed = subprocess.run(
|
|
["node", "-e", harness], capture_output=True, check=True, text=True
|
|
)
|
|
return json.loads(completed.stdout)
|
|
|
|
|
|
def test_week_unplan_undo_rolls_the_offline_shell():
|
|
source = WORKER.read_text()
|
|
|
|
assert "stackchain-dashboard-shell-v139" in source
|
|
assert "BASE + 'static/week-plan.js'" in source
|
|
assert "BASE + 'static/dashboard.css'" in source
|
|
|
|
|
|
def test_private_today_action_mailbox_rolls_the_offline_shell():
|
|
source = WORKER.read_text()
|
|
|
|
assert "stackchain-dashboard-shell-v139" in source
|
|
|
|
|
|
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
|
|
source = WORKER.read_text()
|
|
|
|
assert "stackchain-dashboard-shell-v139" in source
|
|
assert "BASE + 'static/week-plan.js'" in source
|
|
|
|
|
|
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
|
source = WORKER.read_text()
|
|
|
|
assert "stackchain-dashboard-shell-v139" in source
|
|
assert "BASE + 'static/my-work.js'" in source
|
|
assert "BASE + 'static/dashboard.js'" in source
|
|
assert "BASE + 'static/dashboard.css'" in source
|
|
|
|
|
|
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
|
source = WORKER.read_text()
|
|
|
|
assert "stackchain-dashboard-shell-v139" in source
|
|
assert "BASE + 'static/dashboard.js'" in source
|
|
assert "BASE + 'static/authored-outbox.js'" in source
|
|
assert "BASE + 'static/background-issue-sync.js'" in source
|
|
|
|
|
|
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
|
source = WORKER.read_text()
|
|
|
|
assert "stackchain-dashboard-shell-v139" in source
|
|
assert "BASE + 'static/issue-evidence-review.js'" in source
|
|
assert "BASE + 'static/issue-attachment.js'" in source
|
|
|
|
|
|
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
|
source = WORKER.read_text()
|
|
|
|
assert "stackchain-dashboard-shell-v139" 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-v139" in source
|
|
assert "BASE + 'static/today-completion.js'" in source
|
|
assert "BASE + 'static/dashboard.js'" in source
|
|
|
|
|
|
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
|
source = WORKER.read_text()
|
|
|
|
assert "stackchain-dashboard-shell-v139" in source
|
|
assert "BASE + 'static/create-issue-sheet.js'" in source
|
|
assert "BASE + 'static/dashboard.js'" in source
|
|
|
|
|
|
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
|
source = WORKER.read_text()
|
|
|
|
assert "stackchain-dashboard-shell-v139" in source
|
|
assert "BASE + 'static/issue-sheet.js'" in source
|
|
assert "BASE + 'static/checklist-conflict.js'" in source
|
|
assert "BASE + 'static/dashboard.js'" in source
|
|
assert "BASE + 'static/dashboard.css'" in source
|
|
|
|
|
|
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
|
source = WORKER.read_text()
|
|
|
|
assert "stackchain-dashboard-shell-v139" 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-v139" in source
|
|
assert "BASE + 'static/dashboard.css'" in source
|
|
assert "BASE + 'static/dashboard.js'" in source
|
|
assert "BASE + 'static/install-app.js'" in source
|
|
|
|
|
|
def test_today_convergence_ships_in_a_new_shell_cache():
|
|
source = WORKER.read_text()
|
|
|
|
assert "stackchain-dashboard-shell-v139" 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-v139" 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-v139" in source
|
|
assert "BASE + 'static/update-ownership.js'" in source
|
|
|
|
|
|
def test_image_share_target_stages_one_supported_image_and_redirects_to_capture():
|
|
result = run_worker_scenario(
|
|
"""
|
|
const image = new Blob(['pixels'], {type:'image/png'});
|
|
Object.defineProperty(image, 'name', {value:'bug.png'});
|
|
const form = new FormData();
|
|
form.append('title', 'Broken checkout');
|
|
form.append('text', 'Steps to reproduce');
|
|
form.append('url', 'https://example.test/checkout');
|
|
form.append('image', image, 'bug.png');
|
|
const request = new Request('https://forge.example/dashboard/share-target', {
|
|
method:'POST', body:form, headers:{'Sec-Fetch-Site':'none'},
|
|
});
|
|
Object.defineProperty(request, 'mode', {value:'navigate'});
|
|
const response = await dispatch('fetch', request);
|
|
const [id, record] = Object.entries(state.sharedRecords)[0];
|
|
process.stdout.write(JSON.stringify({
|
|
status:response.status, location:response.headers.get('Location'), id,
|
|
title:record.title, text:record.text, url:record.url,
|
|
filename:record.attachments[0].filename,
|
|
contentType:record.attachments[0].contentType,
|
|
size:record.attachments[0].blob.size,
|
|
}));
|
|
"""
|
|
)
|
|
|
|
assert result == {
|
|
"status": 303,
|
|
"location": "/dashboard/?launch=new&shared=bundle",
|
|
"id": "shared-image",
|
|
"title": "Broken checkout",
|
|
"text": "Steps to reproduce",
|
|
"url": "https://example.test/checkout",
|
|
"filename": "bug.png",
|
|
"contentType": "image/png",
|
|
"size": 6,
|
|
}
|
|
|
|
|
|
def test_image_share_target_stages_ordered_evidence_bundle():
|
|
result = run_worker_scenario(
|
|
"""
|
|
const form = new FormData();
|
|
form.append('image', new Blob(['one'], {type:'image/png'}), 'one.png');
|
|
form.append('image', new Blob(['two'], {type:'image/jpeg'}), 'two.jpg');
|
|
const request = new Request('https://forge.example/dashboard/share-target', {
|
|
method:'POST', body:form, headers:{'Sec-Fetch-Site':'same-origin'},
|
|
});
|
|
Object.defineProperty(request, 'mode', {value:'navigate'});
|
|
const response = await dispatch('fetch', request);
|
|
const record=state.sharedRecords['shared-image'];
|
|
process.stdout.write(JSON.stringify({
|
|
status:response.status, location:response.headers.get('Location'),
|
|
names:record.attachments.map(value=>value.filename),sizes:record.attachments.map(value=>value.blob.size),
|
|
}));
|
|
"""
|
|
)
|
|
|
|
assert result == {
|
|
"status": 303,
|
|
"location": "/dashboard/?launch=new&shared=bundle",
|
|
"names": ["one.png", "two.jpg"],
|
|
"sizes": [3, 3],
|
|
}
|
|
|
|
|
|
def test_share_target_rejects_cross_site_posts_without_touching_staged_bundle():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.sharedRecords['shared-image']={title:'Keep me',text:'private',url:'',attachments:[]};
|
|
const form=new FormData();form.append('title','Replace me');
|
|
const request=new Request('https://forge.example/dashboard/share-target',{
|
|
method:'POST',body:form,headers:{'Sec-Fetch-Site':'cross-site'},
|
|
});
|
|
Object.defineProperty(request,'mode',{value:'navigate'});
|
|
const response=await dispatch('fetch',request);
|
|
process.stdout.write(JSON.stringify({status:response.status,record:state.sharedRecords['shared-image']}));
|
|
"""
|
|
)
|
|
|
|
assert result == {
|
|
"status": 403,
|
|
"record": {"title": "Keep me", "text": "private", "url": "", "attachments": []},
|
|
}
|
|
|
|
|
|
def test_invalid_share_target_preserves_the_previous_atomic_bundle():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.sharedRecords['shared-image']={title:'Keep me',text:'private',url:'',attachments:[]};
|
|
const form=new FormData();
|
|
form.append('image',new Blob(['payload'],{type:'application/pdf'}),'secret.pdf');
|
|
const request=new Request('https://forge.example/dashboard/share-target',{
|
|
method:'POST',body:form,headers:{'Sec-Fetch-Site':'none'},
|
|
});
|
|
Object.defineProperty(request,'mode',{value:'navigate'});
|
|
const response=await dispatch('fetch',request);
|
|
process.stdout.write(JSON.stringify({location:response.headers.get('Location'),record:state.sharedRecords['shared-image']}));
|
|
"""
|
|
)
|
|
|
|
assert result == {
|
|
"location": "/dashboard/?launch=new&shared=unsupported",
|
|
"record": {"title": "Keep me", "text": "private", "url": "", "attachments": []},
|
|
}
|
|
|
|
|
|
def test_share_target_storage_failure_preserves_bundle_and_returns_recovery_marker():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.sharedRecords['shared-image']={title:'Keep me',text:'private',url:'',attachments:[]};
|
|
state.failSharedPut=true;
|
|
const form=new FormData();form.append('title','New private content');
|
|
const request=new Request('https://forge.example/dashboard/share-target',{
|
|
method:'POST',body:form,headers:{'Sec-Fetch-Site':'none'},
|
|
});
|
|
Object.defineProperty(request,'mode',{value:'navigate'});
|
|
const response=await dispatch('fetch',request);
|
|
process.stdout.write(JSON.stringify({location:response.headers.get('Location'),record:state.sharedRecords['shared-image']}));
|
|
"""
|
|
)
|
|
|
|
assert result == {
|
|
"location": "/dashboard/?launch=new&shared=unavailable",
|
|
"record": {"title": "Keep me", "text": "private", "url": "", "attachments": []},
|
|
}
|
|
|
|
|
|
def test_share_target_manifest_uses_dedicated_admission_path():
|
|
manifest = json.loads((WORKER.parent / "manifest.webmanifest").read_text())
|
|
|
|
assert manifest["share_target"]["action"] == "./share-target"
|
|
|
|
|
|
def test_background_sync_event_flushes_closed_app_issue_outbox_only_for_its_tag():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatchSync('stackchain-issue-outbox-v1');
|
|
await dispatchSync('another-app-sync');
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["backgroundFlushes"] == 1
|
|
|
|
|
|
def test_background_delivery_preserves_structured_uncertain_error():
|
|
result = run_worker_scenario(
|
|
"""
|
|
context.fetch=async()=>new Response(JSON.stringify({detail:{code:'delivery_uncertain',message:'Verify it was not posted before retrying.'}}),{status:422,headers:{'Content-Type':'application/json'}});
|
|
const outcome=await context.self.__testFetchJson('/dashboard/api/v1/repos/o/r/issues',{method:'POST'})
|
|
.then(()=>({ok:true}),error=>({status:error.status,code:error.code,message:error.message}));
|
|
process.stdout.write(JSON.stringify(outcome));
|
|
"""
|
|
)
|
|
|
|
assert result == {
|
|
"status": 422,
|
|
"code": "delivery_uncertain",
|
|
"message": "Verify it was not posted before retrying.",
|
|
}
|
|
|
|
|
|
def test_revoked_background_session_purges_worker_data_and_notifies_dashboard_clients():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.clientMessages=[];
|
|
state.clientList=[{postMessage:message=>state.clientMessages.push(message)}];
|
|
context.fetch=async()=>new Response(JSON.stringify({detail:'Authentication required',code:'session_revoked'}),{status:401,headers:{'Content-Type':'application/json'}});
|
|
const outcome=await context.self.__testFetchJson('/dashboard/api/v1/repos/o/r/issues',{method:'POST'})
|
|
.then(()=>({ok:true}),error=>({status:error.status,code:error.code}));
|
|
process.stdout.write(JSON.stringify({state,outcome}));
|
|
"""
|
|
)
|
|
|
|
assert result["outcome"] == {"status": 401, "code": "session_revoked"}
|
|
assert result["state"]["outboxPurges"] == 1
|
|
assert result["state"]["deletedDatabases"] == [
|
|
"stackchain-background-outbox-v1",
|
|
"stackchain-offline-work-v2",
|
|
"stackchain-unfiled-captures-v1",
|
|
"stackchain-voice-transcripts-v1",
|
|
"stackchain-search-reply-drafts-v1",
|
|
"stackchain-conversation-reply-drafts-v1",
|
|
"stackchain-today-action-mailbox-v1",
|
|
"stackchain-app-badge-preference-v1",
|
|
]
|
|
assert result["state"]["deleted"] == ["stackchain-dashboard-old"]
|
|
assert result["state"]["clientMessages"] == [
|
|
{"type": "stackchain-session-revoked"}
|
|
]
|
|
|
|
|
|
def test_idle_background_session_preserves_outbox_and_notifies_dashboard_clients():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.clientMessages=[];
|
|
state.clientList=[{postMessage:message=>state.clientMessages.push(message)}];
|
|
context.fetch=async()=>new Response(JSON.stringify({detail:'Authentication required',code:'session_idle'}),{status:401,headers:{'Content-Type':'application/json'}});
|
|
const outcome=await context.self.__testFetchJson('/dashboard/api/v1/repos/o/r/issues',{method:'POST'})
|
|
.then(()=>({ok:true}),error=>({status:error.status,code:error.code}));
|
|
process.stdout.write(JSON.stringify({state,outcome}));
|
|
"""
|
|
)
|
|
|
|
assert result["outcome"] == {"status": 401, "code": "session_idle"}
|
|
assert result["state"]["outboxPurges"] == 0
|
|
assert result["state"]["deleted"] == []
|
|
assert result["state"]["clientMessages"] == [
|
|
{"type": "stackchain-session-idle"}
|
|
]
|
|
|
|
|
|
def test_authenticated_page_message_resumes_queued_background_delivery():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatchMessage({type:'stackchain-resume-outbox'});
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["backgroundFlushes"] == 1
|
|
|
|
|
|
def test_authenticated_resume_rearms_a_previously_purged_worker_before_flushing():
|
|
result = run_worker_scenario(
|
|
"""
|
|
const replies = [];
|
|
await dispatchMessage({type:'stackchain-purge-outbox'}, [{postMessage:value=>replies.push(value)}]);
|
|
await dispatchMessage({type:'stackchain-resume-outbox'});
|
|
process.stdout.write(JSON.stringify({state,replies}));
|
|
"""
|
|
)
|
|
|
|
assert result["replies"] == [{"ok": True}]
|
|
assert result["state"]["outboxLifecycle"] == ["purge", "resume", "flush"]
|
|
assert result["state"]["backgroundResumes"] == 1
|
|
|
|
|
|
def test_active_today_message_replaces_one_privacy_safe_lock_screen_notification():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatchMessage({type:'stackchain-today-lock-screen',active:true,running:true,identity:'secret/repo#42',actionToken:'opaque-token-1234567890'});
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["notifications"] == [
|
|
{
|
|
"title": "Today session running",
|
|
"options": {
|
|
"body": "Your active Today timer is running.",
|
|
"tag": "stackchain-today-session",
|
|
"renotify": False,
|
|
"silent": True,
|
|
"actions": [
|
|
{"action": "pause-today", "title": "Pause"},
|
|
{"action": "finish-today", "title": "Finish current"},
|
|
],
|
|
"data": {"route": "#/my-work/today", "actionToken": "opaque-token-1234567890"},
|
|
},
|
|
}
|
|
]
|
|
assert "secret" not in json.dumps(result["notifications"])
|
|
|
|
|
|
def test_live_today_break_shows_return_time_and_safe_resume_controls():
|
|
result = run_worker_scenario(
|
|
"""
|
|
const deadline = Date.now() + 5 * 60 * 1000;
|
|
await dispatchMessage({type:'stackchain-today-lock-screen',active:true,running:false,breakDeadlineAt:deadline,identity:'secret/repo#42',actionToken:'opaque-token-1234567890'});
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
notification = result["notifications"][0]
|
|
assert notification["title"] == "On a Today break"
|
|
assert notification["options"]["body"].startswith("Return at ")
|
|
assert notification["options"]["actions"] == [
|
|
{"action": "resume-today", "title": "Resume now"},
|
|
{"action": "open-today", "title": "Open Today"},
|
|
]
|
|
assert notification["options"]["data"]["actionToken"] == "opaque-token-1234567890"
|
|
assert "secret" not in json.dumps(notification)
|
|
|
|
|
|
def test_invalid_or_expired_break_deadline_falls_back_to_paused_controls():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatchMessage({type:'stackchain-today-lock-screen',active:true,running:false,breakDeadlineAt:Date.now()-1,actionToken:'opaque-token-1234567890'});
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["notifications"][0]["title"] == "Today session paused"
|
|
assert result["notifications"][0]["options"]["actions"][0] == {
|
|
"action": "resume-today", "title": "Resume"
|
|
}
|
|
|
|
|
|
def test_inactive_today_message_closes_the_lock_screen_notification():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatchMessage({type:'stackchain-today-lock-screen',active:false,running:false});
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["notifications"] == []
|
|
assert result["closedNotifications"] == 1
|
|
|
|
|
|
def test_private_data_purge_also_removes_the_today_lock_screen_notification():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatchMessage({type:'stackchain-purge-outbox'}, [{postMessage:()=>{}}]);
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["outboxPurges"] == 1
|
|
assert result["closedNotifications"] == 1
|
|
|
|
|
|
def test_today_lock_screen_action_updates_an_open_dashboard_without_navigation():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.clientMessages=[];
|
|
state.clientList=[{
|
|
url:'https://forge.example/dashboard/#/my-work/today',
|
|
postMessage:message=>state.clientMessages.push(message),
|
|
focus:async()=>state.focused.push('today'),
|
|
}];
|
|
await dispatchNotificationClick('#/my-work/today','pause-today',null,'stackchain-today-session');
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["clientMessages"] == [
|
|
{"type": "stackchain-today-timer-action", "action": "pause"}
|
|
]
|
|
assert result["focused"] == ["today"]
|
|
assert result["opened"] == []
|
|
|
|
|
|
def test_today_lock_screen_action_opens_clean_route_and_privately_hands_off_once_when_closed():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.openedClient={id:'cold-client',url:'https://forge.example/dashboard/#/my-work/today'};
|
|
await dispatchNotificationClick('#/my-work/today','resume-today',null,'stackchain-today-session',null,'opaque-token-1234567890');
|
|
const messages=[];
|
|
const source={id:'cold-client',url:'https://forge.example/dashboard/#/my-work/today',postMessage:message=>messages.push(message)};
|
|
await dispatchMessage({type:'stackchain-claim-today-action'}, [], source);
|
|
await dispatchMessage({type:'stackchain-claim-today-action'}, [], source);
|
|
state.claimMessages=messages;
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["opened"] == [
|
|
"https://forge.example/dashboard/#/my-work/today"
|
|
]
|
|
assert "today_timer_action" not in result["opened"][0]
|
|
assert "today_action_token" not in result["opened"][0]
|
|
assert result["claimMessages"] == [{
|
|
"type": "stackchain-today-timer-action",
|
|
"action": "resume",
|
|
"actionToken": "opaque-token-1234567890",
|
|
}]
|
|
assert result["todayCommands"] == {}
|
|
|
|
|
|
def test_today_lock_screen_private_handoff_rejects_wrong_client_and_expires_closed():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.openedClient={id:'intended-client',url:'https://forge.example/dashboard/#/my-work/today'};
|
|
await dispatchNotificationClick('#/my-work/today','finish-today',null,'stackchain-today-session',null,'opaque-token-1234567890');
|
|
const wrong=[];
|
|
await dispatchMessage({type:'stackchain-claim-today-action'}, [], {id:'other-client',url:'https://forge.example/dashboard/#/my-work/today',postMessage:message=>wrong.push(message)});
|
|
state.todayCommands['intended-client'].expiresAt=0;
|
|
const expired=[];
|
|
await dispatchMessage({type:'stackchain-claim-today-action'}, [], {id:'intended-client',url:'https://forge.example/dashboard/#/my-work/today',postMessage:message=>expired.push(message)});
|
|
process.stdout.write(JSON.stringify({wrong,expired,commands:state.todayCommands}));
|
|
"""
|
|
)
|
|
|
|
assert result == {"wrong": [], "expired": [], "commands": {}}
|
|
|
|
|
|
def test_today_action_mailbox_persistence_failure_opens_clean_route_without_mutation():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.failTodayCommandPut=true;
|
|
state.openedClient={id:'cold-client',url:'https://forge.example/dashboard/#/my-work/today'};
|
|
const outcome=await dispatchNotificationClick('#/my-work/today','finish-today',null,'stackchain-today-session',null,'opaque-token-1234567890')
|
|
.then(()=> 'safe', error=>error.message);
|
|
process.stdout.write(JSON.stringify({outcome,opened:state.opened,commands:state.todayCommands}));
|
|
"""
|
|
)
|
|
|
|
assert result == {
|
|
"outcome": "safe",
|
|
"opened": ["https://forge.example/dashboard/#/my-work/today"],
|
|
"commands": {},
|
|
}
|
|
|
|
|
|
def test_private_data_purge_removes_pending_today_action_handoffs():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.openedClient={id:'cold-client',url:'https://forge.example/dashboard/#/my-work/today'};
|
|
await dispatchNotificationClick('#/my-work/today','finish-today',null,'stackchain-today-session',null,'opaque-token-1234567890');
|
|
await dispatchMessage({type:'stackchain-purge-outbox'}, [{postMessage:()=>{}}]);
|
|
const messages=[];
|
|
await dispatchMessage({type:'stackchain-claim-today-action'}, [], {id:'cold-client',url:'https://forge.example/dashboard/#/my-work/today',postMessage:message=>messages.push(message)});
|
|
process.stdout.write(JSON.stringify({messages,commands:state.todayCommands}));
|
|
"""
|
|
)
|
|
|
|
assert result == {"messages": [], "commands": {}}
|
|
|
|
|
|
def test_finish_today_lock_screen_action_forwards_only_its_opaque_token():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.clientMessages=[];
|
|
state.clientList=[{
|
|
url:'https://forge.example/dashboard/#/my-work/today',
|
|
postMessage:message=>state.clientMessages.push(message),
|
|
focus:async()=>state.focused.push('today'),
|
|
}];
|
|
await dispatchNotificationClick('#/my-work/today','finish-today',null,'stackchain-today-session',null,'opaque-token-1234567890');
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["clientMessages"] == [{
|
|
"type": "stackchain-today-timer-action",
|
|
"action": "complete",
|
|
"actionToken": "opaque-token-1234567890",
|
|
}]
|
|
|
|
|
|
def test_break_resume_action_forwards_opaque_token_to_reject_stale_breaks():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.clientMessages=[];
|
|
state.clientList=[{
|
|
url:'https://forge.example/dashboard/#/my-work/today',
|
|
postMessage:message=>state.clientMessages.push(message),
|
|
focus:async()=>state.focused.push('today'),
|
|
}];
|
|
await dispatchNotificationClick('#/my-work/today','resume-today',null,'stackchain-today-session',null,'opaque-token-1234567890');
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["clientMessages"] == [{
|
|
"type": "stackchain-today-timer-action",
|
|
"action": "resume",
|
|
"actionToken": "opaque-token-1234567890",
|
|
}]
|
|
|
|
|
|
def test_background_mutation_abort_also_cancels_stalled_csrf_lookup():
|
|
result = run_worker_scenario(
|
|
"""
|
|
let mutations=0;
|
|
context.fetch=(request, options={})=>{
|
|
if(String(request).includes('/api/v1/session')){
|
|
return new Promise((resolve,reject)=>options.signal?.addEventListener('abort',()=>reject(new Error('aborted')), {once:true}));
|
|
}
|
|
mutations+=1;
|
|
return Promise.resolve(new Response('{}',{status:200,headers:{'Content-Type':'application/json'}}));
|
|
};
|
|
const controller=new AbortController();
|
|
const request=context.self.__testWithSessionCsrf(() =>
|
|
context.self.__testFetchJson('/dashboard/api/v1/repos/o/r/issues',{method:'POST',signal:controller.signal})
|
|
).then(()=> 'completed', error=>error.message);
|
|
controller.abort();
|
|
const outcome=await Promise.race([request,new Promise(resolve=>setTimeout(()=>resolve('blocked'),40))]);
|
|
process.stdout.write(JSON.stringify({outcome,mutations}));
|
|
process.exit(0);
|
|
"""
|
|
)
|
|
|
|
assert result == {"outcome": "aborted", "mutations": 0}
|
|
|
|
|
|
def test_device_purge_message_stops_worker_outbox_and_acknowledges_completion():
|
|
result = run_worker_scenario(
|
|
"""
|
|
const replies = [];
|
|
await dispatchMessage({type:'stackchain-purge-outbox'}, [{postMessage: value => replies.push(value)}]);
|
|
process.stdout.write(JSON.stringify({state,replies}));
|
|
"""
|
|
)
|
|
|
|
assert result["state"]["outboxPurges"] == 1
|
|
assert result["state"]["deletedDatabases"] == [
|
|
"stackchain-background-outbox-v1",
|
|
"stackchain-offline-work-v2",
|
|
"stackchain-unfiled-captures-v1",
|
|
"stackchain-voice-transcripts-v1",
|
|
"stackchain-search-reply-drafts-v1",
|
|
"stackchain-conversation-reply-drafts-v1",
|
|
"stackchain-today-action-mailbox-v1",
|
|
"stackchain-app-badge-preference-v1",
|
|
]
|
|
assert result["replies"] == [{"ok": True}]
|
|
|
|
|
|
def test_opted_in_background_sync_notifies_privately_and_receipt_tap_focuses_route():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.receiptLogin = 'timmy';
|
|
state.flushResult = {login:'timmy', receipts:[
|
|
{id:'capture-1',status:'confirmed',kind:'issue',route:'#/my-work/issue/stackchain/api/44'},
|
|
{id:'bad-1',status:'attention',kind:'message',route:'#/my-work/drafts'},
|
|
{id:'review-1',status:'authorization',kind:'message',route:'#/my-work/drafts'},
|
|
]};
|
|
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('#/my-work/issue/stackchain/api/44');
|
|
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": {"route": "#/my-work/issue/stackchain/api/44"},
|
|
},
|
|
},
|
|
{
|
|
"title": "Queued work needs attention",
|
|
"options": {
|
|
"body": "Tap to review it in Drafts.",
|
|
"tag": "stackchain-delivery-bad-1",
|
|
"data": {"route": "#/my-work/drafts"},
|
|
},
|
|
},
|
|
{
|
|
"title": "Queued review needs authorization",
|
|
"options": {
|
|
"body": "Tap to authorize it in the Delivery center.",
|
|
"tag": "stackchain-delivery-review-1",
|
|
"data": {"route": "#/my-work/drafts"},
|
|
},
|
|
},
|
|
]
|
|
assert result["focused"] == [
|
|
"https://forge.example/dashboard/#/my-work/issue/stackchain/api/44"
|
|
]
|
|
assert result["opened"] == []
|
|
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(
|
|
"""
|
|
await dispatchPush({
|
|
title:'New work update', body:'Tap to review it in Stackchain.',
|
|
tag:'stackchain-update-42', route:'#/my-work/update/42', notification_id:42,
|
|
repository:'must-not-render',
|
|
});
|
|
await dispatchNotificationClick('#/my-work/update/42');
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["notifications"] == [{
|
|
"title": "New work update",
|
|
"options": {
|
|
"body": "Tap to review it in Stackchain.",
|
|
"tag": "stackchain-update-42",
|
|
"actions": [
|
|
{"action": "mark-read", "title": "Mark read"},
|
|
{"action": "tomorrow", "title": "Tomorrow"},
|
|
],
|
|
"data": {
|
|
"route": "#/my-work/update/42",
|
|
"notificationId": 42,
|
|
},
|
|
},
|
|
}]
|
|
assert result["opened"] == [
|
|
"https://forge.example/dashboard/#/my-work/update/42"
|
|
]
|
|
assert "must-not-render" not in json.dumps(result["notifications"])
|
|
|
|
|
|
def test_start_day_push_is_private_and_prepare_action_opens_cached_launch_route():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatchPush({
|
|
title:'must-not-render', body:'private/repo#42',
|
|
tag:'stackchain-start-day-2026-08-20', route:'#/my-work/start-day',
|
|
plan_date:'2026-08-20',
|
|
});
|
|
await dispatchNotificationClick(
|
|
'#/my-work/start-day','prepare-today',null,'stackchain-start-day-2026-08-20'
|
|
);
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["notifications"] == [{
|
|
"title": "Your planned day is ready",
|
|
"options": {
|
|
"body": "Open Stackchain to prepare Today.",
|
|
"tag": "stackchain-start-day-2026-08-20",
|
|
"actions": [{"action": "prepare-today", "title": "Prepare Today"}],
|
|
"data": {"route": "#/my-work/start-day"},
|
|
},
|
|
}]
|
|
assert result["opened"] == [
|
|
"https://forge.example/dashboard/#/my-work/start-day"
|
|
]
|
|
assert "private/repo" not in json.dumps(result["notifications"])
|
|
|
|
|
|
def test_update_digest_push_opens_unread_inbox_without_item_actions_or_private_copy():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatchPush({
|
|
title:'must-not-render', body:'private details must-not-render',
|
|
tag:'stackchain-update-digest', route:'#/my-work/updates', update_count:7,
|
|
});
|
|
await dispatchNotificationClick('#/my-work/updates');
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["notifications"] == [{
|
|
"title": "7 new work updates",
|
|
"options": {
|
|
"body": "Tap to review them in Stackchain.",
|
|
"tag": "stackchain-update-digest",
|
|
"data": {"route": "#/my-work/updates"},
|
|
},
|
|
}]
|
|
assert result["opened"] == [
|
|
"https://forge.example/dashboard/#/my-work/updates"
|
|
]
|
|
assert "must-not-render" not in json.dumps(result["notifications"])
|
|
|
|
|
|
def test_opted_in_background_updates_reconcile_authoritative_badge_without_churn():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatchMessage({type:'stackchain-app-badge-preference', enabled:true}, [], {url:'https://forge.example/dashboard/'});
|
|
await dispatchPush({tag:'stackchain-update-42', route:'#/my-work/update/42', notification_id:42, unread_count:7});
|
|
await dispatchPush({tag:'stackchain-update-digest', route:'#/my-work/updates', update_count:2, unread_count:7});
|
|
await dispatchPush({tag:'stackchain-deadline-digest-2026-08-13', route:'#/my-work/agenda', protect_route:'#/my-work/agenda/protect-today', deadline_count:1, unread_count:99});
|
|
await dispatchPush({tag:'stackchain-update-43', route:'#/my-work/update/43', notification_id:43, unread_count:'invalid'});
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["badgeEnabled"] is True
|
|
assert result["appBadges"] == [7]
|
|
assert result["clearedAppBadges"] == 0
|
|
assert len(result["notifications"]) == 4
|
|
|
|
|
|
def test_background_badge_combines_following_and_updates_without_channel_overwrite():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatchMessage({type:'stackchain-app-badge-preference', enabled:true}, [], {url:'https://forge.example/dashboard/'});
|
|
await dispatchPush({tag:'stackchain-following-0123456789abcdef', route:'#/my-work/following', following_count:2});
|
|
await dispatchPush({tag:'stackchain-update-42', route:'#/my-work/update/42', notification_id:42, unread_count:3});
|
|
await dispatchPush({tag:'stackchain-following-fedcba9876543210', route:'#/my-work/following', following_count:'invalid'});
|
|
await dispatchPush({tag:'stackchain-following-1111111111111111', route:'#/my-work/following', following_count:1});
|
|
await dispatchPush({tag:'stackchain-update-digest', route:'#/my-work/updates', update_count:1, unread_count:3});
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["appBadges"] == [2, 5, 4]
|
|
assert result["badgeCounts"] == {"updates": 3, "following": 1}
|
|
assert result["clearedAppBadges"] == 0
|
|
|
|
|
|
def test_worker_accepts_only_authenticated_authoritative_badge_channel_counts():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.badgeEnabled = true;
|
|
await dispatchMessage({type:'stackchain-app-badge-count', channel:'following', count:2}, [], {url:'https://forge.example/dashboard/'});
|
|
await dispatchMessage({type:'stackchain-app-badge-count', channel:'updates', count:8}, [], {url:'https://evil.example/dashboard/'});
|
|
await dispatchMessage({type:'stackchain-app-badge-count', channel:'following', count:-1}, [], {url:'https://forge.example/dashboard/'});
|
|
await dispatchPush({tag:'stackchain-update-42', route:'#/my-work/update/42', notification_id:42, unread_count:3});
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["badgeCounts"] == {"updates": 3, "following": 2}
|
|
assert result["appBadges"] == [5]
|
|
|
|
|
|
def test_foreground_channel_sync_invalidates_worker_render_cache_for_next_push():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.badgeEnabled = true;
|
|
await dispatchPush({tag:'stackchain-following-0123456789abcdef', route:'#/my-work/following', following_count:2});
|
|
await dispatchMessage({type:'stackchain-app-badge-count', channel:'following', count:0}, [], {url:'https://forge.example/dashboard/'});
|
|
await dispatchPush({tag:'stackchain-update-42', route:'#/my-work/update/42', notification_id:42, unread_count:2});
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["badgeCounts"] == {"updates": 2, "following": 0}
|
|
assert result["appBadges"] == [2, 2]
|
|
|
|
|
|
def test_disabling_background_badge_clears_it_and_future_updates_leave_it_off():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.badgeEnabled = true;
|
|
await dispatchPush({tag:'stackchain-update-42', route:'#/my-work/update/42', notification_id:42, unread_count:3});
|
|
await dispatchMessage({type:'stackchain-app-badge-preference', enabled:false}, [], {url:'https://forge.example/dashboard/'});
|
|
await dispatchPush({tag:'stackchain-update-43', route:'#/my-work/update/43', notification_id:43, unread_count:4});
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["badgeEnabled"] is False
|
|
assert result["appBadges"] == [3]
|
|
assert result["clearedAppBadges"] == 1
|
|
|
|
|
|
def test_deadline_digest_push_offers_protect_today_and_snooze_without_rendering_private_copy():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatchPush({
|
|
title:'must-not-render', body:'private details must-not-render',
|
|
tag:'stackchain-deadline-digest-2026-08-13', route:'#/my-work/agenda',
|
|
protect_route:'#/my-work/agenda/protect-today', deadline_count:3,
|
|
});
|
|
await dispatchNotificationClick('#/my-work/agenda/protect-today', 'protect-today', null, 'stackchain-deadline-digest-2026-08-13');
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["notifications"] == [{
|
|
"title": "3 deadlines need attention",
|
|
"options": {
|
|
"body": "Open Agenda to review or replan them.",
|
|
"tag": "stackchain-deadline-digest-2026-08-13",
|
|
"actions": [
|
|
{"action": "protect-today", "title": "Protect Today"},
|
|
{"action": "snooze-deadline", "title": "Remind in 1 hour"},
|
|
],
|
|
"data": {
|
|
"route": "#/my-work/agenda",
|
|
"protectRoute": "#/my-work/agenda/protect-today",
|
|
},
|
|
},
|
|
}]
|
|
assert result["opened"] == [
|
|
"https://forge.example/dashboard/#/my-work/agenda/protect-today"
|
|
]
|
|
assert "must-not-render" not in json.dumps(result["notifications"])
|
|
|
|
|
|
def test_deadline_digest_snooze_action_records_device_bound_reminder_without_opening_app():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatchPush({
|
|
tag:'stackchain-deadline-digest-2026-08-13', route:'#/my-work/agenda',
|
|
protect_route:'#/my-work/agenda/protect-today', deadline_count:1,
|
|
});
|
|
const calls = [];
|
|
context.fetch = async (request, options = {}) => {
|
|
const url = String(request.url || request);
|
|
const headers = new Headers(options.headers || {});
|
|
calls.push({url, method:String(options.method || 'GET'), csrf:headers.get('X-CSRF-Token')});
|
|
if (url.endsWith('/api/v1/session')) {
|
|
return new Response(JSON.stringify({csrf_token:'session-proof'}), {
|
|
status:200, headers:{'Content-Type':'application/json'},
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({snoozed:true}), {
|
|
status:200, headers:{'Content-Type':'application/json'},
|
|
});
|
|
};
|
|
await dispatchNotificationClick('#/my-work/agenda', 'snooze-deadline', null, 'stackchain-deadline-digest-2026-08-13');
|
|
process.stdout.write(JSON.stringify({state,calls}));
|
|
"""
|
|
)
|
|
|
|
assert result["calls"] == [
|
|
{
|
|
"url": "https://forge.example/dashboard/api/v1/session",
|
|
"method": "GET",
|
|
"csrf": None,
|
|
},
|
|
{
|
|
"url": "https://forge.example/dashboard/api/v1/push-subscription/deadlines/snooze",
|
|
"method": "PATCH",
|
|
"csrf": "session-proof",
|
|
},
|
|
]
|
|
assert result["state"]["notificationClosed"] is True
|
|
assert result["state"]["opened"] == []
|
|
|
|
|
|
def test_deadline_digest_failed_snooze_opens_agenda_instead_of_losing_reminder():
|
|
result = run_worker_scenario(
|
|
"""
|
|
context.fetch = async () => { throw new Error('offline'); };
|
|
await dispatchNotificationClick(
|
|
'#/my-work/agenda', 'snooze-deadline', null,
|
|
'stackchain-deadline-digest-2026-08-13'
|
|
);
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["opened"] == ["https://forge.example/dashboard/#/my-work/agenda"]
|
|
assert result["notificationClosed"] is True
|
|
|
|
|
|
def test_push_mark_read_action_confirms_authenticated_mutation_without_opening_app():
|
|
result = run_worker_scenario(
|
|
"""
|
|
const calls = [];
|
|
context.fetch = async (request, options = {}) => {
|
|
const url = String(request.url || request);
|
|
const headers = new Headers(options.headers || {});
|
|
calls.push({url, method:String(options.method || 'GET'), csrf:headers.get('X-CSRF-Token')});
|
|
if (url.endsWith('/api/v1/session')) {
|
|
return new Response(JSON.stringify({csrf_token:'session-proof'}), {
|
|
status:200, headers:{'Content-Type':'application/json'},
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({ok:true}), {
|
|
status:200, headers:{'Content-Type':'application/json'},
|
|
});
|
|
};
|
|
await dispatchNotificationClick('#/my-work/update/42', 'mark-read', 42);
|
|
process.stdout.write(JSON.stringify({state,calls}));
|
|
"""
|
|
)
|
|
|
|
assert result["calls"] == [
|
|
{
|
|
"url": "https://forge.example/dashboard/api/v1/session",
|
|
"method": "GET",
|
|
"csrf": None,
|
|
},
|
|
{
|
|
"url": "https://forge.example/dashboard/api/v1/notifications/42/read",
|
|
"method": "PATCH",
|
|
"csrf": "session-proof",
|
|
},
|
|
]
|
|
assert result["state"]["notificationClosed"] is True
|
|
assert result["state"]["opened"] == []
|
|
assert result["state"]["focused"] == []
|
|
|
|
|
|
def test_push_tomorrow_action_defers_privately_without_opening_or_marking_read():
|
|
result = run_worker_scenario(
|
|
"""
|
|
const calls = [];
|
|
context.self.__STACKCHAIN_NOW = () => new Date('2026-08-11T18:30:00.000Z');
|
|
context.fetch = async (request, options = {}) => {
|
|
const url = String(request.url || request);
|
|
const headers = new Headers(options.headers || {});
|
|
calls.push({
|
|
url, method:String(options.method || 'GET'), csrf:headers.get('X-CSRF-Token'),
|
|
body:options.body ? JSON.parse(options.body) : null,
|
|
});
|
|
if (url.endsWith('/api/v1/session')) {
|
|
return new Response(JSON.stringify({csrf_token:'session-proof'}), {
|
|
status:200, headers:{'Content-Type':'application/json'},
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({id:42,status:'deferred'}), {
|
|
status:200, headers:{'Content-Type':'application/json'},
|
|
});
|
|
};
|
|
await dispatchNotificationClick('#/my-work/update/42', 'tomorrow', 42, 'stackchain-update-42');
|
|
process.stdout.write(JSON.stringify({state,calls}));
|
|
"""
|
|
)
|
|
|
|
assert result["calls"] == [
|
|
{
|
|
"url": "https://forge.example/dashboard/api/v1/session",
|
|
"method": "GET",
|
|
"csrf": None,
|
|
"body": None,
|
|
},
|
|
{
|
|
"url": "https://forge.example/dashboard/api/v1/notifications/42/later",
|
|
"method": "PATCH",
|
|
"csrf": "session-proof",
|
|
"body": {"wake_at": "2026-08-12T09:00:00.000Z"},
|
|
},
|
|
]
|
|
assert result["state"]["notificationClosed"] is True
|
|
assert result["state"]["opened"] == []
|
|
assert all(not call["url"].endswith("/read") for call in result["calls"])
|
|
|
|
|
|
def test_push_mark_read_failure_opens_existing_update_reader():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.clientList = [{
|
|
url:'https://forge.example/dashboard/',
|
|
navigate:async function(url){ this.url=url; },
|
|
focus:async function(){ state.focused.push(this.url); },
|
|
}];
|
|
context.fetch = async (request, options = {}) => {
|
|
const url = String(request.url || request);
|
|
if (url.endsWith('/api/v1/session')) {
|
|
return new Response(JSON.stringify({csrf_token:'session-proof'}), {
|
|
status:200, headers:{'Content-Type':'application/json'},
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({error:'unavailable'}), {
|
|
status:503, headers:{'Content-Type':'application/json'},
|
|
});
|
|
};
|
|
await dispatchNotificationClick('#/my-work/update/42', 'mark-read', 42);
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["focused"] == [
|
|
"https://forge.example/dashboard/#/my-work/update/42"
|
|
]
|
|
assert result["opened"] == []
|
|
assert result["notificationClosed"] is True
|
|
|
|
|
|
def test_push_mark_read_rejects_mismatched_notification_identity():
|
|
result = run_worker_scenario(
|
|
"""
|
|
const calls = [];
|
|
context.fetch = async (request, options = {}) => {
|
|
calls.push({url:String(request.url || request), method:String(options.method || 'GET')});
|
|
return new Response(JSON.stringify({csrf_token:'session-proof'}), {
|
|
status:200, headers:{'Content-Type':'application/json'},
|
|
});
|
|
};
|
|
await dispatchNotificationClick('#/my-work/update/42', 'mark-read', 43);
|
|
process.stdout.write(JSON.stringify({state,calls}));
|
|
"""
|
|
)
|
|
|
|
assert result["calls"] == []
|
|
assert result["state"].get("notificationClosed") is not True
|
|
assert result["state"]["opened"] == []
|
|
|
|
|
|
def test_push_mark_read_timeout_aborts_request_and_opens_update_reader():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.clientList = [{
|
|
url:'https://forge.example/dashboard/',
|
|
navigate:async function(url){ this.url=url; },
|
|
focus:async function(){ state.focused.push(this.url); },
|
|
}];
|
|
context.fetch = async (request, options = {}) => {
|
|
const url = String(request.url || request);
|
|
if (url.endsWith('/api/v1/session')) {
|
|
return new Response(JSON.stringify({csrf_token:'session-proof'}), {
|
|
status:200, headers:{'Content-Type':'application/json'},
|
|
});
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
options.signal?.addEventListener('abort', () => {
|
|
state.actionFetchAborted = true;
|
|
reject(new Error('aborted'));
|
|
}, {once:true});
|
|
});
|
|
};
|
|
dispatchNotificationClick('#/my-work/update/42', 'mark-read', 42);
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
process.stdout.write(JSON.stringify(state));
|
|
process.exit(0);
|
|
"""
|
|
)
|
|
|
|
assert result["actionFetchAborted"] is True
|
|
assert result["focused"] == [
|
|
"https://forge.example/dashboard/#/my-work/update/42"
|
|
]
|
|
assert result["notificationClosed"] is True
|
|
|
|
|
|
def test_background_mutations_obtain_session_bound_csrf_proof():
|
|
source = WORKER.read_text()
|
|
|
|
assert "async function sessionCsrf(signal)" in source
|
|
assert "BASE + 'api/v1/session'" in source
|
|
assert "signal," in source
|
|
assert "headers.set('X-CSRF-Token', csrf)" in source
|
|
|
|
|
|
def test_one_session_bound_csrf_proof_is_reused_for_a_background_drain():
|
|
source = WORKER.read_text()
|
|
|
|
assert "async function withSessionCsrf(work)" in source
|
|
assert "batchedCsrfController = new AbortController()" in source
|
|
assert "batchedCsrf = sessionCsrf(batchedCsrfController.signal)" in source
|
|
assert "batchedCsrfController.abort()" in source
|
|
assert "await (batchedCsrf || sessionCsrf(options.signal))" in source
|
|
assert "if (options.signal?.aborted)" in source
|
|
assert "batch: withSessionCsrf" in source
|
|
|
|
|
|
def test_queue_today_ships_atomically_in_a_new_offline_shell():
|
|
source = WORKER.read_text()
|
|
|
|
assert "stackchain-dashboard-shell-v139" in source
|
|
assert "BASE + 'static/queue-today.js'" in source
|
|
|
|
|
|
def test_install_precaches_complete_subpath_scoped_app_shell():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatch('install');
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["skipped"] is True
|
|
assert result["added"][0] == "/dashboard/"
|
|
assert set(result["added"]) == {
|
|
"/dashboard/",
|
|
"/dashboard/manifest.webmanifest",
|
|
"/dashboard/static/dashboard.css",
|
|
"/dashboard/static/dashboard.js",
|
|
"/dashboard/static/icons/stackchain-192.png",
|
|
"/dashboard/static/icons/stackchain-512.png",
|
|
"/dashboard/static/session.js",
|
|
"/dashboard/static/sign-out-review.js",
|
|
"/dashboard/static/feature-loader.js",
|
|
"/dashboard/static/workspace-bootstrap.js",
|
|
"/dashboard/static/conversation-action-hydrator.js",
|
|
"/dashboard/static/security-center.js",
|
|
"/dashboard/static/markdown.js",
|
|
"/dashboard/static/commands.js",
|
|
"/dashboard/static/saved-searches.js",
|
|
"/dashboard/static/search-preview.js",
|
|
"/dashboard/static/following.js",
|
|
"/dashboard/static/search-reply-draft-store.js",
|
|
"/dashboard/static/conversation-reply-draft-store.js",
|
|
"/dashboard/static/conversation-photo-drafts.js",
|
|
"/dashboard/static/search-defer.js",
|
|
"/dashboard/static/widgets.js",
|
|
"/dashboard/static/drafts.js",
|
|
"/dashboard/static/unfiled-captures.js",
|
|
"/dashboard/static/unfiled-draft-sync.js",
|
|
"/dashboard/static/shared-image-capture.js",
|
|
"/dashboard/static/draft-filing-session.js",
|
|
"/dashboard/static/draft-capacity-dialog.js",
|
|
"/dashboard/static/outbox-coordinator.js",
|
|
"/dashboard/static/issue-outbox.js",
|
|
"/dashboard/static/reconnect-outboxes.js",
|
|
"/dashboard/static/authored-outbox.js",
|
|
"/dashboard/static/offline-issue-close.js",
|
|
"/dashboard/static/offline-issue-blocker.js",
|
|
"/dashboard/static/notification-read-outbox.js",
|
|
"/dashboard/static/offline-work.js",
|
|
"/dashboard/static/offline-today.js",
|
|
"/dashboard/static/my-work.js",
|
|
"/dashboard/static/agenda-replan.js",
|
|
"/dashboard/static/agenda-calendar.js",
|
|
"/dashboard/static/protect-today.js",
|
|
"/dashboard/static/notification-undo.js",
|
|
"/dashboard/static/card-planning.js",
|
|
"/dashboard/static/work-selection.js",
|
|
"/dashboard/static/today-work.js",
|
|
"/dashboard/static/today-timer.js",
|
|
"/dashboard/static/today-break.js",
|
|
"/dashboard/static/today-progress.js",
|
|
"/dashboard/static/today-lock-screen.js",
|
|
"/dashboard/static/today-session-sync.js",
|
|
"/dashboard/static/today-recap.js",
|
|
"/dashboard/static/today-wrap-up.js",
|
|
"/dashboard/static/today-summary.js",
|
|
"/dashboard/static/today-handoff.js",
|
|
"/dashboard/static/today-completion.js",
|
|
"/dashboard/static/today-readiness.js",
|
|
"/dashboard/static/comment-next.js",
|
|
"/dashboard/static/update-reply-read-next.js",
|
|
"/dashboard/static/plan-today.js",
|
|
"/dashboard/static/plan-today-readiness.js",
|
|
"/dashboard/static/plan-today-preview.js",
|
|
"/dashboard/static/tomorrow-plan.js",
|
|
"/dashboard/static/week-calendar.js",
|
|
"/dashboard/static/week-calendar-import.js",
|
|
"/dashboard/static/week-plan.js",
|
|
"/dashboard/static/today-week-reschedule.js",
|
|
"/dashboard/static/search-week-plan.js",
|
|
"/dashboard/static/today-sync.js",
|
|
"/dashboard/static/today-rollover.js",
|
|
"/dashboard/static/update-ownership.js",
|
|
"/dashboard/static/update-follow-up.js",
|
|
"/dashboard/static/later-work.js",
|
|
"/dashboard/static/later-sync.js",
|
|
"/dashboard/static/later-and-start.js",
|
|
"/dashboard/static/detail-defer.js",
|
|
"/dashboard/static/later-picker.js",
|
|
"/dashboard/static/pick-work.js",
|
|
"/dashboard/static/batch-find-work.js",
|
|
"/dashboard/static/search-batch-plan.js",
|
|
"/dashboard/static/conversation.js",
|
|
"/dashboard/static/comment-actions.js",
|
|
"/dashboard/static/issue-evidence-review.js",
|
|
"/dashboard/static/issue-evidence-editor.js",
|
|
"/dashboard/static/issue-attachment.js",
|
|
"/dashboard/static/issue-filing-review.js",
|
|
"/dashboard/static/issue-sheet.js",
|
|
"/dashboard/static/mobile-issue-detail-nav.js",
|
|
"/dashboard/static/mobile-update-detail-nav.js",
|
|
"/dashboard/static/mobile-review-detail-nav.js",
|
|
"/dashboard/static/mobile-search-preview-nav.js",
|
|
"/dashboard/static/mobile-plan-today-nav.js",
|
|
"/dashboard/static/mobile-find-work-nav.js",
|
|
"/dashboard/static/mobile-pull-refresh.js",
|
|
"/dashboard/static/checklist-conflict.js",
|
|
"/dashboard/static/voice-transcript-store.js",
|
|
"/dashboard/static/voice-issue-capture.js",
|
|
"/dashboard/static/voice-conversation-capture.js",
|
|
"/dashboard/static/create-issue-sheet.js",
|
|
"/dashboard/static/create-pull-sheet.js",
|
|
"/dashboard/static/mobile-create-issue-nav.js",
|
|
"/dashboard/static/create-and-start.js",
|
|
"/dashboard/static/assign-and-start.js",
|
|
"/dashboard/static/filed-claim.js",
|
|
"/dashboard/static/queue-today.js",
|
|
"/dashboard/static/pull-sheet.js",
|
|
"/dashboard/static/review-sheet.js",
|
|
"/dashboard/static/release-receipt.js",
|
|
"/dashboard/static/work-route.js",
|
|
"/dashboard/static/task-overlay-history.js",
|
|
"/dashboard/static/context-poller.js",
|
|
"/dashboard/static/live-data-status.js",
|
|
"/dashboard/static/mobile-today-command-bar.js",
|
|
"/dashboard/static/mobile-task-dock.js",
|
|
"/dashboard/static/mobile-first-task.js",
|
|
"/dashboard/static/mobile-work-entry.js",
|
|
"/dashboard/static/mobile-queue-launcher.js",
|
|
"/dashboard/static/mobile-delivery-recovery.js",
|
|
"/dashboard/static/mobile-start-day.js",
|
|
"/dashboard/static/update-triage-session.js",
|
|
"/dashboard/static/update-review-handoff.js",
|
|
"/dashboard/static/update-read-position.js",
|
|
"/dashboard/static/work-detail-position.js",
|
|
"/dashboard/static/update-triage-launcher.js",
|
|
"/dashboard/static/update-triage-gesture.js",
|
|
"/dashboard/static/update-decision-transaction.js",
|
|
"/dashboard/static/agenda-session-launcher.js",
|
|
"/dashboard/static/mobile-launch.js",
|
|
"/dashboard/static/mobile-insights.js",
|
|
"/dashboard/static/mobile-app-shortcuts.js",
|
|
"/dashboard/static/mobile-app-badge.js",
|
|
"/dashboard/static/install-app.js",
|
|
"/dashboard/static/private-data-inventory.js",
|
|
"/dashboard/static/private-device-data.js",
|
|
"/dashboard/static/device-storage.js",
|
|
"/dashboard/static/mobile-device-setup.js",
|
|
"/dashboard/static/mobile-search-viewport.js",
|
|
"/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",
|
|
}
|
|
|
|
|
|
def test_activate_deletes_only_stale_stackchain_caches():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatch('activate');
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["claimed"] is True
|
|
assert result["deleted"] == ["stackchain-dashboard-old"]
|
|
|
|
|
|
def test_offline_activation_migrates_cached_optional_feature_before_deleting_old_cache():
|
|
result = run_worker_scenario(
|
|
"""
|
|
const asset = '/dashboard/feature-issue-capture-test.js';
|
|
state.oldCachedAssets[asset] = 'previous cached feature';
|
|
state.failedAdds = [asset];
|
|
context.self.__testOptionalFeatures.push(asset);
|
|
await dispatch('activate');
|
|
state.failFetch = true;
|
|
const response = await dispatch('fetch', {
|
|
method: 'GET', mode: 'cors',
|
|
url: 'https://forge.example/dashboard/feature-issue-capture-test.js',
|
|
});
|
|
process.stdout.write(JSON.stringify({ body: await response.text(), state }));
|
|
"""
|
|
)
|
|
|
|
assert result["body"] == "previous cached feature"
|
|
state = result["state"]
|
|
assert state["migrated"] == [
|
|
"/dashboard/feature-issue-capture-test.js"
|
|
]
|
|
assert state["individuallyAdded"] == []
|
|
assert "/dashboard/feature-issue-capture-test.js" not in state["addAttempts"]
|
|
assert state["activationOrder"] == [
|
|
"put:/dashboard/feature-issue-capture-test.js",
|
|
"delete:stackchain-dashboard-old",
|
|
]
|
|
assert state["claimed"] is True
|
|
|
|
|
|
def test_successful_optional_feature_fetch_is_cached_for_offline_reopen():
|
|
result = run_worker_scenario(
|
|
"""
|
|
const asset = '/dashboard/feature-today-timer-test.js';
|
|
context.self.__testOptionalFeatures.push(asset);
|
|
const response = await dispatch('fetch', {
|
|
method: 'GET', mode: 'cors',
|
|
url: 'https://forge.example/dashboard/feature-today-timer-test.js',
|
|
});
|
|
process.stdout.write(JSON.stringify({ body: await response.text(), state }));
|
|
"""
|
|
)
|
|
|
|
assert result["body"] == "network"
|
|
assert result["state"]["puts"] == [
|
|
"https://forge.example/dashboard/feature-today-timer-test.js"
|
|
]
|
|
|
|
|
|
def test_activate_warms_optional_features_without_blocking_siblings_or_claim():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.failedAdds = ['/dashboard/feature-security-center-test.js'];
|
|
context.self.__testOptionalFeatures.push(
|
|
'/dashboard/feature-issue-capture-test.js',
|
|
'/dashboard/feature-security-center-test.js',
|
|
'/dashboard/feature-pull-workflow-test.js',
|
|
);
|
|
await dispatch('activate');
|
|
process.stdout.write(JSON.stringify(state));
|
|
"""
|
|
)
|
|
|
|
assert result["claimed"] is True
|
|
assert result["individuallyAdded"] == [
|
|
"/dashboard/feature-issue-capture-test.js",
|
|
"/dashboard/feature-pull-workflow-test.js",
|
|
]
|
|
|
|
|
|
def test_warmed_optional_feature_is_served_from_cache_while_offline():
|
|
result = run_worker_scenario(
|
|
"""
|
|
context.self.__testOptionalFeatures.push('/dashboard/feature-issue-capture-test.js');
|
|
state.cachedBody = 'cached feature';
|
|
state.failFetch = true;
|
|
const response = await dispatch('fetch', {
|
|
method: 'GET', mode: 'cors',
|
|
url: 'https://forge.example/dashboard/feature-issue-capture-test.js',
|
|
});
|
|
process.stdout.write(JSON.stringify({ body: await response.text(), state }));
|
|
"""
|
|
)
|
|
|
|
assert result["body"] == "cached feature"
|
|
assert result["state"]["fetches"] == []
|
|
|
|
|
|
def test_offline_navigation_returns_cached_shell_for_share_target_url():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.failFetch = true;
|
|
state.cachedBody = 'cached dashboard';
|
|
const response = await dispatch('fetch', {
|
|
method: 'GET', mode: 'navigate',
|
|
url: 'https://forge.example/dashboard/?title=Shared&url=https%3A%2F%2Fexample.com',
|
|
});
|
|
process.stdout.write(JSON.stringify({ body: await response.text(), state }));
|
|
"""
|
|
)
|
|
|
|
assert result["body"] == "cached dashboard"
|
|
assert result["state"]["fetches"] == [
|
|
"https://forge.example/dashboard/?title=Shared&url=https%3A%2F%2Fexample.com"
|
|
]
|
|
|
|
|
|
def test_expired_offline_lease_purges_private_worker_data_and_refuses_cached_shell():
|
|
result = run_worker_scenario(
|
|
"""
|
|
await dispatchMessage({type:'stackchain-session-lease', expiresAt:1, idleExpiresAt:1});
|
|
state.failFetch = true;
|
|
state.cachedBody = 'private cached dashboard';
|
|
const response = await dispatch('fetch', {
|
|
method:'GET', mode:'navigate', url:'https://forge.example/dashboard/',
|
|
});
|
|
process.stdout.write(JSON.stringify({
|
|
status: response.status,
|
|
body: await response.text(),
|
|
state,
|
|
}));
|
|
"""
|
|
)
|
|
|
|
assert result["status"] == 401
|
|
assert result["body"] == "Your Stackchain session expired. Reconnect and sign in."
|
|
assert result["state"]["outboxPurges"] == 1
|
|
assert result["state"]["deletedDatabases"] == [
|
|
"stackchain-background-outbox-v1",
|
|
"stackchain-offline-work-v2",
|
|
"stackchain-unfiled-captures-v1",
|
|
"stackchain-voice-transcripts-v1",
|
|
"stackchain-search-reply-drafts-v1",
|
|
"stackchain-conversation-reply-drafts-v1",
|
|
"stackchain-today-action-mailbox-v1",
|
|
"stackchain-app-badge-preference-v1",
|
|
]
|
|
assert result["state"]["deleted"] == ["stackchain-dashboard-old"]
|
|
assert "private cached dashboard" not in result["body"]
|
|
|
|
|
|
def test_expired_idle_lease_locks_cached_shell_without_purging_recoverable_work():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.clientList = [{postMessage: message => { state.clientMessage = message; }}];
|
|
await dispatchMessage({
|
|
type:'stackchain-session-lease',
|
|
expiresAt:Math.floor(Date.now() / 1000) + 3600,
|
|
idleExpiresAt:1,
|
|
});
|
|
state.failFetch = true;
|
|
state.cachedBody = 'private cached dashboard';
|
|
const response = await dispatch('fetch', {
|
|
method:'GET', mode:'navigate', url:'https://forge.example/dashboard/',
|
|
});
|
|
process.stdout.write(JSON.stringify({
|
|
state, status:response.status, body:await response.text(),
|
|
cacheControl:response.headers.get('Cache-Control'),
|
|
}));
|
|
"""
|
|
)
|
|
|
|
assert result["status"] == 401
|
|
assert result["body"] == "Your Stackchain session is locked. Reconnect and sign in."
|
|
assert result["cacheControl"] == "no-store"
|
|
assert result["state"]["outboxPurges"] == 0
|
|
assert result["state"]["deleted"] == []
|
|
assert result["state"]["clientMessage"] == {"type": "stackchain-session-idle"}
|
|
assert "private cached dashboard" not in result["body"]
|
|
|
|
|
|
def test_stalled_navigation_is_aborted_and_returns_cached_shell_within_deadline():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.stallFetch = true;
|
|
state.cachedBody = 'cached dashboard';
|
|
const response = await Promise.race([
|
|
dispatch('fetch', {
|
|
method: 'GET', mode: 'navigate', url: 'https://forge.example/dashboard/',
|
|
}),
|
|
new Promise(resolve => setTimeout(() => resolve(null), 80)),
|
|
]);
|
|
process.stdout.write(JSON.stringify({
|
|
timedOut: response === null,
|
|
body: response ? await response.text() : null,
|
|
state,
|
|
}));
|
|
"""
|
|
)
|
|
|
|
assert result["timedOut"] is False
|
|
assert result["body"] == "cached dashboard"
|
|
assert result["state"]["fetchAborted"] is True
|
|
assert result["state"]["puts"] == []
|
|
|
|
|
|
def test_stalled_navigation_without_cached_shell_returns_deterministic_504():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.stallFetch = true;
|
|
const response = await Promise.race([
|
|
dispatch('fetch', {
|
|
method: 'GET', mode: 'navigate', url: 'https://forge.example/dashboard/',
|
|
}),
|
|
new Promise(resolve => setTimeout(() => resolve(null), 80)),
|
|
]);
|
|
process.stdout.write(JSON.stringify({
|
|
status: response?.status || null,
|
|
body: response ? await response.text() : null,
|
|
contentType: response?.headers.get('Content-Type') || null,
|
|
state,
|
|
}));
|
|
"""
|
|
)
|
|
|
|
assert result["status"] == 504
|
|
assert result["body"] == "Stackchain is offline and the dashboard is not cached yet. Reconnect and try again."
|
|
assert result["contentType"] == "text/plain; charset=utf-8"
|
|
assert result["state"]["fetchAborted"] is True
|
|
|
|
|
|
def test_navigation_deadline_wins_when_fetch_ignores_abort_and_prevents_late_cache_write():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.lateFetch = true;
|
|
state.cachedBody = 'cached dashboard';
|
|
const response = await Promise.race([
|
|
dispatch('fetch', {
|
|
method: 'GET', mode: 'navigate', url: 'https://forge.example/dashboard/',
|
|
}),
|
|
new Promise(resolve => setTimeout(() => resolve(null), 35)),
|
|
]);
|
|
const body = response ? await response.text() : null;
|
|
await new Promise(resolve => setTimeout(resolve, 60));
|
|
process.stdout.write(JSON.stringify({timedOut: response === null, body, state}));
|
|
"""
|
|
)
|
|
|
|
assert result["timedOut"] is False
|
|
assert result["body"] == "cached dashboard"
|
|
assert result["state"]["fetchAborted"] is True
|
|
assert result["state"]["puts"] == []
|
|
|
|
|
|
def test_redirected_login_navigation_does_not_replace_cached_dashboard_shell():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.fetchRedirected = true;
|
|
const response = await dispatch('fetch', {
|
|
method: 'GET', mode: 'navigate',
|
|
url: 'https://forge.example/dashboard/?title=Shared',
|
|
});
|
|
process.stdout.write(JSON.stringify({ status: response.status, state }));
|
|
"""
|
|
)
|
|
|
|
assert result["status"] == 200
|
|
assert result["state"]["puts"] == []
|
|
|
|
|
|
def test_cross_origin_navigation_is_not_intercepted_or_cached():
|
|
result = run_worker_scenario(
|
|
"""
|
|
const response = await dispatch('fetch', {
|
|
method: 'GET', mode: 'navigate', url: 'https://outside.example/page',
|
|
});
|
|
process.stdout.write(JSON.stringify({ intercepted: response !== null, state }));
|
|
"""
|
|
)
|
|
|
|
assert result["intercepted"] is False
|
|
assert result["state"]["fetches"] == []
|
|
assert result["state"]["puts"] == []
|
|
|
|
|
|
@pytest.mark.parametrize("status", [500, 502, 503, 504])
|
|
def test_server_outage_navigation_returns_working_cached_shell_without_replacing_it(status):
|
|
result = run_worker_scenario(
|
|
f"""
|
|
state.fetchStatus = {status};
|
|
state.cachedBody = 'cached dashboard';
|
|
const response = await dispatch('fetch', {{
|
|
method: 'GET', mode: 'navigate', url: 'https://forge.example/dashboard/',
|
|
}});
|
|
process.stdout.write(JSON.stringify({{ status: response.status, body: await response.text(), state }}));
|
|
"""
|
|
)
|
|
|
|
assert result["status"] == 200
|
|
assert result["body"] == "cached dashboard"
|
|
assert result["state"]["puts"] == []
|
|
|
|
|
|
def test_server_outage_without_cached_shell_preserves_network_error():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.fetchStatus = 503;
|
|
const response = await dispatch('fetch', {
|
|
method: 'GET', mode: 'navigate', url: 'https://forge.example/dashboard/',
|
|
});
|
|
process.stdout.write(JSON.stringify({ status: response.status, body: await response.text(), state }));
|
|
"""
|
|
)
|
|
|
|
assert result["status"] == 503
|
|
assert result["body"] == "network"
|
|
assert result["state"]["puts"] == []
|
|
|
|
|
|
def test_client_error_navigation_is_not_hidden_by_cached_shell():
|
|
result = run_worker_scenario(
|
|
"""
|
|
state.fetchStatus = 401;
|
|
state.cachedBody = 'cached dashboard';
|
|
const response = await dispatch('fetch', {
|
|
method: 'GET', mode: 'navigate', url: 'https://forge.example/dashboard/',
|
|
});
|
|
process.stdout.write(JSON.stringify({ status: response.status, body: await response.text(), state }));
|
|
"""
|
|
)
|
|
|
|
assert result["status"] == 401
|
|
assert result["body"] == "network"
|
|
assert result["state"]["puts"] == []
|
|
|
|
|
|
def test_following_push_uses_generic_copy_and_opens_the_following_route():
|
|
result = run_worker_scenario("""
|
|
await dispatchPush({
|
|
title:'leaked title', body:'leaked body', route:'#/my-work/following',
|
|
tag:'stackchain-following-0123456789abcdef', following_count:2,
|
|
});
|
|
await dispatchNotificationClick(
|
|
'#/my-work/following', '', null, 'stackchain-following-0123456789abcdef'
|
|
);
|
|
console.log(JSON.stringify({notifications:state.notifications,opened:state.opened,closed:state.notificationClosed}));
|
|
""")
|
|
|
|
assert result["notifications"] == [{
|
|
"title": "2 watched items changed",
|
|
"options": {
|
|
"body": "Open Following to review the latest activity.",
|
|
"tag": "stackchain-following-0123456789abcdef",
|
|
"data": {"route": "#/my-work/following"},
|
|
},
|
|
}]
|
|
assert result["opened"] == ["https://forge.example/dashboard/#/my-work/following"]
|
|
assert result["closed"] is True
|