mirror of
https://github.com/stablyai/orca.git
synced 2026-10-15 16:02:49 +00:00
Add notification coverage
This commit is contained in:
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user