From 2e66aa42b62ffd8c9454b8e05bdcc08a6e72ac46 Mon Sep 17 00:00:00 2001 From: CRT Date: Tue, 13 Oct 2026 02:03:51 +0800 Subject: [PATCH] desktop: the notification plugin's refused is_permission_granted probe at page load (macOS, Linux; the webview has no plugin API) is no longer logged as a start error in webview.log --- apps/desktop/public/boot.js | 4 ++++ apps/desktop/src/lib/crash.ts | 10 +++++++++- apps/desktop/tests/resilience.test.ts | 10 ++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/desktop/public/boot.js b/apps/desktop/public/boot.js index 7aca380..2fc0d87 100644 --- a/apps/desktop/public/boot.js +++ b/apps/desktop/public/boot.js @@ -118,7 +118,11 @@ if (t && t !== window && (t.src || t.href)) { record('load', 'could not load ' + (t.src || t.href)); return; } record('window.onerror', ev.error || ev.message, ev.error && ev.error.stack); }, true); + // tauri-plugin-notification's init script asks for the notification permission at load (macOS, Linux); the webview + // has no plugin API by design (capabilities/main.json), so that call is refused: expected, not an error + function pluginProbe(e) { return /plugin:notification\|is_permission_granted not allowed/.test(text(e)); } window.addEventListener('unhandledrejection', function (ev) { + if (pluginProbe(ev.reason)) { ev.preventDefault(); return; } if (!booted()) record('unhandledrejection', ev.reason, ev.reason && ev.reason.stack); }); // React's root reports an error no boundary caught (the tree is gone) here: src/main.tsx onUncaughtError diff --git a/apps/desktop/src/lib/crash.ts b/apps/desktop/src/lib/crash.ts index 7a90421..2b78b5a 100644 --- a/apps/desktop/src/lib/crash.ts +++ b/apps/desktop/src/lib/crash.ts @@ -52,12 +52,20 @@ export function report(where: string, e: unknown, componentStack?: string | null return r; } +/** + * tauri-plugin-notification's init script asks for the notification permission when the page loads (macOS, Linux). + * The webview has no plugin API by design (capabilities/main.json), so the call is refused: expected, never reported. + */ +export function ignoredRejection(e: unknown): boolean { + return /plugin:notification\|is_permission_granted not allowed/.test(errorMessage(e)); +} + /** window.onerror and unhandled promise rejections, after React took over from public/boot.js. */ export function installGlobalHandlers(): void { // public/boot.js stops logging on its own from here (window as { __storeboundCrashHandlers?: boolean }).__storeboundCrashHandlers = true; window.addEventListener('error', ev => { report('window.onerror', ev.error ?? ev.message); }); - window.addEventListener('unhandledrejection', ev => { report('unhandledrejection', ev.reason, null, 'warn'); }); + window.addEventListener('unhandledrejection', ev => { if (!ignoredRejection(ev.reason)) report('unhandledrejection', ev.reason, null, 'warn'); }); } /** Clipboard first, then the old selection way (the webview may refuse clipboard access). True when copied. */ diff --git a/apps/desktop/tests/resilience.test.ts b/apps/desktop/tests/resilience.test.ts index 328a94e..c7f40c8 100644 --- a/apps/desktop/tests/resilience.test.ts +++ b/apps/desktop/tests/resilience.test.ts @@ -8,6 +8,7 @@ import { runInNewContext } from 'node:vm'; import { parseAst } from 'vite'; import { rewrite } from '../build/freeze-safe'; import { DEFAULT_SETTINGS } from '../src/api'; +import { ignoredRejection } from '../src/lib/crash'; import { NO_OVERRIDES } from '../src/lib/identity'; import { normalizeAccounts, normalizeProjects, normalizeSettings, normalizeWorkspace } from '../src/lib/normalize'; @@ -85,6 +86,15 @@ describe('error screens are wired in', () => { expect(src('src/main.tsx')).toContain("dataset.booted = '1'"); expect(src('src/main.tsx')).toContain(''); }); + test("the notification plugin's refused permission probe is not reported (boot.js and crash.ts)", () => { + // seen on Linux in CI (Xvfb): the plugin's init script calls is_permission_granted, which the capability refuses + const probe = 'Command plugin:notification|is_permission_granted not allowed by ACL'; + expect(ignoredRejection(probe)).toBe(true); + expect(ignoredRejection(new Error(probe))).toBe(true); + expect(ignoredRejection('Command plugin:notification|notify not allowed by ACL')).toBe(false); + expect(ignoredRejection(new Error('sidecar did not start'))).toBe(false); + expect(src('public/boot.js')).toContain('/plugin:notification\\|is_permission_granted not allowed/.test(text(e))'); + }); test('every screen and app tab has its own boundary', () => { expect(src('src/App.tsx')).toMatch(/