From 4e0c2cdac307adb70d133e633d1dc2ac44132e17 Mon Sep 17 00:00:00 2001 From: brennanb2025 Date: Tue, 19 May 2026 00:22:56 -0700 Subject: [PATCH] Add notification coverage --- src/renderer/src/store/slices/ui.ts | 1 + src/shared/agent-hook-endpoint-file.test.ts | 65 ++++++++++++++ src/shared/agent-hook-endpoint-file.ts | 42 +++++++++ tests/e2e/helpers/agent-hook-endpoint.ts | 47 ++--------- tests/e2e/notification-settings.spec.ts | 94 +++++++++++++++++++++ 5 files changed, 211 insertions(+), 38 deletions(-) create mode 100644 src/shared/agent-hook-endpoint-file.test.ts create mode 100644 src/shared/agent-hook-endpoint-file.ts create mode 100644 tests/e2e/notification-settings.spec.ts diff --git a/src/renderer/src/store/slices/ui.ts b/src/renderer/src/store/slices/ui.ts index 7dbad8869ef..bc3df1a2b5a 100644 --- a/src/renderer/src/store/slices/ui.ts +++ b/src/renderer/src/store/slices/ui.ts @@ -312,6 +312,7 @@ export type UISlice = { | 'input' | 'tasks' | 'terminal' + | 'notifications' | 'computer-use' | 'developer-permissions' | 'shortcuts' diff --git a/src/shared/agent-hook-endpoint-file.test.ts b/src/shared/agent-hook-endpoint-file.test.ts new file mode 100644 index 00000000000..dfa928290ea --- /dev/null +++ b/src/shared/agent-hook-endpoint-file.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest' +import { isAgentHookEndpointFileName, parseAgentHookEndpointFile } from './agent-hook-endpoint-file' + +describe('agent hook endpoint files', () => { + it('recognizes POSIX and Windows endpoint file names', () => { + expect(isAgentHookEndpointFileName('endpoint.env')).toBe(true) + expect(isAgentHookEndpointFileName('endpoint.cmd')).toBe(true) + expect(isAgentHookEndpointFileName('endpoint.ps1')).toBe(false) + }) + + it('parses POSIX endpoint.env contents', () => { + expect( + parseAgentHookEndpointFile( + [ + 'ORCA_AGENT_HOOK_PORT=12345', + 'ORCA_AGENT_HOOK_TOKEN=token-123', + 'ORCA_AGENT_HOOK_ENV=production', + 'ORCA_AGENT_HOOK_VERSION=1' + ].join('\n') + ) + ).toEqual({ + port: '12345', + token: 'token-123', + env: 'production', + version: '1' + }) + }) + + it('parses Windows endpoint.cmd contents', () => { + expect( + parseAgentHookEndpointFile( + [ + 'set ORCA_AGENT_HOOK_PORT=54321', + 'set ORCA_AGENT_HOOK_TOKEN=token-abc', + 'set ORCA_AGENT_HOOK_ENV=development', + 'set ORCA_AGENT_HOOK_VERSION=1' + ].join('\r\n') + ) + ).toEqual({ + port: '54321', + token: 'token-abc', + env: 'development', + version: '1' + }) + }) + + it('preserves equals signs in endpoint values', () => { + expect( + parseAgentHookEndpointFile( + [ + 'ORCA_AGENT_HOOK_PORT=12345', + 'ORCA_AGENT_HOOK_TOKEN=token=with=equals', + 'ORCA_AGENT_HOOK_ENV=production', + 'ORCA_AGENT_HOOK_VERSION=1' + ].join('\n') + ).token + ).toBe('token=with=equals') + }) + + it('throws when required endpoint fields are missing', () => { + expect(() => parseAgentHookEndpointFile('ORCA_AGENT_HOOK_PORT=12345')).toThrow( + 'Agent hook endpoint file is missing required fields' + ) + }) +}) diff --git a/src/shared/agent-hook-endpoint-file.ts b/src/shared/agent-hook-endpoint-file.ts new file mode 100644 index 00000000000..6f9c1c9a717 --- /dev/null +++ b/src/shared/agent-hook-endpoint-file.ts @@ -0,0 +1,42 @@ +export const AGENT_HOOK_ENDPOINT_FILE_NAMES = ['endpoint.env', 'endpoint.cmd'] as const + +export type AgentHookEndpointFileName = (typeof AGENT_HOOK_ENDPOINT_FILE_NAMES)[number] + +export type AgentHookEndpoint = { + port: string + token: string + env: string + version: string +} + +export function isAgentHookEndpointFileName(name: string): name is AgentHookEndpointFileName { + return AGENT_HOOK_ENDPOINT_FILE_NAMES.some((fileName) => fileName === name) +} + +export function parseAgentHookEndpointFile(contents: string): AgentHookEndpoint { + const values = Object.fromEntries( + contents + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + const normalizedLine = line.replace(/^set\s+/i, '') + const [key, ...rest] = normalizedLine.split('=') + return [key, rest.join('=')] + }) + ) + if ( + !values.ORCA_AGENT_HOOK_PORT || + !values.ORCA_AGENT_HOOK_TOKEN || + !values.ORCA_AGENT_HOOK_ENV || + !values.ORCA_AGENT_HOOK_VERSION + ) { + throw new Error('Agent hook endpoint file is missing required fields') + } + return { + port: values.ORCA_AGENT_HOOK_PORT, + token: values.ORCA_AGENT_HOOK_TOKEN, + env: values.ORCA_AGENT_HOOK_ENV, + version: values.ORCA_AGENT_HOOK_VERSION + } +} diff --git a/tests/e2e/helpers/agent-hook-endpoint.ts b/tests/e2e/helpers/agent-hook-endpoint.ts index b0dfae9c972..eefa42daeeb 100644 --- a/tests/e2e/helpers/agent-hook-endpoint.ts +++ b/tests/e2e/helpers/agent-hook-endpoint.ts @@ -1,13 +1,11 @@ import type { ElectronApplication } from '@stablyai/playwright-test' import { existsSync, readdirSync, readFileSync } from 'fs' import path from 'path' - -type HookEndpoint = { - port: string - token: string - env: string - version: string -} +import { + isAgentHookEndpointFileName, + parseAgentHookEndpointFile, + type AgentHookEndpoint +} from '../../../src/shared/agent-hook-endpoint-file' function findEndpointEnvFile(root: string): string | null { if (!existsSync(root)) { @@ -16,7 +14,7 @@ function findEndpointEnvFile(root: string): string | null { const entries = readdirSync(root, { withFileTypes: true }) for (const entry of entries) { const fullPath = path.join(root, entry.name) - if (entry.isFile() && (entry.name === 'endpoint.env' || entry.name === 'endpoint.cmd')) { + if (entry.isFile() && isAgentHookEndpointFileName(entry.name)) { return fullPath } if (entry.isDirectory()) { @@ -29,45 +27,18 @@ function findEndpointEnvFile(root: string): string | null { return null } -function parseEndpointEnv(contents: string): HookEndpoint { - const values = Object.fromEntries( - contents - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => { - const [key, ...rest] = line.replace(/^set\s+/, '').split('=') - return [key, rest.join('=')] - }) - ) - if ( - !values.ORCA_AGENT_HOOK_PORT || - !values.ORCA_AGENT_HOOK_TOKEN || - !values.ORCA_AGENT_HOOK_ENV || - !values.ORCA_AGENT_HOOK_VERSION - ) { - throw new Error('Agent hook endpoint file is missing required fields') - } - return { - port: values.ORCA_AGENT_HOOK_PORT, - token: values.ORCA_AGENT_HOOK_TOKEN, - env: values.ORCA_AGENT_HOOK_ENV, - version: values.ORCA_AGENT_HOOK_VERSION - } -} - -export async function readHookEndpoint(app: ElectronApplication): Promise { +export async function readHookEndpoint(app: ElectronApplication): Promise { const userDataPath = await app.evaluate(({ app: electronApp }) => electronApp.getPath('userData')) const hookRoot = path.join(userDataPath, 'agent-hooks') const endpointPath = findEndpointEnvFile(hookRoot) if (!endpointPath) { throw new Error(`Agent hook endpoint file not found under ${hookRoot}`) } - return parseEndpointEnv(readFileSync(endpointPath, 'utf8')) + return parseAgentHookEndpointFile(readFileSync(endpointPath, 'utf8')) } export async function emitCodexHookStatus( - endpoint: HookEndpoint, + endpoint: AgentHookEndpoint, status: { paneKey: string worktreeId: string diff --git a/tests/e2e/notification-settings.spec.ts b/tests/e2e/notification-settings.spec.ts new file mode 100644 index 00000000000..ae33f26d7ae --- /dev/null +++ b/tests/e2e/notification-settings.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from './helpers/orca-app' +import { waitForSessionReady } from './helpers/store' +import type { GlobalSettings } from '../../src/shared/types' + +async function getSettings( + page: Parameters[0] +): Promise { + return page.evaluate(() => window.api.settings.get()) +} + +async function openNotificationSettings( + page: Parameters[0] +): Promise { + await page.evaluate(() => { + const state = window.__store!.getState() + state.openSettingsTarget({ pane: 'notifications', repoId: null }) + state.openSettingsPage() + }) + await expect(page.getByPlaceholder('Search settings')).toBeVisible({ timeout: 10_000 }) + const featureTipDialog = page.getByRole('dialog', { name: 'Voice Dictation is here' }) + if (await featureTipDialog.isVisible().catch(() => false)) { + await page.getByRole('button', { name: 'Maybe Later' }).click() + } + await expect( + page + .locator('[data-settings-section="notifications"]') + .getByRole('heading', { name: 'Notifications', exact: true }) + ).toBeInViewport({ timeout: 10_000 }) +} + +test.describe('Notification settings', () => { + test.beforeEach(async ({ orcaPage }) => { + await waitForSessionReady(orcaPage) + }) + + test('can be toggled from settings and disables child controls', async ({ orcaPage }) => { + await openNotificationSettings(orcaPage) + + const notificationsSection = orcaPage.locator('[data-settings-section="notifications"]') + const enableNotificationsSwitch = notificationsSection.getByRole('switch', { + name: 'Enable Notifications' + }) + const agentTaskCompleteSwitch = notificationsSection.getByRole('switch', { + name: 'Agent Task Complete' + }) + const terminalBellSwitch = notificationsSection.getByRole('switch', { name: 'Terminal Bell' }) + const suppressWhileFocusedSwitch = notificationsSection.getByRole('switch', { + name: 'Suppress While Focused' + }) + const sendTestButton = notificationsSection.getByRole('button', { + name: 'Send Test Notification' + }) + + await expect(enableNotificationsSwitch).toHaveAttribute('aria-checked', 'true') + await expect(agentTaskCompleteSwitch).toBeEnabled() + await expect(terminalBellSwitch).toBeEnabled() + await expect(suppressWhileFocusedSwitch).toBeEnabled() + await expect(sendTestButton).toBeEnabled() + + await agentTaskCompleteSwitch.click() + await expect(agentTaskCompleteSwitch).toHaveAttribute('aria-checked', 'false') + await expect + .poll(async () => (await getSettings(orcaPage)).notifications.agentTaskComplete, { + timeout: 5_000, + message: 'agent task-complete notification setting did not persist after disabling' + }) + .toBe(false) + + await enableNotificationsSwitch.click() + await expect(enableNotificationsSwitch).toHaveAttribute('aria-checked', 'false') + await expect(agentTaskCompleteSwitch).toBeDisabled() + await expect(terminalBellSwitch).toBeDisabled() + await expect(suppressWhileFocusedSwitch).toBeDisabled() + await expect(sendTestButton).toBeDisabled() + await expect + .poll(async () => (await getSettings(orcaPage)).notifications.enabled, { + timeout: 5_000, + message: 'master notification setting did not persist after disabling' + }) + .toBe(false) + + await enableNotificationsSwitch.click() + await agentTaskCompleteSwitch.click() + await expect + .poll(async () => { + const settings = await getSettings(orcaPage) + return { + enabled: settings.notifications.enabled, + agentTaskComplete: settings.notifications.agentTaskComplete + } + }) + .toEqual({ enabled: true, agentTaskComplete: true }) + }) +})