mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 08:02:51 +00:00
* 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
122 lines
4.5 KiB
TypeScript
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')
|
|
})
|
|
})
|