Files
orca/src/main/ipc/settings.test.ts
T

475 lines
17 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from 'vitest'
const {
applyAppIconMock,
applyElectronProxySettingsMock,
browserWindowGetAllWindowsMock,
handleMock,
previewGhosttyImportMock,
previewWarpThemeImportMock,
prepareLocalWorktreeRootsForReposMock,
rebuildAppMenuMock
} = vi.hoisted(() => ({
applyAppIconMock: vi.fn(),
applyElectronProxySettingsMock: vi.fn(),
browserWindowGetAllWindowsMock: vi.fn(),
handleMock: vi.fn(),
previewGhosttyImportMock: vi.fn(),
previewWarpThemeImportMock: vi.fn(),
prepareLocalWorktreeRootsForReposMock: vi.fn(),
rebuildAppMenuMock: vi.fn()
}))
vi.mock('electron', () => ({
BrowserWindow: { getAllWindows: browserWindowGetAllWindowsMock },
ipcMain: { handle: handleMock },
nativeTheme: { themeSource: 'system' }
}))
vi.mock('../ghostty/index', () => ({
previewGhosttyImport: previewGhosttyImportMock
}))
vi.mock('../warp-themes', () => ({
previewWarpThemeImport: previewWarpThemeImportMock
}))
vi.mock('../network/proxy-settings', () => ({
applyElectronProxySettings: applyElectronProxySettingsMock
}))
vi.mock('../app-icon', () => ({
applyAppIcon: applyAppIconMock
}))
vi.mock('../worktree-root-preparation', () => ({
prepareLocalWorktreeRootsForRepos: prepareLocalWorktreeRootsForReposMock
}))
vi.mock('../menu/register-app-menu', () => ({
rebuildAppMenu: rebuildAppMenuMock
}))
import { registerSettingsHandlers } from './settings'
const settingsInvokeEvent = { sender: { id: 1 } }
type SettingsChangedListener = (
updates: unknown,
settings: unknown,
originWebContentsId?: number
) => void
const store = {
getSettings: vi.fn(),
updateSettings: vi.fn(),
getGitHubCache: vi.fn(),
setGitHubCache: vi.fn(),
onSettingsChanged: vi.fn(() => () => {})
}
describe('registerSettingsHandlers', () => {
beforeEach(() => {
handleMock.mockClear()
applyAppIconMock.mockClear()
applyElectronProxySettingsMock.mockClear()
applyElectronProxySettingsMock.mockResolvedValue({ source: 'settings' })
previewGhosttyImportMock.mockClear()
previewWarpThemeImportMock.mockClear()
prepareLocalWorktreeRootsForReposMock.mockReset().mockResolvedValue(undefined)
rebuildAppMenuMock.mockClear()
browserWindowGetAllWindowsMock.mockReset()
store.getSettings.mockReset()
store.updateSettings.mockReset()
store.onSettingsChanged.mockClear()
})
it('registers settings:previewGhosttyImport handler', () => {
registerSettingsHandlers(store as never)
const channels = handleMock.mock.calls.map((call) => call[0])
expect(channels).toContain('settings:previewGhosttyImport')
})
it('registers settings:previewWarpThemeImport handler', () => {
registerSettingsHandlers(store as never)
const channels = handleMock.mock.calls.map((call) => call[0])
expect(channels).toContain('settings:previewWarpThemeImport')
})
it('settings:previewGhosttyImport returns preview result', async () => {
const expected = { found: false, diff: {}, unsupportedKeys: [] }
previewGhosttyImportMock.mockResolvedValue(expected)
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find(
(call) => call[0] === 'settings:previewGhosttyImport'
)?.[1] as (_event: unknown, args: unknown) => Promise<unknown>
const result = await handler!(null, {})
expect(result).toEqual(expected)
expect(previewGhosttyImportMock).toHaveBeenCalledWith(store)
})
it('settings:previewWarpThemeImport returns preview result', async () => {
const expected = { found: false, themes: [], skippedFiles: [] }
previewWarpThemeImportMock.mockResolvedValue(expected)
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find(
(call) => call[0] === 'settings:previewWarpThemeImport'
)?.[1] as (event: { sender: unknown }, args: { kind: 'auto' }) => Promise<unknown>
const sender = { id: 3 }
const result = await handler!({ sender }, { kind: 'auto' })
expect(result).toEqual(expected)
expect(previewWarpThemeImportMock).toHaveBeenCalledWith(store, { kind: 'auto' }, sender)
})
it('settings:previewWarpThemeImport forwards malformed sources for main validation', async () => {
const expected = {
found: false,
themes: [],
skippedFiles: [],
error: 'Invalid Warp theme import source.'
}
previewWarpThemeImportMock.mockResolvedValue(expected)
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find(
(call) => call[0] === 'settings:previewWarpThemeImport'
)?.[1] as (event: { sender: unknown }, args: unknown) => Promise<unknown>
const invalidSource = { kind: 'unknown' }
const sender = { id: 3 }
const result = await handler!({ sender }, invalidSource)
expect(result).toEqual(expected)
expect(previewWarpThemeImportMock).toHaveBeenCalledWith(store, invalidSource, sender)
await handler!({ sender }, null)
expect(previewWarpThemeImportMock).toHaveBeenCalledWith(store, null, sender)
})
it('broadcasts store-level settings changes to open windows', () => {
const send = vi.fn()
browserWindowGetAllWindowsMock.mockReturnValue([
{ isDestroyed: () => false, webContents: { send } },
{ isDestroyed: () => true, webContents: { send: vi.fn() } }
])
registerSettingsHandlers(store as never)
const onSettingsChanged = store.onSettingsChanged as unknown as {
mock: { calls: [SettingsChangedListener][] }
}
const listener = onSettingsChanged.mock.calls[0]?.[0]
if (!listener) {
throw new Error('settings change listener was not registered')
}
listener({ defaultTuiAgent: 'codex' }, { defaultTuiAgent: 'codex' })
expect(send).toHaveBeenCalledWith('settings:changed', { defaultTuiAgent: 'codex' })
})
it('does not rebroadcast renderer settings writes to the origin window', () => {
const originSend = vi.fn()
const otherSend = vi.fn()
browserWindowGetAllWindowsMock.mockReturnValue([
{ isDestroyed: () => false, webContents: { id: 1, send: originSend } },
{ isDestroyed: () => false, webContents: { id: 2, send: otherSend } }
])
registerSettingsHandlers(store as never)
const onSettingsChanged = store.onSettingsChanged as unknown as {
mock: { calls: [SettingsChangedListener][] }
}
const listener = onSettingsChanged.mock.calls[0]?.[0]
if (!listener) {
throw new Error('settings change listener was not registered')
}
listener({ defaultTuiAgent: 'codex' }, { defaultTuiAgent: 'codex' }, 1)
expect(originSend).not.toHaveBeenCalled()
expect(otherSend).toHaveBeenCalledWith('settings:changed', { defaultTuiAgent: 'codex' })
})
it('updates the agent awake service when the keep-awake setting changes', () => {
const agentAwakeService = { setEnabled: vi.fn() }
store.getSettings.mockReturnValue({ keepComputerAwakeWhileAgentsRun: false })
store.updateSettings.mockReturnValue({ keepComputerAwakeWhileAgentsRun: true })
registerSettingsHandlers(store as never, agentAwakeService as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => unknown
handler(settingsInvokeEvent, { keepComputerAwakeWhileAgentsRun: true })
expect(agentAwakeService.setEnabled).toHaveBeenCalledWith(true)
})
it('does not notify the agent awake service for unrelated setting changes', () => {
const agentAwakeService = { setEnabled: vi.fn() }
store.getSettings.mockReturnValue({ keepComputerAwakeWhileAgentsRun: false })
store.updateSettings.mockReturnValue({ keepComputerAwakeWhileAgentsRun: false })
registerSettingsHandlers(store as never, agentAwakeService as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => unknown
handler(settingsInvokeEvent, { defaultTuiAgent: 'codex' })
expect(agentAwakeService.setEnabled).not.toHaveBeenCalled()
})
it('prepares local worktree roots when workspace directory changes', async () => {
store.getSettings.mockReturnValue({ workspaceDir: '/old/workspaces', nestWorkspaces: false })
store.updateSettings.mockReturnValue({ workspaceDir: '/new/workspaces', nestWorkspaces: false })
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => Promise<unknown>
await handler(settingsInvokeEvent, { workspaceDir: '/new/workspaces' })
expect(prepareLocalWorktreeRootsForReposMock).toHaveBeenCalledWith(store)
})
it('prepares local worktree roots when workspace nesting changes', async () => {
store.getSettings.mockReturnValue({ workspaceDir: '/workspaces', nestWorkspaces: false })
store.updateSettings.mockReturnValue({ workspaceDir: '/workspaces', nestWorkspaces: true })
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => Promise<unknown>
await handler(settingsInvokeEvent, { nestWorkspaces: true })
expect(prepareLocalWorktreeRootsForReposMock).toHaveBeenCalledWith(store)
})
it('does not prepare local worktree roots when workspace layout values do not change', async () => {
store.getSettings.mockReturnValue({ workspaceDir: '/workspaces', nestWorkspaces: false })
store.updateSettings.mockReturnValue({ workspaceDir: '/workspaces', nestWorkspaces: false })
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => Promise<unknown>
await handler(settingsInvokeEvent, { workspaceDir: '/workspaces', nestWorkspaces: false })
expect(prepareLocalWorktreeRootsForReposMock).not.toHaveBeenCalled()
})
it('does not accept floating workspace trust grants from renderer settings IPC', async () => {
store.getSettings.mockReturnValue({ floatingTerminalTrustedCwds: [] })
store.updateSettings.mockReturnValue({ floatingTerminalTrustedCwds: [] })
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => Promise<unknown>
await handler(settingsInvokeEvent, { floatingTerminalTrustedCwds: ['/tmp/notes'] })
expect(store.updateSettings).toHaveBeenCalledWith(
{},
{ notifyListeners: true, originWebContentsId: 1 }
)
})
it('normalizes terminal scrollback row updates and drops legacy byte updates', async () => {
store.getSettings.mockReturnValue({ terminalScrollbackRows: 5_000 })
store.updateSettings.mockReturnValue({ terminalScrollbackRows: 50_000 })
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => Promise<unknown>
await handler(settingsInvokeEvent, {
terminalScrollbackRows: 75_000,
terminalScrollbackBytes: 250_000_000
})
expect(store.updateSettings).toHaveBeenCalledWith(
{ terminalScrollbackRows: 50_000 },
{ notifyListeners: true, originWebContentsId: 1 }
)
})
it('normalizes terminal line height updates before persistence', async () => {
store.getSettings.mockReturnValue({ terminalLineHeight: 1 })
store.updateSettings.mockReturnValue({ terminalLineHeight: 1 })
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => Promise<unknown>
await handler(settingsInvokeEvent, { terminalLineHeight: 0.85 })
expect(store.updateSettings).toHaveBeenCalledWith(
{ terminalLineHeight: 1 },
{ notifyListeners: true, originWebContentsId: 1 }
)
})
it('normalizes custom terminal themes from renderer settings IPC', async () => {
store.getSettings.mockReturnValue({ terminalCustomThemes: [] })
store.updateSettings.mockReturnValue({ terminalCustomThemes: [] })
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => Promise<unknown>
await handler(settingsInvokeEvent, {
terminalCustomThemes: [
{
id: 'warp:Test Theme',
name: 'Test Theme',
source: 'warp',
mode: 'dark',
terminal: {
background: '000',
foreground: 'fff',
black: '123',
red: 'nope'
},
sourcePath: '/Users/alice/.warp/themes/test.yaml'
}
]
})
expect(store.updateSettings).toHaveBeenCalledWith(
{
terminalCustomThemes: [
expect.objectContaining({
id: 'warp:test-theme',
terminal: {
background: '#000000',
foreground: '#ffffff',
black: '#112233'
}
})
]
},
{ notifyListeners: true, originWebContentsId: 1 }
)
})
it('sanitizes and applies proxy settings from renderer settings IPC', async () => {
store.getSettings.mockReturnValue({ httpProxyUrl: '' })
store.updateSettings.mockReturnValue({
httpProxyUrl: 'http://proxy.example:8080',
httpProxyBypassRules: 'localhost;*.internal'
})
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => Promise<unknown>
await handler(settingsInvokeEvent, {
httpProxyUrl: ' http://proxy.example:8080/path#frag ',
httpProxyBypassRules: 'localhost, *.internal'
})
expect(store.updateSettings).toHaveBeenCalledWith(
{
httpProxyUrl: 'http://proxy.example:8080',
httpProxyBypassRules: 'localhost;*.internal'
},
{ notifyListeners: true, originWebContentsId: 1 }
)
expect(applyElectronProxySettingsMock).toHaveBeenCalledWith({
httpProxyUrl: 'http://proxy.example:8080',
httpProxyBypassRules: 'localhost;*.internal'
})
})
it('drops invalid proxy URLs at the settings boundary', async () => {
store.getSettings.mockReturnValue({ httpProxyUrl: 'http://proxy.example:8080' })
store.updateSettings.mockReturnValue({ httpProxyUrl: '' })
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => Promise<unknown>
await handler(settingsInvokeEvent, { httpProxyUrl: 'ftp://proxy.example:2121' })
expect(store.updateSettings).toHaveBeenCalledWith(
{ httpProxyUrl: '' },
{ notifyListeners: true, originWebContentsId: 1 }
)
expect(applyElectronProxySettingsMock).toHaveBeenCalledWith({ httpProxyUrl: '' })
})
it('normalizes and applies app icon changes from renderer settings IPC', async () => {
store.getSettings.mockReturnValue({ appIcon: 'classic' })
store.updateSettings.mockReturnValue({ appIcon: 'watercolor' })
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => Promise<unknown>
await handler(settingsInvokeEvent, { appIcon: 'watercolor' })
expect(store.updateSettings).toHaveBeenCalledWith(
{ appIcon: 'watercolor' },
{ notifyListeners: true, originWebContentsId: 1 }
)
expect(applyAppIconMock).toHaveBeenCalledWith('watercolor')
})
it('falls back to the classic app icon for invalid renderer settings IPC values', async () => {
store.getSettings.mockReturnValue({ appIcon: 'watercolor' })
store.updateSettings.mockReturnValue({ appIcon: 'classic' })
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => Promise<unknown>
await handler(settingsInvokeEvent, { appIcon: 'not-real' })
expect(store.updateSettings).toHaveBeenCalledWith(
{ appIcon: 'classic' },
{ notifyListeners: true, originWebContentsId: 1 }
)
expect(applyAppIconMock).toHaveBeenCalledWith('classic')
})
it('rebuilds the app menu after Automations sidebar visibility changes', async () => {
store.getSettings.mockReturnValue({ showAutomationsButton: true })
store.updateSettings.mockReturnValue({ showAutomationsButton: false })
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
_event: unknown,
args: unknown
) => Promise<unknown>
await handler(settingsInvokeEvent, { showAutomationsButton: false })
expect(rebuildAppMenuMock).toHaveBeenCalledTimes(1)
})
})