Files
orca/src/main/codex/codex-real-home-hook-install.test.ts
T
Neil 26721bd632 fix(codex): stop blocking the main thread on trust grants (#16441) (#16594)
* fix(codex): stop blocking the main thread on trust grants (#16441)

Codex hook trust was granted by blocking the Electron main thread on
`spawnSync` of a bundled ELECTRON_RUN_AS_NODE entry for the whole
app-server deadline: 15s native, 35s WSL, ~45s on the real-home path
(rebase inspect + repair + grant). Cold start and every Codex pane
launch showed "Not Responding"; the reported event-loop gap was
15,049 ms.

The subprocess only ever existed to donate an event loop to a
deliberately blocked parent — `runCodexHookTrustGrantSession` was
already the real async implementation. Make the callers async and the
fork is unnecessary, so the bridge, the forked entry and its envelope
are deleted along with their build/knip/tsconfig registrations. The CLI
`agent hooks prepare-codex` handler is already async, so it awaits the
in-process session and saves a process spawn per managed-home shell.

`resolveCodexTrustGrantHost` is async too; the WSL identity probe moves
from `execFileSync` to `runProcess`, dropping that file from the
child-process import allowlist. Status reads keep a synchronous
native-only stamp path.

Two invariants that held only because the lane blocked:

- Overlapping capability probes were impossible by construction.
  `GitCapabilityCache`'s dedupe engine is extracted to a shared
  `CapabilityProbeCache` and `CodexAppServerCapabilityCache` now
  inherits it, so concurrent launches against a cold host share one
  app-server session instead of one each.
- Two grants on one `config.toml` could not interleave capture and
  restore. A reentrant per-file lane now serializes the whole install
  sequence (managed, WSL runtime, real-home ensure, legacy sweep) and
  the grant and rebase inside it.

Cold-start work moves off the critical path: retained-home
reconciliation (N sequential sessions) is fire-and-forget behind the
daemon provider, and the startup real-home ensure chains into managed
hook reconciliation instead of blocking app init.

Every preserved semantic is unchanged: never throws, the
ORCA_DISABLE_CODEX_TRUST_RPC kill switch, ledger hits, backfill-pending
and cooldown fallbacks, config rollback on every failure path,
pre-grant self-computed trust removal, the verify-failure taxonomy,
diagnostics and telemetry.

* fix(codex): widen the trust-config lane to every config.toml writer

Review follow-ups on #16441's async trust grant:

- `markCodexProjectTrusted` now runs inside the runtime+system config.toml
  lanes, so a project-trust write can no longer land inside a hook grant's
  capture->restore window and be silently reverted. Its callers await it.
- `install`/`refreshRuntimeUserHooks`/`remove` hold the system config.toml
  lane as well as the runtime one — they promote approvals into
  ~/.codex/config.toml and mirror it back. Lock order is runtime-before-system
  everywhere.
- The real-home ensure chain resumes after a rejection instead of returning
  the same rejected promise to every later pane launch, and resolving the real
  home is now inside the module's never-throws boundary.
- `buildSpawnEnv` awaits inside a cancelable pending-spawn registration, so
  shutdown during the (now long) env build stops the PTY from launching.
  `prepareLocalPtySpawn` generalizes into `awaitCancelableLocalPtySpawn`.
- CapabilityProbeCache drops the test-only `nowMs` passthrough; its probe
  backstop comment now describes what it actually guards.
- Preflight is a plain async function; the trust dispatch in orca-runtime
  collapses into one `markWorkspaceTrustedForAgent`.

* test(codex): exercise the trust-config lane under real concurrency

The async grant makes two pane launches overlap for the first time. These
drive the real modules end to end on real files: a rollback swallowing a
sibling's grant, a markCodexProjectTrusted write landing inside a capture
-> restore window, shared capability-probe dedupe on a cold host, the
host-scoped transient cooldown, and reentrancy from inside an installer.

Each was verified to fail against a deliberately broken implementation
(lane removed, dedupe disabled, cooldown made global, reentrancy pass-
through disabled).

* test(codex): stop hook-service suites spawning the developer's real codex

The forked grant bundle never existed under vitest, so the RPC lane was
unreachable in tests on main. Running it in-process makes these suites
spawn a real `codex app-server` when one is installed: 38 spawns and two
failures in hook-service-runtime-trust-repair on a machine with codex,
green in CI where there is none. Stand in for the missing binary so both
environments exercise the same fallback lane.

* docs(codex): scope the trust-RPC kill switch comment to what it actually gates

The comment read as though the flag forces the fallback lane everywhere. It
gates the managed grant only: the real-home rebase still runs its own
inspect/repair app-server sessions when Orca's insertion shifts a user's hook
positions, and never reads the flag.

Verified by exercise, not by reading — with the flag set, both
inspect-user-hook-trust and repair-user-hook-trust still ran. Pre-existing:
main has no check there either, it just blocked the main thread while doing it.

Widening the flag to cover the rebase is a follow-up; this only stops the
comment promising something the constant does not do.
2026-08-26 16:44:55 -07:00

603 lines
22 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
chmodSync,
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
symlinkSync,
writeFileSync
} from 'node:fs'
import type * as NodeOs from 'node:os'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { CodexManagedTrustGrantPlan } from './codex-hook-trust-grant'
import {
computeTrustKey,
readHookTrustEntries,
upsertHookTrustEntriesInContent,
type CodexTrustEntry
} from './config-toml-trust'
const { homedirMock, grantMock } = vi.hoisted(() => ({
homedirMock: vi.fn<() => string>(),
grantMock: vi.fn()
}))
vi.mock('node:os', async () => {
const actual = await vi.importActual<typeof NodeOs>('node:os')
return { ...actual, homedir: homedirMock }
})
vi.mock('./codex-hook-trust-grant', () => ({
CODEX_TRUST_GRANT_TRANSIENT_RETRY_INTERVAL_MS: 300_000,
grantManagedCodexHookTrust: grantMock
}))
import {
ensureRealHomeCodexHookState,
getRealHomeCodexHookLane,
_internals
} from './codex-real-home-hook-install'
import { getCodexManagedHookInstallMaterial } from './hook-service'
import { _internals as rebaseInternals } from './codex-user-hook-trust-rebase'
let fakeHomeDir: string
let userDataDir: string
let previousUserDataPath: string | undefined
function getRealHooksJsonPath(): string {
return join(fakeHomeDir, '.codex', 'hooks.json')
}
function getRealConfigTomlPath(): string {
return join(fakeHomeDir, '.codex', 'config.toml')
}
function readRealHooksJson(): {
hooks?: Record<string, { hooks?: { command?: string }[] }[]>
[key: string]: unknown
} {
return JSON.parse(readFileSync(getRealHooksJsonPath(), 'utf-8'))
}
function grantSucceeds(): void {
grantMock.mockImplementation((plan: CodexManagedTrustGrantPlan) => ({
lane: 'rpc',
entries: plan.managedEntries.map((entry) => ({ ...entry, trustedHash: 'codex-hash' }))
}))
}
function grantUnavailable(): void {
grantMock.mockReturnValue({ lane: 'fallback', reason: 'unsupported' })
}
beforeEach(() => {
grantMock.mockReset()
fakeHomeDir = mkdtempSync(join(tmpdir(), 'orca-real-home-hooks-home-'))
userDataDir = mkdtempSync(join(tmpdir(), 'orca-real-home-hooks-user-data-'))
previousUserDataPath = process.env.ORCA_USER_DATA_PATH
process.env.ORCA_USER_DATA_PATH = userDataDir
homedirMock.mockReturnValue(fakeHomeDir)
mkdirSync(join(fakeHomeDir, '.codex'), { recursive: true })
_internals.setLaneForTesting('pending')
})
afterEach(() => {
rebaseInternals.setSessionRunner(null)
rebaseInternals.resetRetryState()
rmSync(fakeHomeDir, { recursive: true, force: true })
rmSync(userDataDir, { recursive: true, force: true })
if (previousUserDataPath === undefined) {
delete process.env.ORCA_USER_DATA_PATH
} else {
process.env.ORCA_USER_DATA_PATH = previousUserDataPath
}
vi.clearAllMocks()
})
describe('ensureRealHomeCodexHookState (install)', () => {
// Why (#16441): the ensure chain is process-wide; a rejection that escapes it
// would return the same rejected promise to every later pane launch, with no
// retry and no cooldown recovery.
it('recovers from a home-resolution failure instead of poisoning later ensures', async () => {
grantSucceeds()
homedirMock.mockImplementationOnce(() => {
throw new Error('home unavailable')
})
await expect(
ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
).resolves.toBe('unavailable')
await expect(
ensureRealHomeCodexHookState({ hooksEnabled: false, userDataPath: userDataDir })
).resolves.toBe('removed')
})
it('creates hooks.json with the Orca entry in every managed event for a fresh home', async () => {
grantSucceeds()
const lane = await ensureRealHomeCodexHookState({
hooksEnabled: true,
userDataPath: userDataDir
})
expect(lane).toBe('installed')
const material = getCodexManagedHookInstallMaterial()
const config = readRealHooksJson()
for (const eventName of material.events) {
const definitions = config.hooks?.[eventName]
expect(definitions).toHaveLength(1)
expect(definitions?.[0]?.hooks?.[0]?.command).toBe(material.command)
}
// The grant plan targeted the real home with append-position trust keys.
const plan = grantMock.mock.calls[0]![0] as CodexManagedTrustGrantPlan
expect(plan.runtimeHomePath).toBe(join(fakeHomeDir, '.codex'))
expect(plan.host).toEqual({ kind: 'native' })
expect(plan.useDefaultCodexHome).toBe(true)
expect(plan.managedEntries.every((entry) => entry.groupIndex === 0)).toBe(true)
})
it('keeps a symlinked default home logical in the keys sent to Codex', async () => {
grantSucceeds()
const logicalHome = join(fakeHomeDir, '.codex')
const targetHome = join(fakeHomeDir, 'dotfiles-codex')
rmSync(logicalHome, { recursive: true })
mkdirSync(targetHome)
symlinkSync(targetHome, logicalHome, process.platform === 'win32' ? 'junction' : 'dir')
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
).toBe('installed')
const plan = grantMock.mock.calls[0]![0] as CodexManagedTrustGrantPlan
expect(
plan.managedEntries.map(computeTrustKey).every((key) => key.startsWith(logicalHome))
).toBe(true)
})
it('keeps the managed lane for unknown top-level fields Codex cannot load', async () => {
grantSucceeds()
const userConfig = {
hooks: {
Stop: [{ matcher: 'deploy-*', hooks: [{ type: 'command', command: 'my-stop-hook.sh' }] }],
PreCompact: [{ hooks: [{ type: 'command', command: 'my-compact-hook.sh' }] }]
},
_pluginManagerMetadata: { owner: 'someone-else' }
}
const original = `${JSON.stringify(userConfig, null, 2)}\n`
writeFileSync(getRealHooksJsonPath(), original, 'utf-8')
const lane = await ensureRealHomeCodexHookState({
hooksEnabled: true,
userDataPath: userDataDir
})
expect(lane).toBe('unavailable')
expect(readFileSync(getRealHooksJsonPath(), 'utf-8')).toBe(original)
expect(grantMock).not.toHaveBeenCalled()
expect(existsSync(join(userDataDir, 'codex-real-home-hooks', 'hooks.json.pre-orca'))).toBe(
false
)
})
it('appends LAST and preserves user entries and trust positions', async () => {
grantSucceeds()
const userConfig = {
hooks: {
Stop: [{ matcher: 'deploy-*', hooks: [{ type: 'command', command: 'my-stop-hook.sh' }] }],
PreCompact: [{ hooks: [{ type: 'command', command: 'my-compact-hook.sh' }] }]
}
}
const original = `${JSON.stringify(userConfig, null, 2)}\n`
writeFileSync(getRealHooksJsonPath(), original, 'utf-8')
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
).toBe('installed')
const config = readRealHooksJson()
expect(config.hooks?.Stop).toHaveLength(2)
expect(config.hooks?.Stop?.[0]).toEqual(userConfig.hooks.Stop[0])
expect(config.hooks?.PreCompact).toEqual(userConfig.hooks.PreCompact)
const plan = grantMock.mock.calls[0]![0] as CodexManagedTrustGrantPlan
expect(plan.managedEntries.find((entry) => entry.eventLabel === 'stop')?.groupIndex).toBe(1)
expect(
readFileSync(join(userDataDir, 'codex-real-home-hooks', 'hooks.json.pre-orca'), 'utf-8')
).toBe(original)
})
// Why: ordinary Windows CI tokens cannot create file symlinks without Developer Mode.
it.skipIf(process.platform === 'win32')(
'updates a symlinked hooks.json target without replacing the symlink',
async () => {
grantSucceeds()
const dotfilesDir = join(fakeHomeDir, 'dotfiles')
const targetPath = join(dotfilesDir, 'hooks.json')
mkdirSync(dotfilesDir, { recursive: true })
writeFileSync(
targetPath,
`${JSON.stringify({ hooks: { Stop: [{ hooks: [{ type: 'command', command: 'mine.sh' }] }] } }, null, 2)}\n`,
'utf-8'
)
symlinkSync(targetPath, getRealHooksJsonPath())
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
).toBe('installed')
expect(lstatSync(getRealHooksJsonPath()).isSymbolicLink()).toBe(true)
expect(JSON.parse(readFileSync(targetPath, 'utf-8')).hooks.Stop).toHaveLength(2)
}
)
it('keeps the managed lane and original bytes when the pristine backup cannot be created', async () => {
grantSucceeds()
const original = `${JSON.stringify({ hooks: { Stop: [] } }, null, 2)}\n`
writeFileSync(getRealHooksJsonPath(), original, 'utf-8')
writeFileSync(join(userDataDir, 'codex-real-home-hooks'), 'blocks backup directory', 'utf-8')
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
).toBe('unavailable')
expect(readFileSync(getRealHooksJsonPath(), 'utf-8')).toBe(original)
expect(grantMock).not.toHaveBeenCalled()
})
it.skipIf(process.platform === 'win32')(
'preserves restrictive hooks.json permissions',
async () => {
grantSucceeds()
writeFileSync(getRealHooksJsonPath(), '{ "hooks": {} }\n', 'utf-8')
chmodSync(getRealHooksJsonPath(), 0o600)
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
).toBe('installed')
expect(statSync(getRealHooksJsonPath()).mode & 0o777).toBe(0o600)
}
)
it.skipIf(process.platform === 'win32')(
'restores restrictive hooks.json permissions after grant fallback',
async () => {
grantUnavailable()
writeFileSync(getRealHooksJsonPath(), '{ "hooks": {} }\n', 'utf-8')
chmodSync(getRealHooksJsonPath(), 0o600)
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
).toBe('unavailable')
expect(statSync(getRealHooksJsonPath()).mode & 0o777).toBe(0o600)
}
)
it('rolls the file back byte-exactly when the grant lane is unavailable', async () => {
grantUnavailable()
const userRaw = `${JSON.stringify({ hooks: { Stop: [{ hooks: [{ type: 'command', command: 'mine.sh' }] }] } }, null, 2)}\n`
writeFileSync(getRealHooksJsonPath(), userRaw, 'utf-8')
const lane = await ensureRealHomeCodexHookState({
hooksEnabled: true,
userDataPath: userDataDir
})
expect(lane).toBe('unavailable')
expect(getRealHomeCodexHookLane()).toBe('unavailable')
expect(readFileSync(getRealHooksJsonPath(), 'utf-8')).toBe(userRaw)
})
it('removes a freshly created hooks.json when the grant lane is unavailable', async () => {
grantUnavailable()
const lane = await ensureRealHomeCodexHookState({
hooksEnabled: true,
userDataPath: userDataDir
})
expect(lane).toBe('unavailable')
expect(existsSync(getRealHooksJsonPath())).toBe(false)
})
it('surfaces rollback failures to the retry boundary', async () => {
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {})
grantMock.mockImplementation(() => {
rmSync(getRealHooksJsonPath())
mkdirSync(getRealHooksJsonPath())
return { lane: 'fallback', reason: 'unsupported' }
})
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
).toBe('unavailable')
expect(warning).toHaveBeenCalledWith(
'[codex-real-home-hooks] ensure failed; staying on managed lane:',
expect.any(Error)
)
})
it('does no hook-file or grant work on repeated unsupported launches', async () => {
grantUnavailable()
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
).toBe('unavailable')
expect(existsSync(getRealHooksJsonPath())).toBe(false)
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
).toBe('unavailable')
expect(grantMock).toHaveBeenCalledTimes(1)
expect(existsSync(getRealHooksJsonPath())).toBe(false)
})
it('leaves an unparseable hooks.json untouched and keeps the managed lane', async () => {
writeFileSync(getRealHooksJsonPath(), '{not json', 'utf-8')
const lane = await ensureRealHomeCodexHookState({
hooksEnabled: true,
userDataPath: userDataDir
})
expect(lane).toBe('unavailable')
expect(readFileSync(getRealHooksJsonPath(), 'utf-8')).toBe('{not json')
expect(grantMock).not.toHaveBeenCalled()
})
it('is idempotent: a second ensure keeps a single appended entry per event', async () => {
grantSucceeds()
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
const firstRaw = readFileSync(getRealHooksJsonPath(), 'utf-8')
const lane = await ensureRealHomeCodexHookState({
hooksEnabled: true,
userDataPath: userDataDir
})
expect(lane).toBe('installed')
expect(readFileSync(getRealHooksJsonPath(), 'utf-8')).toBe(firstRaw)
})
it('keeps later user hook trust positions stable when reconciling an existing install', async () => {
grantSucceeds()
const userBefore = { hooks: [{ type: 'command', command: 'before.sh' }] }
writeFileSync(
getRealHooksJsonPath(),
`${JSON.stringify({ hooks: { Stop: [userBefore] } }, null, 2)}\n`,
'utf-8'
)
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
const installed = readRealHooksJson()
const userAfter = { hooks: [{ type: 'command', command: 'after.sh' }] }
installed.hooks!.Stop!.push(userAfter)
writeFileSync(getRealHooksJsonPath(), `${JSON.stringify(installed, null, 2)}\n`, 'utf-8')
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
).toBe('installed')
const reconciled = readRealHooksJson().hooks?.Stop
expect(reconciled?.[0]).toEqual(userBefore)
expect(reconciled?.[2]).toEqual(userAfter)
const plan = grantMock.mock.calls.at(-1)![0] as CodexManagedTrustGrantPlan
expect(plan.managedEntries.find((entry) => entry.eventLabel === 'stop')?.groupIndex).toBe(1)
})
it("keeps later user handler trust positions stable inside Orca's hook group", async () => {
grantSucceeds()
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
const installed = readRealHooksJson()
const userAfter = { type: 'command', command: 'after.sh' }
installed.hooks!.Stop![0]!.hooks!.push(userAfter)
writeFileSync(getRealHooksJsonPath(), `${JSON.stringify(installed, null, 2)}\n`, 'utf-8')
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
).toBe('installed')
expect(readRealHooksJson().hooks?.Stop?.[0]?.hooks?.[1]).toEqual(userAfter)
const plan = grantMock.mock.calls.at(-1)![0] as CodexManagedTrustGrantPlan
const stopEntry = plan.managedEntries.find((entry) => entry.eventLabel === 'stop')
expect(stopEntry).toMatchObject({ groupIndex: 0, handlerIndex: 0 })
})
})
describe('ensureRealHomeCodexHookState (opt-out sweep)', () => {
it('keeps the managed lane when hooks.json cannot be read', async () => {
mkdirSync(getRealHooksJsonPath())
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: false, userDataPath: userDataDir })
).toBe('unavailable')
})
it('keeps the managed lane when hooks.json is malformed', async () => {
writeFileSync(getRealHooksJsonPath(), '{ not json', 'utf-8')
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: false, userDataPath: userDataDir })
).toBe('unavailable')
expect(readFileSync(getRealHooksJsonPath(), 'utf-8')).toBe('{ not json')
})
it('rebases trust when a user appended hooks after Orca installed', async () => {
grantSucceeds()
const before = { type: 'command', command: 'before.sh' }
writeFileSync(
getRealHooksJsonPath(),
`${JSON.stringify({ hooks: { Stop: [{ hooks: [before] }] } }, null, 2)}\n`
)
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
const installed = readRealHooksJson()
const after = { type: 'command', command: 'after.sh' }
installed.hooks!.Stop!.push({ hooks: [after] })
writeFileSync(getRealHooksJsonPath(), `${JSON.stringify(installed, null, 2)}\n`)
const operations: string[] = []
rebaseInternals.setSessionRunner(async (request) => {
operations.push(request.operation)
if (request.operation === 'inspect-user-hook-trust') {
expect(readRealHooksJson().hooks?.Stop?.[2]?.hooks?.[0]?.command).toBe('after.sh')
return {
outcome: 'inspected',
moves: request.moves.map((move) => ({
...move,
reportedOldKey: move.oldKey,
wasTrusted: true,
enabled: true
}))
}
}
expect(readRealHooksJson().hooks?.Stop?.[1]?.hooks?.[0]?.command).toBe('after.sh')
return { outcome: 'repaired', repaired: 1 }
})
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: false, userDataPath: userDataDir })
).toBe('removed')
expect(operations).toEqual(['inspect-user-hook-trust', 'repair-user-hook-trust'])
expect(readRealHooksJson().hooks?.Stop).toEqual([{ hooks: [before] }, { hooks: [after] }])
})
it('aborts without writing when hooks.json changes during the trust inspection', async () => {
grantSucceeds()
const before = { type: 'command', command: 'before.sh' }
writeFileSync(
getRealHooksJsonPath(),
`${JSON.stringify({ hooks: { Stop: [{ hooks: [before] }] } }, null, 2)}\n`
)
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
const installed = readRealHooksJson()
const after = { type: 'command', command: 'after.sh' }
installed.hooks!.Stop!.push({ hooks: [after] })
writeFileSync(getRealHooksJsonPath(), `${JSON.stringify(installed, null, 2)}\n`)
const userTrustToml = '[hooks.state."x:stop:0:0"]\ntrusted_hash = "user"\n'
writeFileSync(getRealConfigTomlPath(), userTrustToml, 'utf-8')
const concurrentSave = `${JSON.stringify({ hooks: { Stop: [{ hooks: [before] }] } }, null, 2)}\n`
const operations: string[] = []
rebaseInternals.setSessionRunner(async (request) => {
operations.push(request.operation)
// A user save (or a second Orca instance) lands while the RPC runs.
writeFileSync(getRealHooksJsonPath(), concurrentSave, 'utf-8')
return {
outcome: 'inspected',
moves: request.moves.map((move) => ({
...move,
reportedOldKey: move.oldKey,
wasTrusted: true,
enabled: true
}))
}
})
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: false, userDataPath: userDataDir })
).toBe('unavailable')
expect(operations).toEqual(['inspect-user-hook-trust'])
expect(readFileSync(getRealHooksJsonPath(), 'utf-8')).toBe(concurrentSave)
expect(readFileSync(getRealConfigTomlPath(), 'utf-8')).toBe(userTrustToml)
})
it('removes only Orca entries and reports the removed lane', async () => {
grantSucceeds()
const userStop = {
matcher: 'deploy-*',
hooks: [{ type: 'command', command: 'my-stop-hook.sh' }]
}
writeFileSync(
getRealHooksJsonPath(),
`${JSON.stringify({ hooks: { Stop: [userStop] } }, null, 2)}\n`,
'utf-8'
)
await ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })
expect(readRealHooksJson().hooks?.Stop).toHaveLength(2)
const lane = await ensureRealHomeCodexHookState({
hooksEnabled: false,
userDataPath: userDataDir
})
expect(lane).toBe('removed')
const config = readRealHooksJson()
expect(config.hooks?.Stop).toEqual([userStop])
const material = getCodexManagedHookInstallMaterial()
for (const eventName of material.events) {
if (eventName === 'Stop') {
continue
}
expect(config.hooks?.[eventName]).toBeUndefined()
}
})
it('no-ops the sweep when the real home has no hooks.json', async () => {
const lane = await ensureRealHomeCodexHookState({
hooksEnabled: false,
userDataPath: userDataDir
})
expect(lane).toBe('removed')
expect(existsSync(getRealHooksJsonPath())).toBe(false)
})
it('removes only hash-proven Orca trust from a mixed hook group', async () => {
const material = getCodexManagedHookInstallMaterial()
const userCommand = 'my-user-hook.sh'
writeFileSync(
getRealHooksJsonPath(),
`${JSON.stringify(
{
hooks: {
Stop: [
{
hooks: [
{ type: 'command', command: userCommand },
{ type: 'command', command: material.command, timeout: 10 }
]
}
]
}
},
null,
2
)}\n`,
'utf-8'
)
const entries: CodexTrustEntry[] = [
{
sourcePath: getRealHooksJsonPath(),
eventLabel: 'stop',
groupIndex: 0,
handlerIndex: 0,
command: userCommand
},
{
sourcePath: getRealHooksJsonPath(),
eventLabel: 'stop',
groupIndex: 0,
handlerIndex: 1,
command: material.command,
timeoutSec: 10
}
]
writeFileSync(getRealConfigTomlPath(), upsertHookTrustEntriesInContent('', entries), 'utf-8')
expect(
await ensureRealHomeCodexHookState({ hooksEnabled: false, userDataPath: userDataDir })
).toBe('removed')
expect(readRealHooksJson().hooks?.Stop).toEqual([
{ hooks: [{ type: 'command', command: userCommand }] }
])
const trust = readHookTrustEntries(getRealConfigTomlPath())
expect(trust.has(computeTrustKey(entries[0]!))).toBe(true)
expect(trust.has(computeTrustKey(entries[1]!))).toBe(false)
})
})