Files
orca/src/main/ipc/codex-config-sync.test.ts
T
Brennan Benson 677718c4a5 fix(codex): stop rebuilding shared Codex state from a read that failed (STA-4823) (#15417)
* fix(codex): stop rebuilding shared Codex state from a read that failed (STA-4823)

Six shared files were rebuilt, erased or reported healthy after a read that had
only failed. Batch A of the STA-4606 split: every one of these is reachable and
testable on the host lane, so none of them wait on the WSL work.

- `config-toml-trust.ts` upsertHookTrustEntries: `existsSync` reported a locked
  config.toml as absent, so the base content became '' and the upsert rewrote
  the file from the trust entries alone — a trust-only stub, with the user's
  model, provider, MCP servers, approvals and comments gone. It refuses now;
  every hook-service caller already turns that into "trust entries could not be
  written. Run /hooks in Codex to approve."
- `codex-trust-grant-ledger.ts`: an unreadable ledger degraded to empty and the
  next write persisted a file holding only the home being written, dropping
  every other home's grants. The write paths refuse; the read path still
  degrades, and a corrupt ledger is still rebuilt.
- `codex-pane-account-registry.ts`: an unreadable registry erased every pane's
  attribution AND cached that erasure, so it survived the file recovering. The
  failure is no longer cached, and both write sites refuse rather than persist a
  registry derived from an empty stand-in.
- `hooks-json-read.ts`: the read arm already separated "no hooks" from "could
  not read", but the `existsSync` arm in front of it returned a valid empty
  config for a file that could not be opened. One read now classifies both.
- `config-settings-baseline.ts`: absent, unparseable and unreadable all collapsed
  into `null`, so the snapshot rebuilt a baseline it could not read — recording
  an in-Codex edit as Orca's own write, after which promotion skips it forever.
- `config-sync-stall.ts`: an unreadable runtime config read as absent and the
  status reported `synced` while the mirror was refusing. It reports
  `managed-home-unavailable`, the existing reason for exactly this, rather than
  borrowing a source-side one and blaming the wrong path.

Absent and malformed still rebuild throughout — resetting corrupt state is the
intent, and conflating it with unreadable would wedge a user on a broken file.

* fix(codex): close shared state read-denial gaps

* fix(codex): recover oversized settings baselines

* fix(codex): name the stalled managed config

* fix(codex): preserve hooks after failed source reads

* test(codex): correct what the denyExistence rig actually models

MEASURED on both platforms: a file-permission denial leaves existsSync TRUE
and fails only the content read — chmod 000 gives EACCES on macOS, icacls
/deny (R) gives EPERM errno -4048 on Windows, with stat/lstat succeeding in
both. The rig's docblock claimed this mode modelled that denial. It does not.

What it models is the UNC / \\wsl$ transport, where an unreachable distro
reports errno UNKNOWN at every level and existsSync folds it to false.

The distinction decides what the D29 guard is worth: under a permission denial
the pre-fix code already failed safe, because existsSync was true so it took
the read branch and threw. Only a transport that lies about existence reaches
the rebuild-from-empty path. No behaviour change; the comment was wrong, not
the code.

* test(codex): exercise live baseline read denial

* fix(codex): retry pane attribution writes

* fix(codex): retry reconciliation registry writes

* fix(codex): report unreadable sync baselines
2026-08-23 13:52:18 -07:00

122 lines
4.5 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type * as NodeOs from 'node:os'
const { handleMock, removeHandlerMock, homedirMock } = vi.hoisted(() => ({
handleMock: vi.fn(),
removeHandlerMock: vi.fn(),
homedirMock: vi.fn<() => string>()
}))
vi.mock('electron', () => ({
ipcMain: { handle: handleMock, removeHandler: removeHandlerMock }
}))
vi.mock('node:os', async (importOriginal) => {
const actual = await importOriginal<typeof NodeOs>()
return { ...actual, homedir: homedirMock }
})
import { registerCodexConfigSyncHandlers } from './codex-config-sync'
import type { CodexConfigSyncStatus } from '../../shared/codex-config-sync-types'
import { getCodexSettingsBaselinePath } from '../codex/config-settings-baseline'
let root: string
function invokeHandler(mirroredHome: string | null): CodexConfigSyncStatus {
return invokeHandlerWithStatus({ kind: 'ready', homePath: mirroredHome })
}
function invokeHandlerWithStatus(
mirrored: { kind: 'ready'; homePath: string | null } | { kind: 'unavailable' }
): CodexConfigSyncStatus {
handleMock.mockClear()
registerCodexConfigSyncHandlers({
getMirroredHostHomePathForStatus: () => mirrored
})
const handler = handleMock.mock.calls.at(-1)?.[1] as () => CodexConfigSyncStatus
return handler()
}
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'orca-config-sync-ipc-'))
homedirMock.mockReturnValue(root)
mkdirSync(join(root, '.codex'), { recursive: true })
})
afterEach(() => {
rmSync(root, { recursive: true, force: true })
vi.clearAllMocks()
})
describe('codexConfigSync:status handler', () => {
it('reports the stall for the home the current selection actually mirrors into', () => {
// Why: a managed account mirrors into its own per-account home, not the
// shared one — reporting on the shared home would miss the stall entirely.
const perAccountHome = join(root, 'codex-accounts', 'acct-1', 'home')
mkdirSync(perAccountHome, { recursive: true })
writeFileSync(join(perAccountHome, 'config.toml'), 'model = "runtime-model"\n', 'utf-8')
expect(invokeHandler(perAccountHome)).toEqual({
state: 'stalled',
reason: 'missing-source',
systemConfigPath: join(root, '.codex', 'config.toml')
})
})
it('reports synced when the selection runs directly on the real home', () => {
// Why: the system default has no mirror, so a stale shared runtime home
// must not produce a warning about a config that lane never reads.
const staleSharedHome = join(root, 'codex-runtime-home', 'home')
mkdirSync(staleSharedHome, { recursive: true })
writeFileSync(join(staleSharedHome, 'config.toml'), 'model = "stale"\n', 'utf-8')
expect(invokeHandler(null)).toEqual({
state: 'synced',
reason: null,
systemConfigPath: join(root, '.codex', 'config.toml')
})
})
it('reports a stall when the selected home baseline cannot be read', () => {
const perAccountHome = join(root, 'codex-accounts', 'acct-1', 'home')
mkdirSync(perAccountHome, { recursive: true })
writeFileSync(join(perAccountHome, 'config.toml'), 'model = "runtime-model"\n', 'utf-8')
writeFileSync(join(root, '.codex', 'config.toml'), 'model = "system-model"\n', 'utf-8')
const baselinePath = getCodexSettingsBaselinePath(perAccountHome)
mkdirSync(baselinePath)
expect(invokeHandler(perAccountHome)).toEqual({
state: 'stalled',
reason: 'managed-home-unavailable',
systemConfigPath: join(root, '.codex', 'config.toml'),
managedStatePath: baselinePath
})
})
it('re-registers cleanly so a reload cannot leak a duplicate handler', () => {
invokeHandler(null)
expect(removeHandlerMock).toHaveBeenCalledWith('codexConfigSync:status')
})
})
// STA-4422: an unreadable managed home must not be reported as healthy. Before
// the fix the resolver collapsed to `null`, which this channel reads as "no
// mirror exists" and reports as synced.
it('reports a managed-home-unavailable stall instead of synced when the home is unreadable', () => {
// Anchor: a genuine no-mirror lane still reports synced.
expect(invokeHandlerWithStatus({ kind: 'ready', homePath: null })).toEqual({
state: 'synced',
reason: null,
systemConfigPath: join(root, '.codex', 'config.toml')
})
expect(invokeHandlerWithStatus({ kind: 'unavailable' })).toEqual({
state: 'stalled',
reason: 'managed-home-unavailable',
systemConfigPath: join(root, '.codex', 'config.toml')
})
})