Merge pull request 'Share one cold-launch live snapshot across progressive Human Gates and My Work' (#1426) from timmy/1425-shared-progressive-live-snapshot into main
This commit is contained in:
commit
edd653eaaf
|
|
@ -2323,6 +2323,7 @@
|
||||||
<script src="static/offline-work.js"></script>
|
<script src="static/offline-work.js"></script>
|
||||||
<script src="static/offline-today.js"></script>
|
<script src="static/offline-today.js"></script>
|
||||||
<script src="static/my-work.js"></script>
|
<script src="static/my-work.js"></script>
|
||||||
|
<script src="static/progressive-live-snapshot.js"></script>
|
||||||
<script src="static/progressive-my-work.js"></script>
|
<script src="static/progressive-my-work.js"></script>
|
||||||
<script src="static/progressive-capture.js"></script>
|
<script src="static/progressive-capture.js"></script>
|
||||||
<script src="static/agenda-replan.js"></script>
|
<script src="static/agenda-replan.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@ function createProgressiveHumanGates(options = {}) {
|
||||||
if (!response.ok) throw new Error(payload.error || payload.detail?.message || payload.detail || 'Review request failed.');
|
if (!response.ok) throw new Error(payload.error || payload.detail?.message || payload.detail || 'Review request failed.');
|
||||||
return payload;
|
return payload;
|
||||||
});
|
});
|
||||||
|
const liveSnapshot = options.liveSnapshot || null;
|
||||||
const getIdentity = options.getIdentity || (async () => {
|
const getIdentity = options.getIdentity || (async () => {
|
||||||
const response = await fetchJson('api/v1/live', {headers:{Accept:'application/json'}});
|
const response = liveSnapshot ? await liveSnapshot.acquire() :
|
||||||
|
await fetchJson('api/v1/live', {headers:{Accept:'application/json'}});
|
||||||
const user = response?.context?.user || {};
|
const user = response?.context?.user || {};
|
||||||
const login = String(user.login || '').trim();
|
const login = String(user.login || '').trim();
|
||||||
return {login, accountKey:login && user.id ? String(user.id) + ':' + login : login};
|
return {login, accountKey:login && user.id ? String(user.id) + ':' + login : login};
|
||||||
|
|
@ -91,7 +93,9 @@ function createProgressiveHumanGates(options = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||||
window.stackchainProgressiveHumanGates = createProgressiveHumanGates({document});
|
window.stackchainProgressiveHumanGates = createProgressiveHumanGates({
|
||||||
|
document, liveSnapshot:window.stackchainProgressiveLiveSnapshot,
|
||||||
|
});
|
||||||
void window.stackchainProgressiveHumanGates.start().catch(() => {});
|
void window.stackchainProgressiveHumanGates.start().catch(() => {});
|
||||||
}
|
}
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
|
|
||||||
40
frontend/progressive-live-snapshot.js
Normal file
40
frontend/progressive-live-snapshot.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
function createProgressiveLiveSnapshot({fetchSnapshot}) {
|
||||||
|
let value = null;
|
||||||
|
let flight = null;
|
||||||
|
|
||||||
|
const acquire = () => {
|
||||||
|
if (value) return Promise.resolve(value);
|
||||||
|
if (flight) return flight;
|
||||||
|
flight = Promise.resolve().then(() => fetchSnapshot()).then(snapshot => {
|
||||||
|
value = snapshot;
|
||||||
|
flight = null;
|
||||||
|
return snapshot;
|
||||||
|
}, error => {
|
||||||
|
flight = null;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
return flight;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
acquire,
|
||||||
|
snapshot:() => value,
|
||||||
|
pending:() => flight,
|
||||||
|
identity() {
|
||||||
|
const user = value?.context?.user || {};
|
||||||
|
const login = String(user.login || '').trim();
|
||||||
|
return {login, accountKey:login && user.id ? String(user.id) + ':' + login : login};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.stackchainProgressiveLiveSnapshot = createProgressiveLiveSnapshot({
|
||||||
|
fetchSnapshot:async () => {
|
||||||
|
const response = await fetch('api/v1/live', {headers:{Accept:'application/json'}});
|
||||||
|
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (typeof module !== 'undefined' && module.exports) module.exports = createProgressiveLiveSnapshot;
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
function createProgressiveMyWork({
|
function createProgressiveMyWork({
|
||||||
document, fetchSnapshot, pollerOptions = {}, lifecycleTarget = globalThis,
|
document, fetchSnapshot, liveSnapshot: snapshotBroker = null,
|
||||||
|
pollerOptions = {}, lifecycleTarget = globalThis,
|
||||||
}) {
|
}) {
|
||||||
const list = document.querySelector('#my-work-list');
|
const list = document.querySelector('#my-work-list');
|
||||||
const status = document.querySelector('#my-work-status');
|
const status = document.querySelector('#my-work-status');
|
||||||
|
|
@ -25,6 +26,9 @@ function createProgressiveMyWork({
|
||||||
const deferredQueues = {
|
const deferredQueues = {
|
||||||
today:'Today', agenda:'Agenda', later:'Later', draft:'Drafts',
|
today:'Today', agenda:'Agenda', later:'Later', draft:'Drafts',
|
||||||
};
|
};
|
||||||
|
const fetchProgressiveSnapshot = (revisions = {}, options = {}) =>
|
||||||
|
snapshotBroker && Object.keys(revisions || {}).length === 0 ?
|
||||||
|
snapshotBroker.acquire() : fetchSnapshot(revisions, options);
|
||||||
|
|
||||||
const escapeHtml = value => String(value || '').replace(/[&<>"']/g, character => ({
|
const escapeHtml = value => String(value || '').replace(/[&<>"']/g, character => ({
|
||||||
'&':'&', '<':'<', '>':'>', '"':'"', "'":''',
|
'&':'&', '<':'<', '>':'>', '"':'"', "'":''',
|
||||||
|
|
@ -128,7 +132,7 @@ function createProgressiveMyWork({
|
||||||
if (typeof createContextPoller === 'function') {
|
if (typeof createContextPoller === 'function') {
|
||||||
poller = createContextPoller({
|
poller = createContextPoller({
|
||||||
...pollerOptions,
|
...pollerOptions,
|
||||||
fetchContext: fetchSnapshot,
|
fetchContext: fetchProgressiveSnapshot,
|
||||||
onSnapshot: applySnapshot,
|
onSnapshot: applySnapshot,
|
||||||
onError: () => {
|
onError: () => {
|
||||||
if (status && !stopped) status.textContent = 'Assigned work is reconnecting…';
|
if (status && !stopped) status.textContent = 'Assigned work is reconnecting…';
|
||||||
|
|
@ -175,7 +179,7 @@ function createProgressiveMyWork({
|
||||||
));
|
));
|
||||||
return Boolean(await request);
|
return Boolean(await request);
|
||||||
}
|
}
|
||||||
const request = Promise.resolve().then(() => fetchSnapshot());
|
const request = Promise.resolve().then(() => fetchProgressiveSnapshot());
|
||||||
liveSnapshotPromise = request.then(snapshot => (
|
liveSnapshotPromise = request.then(snapshot => (
|
||||||
snapshot && typeof snapshot === 'object' &&
|
snapshot && typeof snapshot === 'object' &&
|
||||||
Object.prototype.hasOwnProperty.call(snapshot, 'context') ? snapshot : null
|
Object.prototype.hasOwnProperty.call(snapshot, 'context') ? snapshot : null
|
||||||
|
|
@ -208,6 +212,7 @@ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||||
window.stackchainProgressiveMyWork = createProgressiveMyWork({
|
window.stackchainProgressiveMyWork = createProgressiveMyWork({
|
||||||
document,
|
document,
|
||||||
lifecycleTarget: window,
|
lifecycleTarget: window,
|
||||||
|
liveSnapshot:window.stackchainProgressiveLiveSnapshot,
|
||||||
fetchSnapshot: async (revisions = {}, { signal } = {}) => {
|
fetchSnapshot: async (revisions = {}, { signal } = {}) => {
|
||||||
const query = createContextPoller.buildRevisionQuery(revisions);
|
const query = createContextPoller.buildRevisionQuery(revisions);
|
||||||
const response = await fetch('api/v1/live' + (query ? '?' + query : ''), {
|
const response = await fetch('api/v1/live' + (query ? '?' + query : ''), {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
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-v141';
|
const CACHE = 'stackchain-dashboard-shell-v142';
|
||||||
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;
|
||||||
|
|
@ -188,6 +188,7 @@ const SHELL = [
|
||||||
BASE + 'static/offline-work.js',
|
BASE + 'static/offline-work.js',
|
||||||
BASE + 'static/offline-today.js',
|
BASE + 'static/offline-today.js',
|
||||||
BASE + 'static/my-work.js',
|
BASE + 'static/my-work.js',
|
||||||
|
BASE + 'static/progressive-live-snapshot.js',
|
||||||
BASE + 'static/progressive-my-work.js',
|
BASE + 'static/progressive-my-work.js',
|
||||||
BASE + 'static/progressive-capture.js',
|
BASE + 'static/progressive-capture.js',
|
||||||
BASE + 'static/agenda-replan.js',
|
BASE + 'static/agenda-replan.js',
|
||||||
|
|
|
||||||
|
|
@ -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-v141" in worker
|
assert "stackchain-dashboard-shell-v142" in worker
|
||||||
|
|
|
||||||
|
|
@ -593,7 +593,7 @@ process.stdout.write(JSON.stringify({{
|
||||||
assert ".following-disposition-mode" in css
|
assert ".following-disposition-mode" in css
|
||||||
assert "if (searchPreviewReturnKind === 'following')" in dashboard
|
assert "if (searchPreviewReturnKind === 'following')" in dashboard
|
||||||
assert "e.key === 'Escape' && searchPreviewReturnKind === 'following'" in dashboard
|
assert "e.key === 'Escape' && searchPreviewReturnKind === 'following'" in dashboard
|
||||||
assert "stackchain-dashboard-shell-v141" in service_worker
|
assert "stackchain-dashboard-shell-v142" in service_worker
|
||||||
|
|
||||||
|
|
||||||
def test_prepare_today_lazily_refreshes_and_directly_reviews_following():
|
def test_prepare_today_lazily_refreshes_and_directly_reviews_following():
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ PROGRESSIVE = Path(__file__).parents[1] / "frontend" / "progressive-human-gates.
|
||||||
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
|
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
|
||||||
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
||||||
WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js"
|
WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js"
|
||||||
|
LIVE_SNAPSHOT = Path(__file__).parents[1] / "frontend" / "progressive-live-snapshot.js"
|
||||||
|
PROGRESSIVE_MY_WORK = Path(__file__).parents[1] / "frontend" / "progressive-my-work.js"
|
||||||
|
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
|
||||||
|
|
||||||
|
|
||||||
def run_node(body):
|
def run_node(body):
|
||||||
|
|
@ -16,6 +19,63 @@ def run_node(body):
|
||||||
return json.loads(result.stdout)
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def test_progressive_my_work_and_human_gates_share_one_cold_live_snapshot():
|
||||||
|
script = f"""
|
||||||
|
const createProgressiveLiveSnapshot=require({json.dumps(str(LIVE_SNAPSHOT))});
|
||||||
|
globalThis.buildMyWork=require({json.dumps(str(MY_WORK))});
|
||||||
|
const createProgressiveMyWork=require({json.dumps(str(PROGRESSIVE_MY_WORK))});
|
||||||
|
const createProgressiveHumanGates=require({json.dumps(str(PROGRESSIVE))});
|
||||||
|
let liveCalls=0, directMyWorkCalls=0, resolveLive;
|
||||||
|
const broker=createProgressiveLiveSnapshot({{fetchSnapshot:()=>{{
|
||||||
|
liveCalls += 1;
|
||||||
|
return new Promise(resolve=>{{resolveLive=resolve;}});
|
||||||
|
}}}});
|
||||||
|
const workDocument={{
|
||||||
|
hidden:false,
|
||||||
|
querySelector:selector=>selector==='#my-work-list'?{{innerHTML:''}}:selector==='#my-work-status'?{{textContent:''}}:null,
|
||||||
|
querySelectorAll:()=>[],
|
||||||
|
}};
|
||||||
|
const work=createProgressiveMyWork({{
|
||||||
|
document:workDocument, liveSnapshot:broker,
|
||||||
|
fetchSnapshot:async()=>{{directMyWorkCalls += 1; return {{}};}},
|
||||||
|
}});
|
||||||
|
const nodes={{
|
||||||
|
'#human-gates-count':{{}}, '#human-gates-list':{{innerHTML:'',addEventListener(){{}}}},
|
||||||
|
'#human-gates-status':{{}}, '#human-gates':{{hidden:true}},
|
||||||
|
'#human-gate-detail':{{innerHTML:'',addEventListener(){{}},querySelectorAll:()=>[]}},
|
||||||
|
'#open-human-gates':{{addEventListener(){{}}}}, '#close-human-gates':{{addEventListener(){{}}}},
|
||||||
|
}};
|
||||||
|
const gates=createProgressiveHumanGates({{
|
||||||
|
document:{{querySelector:selector=>nodes[selector]||null}},
|
||||||
|
location:{{hash:'#/my-work/human-gates'}}, history:{{replaceState(){{}}}},
|
||||||
|
storage:{{getItem:()=>null,setItem(){{}}}}, isOnline:()=>true,
|
||||||
|
liveSnapshot:broker,
|
||||||
|
fetchJson:async path=>({{pending_count:1,items:[{{id:'g1',title:'Shared identity',candidate_hash:'abc',revision:1,checks:[]}}]}}),
|
||||||
|
}});
|
||||||
|
(async()=>{{
|
||||||
|
const workStart=work.start(); const gateStart=gates.start();
|
||||||
|
await Promise.resolve(); await Promise.resolve();
|
||||||
|
const callsWhilePending=liveCalls;
|
||||||
|
resolveLive({{context:{{user:{{id:7,login:'timmy'}},issues:[],pull_requests:[]}},events:[],notifications:[]}});
|
||||||
|
await Promise.all([workStart,gateStart]);
|
||||||
|
process.stdout.write(JSON.stringify({{
|
||||||
|
callsWhilePending,liveCalls,directMyWorkCalls,
|
||||||
|
login:work.login(),gateStarted:gates.handoff().started,
|
||||||
|
}}));
|
||||||
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"callsWhilePending": 1,
|
||||||
|
"liveCalls": 1,
|
||||||
|
"directMyWorkCalls": 0,
|
||||||
|
"login": "timmy",
|
||||||
|
"gateStarted": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_queue_loads_pending_count_uses_account_cache_and_renders_inbox_zero():
|
def test_queue_loads_pending_count_uses_account_cache_and_renders_inbox_zero():
|
||||||
output = run_node(r"""
|
output = run_node(r"""
|
||||||
const values=new Map(); const storage={getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)};
|
const values=new Map(); const storage={getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)};
|
||||||
|
|
@ -202,7 +262,7 @@ def test_human_gate_mobile_shell_and_deep_route_are_wired():
|
||||||
assert "mobileStartDay.reconcile({authoritative:true, authoritativePhases:['gate']})" in dashboard
|
assert "mobileStartDay.reconcile({authoritative:true, authoritativePhases:['gate']})" in dashboard
|
||||||
assert "counts.gate = queueCounts.gate" in dashboard
|
assert "counts.gate = queueCounts.gate" in dashboard
|
||||||
assert "gate:preparationItems.gate || []" in dashboard
|
assert "gate:preparationItems.gate || []" in dashboard
|
||||||
assert "stackchain-dashboard-shell-v141" in WORKER.read_text()
|
assert "stackchain-dashboard-shell-v142" in WORKER.read_text()
|
||||||
|
|
||||||
|
|
||||||
def test_deep_link_opens_human_gates_without_waiting_for_optional_workspace():
|
def test_deep_link_opens_human_gates_without_waiting_for_optional_workspace():
|
||||||
|
|
|
||||||
|
|
@ -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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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-v141" in worker
|
assert "stackchain-dashboard-shell-v142" 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-v141" in worker
|
assert "stackchain-dashboard-shell-v142" 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-v141" in worker
|
assert "stackchain-dashboard-shell-v142" 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
|
||||||
|
|
|
||||||
|
|
@ -274,5 +274,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-v141" in worker
|
assert "stackchain-dashboard-shell-v142" in worker
|
||||||
assert "BASE + 'static/mobile-insights.js'" in worker
|
assert "BASE + 'static/mobile-insights.js'" in worker
|
||||||
|
|
|
||||||
|
|
@ -469,7 +469,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-v141" in service_worker
|
assert "stackchain-dashboard-shell-v142" in service_worker
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|
|
||||||
|
|
@ -418,7 +418,7 @@ async def test_starting_saved_today_work_closes_a_concurrent_rollover_planner():
|
||||||
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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,80 @@ from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
MODULE = Path(__file__).parents[1] / "frontend" / "progressive-my-work.js"
|
MODULE = Path(__file__).parents[1] / "frontend" / "progressive-my-work.js"
|
||||||
|
LIVE_SNAPSHOT = Path(__file__).parents[1] / "frontend" / "progressive-live-snapshot.js"
|
||||||
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
|
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
|
||||||
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
||||||
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
|
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
|
||||||
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
|
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
|
||||||
|
|
||||||
|
|
||||||
|
def test_progressive_live_snapshot_coalesces_concurrent_cold_launch_consumers():
|
||||||
|
harness = f"""
|
||||||
|
const createProgressiveLiveSnapshot=require({json.dumps(str(LIVE_SNAPSHOT))});
|
||||||
|
let calls=0, resolveRequest;
|
||||||
|
const broker=createProgressiveLiveSnapshot({{fetchSnapshot:()=>{{
|
||||||
|
calls += 1;
|
||||||
|
return new Promise(resolve=>{{resolveRequest=resolve;}});
|
||||||
|
}}}});
|
||||||
|
(async()=>{{
|
||||||
|
const myWork=broker.acquire();
|
||||||
|
const humanGates=broker.acquire();
|
||||||
|
await Promise.resolve();
|
||||||
|
const callsWhilePending=calls;
|
||||||
|
const snapshot={{context:{{user:{{id:7,login:'timmy'}}}},events:[],notifications:[]}};
|
||||||
|
resolveRequest(snapshot);
|
||||||
|
const [first,second]=await Promise.all([myWork,humanGates]);
|
||||||
|
const cached=await broker.acquire();
|
||||||
|
process.stdout.write(JSON.stringify({{
|
||||||
|
callsWhilePending,calls,same:first===second && second===cached,
|
||||||
|
identity:broker.identity(),snapshot:broker.snapshot(),
|
||||||
|
}}));
|
||||||
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", harness], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"callsWhilePending": 1,
|
||||||
|
"calls": 1,
|
||||||
|
"same": True,
|
||||||
|
"identity": {"login": "timmy", "accountKey": "7:timmy"},
|
||||||
|
"snapshot": {
|
||||||
|
"context": {"user": {"id": 7, "login": "timmy"}},
|
||||||
|
"events": [],
|
||||||
|
"notifications": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_progressive_live_snapshot_retries_after_a_failed_shared_flight():
|
||||||
|
harness = f"""
|
||||||
|
const createProgressiveLiveSnapshot=require({json.dumps(str(LIVE_SNAPSHOT))});
|
||||||
|
let calls=0;
|
||||||
|
const snapshot={{context:{{user:{{login:'timmy'}}}},events:[],notifications:[]}};
|
||||||
|
const broker=createProgressiveLiveSnapshot({{fetchSnapshot:async()=>{{
|
||||||
|
calls += 1;
|
||||||
|
if(calls===1) throw new Error('temporary outage');
|
||||||
|
return snapshot;
|
||||||
|
}}}});
|
||||||
|
(async()=>{{
|
||||||
|
const first=await Promise.allSettled([broker.acquire(),broker.acquire()]);
|
||||||
|
const recovered=await broker.acquire();
|
||||||
|
process.stdout.write(JSON.stringify({{
|
||||||
|
calls,first:first.map(result=>result.status),recovered:recovered===snapshot,
|
||||||
|
}}));
|
||||||
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", harness], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"calls": 2,
|
||||||
|
"first": ["rejected", "rejected"],
|
||||||
|
"recovered": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_progressive_my_work_renders_and_filters_assigned_work_before_full_workspace():
|
def test_progressive_my_work_renders_and_filters_assigned_work_before_full_workspace():
|
||||||
harness = f"""
|
harness = f"""
|
||||||
const fs=require('fs'); const vm=require('vm');
|
const fs=require('fs'); const vm=require('vm');
|
||||||
|
|
|
||||||
|
|
@ -186,10 +186,17 @@ async function dispatchPush(payload) {{
|
||||||
return json.loads(completed.stdout)
|
return json.loads(completed.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def test_shared_progressive_snapshot_broker_rolls_the_offline_shell():
|
||||||
|
source = WORKER.read_text()
|
||||||
|
|
||||||
|
assert "stackchain-dashboard-shell-v142" in source
|
||||||
|
assert "BASE + 'static/progressive-live-snapshot.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_week_unplan_undo_rolls_the_offline_shell():
|
def test_week_unplan_undo_rolls_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v141" in source
|
assert "stackchain-dashboard-shell-v142" in source
|
||||||
assert "BASE + 'static/week-plan.js'" in source
|
assert "BASE + 'static/week-plan.js'" in source
|
||||||
assert "BASE + 'static/dashboard.css'" in source
|
assert "BASE + 'static/dashboard.css'" in source
|
||||||
|
|
||||||
|
|
@ -197,20 +204,20 @@ def test_week_unplan_undo_rolls_the_offline_shell():
|
||||||
def test_private_today_action_mailbox_rolls_the_offline_shell():
|
def test_private_today_action_mailbox_rolls_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v141" in source
|
assert "stackchain-dashboard-shell-v142" in source
|
||||||
|
|
||||||
|
|
||||||
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
|
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v141" in source
|
assert "stackchain-dashboard-shell-v142" in source
|
||||||
assert "BASE + 'static/week-plan.js'" in source
|
assert "BASE + 'static/week-plan.js'" 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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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
|
||||||
|
|
@ -219,7 +226,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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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
|
||||||
|
|
@ -228,7 +235,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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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
|
||||||
|
|
||||||
|
|
@ -236,14 +243,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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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
|
||||||
|
|
||||||
|
|
@ -251,7 +258,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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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
|
||||||
|
|
||||||
|
|
@ -259,7 +266,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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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
|
||||||
|
|
@ -269,14 +276,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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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
|
||||||
|
|
@ -285,21 +292,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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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-v141" in source
|
assert "stackchain-dashboard-shell-v142" in source
|
||||||
assert "BASE + 'static/update-ownership.js'" in source
|
assert "BASE + 'static/update-ownership.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1354,7 +1361,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-v141" in source
|
assert "stackchain-dashboard-shell-v142" in source
|
||||||
assert "BASE + 'static/queue-today.js'" in source
|
assert "BASE + 'static/queue-today.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1410,6 +1417,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/offline-work.js",
|
"/dashboard/static/offline-work.js",
|
||||||
"/dashboard/static/offline-today.js",
|
"/dashboard/static/offline-today.js",
|
||||||
"/dashboard/static/my-work.js",
|
"/dashboard/static/my-work.js",
|
||||||
|
"/dashboard/static/progressive-live-snapshot.js",
|
||||||
"/dashboard/static/progressive-my-work.js",
|
"/dashboard/static/progressive-my-work.js",
|
||||||
"/dashboard/static/progressive-capture.js",
|
"/dashboard/static/progressive-capture.js",
|
||||||
"/dashboard/static/agenda-replan.js",
|
"/dashboard/static/agenda-replan.js",
|
||||||
|
|
|
||||||
|
|
@ -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-v141';" in service_worker
|
assert "const CACHE = 'stackchain-dashboard-shell-v142';" 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-v141" in source
|
assert "stackchain-dashboard-shell-v142" 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