Add notification coverage

This commit is contained in:
brennanb2025
2026-05-19 00:22:56 -07:00
parent 9e79382340
commit 4e0c2cdac3
5 changed files with 211 additions and 38 deletions
+1
View File
@@ -312,6 +312,7 @@ export type UISlice = {
| 'input'
| 'tasks'
| 'terminal'
| 'notifications'
| 'computer-use'
| 'developer-permissions'
| 'shortcuts'
@@ -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'
)
})
})
+42
View File
@@ -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
}
}
+9 -38
View File
@@ -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<HookEndpoint> {
export async function readHookEndpoint(app: ElectronApplication): Promise<AgentHookEndpoint> {
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
+94
View File
@@ -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<typeof waitForSessionReady>[0]
): Promise<GlobalSettings> {
return page.evaluate(() => window.api.settings.get())
}
async function openNotificationSettings(
page: Parameters<typeof waitForSessionReady>[0]
): Promise<void> {
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 })
})
})