From b5e6fa2c25d6fd56ab8148f619f8f381faf55e55 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:25:54 -0700 Subject: [PATCH] fix(macos): show TCC notice after first prompt --- src/main/macos-tcc-prompt-notice.test.ts | 41 ++++++++++++++---------- src/main/macos-tcc-prompt-notice.ts | 6 ++-- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/main/macos-tcc-prompt-notice.test.ts b/src/main/macos-tcc-prompt-notice.test.ts index 9ee688cea4b..764ca153943 100644 --- a/src/main/macos-tcc-prompt-notice.test.ts +++ b/src/main/macos-tcc-prompt-notice.test.ts @@ -54,16 +54,11 @@ beforeEach(() => { }) describe('tcc prompt notice threshold', () => { - it('stays silent until the third dialog, then fires exactly once', () => { - expect(handleTccPromptForTests()).toBeNull() - expect(handleTccPromptForTests()).toBeNull() - - const payload = handleTccPromptForTests() - expect(payload).toEqual({ + it('fires on the first dialog, then stays silent', () => { + expect(handleTccPromptForTests()).toEqual({ promptCount: TCC_PROMPT_NOTICE_THRESHOLD }) - // Why: a recurring prompt loop must not produce a toast per dialog. expect(handleTccPromptForTests()).toBeNull() expect(handleTccPromptForTests()).toBeNull() }) @@ -109,7 +104,10 @@ describe('tcc prompt notice threshold', () => { expect(consumePendingTccPromptNotice(2)).toBeNull() expect(writeFileAtomically).toHaveBeenCalledOnce() const [, contents] = writeFileAtomically.mock.calls.at(-1) as [string, string] - expect(JSON.parse(contents)).toMatchObject({ promptCount: 3, notified: true }) + expect(JSON.parse(contents)).toMatchObject({ + promptCount: TCC_PROMPT_NOTICE_THRESHOLD, + notified: true + }) }) it('releases an unacknowledged claim for a replacement renderer', () => { @@ -123,11 +121,14 @@ describe('tcc prompt notice threshold', () => { releasePendingTccPromptNotice(1, oldClaim!.claimId) const replacementClaim = consumePendingTccPromptNotice(2) - expect(oldClaim).toEqual({ claimId: 1, promptCount: 3 }) - expect(replacementClaim).toEqual({ claimId: 2, promptCount: 3 }) + expect(oldClaim).toEqual({ claimId: 1, promptCount: TCC_PROMPT_NOTICE_THRESHOLD }) + expect(replacementClaim).toEqual({ claimId: 2, promptCount: TCC_PROMPT_NOTICE_THRESHOLD }) acknowledgePendingTccPromptNotice(1, oldClaim!.claimId) releasePendingTccPromptNotice(2) - expect(consumePendingTccPromptNotice(3)).toEqual({ claimId: 3, promptCount: 3 }) + expect(consumePendingTccPromptNotice(3)).toEqual({ + claimId: 3, + promptCount: TCC_PROMPT_NOTICE_THRESHOLD + }) }) it('invalidates an outstanding claim when the user dismisses the notice', () => { @@ -262,7 +263,10 @@ describe('tcc prompt notice threshold', () => { }).not.toThrow() expect(mainWindow.webContents.send).not.toHaveBeenCalled() expect(watchStop).toHaveBeenCalledTimes(1) - expect(consumePendingTccPromptNotice(1)).toEqual({ claimId: 1, promptCount: 3 }) + expect(consumePendingTccPromptNotice(1)).toEqual({ + claimId: 1, + promptCount: TCC_PROMPT_NOTICE_THRESHOLD + }) } finally { Object.defineProperty(process, 'platform', platform!) } @@ -284,17 +288,20 @@ describe('tcc prompt notice threshold', () => { }).not.toThrow() expect(watchStop).toHaveBeenCalledOnce() - expect(consumePendingTccPromptNotice(1)).toEqual({ claimId: 1, promptCount: 3 }) + expect(consumePendingTccPromptNotice(1)).toEqual({ + claimId: 1, + promptCount: TCC_PROMPT_NOTICE_THRESHOLD + }) } finally { Object.defineProperty(process, 'platform', platform!) } }) - it('does not respawn the watcher for a persisted pending threshold', () => { + it('delivers a tally persisted below the old threshold without respawning the watcher', () => { const platform = Object.getOwnPropertyDescriptor(process, 'platform') Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) readTallyFile.mockReturnValue( - JSON.stringify({ promptCount: 3, notified: false, dismissed: false }) + JSON.stringify({ promptCount: 2, notified: false, dismissed: false }) ) try { const mainWindow = createWindowStub() @@ -302,10 +309,10 @@ describe('tcc prompt notice threshold', () => { expect(watchStart).not.toHaveBeenCalled() expect(mainWindow.webContents.send).toHaveBeenCalledWith('macosTccPrompts:threshold', { - promptCount: 3 + promptCount: 2 }) expect(mainWindow.once).not.toHaveBeenCalled() - expect(consumePendingTccPromptNotice(1)).toEqual({ claimId: 1, promptCount: 3 }) + expect(consumePendingTccPromptNotice(1)).toEqual({ claimId: 1, promptCount: 2 }) } finally { Object.defineProperty(process, 'platform', platform!) } diff --git a/src/main/macos-tcc-prompt-notice.ts b/src/main/macos-tcc-prompt-notice.ts index 30b96f36c39..d76b0470453 100644 --- a/src/main/macos-tcc-prompt-notice.ts +++ b/src/main/macos-tcc-prompt-notice.ts @@ -8,11 +8,11 @@ import { MacosTccPromptWatch } from './macos-tcc-prompt-watch' /** * Surfaces Full Disk Access guidance only to users macOS is actually prompting * (#9756), instead of nudging every Mac user. Counts Orca-attributed TCC - * dialogs across launches and tells the renderer once the third one lands. + * dialogs across launches and tells the renderer when the first one lands. */ -/** Why: one dialog is normal and two is bad luck; three means it is recurring for this user. */ -export const TCC_PROMPT_NOTICE_THRESHOLD = 3 +/** Why: the first detected dialog identifies an affected user; the notice remains one-time. */ +export const TCC_PROMPT_NOTICE_THRESHOLD = 1 /** Why: preserve detection when Electron never emits `ready-to-show` without competing with startup. */ export const TCC_PROMPT_WATCH_START_FALLBACK_MS = 10_000