codeburn/app/electron/quota/security.test.ts
iamtoruk 8d1a10369f fix(app): quota connects via macOS keychain + Connect affordance
Root causes (verified live): the Claude keychain branch was never
enabled (allowClaudeKeychain never passed), security -w returns
hex-encoded JSON the parser rejected, and Codex had no keychain
discovery at all.

- shared readKeychainPassword: Apple-signed /usr/bin/security, 90s
  window so the user can answer the macOS dialog, hex decode,
  notFound vs accessDenied classification; secrets never logged
- Claude: keychain fallback active (gated to explicit Refresh so
  launch never ambushes with a dialog); connection retained across
  keychain-less background polls
- Codex sources in order: the menubar's own cached OAuth (read-only,
  401 = one re-read, never rotate), legacy ~/.codex/auth.json
  (writable), com.openai.codex plaintext if usable; Safe Storage
  never decrypted
- new accessDenied state (amber, 'locked') + ConnectAffordance on
  Plans cards and Settings rows: login command, keychain-Allow note,
  force Refresh

Live verification: Claude connected with its real tier and windows;
Codex correctly accessDenied until the keychain dialog is allowed.

273/273 (26 files), typecheck + build green.
2026-07-16 06:20:44 -07:00

57 lines
3.1 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import { readKeychainPassword } from './security'
// readKeychainPassword short-circuits off darwin; pin the platform so the
// classification logic is exercised on any CI host.
const originalPlatform = process.platform
beforeAll(() => Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true }))
afterAll(() => Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }))
const denied = (message: string) => () => { throw new Error(message) }
const notFound = () => { const error = new Error('The specified item could not be found in the keychain.') as Error & { code: number }; error.code = 44; throw error }
describe('readKeychainPassword', () => {
it('decodes a hex-dumped security(1) payload back to its JSON text', async () => {
const json = JSON.stringify({ claudeAiOauth: { accessToken: 'x' } })
const hex = Buffer.from(json, 'utf8').toString('hex')
const exec = vi.fn(async () => ({ stdout: `${hex}\n` }))
expect(await readKeychainPassword('svc', [null], exec)).toEqual({ status: 'found', value: json })
})
it('passes plain (non-hex) JSON through unchanged', async () => {
const exec = vi.fn(async () => ({ stdout: '{"a":1}' }))
expect(await readKeychainPassword('svc', [null], exec)).toEqual({ status: 'found', value: '{"a":1}' })
})
it('classifies exit 44 / "could not be found" as notFound', async () => {
expect(await readKeychainPassword('svc', [null], vi.fn(notFound))).toEqual({ status: 'notFound' })
})
it('classifies a timeout kill left open on the dialog as accessDenied', async () => {
const exec = vi.fn(async () => { const error = new Error('Command failed') as Error & { killed: boolean; signal: string }; error.killed = true; error.signal = 'SIGTERM'; throw error })
expect(await readKeychainPassword('svc', [null], exec)).toEqual({ status: 'accessDenied' })
})
it('classifies "User interaction is not allowed" and user cancel as accessDenied', async () => {
expect(await readKeychainPassword('svc', [null], vi.fn(denied('SecKeychainItemCopyContent: User interaction is not allowed.')))).toEqual({ status: 'accessDenied' })
expect(await readKeychainPassword('svc', [null], vi.fn(denied('User canceled the operation.')))).toEqual({ status: 'accessDenied' })
})
it('falls through a user-scoped miss to the service-only lookup', async () => {
const exec = vi.fn(async (_file: string, args: string[]) => {
if (args.includes('-a')) return notFound()
return { stdout: '{"ok":true}' }
})
expect(await readKeychainPassword('svc', ['alice', null], exec)).toEqual({ status: 'found', value: '{"ok":true}' })
expect(exec).toHaveBeenCalledTimes(2)
})
it('reports accessDenied when a later candidate is denied after an earlier miss', async () => {
const exec = vi.fn(async (_file: string, args: string[]) => {
if (args.includes('-a')) return notFound()
throw new Error('User interaction is not allowed.')
})
expect(await readKeychainPassword('svc', ['alice', null], exec)).toEqual({ status: 'accessDenied' })
})
})