mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 08:02:43 +00:00
fix(ui): stop synthesizing the caffeinate badge's active state from settings
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.
This commit is contained in:
@@ -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<string, unknown>) => 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 }) => <div role="tooltip">{children}</div>
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/dropdown-menu', () => ({
|
||||
DropdownMenu: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
DropdownMenuContent: ({ children }: { children: ReactNode }) => <div role="menu">{children}</div>,
|
||||
DropdownMenuLabel: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenuRadioGroup: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenuRadioItem: ({ children }: { children: ReactNode }) => (
|
||||
<div role="menuitemradio" aria-checked="false">
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DropdownMenuSeparator: () => <hr />
|
||||
}))
|
||||
|
||||
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(<CaffeinateStatusSegment iconOnly={false} />)
|
||||
|
||||
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(<CaffeinateStatusSegment iconOnly={false} />)
|
||||
|
||||
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(<CaffeinateStatusSegment iconOnly={false} />)
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -44,12 +44,14 @@ export function CaffeinateStatusSegment({
|
||||
settings?.keepComputerAwakeWhileAgentsRun
|
||||
)
|
||||
const [serviceStatus, setServiceStatus] = useState<ComputerAwakeStatus>(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(
|
||||
|
||||
Reference in New Issue
Block a user