Files
hapi/shared/src/modes.test.ts
T
ffd950a220 fix(codex): drop safe-yolo from Codex permission modes (#2183)
* fix(codex): drop safe-yolo from Codex permission modes

Every Codex session runs on the native shared runtime (runCodex is
runSharedCodex), which rejects safe-yolo. The mode stayed in
CODEX_PERMISSION_MODES and six filters hid it again in the web picker,
composer, hub permission-mode route, hapi set-mode and both native
apps. API callers that validate against the flavor catalog (e.g. a
spawn with permissionMode: 'safe-yolo') still passed it through and
only failed inside the runner.

Remove it from the Codex catalog (shared, iOS, Android, fixture) and
delete the filters. autoReview is the native replacement. Safe Yolo
stays for Gemini, Kimi and Copilot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ios): Codex option list now has four modes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:12:55 +08:00

191 lines
7.9 KiB
TypeScript

import { describe, expect, it, test } from 'bun:test'
import {
AGENT_FLAVORS,
AgentFlavorSchema,
CREATABLE_AGENT_FLAVORS,
getPermissionModeLabel,
getPermissionModeOptionsForFlavor,
getPermissionModeTone,
getPermissionModesForFlavor,
isPermissionModeAllowedForFlavor,
isSteeringSupportedForFlavor,
isSteeringSupportedForSession,
} from './modes'
describe('Gemini CLI sunset (read-only, not creatable)', () => {
test('gemini stays a valid flavor so existing stored sessions still validate/load', () => {
expect(AGENT_FLAVORS).toContain('gemini')
expect(AgentFlavorSchema.safeParse('gemini').success).toBe(true)
})
test('gemini is excluded from creatable flavors (not offered for new sessions)', () => {
expect(CREATABLE_AGENT_FLAVORS).not.toContain('gemini')
})
test('all other flavors remain creatable', () => {
for (const flavor of AGENT_FLAVORS) {
if (flavor === 'gemini') continue
expect(CREATABLE_AGENT_FLAVORS).toContain(flavor)
}
})
})
describe('getPermissionModesForFlavor', () => {
test("returns the conservative Grok modes", () => {
expect(getPermissionModesForFlavor('grok')).toEqual([
'default',
'auto',
'plan',
'bypassPermissions'
])
})
test("returns no HAPI mode selector for DSH's server-owned permission policy", () => {
expect(getPermissionModesForFlavor('dsh')).toEqual([])
})
test("returns [] for flavor 'pi' (RPC mode has no runtime permission switching)", () => {
expect(getPermissionModesForFlavor('pi')).toEqual([])
})
test("returns [] for pi and does not fall back to Claude modes", () => {
// Ensure Pi is opt-in empty, not silently inheriting Claude defaults.
expect(getPermissionModesForFlavor('pi')).not.toEqual(getPermissionModesForFlavor('claude'))
expect(getPermissionModesForFlavor('pi')).not.toEqual(getPermissionModesForFlavor(null))
})
test("unknown flavors fall back to Claude modes, not Pi's empty list", () => {
expect(getPermissionModesForFlavor(null)).not.toEqual([])
expect(getPermissionModesForFlavor(undefined)).not.toEqual([])
expect(getPermissionModesForFlavor('PI')).not.toEqual([])
expect(getPermissionModesForFlavor('Pi')).not.toEqual([])
})
})
describe('getPermissionModeOptionsForFlavor', () => {
test("returns [] for pi (no permission options offered)", () => {
expect(getPermissionModeOptionsForFlavor('pi')).toEqual([])
})
})
describe('isPermissionModeAllowedForFlavor', () => {
test("allows only the supported Grok modes", () => {
expect(isPermissionModeAllowedForFlavor('default', 'grok')).toBe(true)
expect(isPermissionModeAllowedForFlavor('plan', 'grok')).toBe(true)
expect(isPermissionModeAllowedForFlavor('bypassPermissions', 'grok')).toBe(true)
expect(isPermissionModeAllowedForFlavor('acceptEdits', 'grok')).toBe(false)
expect(isPermissionModeAllowedForFlavor('auto', 'grok')).toBe(true)
expect(isPermissionModeAllowedForFlavor('yolo', 'grok')).toBe(false)
expect(isPermissionModeAllowedForFlavor('read-only', 'dsh')).toBe(false)
expect(isPermissionModeAllowedForFlavor('plan', 'dsh')).toBe(false)
})
test("no mode is allowed for pi", () => {
expect(isPermissionModeAllowedForFlavor('yolo', 'pi')).toBe(false)
expect(isPermissionModeAllowedForFlavor('default', 'pi')).toBe(false)
expect(isPermissionModeAllowedForFlavor('plan', 'pi')).toBe(false)
expect(isPermissionModeAllowedForFlavor('acceptEdits', 'pi')).toBe(false)
expect(isPermissionModeAllowedForFlavor('bypassPermissions', 'pi')).toBe(false)
expect(isPermissionModeAllowedForFlavor('auto', 'pi')).toBe(false)
expect(isPermissionModeAllowedForFlavor('read-only', 'pi')).toBe(false)
expect(isPermissionModeAllowedForFlavor('safe-yolo', 'pi')).toBe(false)
expect(isPermissionModeAllowedForFlavor('ask', 'pi')).toBe(false)
})
test("cursor includes autoReview", () => {
expect(getPermissionModesForFlavor('cursor')).toContain('autoReview')
expect(getPermissionModeLabel('autoReview')).toBe('Auto')
expect(getPermissionModeTone('autoReview')).toBe('warning')
expect(isPermissionModeAllowedForFlavor('autoReview', 'cursor')).toBe(true)
expect(isPermissionModeAllowedForFlavor('autoReview', 'claude')).toBe(false)
})
test("codex offers autoReview at launch and mid-session", () => {
expect(getPermissionModesForFlavor('codex')).toEqual(['default', 'read-only', 'autoReview', 'yolo'])
expect(isPermissionModeAllowedForFlavor('autoReview', 'codex')).toBe(true)
expect(isPermissionModeAllowedForFlavor('autoReview', 'gemini')).toBe(false)
})
})
describe('getPermissionModeLabel', () => {
test("yolo label is 'Yolo'", () => {
expect(getPermissionModeLabel('yolo')).toBe('Yolo')
})
test("default label is 'Default'", () => {
expect(getPermissionModeLabel('default')).toBe('Default')
})
})
describe('getPermissionModeTone', () => {
test("yolo tone is danger", () => {
expect(getPermissionModeTone('yolo')).toBe('danger')
})
test("default tone is neutral", () => {
expect(getPermissionModeTone('default')).toBe('neutral')
})
})
describe('claude auto permission mode', () => {
it('is allowed for claude only', () => {
expect(isPermissionModeAllowedForFlavor('auto', 'claude')).toBe(true)
expect(isPermissionModeAllowedForFlavor('auto', 'codex')).toBe(false)
expect(isPermissionModeAllowedForFlavor('auto', 'gemini')).toBe(false)
expect(isPermissionModeAllowedForFlavor('auto', 'cursor')).toBe(false)
expect(isPermissionModeAllowedForFlavor('auto', 'opencode')).toBe(false)
expect(isPermissionModeAllowedForFlavor('auto', 'kimi')).toBe(false)
expect(isPermissionModeAllowedForFlavor('auto', 'pi')).toBe(false)
})
it('has a label and tone', () => {
expect(getPermissionModeLabel('auto')).toBe('Auto')
expect(getPermissionModeTone('auto')).toBe('warning')
})
})
describe('isSteeringSupportedForFlavor', () => {
it('supports codex, cursor and pi', () => {
expect(isSteeringSupportedForFlavor('codex')).toBe(true)
expect(isSteeringSupportedForFlavor('cursor')).toBe(true)
expect(isSteeringSupportedForFlavor('pi')).toBe(true)
expect(isSteeringSupportedForFlavor('claude')).toBe(false)
expect(isSteeringSupportedForFlavor('opencode')).toBe(false)
expect(isSteeringSupportedForFlavor(undefined)).toBe(false)
expect(isSteeringSupportedForFlavor(null)).toBe(false)
})
})
describe('isSteeringSupportedForSession', () => {
it('supports codex and pi sessions', () => {
expect(isSteeringSupportedForSession({ flavor: 'codex' })).toBe(true)
expect(isSteeringSupportedForSession({ flavor: 'pi' })).toBe(true)
})
it('supports Cursor ACP sessions', () => {
expect(isSteeringSupportedForSession({
flavor: 'cursor',
cursorSessionProtocol: 'acp',
cursorSessionId: 'sess-1',
})).toBe(true)
expect(isSteeringSupportedForSession({ flavor: 'cursor' })).toBe(true)
})
it('rejects legacy Cursor stream-json sessions', () => {
expect(isSteeringSupportedForSession({
flavor: 'cursor',
cursorSessionProtocol: 'stream-json',
cursorSessionId: 'legacy-1',
})).toBe(false)
expect(isSteeringSupportedForSession({
flavor: 'cursor',
cursorSessionId: 'legacy-without-protocol',
})).toBe(false)
})
it('rejects non-steerable flavors', () => {
expect(isSteeringSupportedForSession({ flavor: 'claude' })).toBe(false)
expect(isSteeringSupportedForSession(null)).toBe(false)
})
})