parent
718f0d1209
commit
5ec4dd7f6e
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
Human Gates is an account-bound release-candidate inbox. The canonical mobile route is `#/my-work/human-gates`. Reads may use the last account-scoped browser cache, but Release/Hold decisions require a live authenticated identity and an online server round trip.
|
||||
|
||||
A cold open of the canonical route confirms the account and opens Human Gates from the core browser runtime while optional Today and Planning bundles continue hydrating or recovering. The full workspace adopts that controller and fixed review snapshot without a second queue request or duplicate decision handlers.
|
||||
|
||||
## Producer intake
|
||||
|
||||
Authenticated producers submit `POST /api/v1/human-gates/intake` with a unique `Idempotency-Key` header and JSON such as:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
await workspaceLifecycle.optionalReady;
|
||||
const progressiveCaptureHandoff = window.stackchainProgressiveCapture?.handoff?.();
|
||||
const progressiveWorkHandoff = window.stackchainProgressiveMyWork?.handoff?.();
|
||||
const progressiveHumanGatesHandoff = window.stackchainProgressiveHumanGates?.handoff?.();
|
||||
window.stackchainProgressiveMyWork?.stop();
|
||||
const qs = (s, el=document) => el.querySelector(s);
|
||||
const announceWork = message => qs('#my-work-action-status').textContent = message;
|
||||
|
|
@ -636,30 +637,34 @@
|
|||
return payload;
|
||||
}
|
||||
|
||||
const humanGates = createHumanGates({
|
||||
storage:localStorage,
|
||||
getLogin:()=>planningOwnerLogin,
|
||||
getAccountKey:()=>planningOwnerAccountKey,
|
||||
isOnline:()=>navigator.onLine,
|
||||
location:window.location,
|
||||
fetchJson:fetchReviewJson,
|
||||
onChange:(snapshot, state)=>{
|
||||
const humanGatesOnChange = (snapshot, state)=>{
|
||||
queueCounts.gate = snapshot.pending_count;
|
||||
queueCounts.gateUnavailable = state.available === false;
|
||||
preparationItems.gate = snapshot.items;
|
||||
mobileTaskDock.updateQueues(queueCounts);
|
||||
if (state.authoritative) mobileStartDay.reconcile({authoritative:true, authoritativePhases:['gate']});
|
||||
mobileStartDay.render();
|
||||
},
|
||||
};
|
||||
const humanGates = progressiveHumanGatesHandoff?.controller || createHumanGates({
|
||||
storage:localStorage,
|
||||
getLogin:()=>planningOwnerLogin,
|
||||
getAccountKey:()=>planningOwnerAccountKey,
|
||||
isOnline:()=>navigator.onLine,
|
||||
location:window.location,
|
||||
fetchJson:fetchReviewJson,
|
||||
onChange:humanGatesOnChange,
|
||||
nodes:{
|
||||
count:qs('#human-gates-count'), list:qs('#human-gates-list'),
|
||||
status:qs('#human-gates-status'), panel:qs('#human-gates'),
|
||||
detail:qs('#human-gate-detail'),
|
||||
},
|
||||
});
|
||||
progressiveHumanGatesHandoff?.adoptIdentity(planningOwnerLogin, planningOwnerAccountKey);
|
||||
humanGates.setOnChange?.(humanGatesOnChange);
|
||||
const openHumanGates = () => humanGates.open().catch(error => {
|
||||
qs('#human-gates-status').textContent = error.message || 'Human Gates are unavailable.';
|
||||
});
|
||||
if (!progressiveHumanGatesHandoff) {
|
||||
qs('#open-human-gates').addEventListener('click', openHumanGates);
|
||||
qs('#close-human-gates').addEventListener('click', () => {
|
||||
qs('#human-gates').hidden = true;
|
||||
|
|
@ -681,8 +686,9 @@
|
|||
override_reason:detail.querySelector('[data-gate-override]')?.value || '',
|
||||
}).catch(error => { qs('#human-gates-status').textContent = error.message; });
|
||||
});
|
||||
humanGates.load().catch(() => {});
|
||||
if (window.location.hash === '#/my-work/human-gates') openHumanGates();
|
||||
}
|
||||
if (!progressiveHumanGatesHandoff?.started) humanGates.load().catch(() => {});
|
||||
if (window.location.hash === '#/my-work/human-gates' && !progressiveHumanGatesHandoff?.started) openHumanGates();
|
||||
|
||||
function syncCompletedFiledReviews() {
|
||||
if (!planningOwnerLogin) return Promise.resolve(false);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ function createHumanGates(options = {}) {
|
|||
let loadEpoch = 0;
|
||||
const decisionKeys = new Map();
|
||||
let decisionFlight = null;
|
||||
let onChange = options.onChange;
|
||||
|
||||
const escape = value => String(value ?? '').replace(/[&<>"']/g, character => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
||||
|
|
@ -20,7 +21,7 @@ function createHumanGates(options = {}) {
|
|||
const cacheKey = () => 'stackchain.human-gates.v1:' + String(getAccountKey() || '').trim().toLowerCase();
|
||||
const setText = (node, value) => { if (node) node.textContent = value; };
|
||||
const setHtml = (node, value) => { if (node) node.innerHTML = value; };
|
||||
const publish = state => options.onChange?.(JSON.parse(JSON.stringify(queue)), state);
|
||||
const publish = state => onChange?.(JSON.parse(JSON.stringify(queue)), state);
|
||||
|
||||
function validSnapshot(value) {
|
||||
return value && Number.isInteger(value.pending_count) && Array.isArray(value.items) ? value : null;
|
||||
|
|
@ -207,6 +208,7 @@ function createHumanGates(options = {}) {
|
|||
|
||||
return {
|
||||
load, open, reviewNext, select, decideAndNext, current,
|
||||
setOnChange(callback) { onChange = callback; },
|
||||
restoreCached: restore,
|
||||
snapshot: () => JSON.parse(JSON.stringify(queue)),
|
||||
route: () => location.hash,
|
||||
|
|
|
|||
|
|
@ -2412,6 +2412,7 @@
|
|||
<script src="static/mobile-find-work-nav.js"></script>
|
||||
<script src="static/workspace-bootstrap.js"></script>
|
||||
<script src="static/human-gates.js"></script>
|
||||
<script src="static/progressive-human-gates.js"></script>
|
||||
<script src="static/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
100
frontend/progressive-human-gates.js
Normal file
100
frontend/progressive-human-gates.js
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
function createProgressiveHumanGates(options = {}) {
|
||||
const document = options.document || globalThis.document;
|
||||
const location = options.location || globalThis.location;
|
||||
const history = options.history || globalThis.history;
|
||||
const storage = options.storage || globalThis.localStorage;
|
||||
const isOnline = options.isOnline || (() => globalThis.navigator?.onLine !== false);
|
||||
const fetchJson = options.fetchJson || (async (path, requestOptions = {}) => {
|
||||
const response = await fetch(path, requestOptions);
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(payload.error || payload.detail?.message || payload.detail || 'Review request failed.');
|
||||
return payload;
|
||||
});
|
||||
const getIdentity = options.getIdentity || (async () => {
|
||||
const response = await fetchJson('api/v1/live', {headers:{Accept:'application/json'}});
|
||||
const user = response?.context?.user || {};
|
||||
const login = String(user.login || '').trim();
|
||||
return {login, accountKey:login && user.id ? String(user.id) + ':' + login : login};
|
||||
});
|
||||
const query = selector => document.querySelector(selector);
|
||||
const nodes = {
|
||||
count:query('#human-gates-count'), list:query('#human-gates-list'),
|
||||
status:query('#human-gates-status'), panel:query('#human-gates'),
|
||||
detail:query('#human-gate-detail'),
|
||||
};
|
||||
let login = '';
|
||||
let accountKey = '';
|
||||
let started = false;
|
||||
let startFlight = null;
|
||||
|
||||
const controller = createHumanGates({
|
||||
storage, isOnline, location, fetchJson,
|
||||
getLogin:() => login, getAccountKey:() => accountKey, nodes,
|
||||
});
|
||||
|
||||
const showError = error => {
|
||||
if (nodes.status) nodes.status.textContent = error?.message || 'Human Gates are unavailable.';
|
||||
};
|
||||
const open = () => start(true).catch(showError);
|
||||
query('#open-human-gates')?.addEventListener?.('click', open);
|
||||
query('#close-human-gates')?.addEventListener?.('click', () => {
|
||||
if (nodes.panel) nodes.panel.hidden = true;
|
||||
if (location.hash === '#/my-work/human-gates') history.replaceState({}, '', '#/my-work');
|
||||
});
|
||||
nodes.list?.addEventListener?.('click', event => {
|
||||
const card = event.target?.closest?.('[data-human-gate-id]');
|
||||
if (card) controller.select(card.dataset.humanGateId);
|
||||
});
|
||||
nodes.detail?.addEventListener?.('click', event => {
|
||||
const decision = event.target?.closest?.('[data-gate-decision]')?.dataset.gateDecision;
|
||||
if (!decision) return;
|
||||
const checklist = Object.fromEntries(Array.from(nodes.detail.querySelectorAll('[data-gate-checklist]')).map(input => [input.dataset.gateChecklist, input.checked]));
|
||||
controller.decideAndNext(decision, {
|
||||
checklist,
|
||||
reason:nodes.detail.querySelector?.('[data-gate-reason]')?.value || '',
|
||||
override_reason:nodes.detail.querySelector?.('[data-gate-override]')?.value || '',
|
||||
}).catch(showError);
|
||||
});
|
||||
|
||||
async function start(force = false) {
|
||||
if (!force && location.hash !== '#/my-work/human-gates') return false;
|
||||
if (started) {
|
||||
if (nodes.panel) nodes.panel.hidden = false;
|
||||
return true;
|
||||
}
|
||||
if (startFlight) return startFlight;
|
||||
startFlight = (async () => {
|
||||
const identity = await getIdentity();
|
||||
login = String(identity?.login || '').trim();
|
||||
accountKey = String(identity?.accountKey || login).trim();
|
||||
if (!login) throw new Error('Authenticated account identity is required.');
|
||||
await controller.open();
|
||||
started = true;
|
||||
return true;
|
||||
})();
|
||||
try { return await startFlight; }
|
||||
finally { startFlight = null; }
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
handoff() {
|
||||
return {
|
||||
controller, started,
|
||||
adoptIdentity(nextLogin, nextAccountKey) {
|
||||
login = String(nextLogin || '').trim();
|
||||
accountKey = String(nextAccountKey || login).trim();
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
window.stackchainProgressiveHumanGates = createProgressiveHumanGates({document});
|
||||
void window.stackchainProgressiveHumanGates.start().catch(() => {});
|
||||
}
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
globalThis.createHumanGates = globalThis.createHumanGates || require('./human-gates.js');
|
||||
module.exports = createProgressiveHumanGates;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/private-data-registry.js');
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v140';
|
||||
const CACHE = 'stackchain-dashboard-shell-v141';
|
||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||
|
|
@ -152,6 +152,7 @@ const SHELL = [
|
|||
BASE + 'static/dashboard.css',
|
||||
BASE + 'static/dashboard.js',
|
||||
BASE + 'static/human-gates.js',
|
||||
BASE + 'static/progressive-human-gates.js',
|
||||
BASE + 'static/icons/stackchain-192.png',
|
||||
BASE + 'static/icons/stackchain-512.png',
|
||||
BASE + 'static/session.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 button { min-height:44px;' in html
|
||||
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
assert "stackchain-dashboard-shell-v140" in worker
|
||||
assert "stackchain-dashboard-shell-v141" in worker
|
||||
|
|
|
|||
|
|
@ -593,7 +593,7 @@ process.stdout.write(JSON.stringify({{
|
|||
assert ".following-disposition-mode" in css
|
||||
assert "if (searchPreviewReturnKind === 'following')" in dashboard
|
||||
assert "e.key === 'Escape' && searchPreviewReturnKind === 'following'" in dashboard
|
||||
assert "stackchain-dashboard-shell-v140" in service_worker
|
||||
assert "stackchain-dashboard-shell-v141" in service_worker
|
||||
|
||||
|
||||
def test_prepare_today_lazily_refreshes_and_directly_reviews_following():
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from pathlib import Path
|
|||
|
||||
|
||||
MODULE = Path(__file__).parents[1] / "frontend" / "human-gates.js"
|
||||
PROGRESSIVE = Path(__file__).parents[1] / "frontend" / "progressive-human-gates.js"
|
||||
INDEX = Path(__file__).parents[1] / "frontend" / "index.html"
|
||||
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
||||
WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js"
|
||||
|
|
@ -195,10 +196,57 @@ def test_human_gate_mobile_shell_and_deep_route_are_wired():
|
|||
assert 'data-mobile-queue="gate"' in index
|
||||
assert 'data-mobile-queue-count="gate"' in index
|
||||
assert "openHumanGates: () => openHumanGates()" in dashboard
|
||||
assert "onChange:(snapshot, state)=>" in dashboard
|
||||
assert "const humanGatesOnChange = (snapshot, state)=>" in dashboard
|
||||
assert "queueCounts.gate = snapshot.pending_count" in dashboard
|
||||
assert "preparationItems.gate = snapshot.items" in dashboard
|
||||
assert "mobileStartDay.reconcile({authoritative:true, authoritativePhases:['gate']})" in dashboard
|
||||
assert "counts.gate = queueCounts.gate" in dashboard
|
||||
assert "gate:preparationItems.gate || []" in dashboard
|
||||
assert "stackchain-dashboard-shell-v140" in WORKER.read_text()
|
||||
assert "stackchain-dashboard-shell-v141" in WORKER.read_text()
|
||||
|
||||
|
||||
def test_deep_link_opens_human_gates_without_waiting_for_optional_workspace():
|
||||
script = f"""
|
||||
const createProgressiveHumanGates=require({json.dumps(str(PROGRESSIVE))});
|
||||
const listeners={{}}; const requests=[];
|
||||
const nodes={{
|
||||
'#human-gates-count':{{}}, '#human-gates-list':{{innerHTML:''}},
|
||||
'#human-gates-status':{{}}, '#human-gates':{{hidden:true}},
|
||||
'#human-gate-detail':{{innerHTML:'',querySelectorAll:()=>[]}},
|
||||
'#open-human-gates':{{addEventListener:(name,fn)=>listeners.open=fn}},
|
||||
'#close-human-gates':{{addEventListener:(name,fn)=>listeners.close=fn}},
|
||||
}};
|
||||
nodes['#human-gates-list'].addEventListener=(name,fn)=>listeners.list=fn;
|
||||
nodes['#human-gate-detail'].addEventListener=(name,fn)=>listeners.detail=fn;
|
||||
const document={{querySelector:selector=>nodes[selector]||null}};
|
||||
const app=createProgressiveHumanGates({{
|
||||
document, location:{{hash:'#/my-work/human-gates'}},
|
||||
history:{{replaceState(){{}}}}, storage:{{getItem:()=>null,setItem(){{}}}},
|
||||
isOnline:()=>true, getIdentity:async()=>({{login:'timmy',accountKey:'7:timmy'}}),
|
||||
fetchJson:async path=>{{requests.push(path);return {{pending_count:1,items:[{{id:'g1',title:'Ship it',candidate_hash:'abc',revision:1,checks:[]}}]}};}},
|
||||
}});
|
||||
(async()=>{{const started=await app.start();process.stdout.write(JSON.stringify({{
|
||||
started,hidden:nodes['#human-gates'].hidden,html:nodes['#human-gates-list'].innerHTML,
|
||||
requests,listeners:Object.keys(listeners).sort(),handoff:app.handoff().started,
|
||||
}}));}})();
|
||||
"""
|
||||
result = subprocess.run(["node", "-e", script], check=True, text=True, capture_output=True)
|
||||
output = json.loads(result.stdout)
|
||||
assert output == {
|
||||
"started": True,
|
||||
"hidden": False,
|
||||
"html": '<button class="human-gate-card" type="button" data-human-gate-id="g1"><strong>Ship it</strong><code>abc</code><span>Priority 0</span></button>',
|
||||
"requests": ["api/v1/human-gates", "api/v1/human-gates/g1"],
|
||||
"listeners": ["close", "detail", "list", "open"],
|
||||
"handoff": True,
|
||||
}
|
||||
|
||||
|
||||
def test_dashboard_adopts_progressive_human_gates_without_a_second_list_load():
|
||||
dashboard = DASHBOARD.read_text()
|
||||
index = INDEX.read_text()
|
||||
assert 'static/progressive-human-gates.js' in index
|
||||
assert "window.stackchainProgressiveHumanGates?.handoff?.()" in dashboard
|
||||
assert "progressiveHumanGatesHandoff?.controller || createHumanGates" in dashboard
|
||||
assert "if (!progressiveHumanGatesHandoff?.started) humanGates.load()" in dashboard
|
||||
assert "if (window.location.hash === '#/my-work/human-gates' && !progressiveHumanGatesHandoff?.started) openHumanGates()" in dashboard
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" 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 pre { max-width:100%; overflow-x:auto;" in css
|
||||
assert ".markdown-content a { min-height:44px;" in css
|
||||
assert "stackchain-dashboard-shell-v140" in worker
|
||||
assert "stackchain-dashboard-shell-v141" 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]))
|
||||
|
||||
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
|
||||
assert "stackchain-dashboard-shell-v140" in worker
|
||||
assert "stackchain-dashboard-shell-v141" in worker
|
||||
|
||||
|
||||
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 "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
||||
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
||||
assert "stackchain-dashboard-shell-v140" in worker
|
||||
assert "stackchain-dashboard-shell-v141" in worker
|
||||
assert ".device-setup-panel" in css
|
||||
assert ".device-readiness-card" 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():
|
||||
worker = (CONTROLLER.parent / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in worker
|
||||
assert "stackchain-dashboard-shell-v141" 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 "max-width:100%; overflow-wrap:anywhere;" in html
|
||||
assert "BASE + 'static/mobile-start-day.js'" in service_worker
|
||||
assert "stackchain-dashboard-shell-v140" in service_worker
|
||||
assert "stackchain-dashboard-shell-v141" in service_worker
|
||||
|
||||
|
||||
@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():
|
||||
source = SERVICE_WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" in source
|
||||
assert "BASE + 'static/plan-today.js'" in source
|
||||
assert "BASE + 'static/plan-today-readiness.js'" in source
|
||||
assert "BASE + 'static/plan-today-preview.js'" in source
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ async function dispatchPush(payload) {{
|
|||
def test_week_unplan_undo_rolls_the_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" in source
|
||||
assert "BASE + 'static/week-plan.js'" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
|
||||
|
|
@ -197,20 +197,20 @@ def test_week_unplan_undo_rolls_the_offline_shell():
|
|||
def test_private_today_action_mailbox_rolls_the_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" in source
|
||||
|
||||
|
||||
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" 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-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" in source
|
||||
assert "BASE + 'static/my-work.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
|
|
@ -219,7 +219,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
|||
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" 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
|
||||
|
|
@ -228,7 +228,7 @@ def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
|
|||
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" in source
|
||||
assert "BASE + 'static/issue-evidence-review.js'" in source
|
||||
assert "BASE + 'static/issue-attachment.js'" in source
|
||||
|
||||
|
|
@ -236,14 +236,14 @@ def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
|
|||
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" 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-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" in source
|
||||
assert "BASE + 'static/today-completion.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
|
@ -251,7 +251,7 @@ def test_offline_review_next_ships_today_completion_atomically():
|
|||
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" in source
|
||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
|
|
@ -259,7 +259,7 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
|||
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" 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
|
||||
|
|
@ -269,14 +269,14 @@ def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
|
|||
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" 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-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
|
|
@ -285,21 +285,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
|
|||
def test_today_convergence_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" 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-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" 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-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
|
|
@ -1354,7 +1354,7 @@ def test_one_session_bound_csrf_proof_is_reused_for_a_background_drain():
|
|||
def test_queue_today_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" in source
|
||||
assert "BASE + 'static/queue-today.js'" in source
|
||||
|
||||
|
||||
|
|
@ -1374,6 +1374,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
|||
"/dashboard/static/dashboard.css",
|
||||
"/dashboard/static/dashboard.js",
|
||||
"/dashboard/static/human-gates.js",
|
||||
"/dashboard/static/progressive-human-gates.js",
|
||||
"/dashboard/static/icons/stackchain-192.png",
|
||||
"/dashboard/static/icons/stackchain-512.png",
|
||||
"/dashboard/static/session.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():
|
||||
service_worker = SERVICE_WORKER.read_text()
|
||||
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v140';" in service_worker
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v141';" 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():
|
||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v140" in source
|
||||
assert "stackchain-dashboard-shell-v141" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user