Files
orca/src/cli/handlers/agent-hooks.test.ts
T
Brennan Benson 881c7884e9 Merge origin/main into brennanb2025/check-clis-before-hooks
Reconcile presence-gated Codex launch prep with main's #7969 WSL runtime-home
status-hook install:
- WSL runtime-home installs (installForRuntimeHome/refreshRuntimeUserHooksForRuntimeHome)
  run unconditionally and synchronously — never presence-gated, since the
  Windows-side PATH probe cannot see a codex CLI living inside the WSL distro
  (gating it would regress #7969).
- Non-WSL launches keep the presence gate plus the always-run #7896 approval
  promotion from this branch's review fixes.
- Keep both new tsconfig.cli entries and the added imports (getDefaultWslDistro,
  codexLaunchNeedsDirectApprovalPromotion).
2026-07-09 18:15:59 -07:00

188 lines
5.9 KiB
TypeScript

import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getDefaultPersistedState } from '../../shared/constants'
import type { PersistedState } from '../../shared/types'
const {
applyAgentStatusHooksEnabledMock,
callMock,
getCliStatusMock,
getDefaultUserDataPathMock,
getManagedAgentHookStatusesMock
} = vi.hoisted(() => ({
applyAgentStatusHooksEnabledMock: vi.fn(),
callMock: vi.fn(),
getCliStatusMock: vi.fn(() =>
Promise.resolve({
id: 'test-status',
ok: true,
result: {
app: { running: false, pid: null },
runtime: { state: 'not_running', reachable: false, runtimeId: null },
graph: { state: 'not_running' }
},
_meta: { runtimeId: 'test' }
})
),
getDefaultUserDataPathMock: vi.fn(),
getManagedAgentHookStatusesMock: vi.fn()
}))
vi.mock('../runtime-client', () => {
class RuntimeClient {
call = callMock
getCliStatus = getCliStatusMock
}
class RuntimeClientError extends Error {
readonly code: string
constructor(code: string, message: string) {
super(message)
this.code = code
}
}
return {
RuntimeClient,
RuntimeClientError,
getDefaultUserDataPath: getDefaultUserDataPathMock
}
})
vi.mock('../../main/agent-hooks/managed-agent-hook-controls', () => ({
applyAgentStatusHooksEnabled: applyAgentStatusHooksEnabledMock,
getManagedAgentHookStatuses: getManagedAgentHookStatusesMock
}))
import { main } from '../index'
function readDataFile(userDataPath: string): PersistedState {
return JSON.parse(readFileSync(join(userDataPath, 'orca-data.json'), 'utf-8')) as PersistedState
}
function writeDataFile(userDataPath: string, state: PersistedState): void {
mkdirSync(userDataPath, { recursive: true })
writeFileSync(join(userDataPath, 'orca-data.json'), JSON.stringify(state, null, 2), 'utf-8')
}
async function runAgentHooksOff(userDataPath: string): Promise<void> {
getDefaultUserDataPathMock.mockReturnValue(userDataPath)
await main(['agent', 'hooks', 'off', '--json'], userDataPath)
}
async function runAgentHooksOn(userDataPath: string): Promise<void> {
getDefaultUserDataPathMock.mockReturnValue(userDataPath)
await main(['agent', 'hooks', 'on', '--json'], userDataPath)
}
async function runAgentHooksOnText(userDataPath: string): Promise<void> {
getDefaultUserDataPathMock.mockReturnValue(userDataPath)
await main(['agent', 'hooks', 'on'], userDataPath)
}
describe('agent hooks CLI handler', () => {
let userDataPath: string
beforeEach(() => {
userDataPath = mkdtempSync(join(tmpdir(), 'orca-agent-hooks-cli-'))
applyAgentStatusHooksEnabledMock.mockResolvedValue([])
callMock.mockReset()
getCliStatusMock.mockClear()
getManagedAgentHookStatusesMock.mockReturnValue([])
process.exitCode = undefined
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
rmSync(userDataPath, { recursive: true, force: true })
})
it('keeps new card style off when creating offline settings for a fresh profile', async () => {
await runAgentHooksOff(userDataPath)
const persisted = readDataFile(userDataPath)
expect(persisted.settings.experimentalNewWorktreeCardStyle).toBe(false)
expect(persisted.settings.agentStatusHooksEnabled).toBe(false)
})
it('keeps missing new card style off when updating offline settings', async () => {
const existing = getDefaultPersistedState(userDataPath)
delete existing.settings.experimentalNewWorktreeCardStyle
writeDataFile(userDataPath, existing)
await runAgentHooksOff(userDataPath)
expect(readDataFile(userDataPath).settings.experimentalNewWorktreeCardStyle).toBe(false)
})
it('preserves an existing explicit new card style opt-in when updating offline settings', async () => {
const existing = getDefaultPersistedState(userDataPath)
existing.settings.experimentalNewWorktreeCardStyle = true
writeDataFile(userDataPath, existing)
await runAgentHooksOff(userDataPath)
expect(readDataFile(userDataPath).settings.experimentalNewWorktreeCardStyle).toBe(true)
})
it('awaits offline hook application before formatting the command result', async () => {
applyAgentStatusHooksEnabledMock.mockResolvedValue([
{
agent: 'claude',
state: 'skipped',
skipReason: 'cli_not_found'
}
])
await runAgentHooksOff(userDataPath)
const printed = vi.mocked(console.log).mock.calls.at(-1)?.[0]
const parsed = JSON.parse(String(printed)) as {
result: { statuses: { agent: string; state: string; skipReason: string }[] }
}
expect(parsed.result.statuses).toEqual([
{
agent: 'claude',
state: 'skipped',
skipReason: 'cli_not_found'
}
])
})
it('passes persisted command overrides into offline hook application', async () => {
const existing = getDefaultPersistedState(userDataPath)
existing.settings.agentCmdOverrides = { codex: '/custom/bin/codex --profile work' }
writeDataFile(userDataPath, existing)
await runAgentHooksOn(userDataPath)
expect(applyAgentStatusHooksEnabledMock).toHaveBeenCalledWith(true, {
agentCmdOverrides: { codex: '/custom/bin/codex --profile work' }
})
})
it('includes skipped hook reasons in human-readable output', async () => {
applyAgentStatusHooksEnabledMock.mockResolvedValue([
{
agent: 'claude',
state: 'skipped',
configPath: '',
managedHooksPresent: false,
detail: 'CLI not found; managed hook install skipped.',
skipReason: 'cli_not_found'
}
])
await runAgentHooksOnText(userDataPath)
const printed = vi.mocked(console.log).mock.calls.at(-1)?.[0]
expect(String(printed)).toContain('claude: skipped (cli_not_found)')
})
})