From 16acb63a6da396983be72313f7d88c19505d5214 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Thu, 24 Sep 2026 03:12:31 +0000 Subject: [PATCH] fix(notifications): auto-request 'default' permission at most once per page A pending approval/clarify prompt re-surfaces on every 1.5s poll tick, and with permission still 'default' (second browser/device) each tick issued a non-gesture Notification.requestPermission() and toasted 'notifications denied'. Auto requests are now made once per page; the explicit Send-test path still always asks. Release-gate hardening on top of #7493. --- static/messages.js | 8 +++++++ tests/test_prompt_notification_lifecycle.py | 26 +++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/static/messages.js b/static/messages.js index 023ed23b..dc2aa918 100644 --- a/static/messages.js +++ b/static/messages.js @@ -9643,6 +9643,14 @@ function sendBrowserNotification(title,body,options={}){ if(force&&typeof showToast==='function') showToast(t('notifications_denied'),3500,'error'); return false; } + // Permission still 'default': prompt-card owners retry on every 1.5s poll + // tick while pending, so an automatic (non-gesture) request is made at most + // once per page — otherwise each tick re-prompts and re-toasts "denied". + // The explicit "Send test" (force) path always asks. + if(!force){ + if(sendBrowserNotification._autoPermissionRequested) return false; + sendBrowserNotification._autoPermissionRequested=true; + } return requestNotificationPermission().then(p => p==='granted' ? deliver() : false).catch(() => false); } diff --git a/tests/test_prompt_notification_lifecycle.py b/tests/test_prompt_notification_lifecycle.py index 9c53b182..137e145e 100644 --- a/tests/test_prompt_notification_lifecycle.py +++ b/tests/test_prompt_notification_lifecycle.py @@ -134,3 +134,29 @@ console.log(JSON.stringify({shown})); ''', ('_startClarifyFallbackPoll',)) assert result == dict(shown=[]) + + +def test_default_permission_is_auto_requested_once_not_every_poll_tick(): + """A pending prompt re-surfaces on every 1.5s poll tick; with permission still + 'default' (a second browser/device) the automatic, non-gesture request must be + made at most once per page, so the user is not re-prompted and re-toasted + "notifications denied" on every tick. The explicit Send-test path still asks.""" + result = run(r''' +const window=global.window={_notificationsEnabled:true}; +let requests=0; const toasts=[]; +global.t=(k)=>k; +global.showToast=(msg)=>toasts.push(msg); +global.updateNotificationPermissionStatus=()=>{}; +window.Notification=global.Notification=function(){}; +Notification.permission='default'; +Notification.requestPermission=()=>{requests++;return Promise.resolve('default');}; +global.assistantDisplayName=()=> 'Hermes'; +global._notificationOptions=()=>({}); +Object.defineProperty(global,'navigator',{value:{}}); +const p={approval_id:'id'}; +for (let i=0;i<5;i++){ _notifyPromptCard('approval','s',p); await flush(); await flush(); } +const autoRequests=requests, autoToasts=toasts.length, seenAfterAuto=_promptNotifySeen.size; +await sendBrowserNotification('Test','body',{force:true}); await flush(); +console.log(JSON.stringify({autoRequests,autoToasts,seenAfterAuto,forcedRequests:requests-autoRequests})); +''', ('sendBrowserNotification', '_showPwaNotification', 'requestNotificationPermission')) + assert result == dict(autoRequests=1, autoToasts=1, seenAfterAuto=0, forcedRequests=1)