Merge pull request 'Hand off Today lock-screen actions without URL capabilities' (#1173) from timmy/1172-private-today-action-handoff into main
This commit is contained in:
commit
58c2d3f5d8
|
|
@ -6,6 +6,7 @@
|
||||||
'stackchain-voice-transcripts-v1',
|
'stackchain-voice-transcripts-v1',
|
||||||
'stackchain-search-reply-drafts-v1',
|
'stackchain-search-reply-drafts-v1',
|
||||||
'stackchain-conversation-reply-drafts-v1',
|
'stackchain-conversation-reply-drafts-v1',
|
||||||
|
'stackchain-today-action-mailbox-v1',
|
||||||
]);
|
]);
|
||||||
if (typeof module !== 'undefined' && module.exports) module.exports = databases;
|
if (typeof module !== 'undefined' && module.exports) module.exports = databases;
|
||||||
else root.stackchainPrivateDatabases = databases;
|
else root.stackchainPrivateDatabases = databases;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,67 @@
|
||||||
const BASE = new URL('./', self.location.href).pathname;
|
const BASE = new URL('./', self.location.href).pathname;
|
||||||
importScripts(BASE + 'static/private-data-registry.js');
|
importScripts(BASE + 'static/private-data-registry.js');
|
||||||
importScripts(BASE + 'static/background-issue-sync.js');
|
importScripts(BASE + 'static/background-issue-sync.js');
|
||||||
const CACHE = 'stackchain-dashboard-shell-v125';
|
const CACHE = 'stackchain-dashboard-shell-v126';
|
||||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||||
const PUSH_ACTION_TIMEOUT_MS = self.__STACKCHAIN_PUSH_ACTION_TIMEOUT_MS || 8000;
|
const PUSH_ACTION_TIMEOUT_MS = self.__STACKCHAIN_PUSH_ACTION_TIMEOUT_MS || 8000;
|
||||||
|
const TODAY_ACTION_TTL_MS = 2 * 60 * 1000;
|
||||||
const PRIVATE_DATABASES = self.stackchainPrivateDatabases;
|
const PRIVATE_DATABASES = self.stackchainPrivateDatabases;
|
||||||
|
|
||||||
|
function createTodayActionStore() {
|
||||||
|
const dbName = 'stackchain-today-action-mailbox-v1';
|
||||||
|
const storeName = 'commands';
|
||||||
|
const open = () => new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(dbName, 1);
|
||||||
|
request.onupgradeneeded = () => request.result.createObjectStore(storeName, {keyPath:'clientId'});
|
||||||
|
request.onsuccess = () => resolve(request.result);
|
||||||
|
request.onerror = () => reject(request.error || new Error('Today action mailbox unavailable.'));
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
async put(clientId, command) {
|
||||||
|
const database = await open();
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const transaction = database.transaction(storeName, 'readwrite');
|
||||||
|
transaction.objectStore(storeName).put({...command, clientId});
|
||||||
|
transaction.oncomplete = resolve;
|
||||||
|
transaction.onerror = () => reject(transaction.error || new Error('Today action could not be saved.'));
|
||||||
|
transaction.onabort = transaction.onerror;
|
||||||
|
});
|
||||||
|
database.close();
|
||||||
|
},
|
||||||
|
async claim(clientId, now) {
|
||||||
|
const database = await open();
|
||||||
|
let command = null;
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const transaction = database.transaction(storeName, 'readwrite');
|
||||||
|
const store = transaction.objectStore(storeName);
|
||||||
|
const request = store.get(clientId);
|
||||||
|
request.onsuccess = () => {
|
||||||
|
command = request.result || null;
|
||||||
|
if (command) store.delete(clientId);
|
||||||
|
};
|
||||||
|
transaction.oncomplete = resolve;
|
||||||
|
transaction.onerror = () => reject(transaction.error || new Error('Today action could not be claimed.'));
|
||||||
|
transaction.onabort = transaction.onerror;
|
||||||
|
});
|
||||||
|
database.close();
|
||||||
|
return command && command.expiresAt > now ? command : null;
|
||||||
|
},
|
||||||
|
async purge() {
|
||||||
|
const database = await open();
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const transaction = database.transaction(storeName, 'readwrite');
|
||||||
|
transaction.objectStore(storeName).clear();
|
||||||
|
transaction.oncomplete = resolve;
|
||||||
|
transaction.onerror = () => reject(transaction.error || new Error('Today actions could not be purged.'));
|
||||||
|
transaction.onabort = transaction.onerror;
|
||||||
|
});
|
||||||
|
database.close();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const todayActionStore = self.__STACKCHAIN_TODAY_ACTION_STORE || createTodayActionStore();
|
||||||
const SHELL = [
|
const SHELL = [
|
||||||
BASE,
|
BASE,
|
||||||
BASE + 'manifest.webmanifest',
|
BASE + 'manifest.webmanifest',
|
||||||
|
|
@ -473,6 +528,7 @@ self.addEventListener('message', event => {
|
||||||
if (event.data?.type === 'stackchain-purge-outbox') event.waitUntil((async () => {
|
if (event.data?.type === 'stackchain-purge-outbox') event.waitUntil((async () => {
|
||||||
try {
|
try {
|
||||||
await issueSync.purge();
|
await issueSync.purge();
|
||||||
|
await todayActionStore.purge();
|
||||||
await deletePrivateDatabases();
|
await deletePrivateDatabases();
|
||||||
await updateTodayLockScreen(false, false);
|
await updateTodayLockScreen(false, false);
|
||||||
event.ports?.[0]?.postMessage({ ok: true });
|
event.ports?.[0]?.postMessage({ ok: true });
|
||||||
|
|
@ -481,12 +537,33 @@ self.addEventListener('message', event => {
|
||||||
}
|
}
|
||||||
})());
|
})());
|
||||||
if (event.data?.type === 'stackchain-today-lock-screen') {
|
if (event.data?.type === 'stackchain-today-lock-screen') {
|
||||||
event.waitUntil(updateTodayLockScreen(
|
event.waitUntil((async () => {
|
||||||
event.data.active === true,
|
const active = event.data.active === true;
|
||||||
event.data.running === true,
|
if (!active) await todayActionStore.purge();
|
||||||
String(event.data.actionToken || ''),
|
await updateTodayLockScreen(
|
||||||
Number(event.data.breakDeadlineAt || 0)
|
active,
|
||||||
));
|
event.data.running === true,
|
||||||
|
String(event.data.actionToken || ''),
|
||||||
|
Number(event.data.breakDeadlineAt || 0)
|
||||||
|
);
|
||||||
|
})());
|
||||||
|
}
|
||||||
|
if (event.data?.type === 'stackchain-claim-today-action') {
|
||||||
|
event.waitUntil((async () => {
|
||||||
|
const source = event.source;
|
||||||
|
if (!source?.id || !String(source.url || '').startsWith(self.location.origin + BASE)) return;
|
||||||
|
let command;
|
||||||
|
try { command = await todayActionStore.claim(source.id, Date.now()); }
|
||||||
|
catch (_error) { return; }
|
||||||
|
if (!command || !['pause', 'resume', 'complete'].includes(command.action)) return;
|
||||||
|
if (command.actionToken && !/^[A-Za-z0-9_-]{16,128}$/.test(command.actionToken)) return;
|
||||||
|
if (command.action === 'complete' && !command.actionToken) return;
|
||||||
|
source.postMessage?.({
|
||||||
|
type:'stackchain-today-timer-action',
|
||||||
|
action:command.action,
|
||||||
|
...(command.actionToken ? {actionToken:command.actionToken} : {}),
|
||||||
|
});
|
||||||
|
})());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -608,10 +685,17 @@ async function applyTodayTimerAction(action, actionToken = '') {
|
||||||
});
|
});
|
||||||
return client.focus?.();
|
return client.focus?.();
|
||||||
}
|
}
|
||||||
const query = '?today_timer_action=' + action +
|
const target = new URL(BASE + route, self.location.origin).href;
|
||||||
(actionToken ? '&today_action_token=' + encodeURIComponent(actionToken) : '');
|
const opened = await self.clients.openWindow(target);
|
||||||
const target = new URL(BASE + query + route, self.location.origin).href;
|
if (!opened?.id) return opened;
|
||||||
return self.clients.openWindow(target);
|
try {
|
||||||
|
await todayActionStore.put(opened.id, {
|
||||||
|
action,
|
||||||
|
...(actionToken ? {actionToken} : {}),
|
||||||
|
expiresAt:Date.now() + TODAY_ACTION_TTL_MS,
|
||||||
|
});
|
||||||
|
} catch (_error) { /* The clean Today route remains safe; fail the action closed. */ }
|
||||||
|
return opened;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addEventListener('notificationclick', event => {
|
self.addEventListener('notificationclick', event => {
|
||||||
|
|
|
||||||
|
|
@ -149,16 +149,9 @@ function createTodayLockScreen({
|
||||||
},
|
},
|
||||||
consumeAction,
|
consumeAction,
|
||||||
async consumeLaunchAction() {
|
async consumeLaunchAction() {
|
||||||
let url;
|
if (!enabled()) return false;
|
||||||
try { url = new URL(locationRef?.href || ''); }
|
await post({type:'stackchain-claim-today-action'});
|
||||||
catch (_error) { return false; }
|
return true;
|
||||||
const action = url.searchParams.get('today_timer_action');
|
|
||||||
if (!['pause', 'resume', 'complete'].includes(action)) return false;
|
|
||||||
const token = url.searchParams.get('today_action_token') || '';
|
|
||||||
url.searchParams.delete('today_timer_action');
|
|
||||||
url.searchParams.delete('today_action_token');
|
|
||||||
historyRef?.replaceState?.(null, '', url.pathname + url.search + url.hash);
|
|
||||||
return await consumeAction(action, token);
|
|
||||||
},
|
},
|
||||||
render,
|
render,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
|
||||||
assert '.update-reply-actions button { min-height:44px;' in html
|
assert '.update-reply-actions button { min-height:44px;' in html
|
||||||
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||||
assert "stackchain-dashboard-shell-v125" in worker
|
assert "stackchain-dashboard-shell-v126" in worker
|
||||||
|
|
|
||||||
|
|
@ -364,6 +364,7 @@ process.stdout.write(JSON.stringify(state));
|
||||||
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
||||||
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
||||||
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
||||||
|
"stackchain-today-action-mailbox-v1",
|
||||||
]
|
]
|
||||||
assert result["remaining"] == ["gitea.preference"]
|
assert result["remaining"] == ["gitea.preference"]
|
||||||
assert result["replaced"] == [
|
assert result["replaced"] == [
|
||||||
|
|
@ -391,6 +392,7 @@ process.stdout.write(JSON.stringify(state));
|
||||||
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
||||||
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
||||||
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
||||||
|
"stackchain-today-action-mailbox-v1",
|
||||||
]
|
]
|
||||||
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
|
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
|
||||||
assert result["replaced"] == ["/dashboard/login?reason=session-expired"]
|
assert result["replaced"] == ["/dashboard/login?reason=session-expired"]
|
||||||
|
|
@ -523,6 +525,7 @@ process.stdout.write(JSON.stringify(state));
|
||||||
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
||||||
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
||||||
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
||||||
|
"stackchain-today-action-mailbox-v1",
|
||||||
]
|
]
|
||||||
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
|
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
|
||||||
assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
|
assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
|
||||||
|
|
@ -547,6 +550,7 @@ process.stdout.write(JSON.stringify(state));
|
||||||
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
||||||
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
||||||
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
||||||
|
"stackchain-today-action-mailbox-v1",
|
||||||
]
|
]
|
||||||
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
|
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
|
||||||
assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
|
assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
|
||||||
|
|
@ -644,6 +648,7 @@ process.stdout.write(JSON.stringify(state));
|
||||||
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
"stackchain-background-outbox-v1", "stackchain-offline-work-v2",
|
||||||
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
"stackchain-unfiled-captures-v1", "stackchain-voice-transcripts-v1",
|
||||||
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
"stackchain-search-reply-drafts-v1", "stackchain-conversation-reply-drafts-v1",
|
||||||
|
"stackchain-today-action-mailbox-v1",
|
||||||
]
|
]
|
||||||
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
|
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
|
||||||
assert result["assigned"] == "/dashboard/login"
|
assert result["assigned"] == "/dashboard/login"
|
||||||
|
|
|
||||||
|
|
@ -435,5 +435,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
|
||||||
def test_later_sync_ships_atomically_in_the_offline_shell():
|
def test_later_sync_ships_atomically_in_the_offline_shell():
|
||||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/later-sync.js'" in source
|
assert "BASE + 'static/later-sync.js'" in source
|
||||||
|
|
|
||||||
|
|
@ -256,4 +256,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 { 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 pre { max-width:100%; overflow-x:auto;" in css
|
||||||
assert ".markdown-content a { min-height:44px;" in css
|
assert ".markdown-content a { min-height:44px;" in css
|
||||||
assert "stackchain-dashboard-shell-v125" in worker
|
assert "stackchain-dashboard-shell-v126" in worker
|
||||||
|
|
|
||||||
|
|
@ -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]))
|
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 local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
|
||||||
assert "stackchain-dashboard-shell-v125" in worker
|
assert "stackchain-dashboard-shell-v126" in worker
|
||||||
|
|
||||||
|
|
||||||
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
||||||
|
|
|
||||||
|
|
@ -383,7 +383,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
|
||||||
assert "controller.recoverPermission('deadline')" in dashboard
|
assert "controller.recoverPermission('deadline')" in dashboard
|
||||||
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
||||||
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
||||||
assert "stackchain-dashboard-shell-v125" in worker
|
assert "stackchain-dashboard-shell-v126" in worker
|
||||||
assert ".device-setup-panel" in css
|
assert ".device-setup-panel" in css
|
||||||
assert ".device-readiness-card" in css
|
assert ".device-readiness-card" in css
|
||||||
assert "overflow-x:hidden" in css
|
assert "overflow-x:hidden" in css
|
||||||
|
|
|
||||||
|
|
@ -243,5 +243,5 @@ async def test_mobile_home_progressively_discloses_secondary_panels_as_insights(
|
||||||
def test_mobile_insights_rolls_into_the_offline_shell():
|
def test_mobile_insights_rolls_into_the_offline_shell():
|
||||||
worker = (CONTROLLER.parent / "service-worker.js").read_text()
|
worker = (CONTROLLER.parent / "service-worker.js").read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in worker
|
assert "stackchain-dashboard-shell-v126" in worker
|
||||||
assert "BASE + 'static/mobile-insights.js'" in worker
|
assert "BASE + 'static/mobile-insights.js'" in worker
|
||||||
|
|
|
||||||
|
|
@ -358,7 +358,7 @@ async def test_dashboard_wires_thumb_safe_start_day_briefing_into_offline_mobile
|
||||||
assert ".mobile-start-day-finish { min-height:44px;" in html
|
assert ".mobile-start-day-finish { min-height:44px;" in html
|
||||||
assert "max-width:100%; overflow-wrap:anywhere;" in html
|
assert "max-width:100%; overflow-wrap:anywhere;" in html
|
||||||
assert "BASE + 'static/mobile-start-day.js'" in service_worker
|
assert "BASE + 'static/mobile-start-day.js'" in service_worker
|
||||||
assert "stackchain-dashboard-shell-v125" in service_worker
|
assert "stackchain-dashboard-shell-v126" in service_worker
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|
|
||||||
|
|
@ -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():
|
def test_plan_today_controller_is_available_in_the_offline_shell():
|
||||||
source = SERVICE_WORKER.read_text()
|
source = SERVICE_WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/plan-today.js'" in source
|
assert "BASE + 'static/plan-today.js'" in source
|
||||||
assert "BASE + 'static/plan-today-readiness.js'" in source
|
assert "BASE + 'static/plan-today-readiness.js'" in source
|
||||||
assert "BASE + 'static/plan-today-preview.js'" in source
|
assert "BASE + 'static/plan-today-preview.js'" in source
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ process.stdout.write(JSON.stringify(databases));
|
||||||
"stackchain-voice-transcripts-v1",
|
"stackchain-voice-transcripts-v1",
|
||||||
"stackchain-search-reply-drafts-v1",
|
"stackchain-search-reply-drafts-v1",
|
||||||
"stackchain-conversation-reply-drafts-v1",
|
"stackchain-conversation-reply-drafts-v1",
|
||||||
|
"stackchain-today-action-mailbox-v1",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ const clear = createPrivateDeviceDataPurger({{
|
||||||
"stackchain-voice-transcripts-v1",
|
"stackchain-voice-transcripts-v1",
|
||||||
"stackchain-search-reply-drafts-v1",
|
"stackchain-search-reply-drafts-v1",
|
||||||
"stackchain-conversation-reply-drafts-v1",
|
"stackchain-conversation-reply-drafts-v1",
|
||||||
|
"stackchain-today-action-mailbox-v1",
|
||||||
]
|
]
|
||||||
assert state["caches"] == ["stackchain-dashboard-shell-v37"]
|
assert state["caches"] == ["stackchain-dashboard-shell-v37"]
|
||||||
assert state["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
|
assert state["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ def run_worker_scenario(scenario: str) -> dict:
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const vm = require('vm');
|
const vm = require('vm');
|
||||||
const listeners = {{}};
|
const listeners = {{}};
|
||||||
const state = {{ added: [], addAttempts: [], individuallyAdded: [], failedAdds: [], deleted: [], deletedDatabases: [], claimed: false, skipped: false, fetches: [], puts: [], migrated: [], activationOrder: [], oldCachedAssets: {{}}, sharedRecords: {{}}, failSharedPut: false, backgroundFlushes: 0, backgroundResumes: 0, outboxPurges: 0, outboxLifecycle: [], notifications: [], closedNotifications: 0, focused: [], opened: [], failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }};
|
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: [], failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }};
|
||||||
const storedResponses = new Map();
|
const storedResponses = new Map();
|
||||||
storedResponses.set(
|
storedResponses.set(
|
||||||
'https://forge.example/dashboard/__offline-session-lease',
|
'https://forge.example/dashboard/__offline-session-lease',
|
||||||
|
|
@ -66,12 +66,24 @@ const context = {{
|
||||||
get: async id => state.sharedRecords[id] || null,
|
get: async id => state.sharedRecords[id] || null,
|
||||||
delete: async id => {{ delete state.sharedRecords[id]; }},
|
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 = {{}}; }},
|
||||||
|
}},
|
||||||
addEventListener: (name, handler) => {{ listeners[name] = handler; }},
|
addEventListener: (name, handler) => {{ listeners[name] = handler; }},
|
||||||
skipWaiting: async () => {{ state.skipped = true; }},
|
skipWaiting: async () => {{ state.skipped = true; }},
|
||||||
clients: {{
|
clients: {{
|
||||||
claim: async () => {{ state.claimed = true; }},
|
claim: async () => {{ state.claimed = true; }},
|
||||||
matchAll: async () => state.clientList || [],
|
matchAll: async () => state.clientList || [],
|
||||||
openWindow: async url => {{ state.opened.push(url); }},
|
openWindow: async url => {{ state.opened.push(url); return state.openedClient || {{id:'opened-client',url}}; }},
|
||||||
}},
|
}},
|
||||||
registration: {{
|
registration: {{
|
||||||
showNotification: async (title, options) => state.notifications.push({{title,options}}),
|
showNotification: async (title, options) => state.notifications.push({{title,options}}),
|
||||||
|
|
@ -133,9 +145,9 @@ async function dispatchSync(tag) {{
|
||||||
listeners.sync({{ tag, waitUntil: promise => {{ pending = promise; }} }});
|
listeners.sync({{ tag, waitUntil: promise => {{ pending = promise; }} }});
|
||||||
if (pending) await pending;
|
if (pending) await pending;
|
||||||
}}
|
}}
|
||||||
async function dispatchMessage(data, ports = []) {{
|
async function dispatchMessage(data, ports = [], source = null) {{
|
||||||
let pending;
|
let pending;
|
||||||
listeners.message({{ data, ports, waitUntil: promise => {{ pending = promise; }} }});
|
listeners.message({{ data, ports, source, waitUntil: promise => {{ pending = promise; }} }});
|
||||||
if (pending) await pending;
|
if (pending) await pending;
|
||||||
}}
|
}}
|
||||||
async function dispatchNotificationClick(route, action = '', notificationId = null, tag = null, url = null, actionToken = '') {{
|
async function dispatchNotificationClick(route, action = '', notificationId = null, tag = null, url = null, actionToken = '') {{
|
||||||
|
|
@ -165,16 +177,16 @@ async function dispatchPush(payload) {{
|
||||||
return json.loads(completed.stdout)
|
return json.loads(completed.stdout)
|
||||||
|
|
||||||
|
|
||||||
def test_offline_activation_migration_rolls_the_shell_cache():
|
def test_private_today_action_mailbox_rolls_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
|
|
||||||
|
|
||||||
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/my-work.js'" in source
|
assert "BASE + 'static/my-work.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
assert "BASE + 'static/dashboard.css'" in source
|
assert "BASE + 'static/dashboard.css'" in source
|
||||||
|
|
@ -183,7 +195,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||||
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
assert "BASE + 'static/authored-outbox.js'" in source
|
assert "BASE + 'static/authored-outbox.js'" in source
|
||||||
assert "BASE + 'static/background-issue-sync.js'" in source
|
assert "BASE + 'static/background-issue-sync.js'" in source
|
||||||
|
|
@ -192,7 +204,7 @@ def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
||||||
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/issue-evidence-review.js'" in source
|
assert "BASE + 'static/issue-evidence-review.js'" in source
|
||||||
assert "BASE + 'static/issue-attachment.js'" in source
|
assert "BASE + 'static/issue-attachment.js'" in source
|
||||||
|
|
||||||
|
|
@ -200,14 +212,14 @@ def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
||||||
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_offline_review_next_ships_today_completion_atomically():
|
def test_offline_review_next_ships_today_completion_atomically():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/today-completion.js'" in source
|
assert "BASE + 'static/today-completion.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
||||||
|
|
@ -215,7 +227,7 @@ def test_offline_review_next_ships_today_completion_atomically():
|
||||||
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
||||||
|
|
@ -223,7 +235,7 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||||
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/issue-sheet.js'" in source
|
assert "BASE + 'static/issue-sheet.js'" in source
|
||||||
assert "BASE + 'static/checklist-conflict.js'" in source
|
assert "BASE + 'static/checklist-conflict.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
@ -233,14 +245,14 @@ def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
||||||
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/later-picker.js'" in source
|
assert "BASE + 'static/later-picker.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/dashboard.css'" in source
|
assert "BASE + 'static/dashboard.css'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
assert "BASE + 'static/install-app.js'" in source
|
assert "BASE + 'static/install-app.js'" in source
|
||||||
|
|
@ -249,21 +261,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||||
def test_today_convergence_ships_in_a_new_shell_cache():
|
def test_today_convergence_ships_in_a_new_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/today-sync.js'" in source
|
assert "BASE + 'static/today-sync.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/update-ownership.js'" in source
|
assert "BASE + 'static/update-ownership.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -451,6 +463,7 @@ def test_revoked_background_session_purges_worker_data_and_notifies_dashboard_cl
|
||||||
"stackchain-voice-transcripts-v1",
|
"stackchain-voice-transcripts-v1",
|
||||||
"stackchain-search-reply-drafts-v1",
|
"stackchain-search-reply-drafts-v1",
|
||||||
"stackchain-conversation-reply-drafts-v1",
|
"stackchain-conversation-reply-drafts-v1",
|
||||||
|
"stackchain-today-action-mailbox-v1",
|
||||||
]
|
]
|
||||||
assert result["state"]["deleted"] == ["stackchain-dashboard-old"]
|
assert result["state"]["deleted"] == ["stackchain-dashboard-old"]
|
||||||
assert result["state"]["clientMessages"] == [
|
assert result["state"]["clientMessages"] == [
|
||||||
|
|
@ -610,17 +623,81 @@ def test_today_lock_screen_action_updates_an_open_dashboard_without_navigation()
|
||||||
assert result["opened"] == []
|
assert result["opened"] == []
|
||||||
|
|
||||||
|
|
||||||
def test_today_lock_screen_action_opens_a_validated_one_shot_route_when_closed():
|
def test_today_lock_screen_action_opens_clean_route_and_privately_hands_off_once_when_closed():
|
||||||
result = run_worker_scenario(
|
result = run_worker_scenario(
|
||||||
"""
|
"""
|
||||||
await dispatchNotificationClick('#/my-work/today','resume-today',null,'stackchain-today-session');
|
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));
|
process.stdout.write(JSON.stringify(state));
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["opened"] == [
|
assert result["opened"] == [
|
||||||
"https://forge.example/dashboard/?today_timer_action=resume#/my-work/today"
|
"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():
|
def test_finish_today_lock_screen_action_forwards_only_its_opaque_token():
|
||||||
|
|
@ -707,6 +784,7 @@ def test_device_purge_message_stops_worker_outbox_and_acknowledges_completion():
|
||||||
"stackchain-voice-transcripts-v1",
|
"stackchain-voice-transcripts-v1",
|
||||||
"stackchain-search-reply-drafts-v1",
|
"stackchain-search-reply-drafts-v1",
|
||||||
"stackchain-conversation-reply-drafts-v1",
|
"stackchain-conversation-reply-drafts-v1",
|
||||||
|
"stackchain-today-action-mailbox-v1",
|
||||||
]
|
]
|
||||||
assert result["replies"] == [{"ok": True}]
|
assert result["replies"] == [{"ok": True}]
|
||||||
|
|
||||||
|
|
@ -1167,7 +1245,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():
|
def test_queue_today_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/queue-today.js'" in source
|
assert "BASE + 'static/queue-today.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1465,6 +1543,7 @@ def test_expired_offline_lease_purges_private_worker_data_and_refuses_cached_she
|
||||||
"stackchain-voice-transcripts-v1",
|
"stackchain-voice-transcripts-v1",
|
||||||
"stackchain-search-reply-drafts-v1",
|
"stackchain-search-reply-drafts-v1",
|
||||||
"stackchain-conversation-reply-drafts-v1",
|
"stackchain-conversation-reply-drafts-v1",
|
||||||
|
"stackchain-today-action-mailbox-v1",
|
||||||
]
|
]
|
||||||
assert result["state"]["deleted"] == ["stackchain-dashboard-old"]
|
assert result["state"]["deleted"] == ["stackchain-dashboard-old"]
|
||||||
assert "private cached dashboard" not in result["body"]
|
assert "private cached dashboard" not in result["body"]
|
||||||
|
|
|
||||||
|
|
@ -74,21 +74,19 @@ def test_disabling_or_ending_session_removes_notification():
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_valid_notification_action_is_consumed_once_and_removed_from_url():
|
def test_cold_launch_requests_one_private_action_claim_without_url_capabilities():
|
||||||
result = run_scenario(r"""
|
result = run_scenario(r"""
|
||||||
values.set('stackchain.today-lock-screen.v1.timmy','1');
|
values.set('stackchain.today-lock-screen.v1.timmy','1');
|
||||||
locationRef.href='https://forge.example/dashboard/?today_timer_action=pause#/my-work/today';
|
|
||||||
const consumed = await lockScreen.consumeLaunchAction();
|
const consumed = await lockScreen.consumeLaunchAction();
|
||||||
const second = await lockScreen.consumeLaunchAction();
|
|
||||||
listeners['sw-message']({data:{type:'stackchain-today-timer-action',action:'resume'}});
|
listeners['sw-message']({data:{type:'stackchain-today-timer-action',action:'resume'}});
|
||||||
process.stdout.write(JSON.stringify({consumed,second,actions,href:locationRef.href}));
|
process.stdout.write(JSON.stringify({consumed,actions,href:locationRef.href,messages}));
|
||||||
""")
|
""")
|
||||||
|
|
||||||
assert result == {
|
assert result == {
|
||||||
"consumed": True,
|
"consumed": True,
|
||||||
"second": False,
|
"actions": [["resume", None]],
|
||||||
"actions": [["pause", None], ["resume", None]],
|
|
||||||
"href": "https://forge.example/dashboard/#/my-work/today",
|
"href": "https://forge.example/dashboard/#/my-work/today",
|
||||||
|
"messages": [{"type": "stackchain-claim-today-action"}],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -133,20 +131,18 @@ def test_break_sync_carries_deadline_and_resume_is_bound_to_that_exact_break():
|
||||||
assert "private/repo" not in json.dumps(result["messages"])
|
assert "private/repo" not in json.dumps(result["messages"])
|
||||||
|
|
||||||
|
|
||||||
def test_cold_launch_finish_action_is_removed_from_history_before_completion():
|
def test_private_cold_launch_finish_action_is_bound_to_the_active_identity():
|
||||||
result = run_scenario(r"""
|
result = run_scenario(r"""
|
||||||
await lockScreen.enable();
|
await lockScreen.enable();
|
||||||
await lockScreen.sync({identity:'issue:private/repo:42:',running:true}, true);
|
await lockScreen.sync({identity:'issue:private/repo:42:',running:true}, true);
|
||||||
locationRef.href='https://forge.example/dashboard/?today_timer_action=complete&today_action_token=opaque-token-1234567890#/my-work/today';
|
await lockScreen.consumeLaunchAction();
|
||||||
const consumed = await lockScreen.consumeLaunchAction();
|
await listeners['sw-message']({data:{type:'stackchain-today-timer-action',action:'complete',actionToken:'opaque-token-1234567890'}});
|
||||||
process.stdout.write(JSON.stringify({consumed,actions,href:locationRef.href}));
|
process.stdout.write(JSON.stringify({actions,href:locationRef.href,messages}));
|
||||||
""")
|
""")
|
||||||
|
|
||||||
assert result == {
|
assert result["actions"] == [["complete", "issue:private/repo:42:"]]
|
||||||
"consumed": True,
|
assert result["href"] == "https://forge.example/dashboard/#/my-work/today"
|
||||||
"actions": [["complete", "issue:private/repo:42:"]],
|
assert result["messages"][-1] == {"type": "stackchain-claim-today-action"}
|
||||||
"href": "https://forge.example/dashboard/#/my-work/today",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_permission_denial_is_truthful_and_does_not_persist_opt_in():
|
def test_permission_denial_is_truthful_and_does_not_persist_opt_in():
|
||||||
|
|
|
||||||
|
|
@ -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():
|
def test_readiness_runtime_is_available_in_offline_shell():
|
||||||
service_worker = SERVICE_WORKER.read_text()
|
service_worker = SERVICE_WORKER.read_text()
|
||||||
|
|
||||||
assert "const CACHE = 'stackchain-dashboard-shell-v125';" in service_worker
|
assert "const CACHE = 'stackchain-dashboard-shell-v126';" in service_worker
|
||||||
assert "BASE + 'static/today-readiness.js'" in service_worker
|
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,7 @@ listeners['stackchain:first-task-complete']();
|
||||||
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
||||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v125" in source
|
assert "stackchain-dashboard-shell-v126" in source
|
||||||
assert "BASE + 'static/today-sync.js'" in source
|
assert "BASE + 'static/today-sync.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user