mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 16:02:42 +00:00
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).
188 lines
5.9 KiB
TypeScript
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)')
|
|
})
|
|
})
|