From 7f451cf68c5170bc6c03bda94f90173b2422df25 Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Fri, 11 Sep 2026 11:03:58 -0700 Subject: [PATCH] fix(ui): stop synthesizing the caffeinate badge's active state from settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The badge derived `active` from the configured mode whenever the service's reported mode disagreed with it, so a real AgentAwakeService outage still read "On · Active" for as long as the setting said on. Track whether a real status has arrived; before that, keep the optimistic no-flash default, but once the service has reported in, its `active` value is authoritative. --- .../CaffeinateStatusSegment.truth.test.tsx | 119 ++++++++++++++++++ .../status-bar/CaffeinateStatusSegment.tsx | 11 +- 2 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 src/renderer/src/components/status-bar/CaffeinateStatusSegment.truth.test.tsx diff --git a/src/renderer/src/components/status-bar/CaffeinateStatusSegment.truth.test.tsx b/src/renderer/src/components/status-bar/CaffeinateStatusSegment.truth.test.tsx new file mode 100644 index 00000000000..d84ede21e8a --- /dev/null +++ b/src/renderer/src/components/status-bar/CaffeinateStatusSegment.truth.test.tsx @@ -0,0 +1,119 @@ +// @vitest-environment happy-dom + +import type { ReactNode } from 'react' +import { cleanup, render, screen } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { CaffeinateStatusSegment } from './CaffeinateStatusSegment' + +const storeMocks = vi.hoisted(() => ({ + settings: { + computerAwakeMode: 'on', + keepComputerAwakeWhileAgentsRun: true + }, + updateSettings: vi.fn() +})) + +const awakeMocks = vi.hoisted(() => ({ + status: { mode: 'on', active: true }, + unsubscribe: vi.fn(), + getStatus: vi.fn(), + onChangedHandler: undefined as ((status: { mode: string; active: boolean }) => void) | undefined +})) + +vi.mock('@/store', () => ({ + useAppStore: (selector: (state: Record) => unknown) => + selector({ settings: storeMocks.settings, updateSettings: storeMocks.updateSettings }) +})) + +vi.mock('@/lib/desktop-window-chrome', () => ({ + isPairedWebClientWindow: () => false +})) + +vi.mock('@/components/ui/tooltip', () => ({ + Tooltip: ({ children }: { children: ReactNode }) => <>{children}, + TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}, + TooltipContent: ({ children }: { children: ReactNode }) =>
{children}
+})) + +vi.mock('@/components/ui/dropdown-menu', () => ({ + DropdownMenu: ({ children }: { children: ReactNode }) => <>{children}, + DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <>{children}, + DropdownMenuContent: ({ children }: { children: ReactNode }) =>
{children}
, + DropdownMenuLabel: ({ children }: { children: ReactNode }) =>
{children}
, + DropdownMenuRadioGroup: ({ children }: { children: ReactNode }) =>
{children}
, + DropdownMenuRadioItem: ({ children }: { children: ReactNode }) => ( +
+ {children} +
+ ), + DropdownMenuSeparator: () =>
+})) + +beforeEach(() => { + storeMocks.settings = { + computerAwakeMode: 'on', + keepComputerAwakeWhileAgentsRun: true + } + awakeMocks.status = { mode: 'on', active: true } + awakeMocks.unsubscribe.mockClear() + awakeMocks.onChangedHandler = undefined + // getStatus never resolves within these tests unless a test awaits it explicitly - this lets + // us assert the pre-first-status render without racing a real promise resolution. + awakeMocks.getStatus.mockReset() + Object.defineProperty(window, 'api', { + configurable: true, + value: { + agentAwake: { + getStatus: awakeMocks.getStatus, + onChanged: vi.fn().mockImplementation((handler) => { + awakeMocks.onChangedHandler = handler + return awakeMocks.unsubscribe + }) + } + } + }) +}) + +afterEach(cleanup) + +describe('CaffeinateStatusSegment status truth', () => { + it('does not flash Inactive before the first status arrives when configured mode is on', async () => { + // getStatus deliberately never resolves in this test - only the pre-status render is asserted. + awakeMocks.getStatus.mockReturnValue(new Promise(() => {})) + + render() + + const trigger = await screen.findByRole('button') + expect(trigger.getAttribute('aria-label')).toContain('Active') + expect(trigger.getAttribute('aria-label')).not.toContain('Inactive') + }) + + it('reports Inactive once the service disagrees with an "on" configured setting', async () => { + awakeMocks.getStatus.mockResolvedValue({ mode: 'off', active: false }) + + render() + + const trigger = await screen.findByRole('button') + await vi.waitFor(() => { + expect(trigger.getAttribute('aria-label')).toContain('Inactive') + }) + expect(trigger.getAttribute('aria-label')).not.toContain('· Active') + }) + + it('reports Inactive when a later onChanged push disagrees with the configured setting', async () => { + awakeMocks.getStatus.mockResolvedValue({ mode: 'on', active: true }) + + render() + + const trigger = await screen.findByRole('button') + await vi.waitFor(() => { + expect(trigger.getAttribute('aria-label')).toContain('· Active') + }) + + awakeMocks.onChangedHandler?.({ mode: 'off', active: false }) + + await vi.waitFor(() => { + expect(trigger.getAttribute('aria-label')).toContain('Inactive') + }) + }) +}) diff --git a/src/renderer/src/components/status-bar/CaffeinateStatusSegment.tsx b/src/renderer/src/components/status-bar/CaffeinateStatusSegment.tsx index 89c6f0266bb..e19183ae79c 100644 --- a/src/renderer/src/components/status-bar/CaffeinateStatusSegment.tsx +++ b/src/renderer/src/components/status-bar/CaffeinateStatusSegment.tsx @@ -44,12 +44,14 @@ export function CaffeinateStatusSegment({ settings?.keepComputerAwakeWhileAgentsRun ) const [serviceStatus, setServiceStatus] = useState(INACTIVE_STATUS) + const [hasServiceStatus, setHasServiceStatus] = useState(false) useEffect(() => { let mounted = true const unsubscribe = window.api.agentAwake.onChanged((status) => { if (mounted) { setServiceStatus(status) + setHasServiceStatus(true) } }) void window.api.agentAwake @@ -57,6 +59,7 @@ export function CaffeinateStatusSegment({ .then((status) => { if (mounted) { setServiceStatus(status) + setHasServiceStatus(true) } }) .catch(() => {}) @@ -70,9 +73,11 @@ export function CaffeinateStatusSegment({ return null } - const mode = serviceStatus.mode === configuredMode ? serviceStatus.mode : configuredMode - const active = - serviceStatus.mode === configuredMode ? serviceStatus.active : configuredMode === 'on' + // Before the first real status arrives, show the configured setting optimistically to avoid + // an inactive flash. Once the service has reported in, its `active` value is the truth — never + // synthesize activity from the setting alone, or a real outage would read as "Active". + const mode = configuredMode + const active = hasServiceStatus ? serviceStatus.active : configuredMode === 'on' const title = getAgentAwakeTitle() const statusText = `${getAgentAwakeModeLabel(mode)} · ${activityLabel(active)}` const ariaLabel = translate(