| 1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' |
| 2 | |
| 3 | const settingsHandlersState = vi.hoisted(() => { |
| 4 | const handlers = new Map<string, (...args: unknown[]) => Promise<unknown>>() |
| 5 | |
| 6 | return { |
| 7 | appMock: { |
| 8 | getVersion: vi.fn(() => '1.0.0') |
| 9 | }, |
| 10 | applyProxyMock: vi.fn(), |
| 11 | dialogMock: { |
| 12 | showOpenDialog: vi.fn() |
| 13 | }, |
| 14 | handlers, |
| 15 | ipcMainMock: { |
| 16 | handle: vi.fn((channel: string, handler: (...args: unknown[]) => Promise<unknown>) => { |
| 17 | handlers.set(channel, handler) |
| 18 | }) |
| 19 | }, |
| 20 | localeMock: { |
| 21 | readAppLocale: vi.fn(async () => 'zh'), |
| 22 | uiText: vi.fn((locale: string, zh: string, en: string) => (locale === 'en' ? en : zh)) |
| 23 | }, |
| 24 | logMock: { |
| 25 | error: vi.fn(), |
| 26 | info: vi.fn(), |
| 27 | warn: vi.fn() |
| 28 | }, |
| 29 | resolveModelMock: vi.fn() |
| 30 | } |
| 31 | }) |
| 32 | |
| 33 | vi.mock('electron', () => ({ |
| 34 | BrowserWindow: { |
| 35 | fromWebContents: vi.fn(), |
| 36 | getFocusedWindow: vi.fn() |
| 37 | }, |
| 38 | app: settingsHandlersState.appMock, |
| 39 | dialog: settingsHandlersState.dialogMock, |
| 40 | ipcMain: settingsHandlersState.ipcMainMock |
| 41 | })) |
| 42 | |
| 43 | vi.mock('electron-log/main.js', () => ({ |
| 44 | default: settingsHandlersState.logMock |
| 45 | })) |
| 46 | |
| 47 | vi.mock('../../../src/main/agent-runtime/model', () => ({ |
| 48 | resolveModel: settingsHandlersState.resolveModelMock, |
| 49 | runWithModelTemperatureControl: <T>(_config: unknown, task: () => T): T => task(), |
| 50 | OPENAI_RESPONSES_FORMAT_ERROR_EN: 'Invalid OpenAI Responses API payload.', |
| 51 | OPENAI_RESPONSES_FORMAT_ERROR_ZH: '当前 provider 返回的不是 OpenAI Responses API 格式。', |
| 52 | isOpenAIResponsesFormatError: (error: unknown) => |
| 53 | /Cannot read propert(?:y|ies).*undefined.*map|Cannot read propert(?:y|ies).*map.*undefined/i.test( |
| 54 | error instanceof Error ? error.message : '' |
| 55 | ) |
| 56 | })) |
| 57 | |
| 58 | vi.mock('../../../src/main/utils/proxy', () => ({ |
| 59 | applyProxy: settingsHandlersState.applyProxyMock |
| 60 | })) |
| 61 | |
| 62 | vi.mock('../../../src/main/config/locale-utils', () => ({ |
| 63 | readAppLocale: settingsHandlersState.localeMock.readAppLocale, |
| 64 | uiText: settingsHandlersState.localeMock.uiText |
| 65 | })) |
| 66 | |
| 67 | vi.mock('@shared/model-timeout', () => ({ |
| 68 | CONFIGURABLE_MODEL_TIMEOUT_PROFILES: ['planning', 'design', 'agent', 'document'], |
| 69 | resolveModelTimeoutMs: vi.fn((value: unknown, profile: string) => { |
| 70 | if (typeof value === 'number' && Number.isFinite(value)) return value |
| 71 | const defaults: Record<string, number> = { |
| 72 | planning: 300000, |
| 73 | design: 300000, |
| 74 | agent: 600000, |
| 75 | document: 600000 |
| 76 | } |
| 77 | return defaults[profile] ?? 300000 |
| 78 | }) |
| 79 | })) |
| 80 | |
| 81 | async function registerWithDb(overrides: Partial<Record<string, unknown>> = {}) { |
| 82 | vi.resetModules() |
| 83 | settingsHandlersState.handlers.clear() |
| 84 | |
| 85 | const { registerSettingsHandlers } = await import('../../../src/main/config/settings-handlers') |
| 86 | |
| 87 | const db = { |
| 88 | getAllSettings: vi.fn(async () => ({})), |
| 89 | listModelConfigs: vi.fn(async () => []), |
| 90 | setSetting: vi.fn(async () => undefined), |
| 91 | setStoragePath: vi.fn(async () => undefined), |
| 92 | ...overrides |
| 93 | } |
| 94 | |
| 95 | const ctx = { |
| 96 | mainWindow: {} as never, |
| 97 | db, |
| 98 | encryptApiKey: vi.fn((value: string) => value), |
| 99 | decryptApiKey: vi.fn((value: unknown) => String(value ?? '')) |
| 100 | } |
| 101 | |
| 102 | registerSettingsHandlers(ctx as never) |
| 103 | |
| 104 | return { |
| 105 | db, |
| 106 | getHandler: (channel: string) => settingsHandlersState.handlers.get(channel) |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | describe('registerSettingsHandlers proxy settings', () => { |
| 111 | beforeEach(() => { |
| 112 | settingsHandlersState.applyProxyMock.mockReset() |
| 113 | settingsHandlersState.appMock.getVersion.mockClear() |
| 114 | settingsHandlersState.dialogMock.showOpenDialog.mockReset() |
| 115 | settingsHandlersState.handlers.clear() |
| 116 | settingsHandlersState.ipcMainMock.handle.mockClear() |
| 117 | settingsHandlersState.localeMock.readAppLocale.mockReset() |
| 118 | settingsHandlersState.localeMock.readAppLocale.mockResolvedValue('zh') |
| 119 | settingsHandlersState.localeMock.uiText.mockClear() |
| 120 | settingsHandlersState.logMock.error.mockClear() |
| 121 | settingsHandlersState.logMock.info.mockClear() |
| 122 | settingsHandlersState.resolveModelMock.mockReset() |
| 123 | }) |
| 124 | |
| 125 | it('returns trimmed proxyUrl from settings:get', async () => { |
| 126 | const { getHandler } = await registerWithDb({ |
| 127 | getAllSettings: vi.fn(async () => ({ |
| 128 | locale: 'en', |
| 129 | proxy_url: ' http://127.0.0.1:7890 ', |
| 130 | storage_path: ' /tmp/workspace ' |
| 131 | })) |
| 132 | }) |
| 133 | |
| 134 | const getSettings = getHandler('settings:get') |
| 135 | const result = await getSettings?.() |
| 136 | |
| 137 | expect(result).toMatchObject({ |
| 138 | locale: 'en', |
| 139 | proxyUrl: 'http://127.0.0.1:7890', |
| 140 | storagePath: '/tmp/workspace' |
| 141 | }) |
| 142 | }) |
| 143 | |
| 144 | it('applies proxy before persisting proxy_url in settings:save', async () => { |
| 145 | const callOrder: string[] = [] |
| 146 | settingsHandlersState.applyProxyMock.mockImplementation(() => { |
| 147 | callOrder.push('apply') |
| 148 | }) |
| 149 | const db = { |
| 150 | setSetting: vi.fn(async (key: string) => { |
| 151 | if (key === 'proxy_url') { |
| 152 | callOrder.push('persist') |
| 153 | } |
| 154 | }) |
| 155 | } |
| 156 | const { getHandler } = await registerWithDb(db) |
| 157 | |
| 158 | const saveSettings = getHandler('settings:save') |
| 159 | const result = await saveSettings?.(undefined, { |
| 160 | proxyUrl: ' http://127.0.0.1:7890 ' |
| 161 | }) |
| 162 | |
| 163 | expect(result).toEqual({ success: true }) |
| 164 | expect(settingsHandlersState.applyProxyMock).toHaveBeenCalledWith({ |
| 165 | url: 'http://127.0.0.1:7890', |
| 166 | username: '', |
| 167 | password: '', |
| 168 | noProxy: '' |
| 169 | }) |
| 170 | expect(db.setSetting).toHaveBeenCalledWith('proxy_url', 'http://127.0.0.1:7890') |
| 171 | expect(callOrder).toEqual(['apply', 'persist']) |
| 172 | }) |
| 173 | |
| 174 | it('does not persist proxy_url when applyProxy fails', async () => { |
| 175 | settingsHandlersState.applyProxyMock.mockImplementation(() => { |
| 176 | throw new Error('bad proxy') |
| 177 | }) |
| 178 | const db = { |
| 179 | setSetting: vi.fn(async () => undefined) |
| 180 | } |
| 181 | const { getHandler } = await registerWithDb(db) |
| 182 | |
| 183 | const saveSettings = getHandler('settings:save') |
| 184 | |
| 185 | await expect( |
| 186 | saveSettings?.(undefined, { |
| 187 | proxyUrl: 'http://broken-proxy' |
| 188 | }) |
| 189 | ).rejects.toThrow('代理设置无效:bad proxy') |
| 190 | |
| 191 | expect(settingsHandlersState.localeMock.readAppLocale).toHaveBeenCalled() |
| 192 | expect(db.setSetting).not.toHaveBeenCalledWith('proxy_url', expect.anything()) |
| 193 | }) |
| 194 | |
| 195 | it('clears persisted proxy when saving an empty proxyUrl', async () => { |
| 196 | const db = { |
| 197 | setSetting: vi.fn(async () => undefined) |
| 198 | } |
| 199 | const { getHandler } = await registerWithDb(db) |
| 200 | |
| 201 | const saveSettings = getHandler('settings:save') |
| 202 | await saveSettings?.(undefined, { proxyUrl: ' ' }) |
| 203 | |
| 204 | expect(settingsHandlersState.applyProxyMock).toHaveBeenCalledWith({ |
| 205 | url: '', |
| 206 | username: '', |
| 207 | password: '', |
| 208 | noProxy: '' |
| 209 | }) |
| 210 | expect(db.setSetting).toHaveBeenCalledWith('proxy_url', '') |
| 211 | }) |
| 212 | |
| 213 | it('saves proxy credentials and no_proxy list with the proxy address', async () => { |
| 214 | const db = { |
| 215 | setSetting: vi.fn(async () => undefined) |
| 216 | } |
| 217 | const { getHandler } = await registerWithDb(db) |
| 218 | |
| 219 | const saveSettings = getHandler('settings:save') |
| 220 | await saveSettings?.(undefined, { |
| 221 | proxyUrl: 'http://127.0.0.1:7890', |
| 222 | proxyUsername: ' corp\\user ', |
| 223 | proxyPassword: 'secret', |
| 224 | proxyNoProxy: ' localhost,10.0.0.0/8 ' |
| 225 | }) |
| 226 | |
| 227 | expect(settingsHandlersState.applyProxyMock).toHaveBeenCalledWith({ |
| 228 | url: 'http://127.0.0.1:7890', |
| 229 | username: 'corp\\user', |
| 230 | password: 'secret', |
| 231 | noProxy: 'localhost,10.0.0.0/8' |
| 232 | }) |
| 233 | expect(db.setSetting).toHaveBeenCalledWith('proxy_username', 'corp\\user') |
| 234 | expect(db.setSetting).toHaveBeenCalledWith('proxy_password', 'secret') |
| 235 | expect(db.setSetting).toHaveBeenCalledWith('proxy_no_proxy', 'localhost,10.0.0.0/8') |
| 236 | }) |
| 237 | |
| 238 | it('keeps stored proxy credentials when saving only the proxy address', async () => { |
| 239 | const db = { |
| 240 | getAllSettings: vi.fn(async () => ({ |
| 241 | proxy_username: 'corp\\user', |
| 242 | proxy_password: 'enc:v1:cipher', |
| 243 | proxy_no_proxy: '10.0.0.0/8' |
| 244 | })), |
| 245 | setSetting: vi.fn(async () => undefined) |
| 246 | } |
| 247 | const { getHandler, db: registeredDb } = await registerWithDb(db) |
| 248 | |
| 249 | const saveSettings = getHandler('settings:save') |
| 250 | await saveSettings?.(undefined, { proxyUrl: 'http://127.0.0.1:7890' }) |
| 251 | |
| 252 | expect(settingsHandlersState.applyProxyMock).toHaveBeenCalledWith({ |
| 253 | url: 'http://127.0.0.1:7890', |
| 254 | username: 'corp\\user', |
| 255 | password: 'enc:v1:cipher', |
| 256 | noProxy: '10.0.0.0/8' |
| 257 | }) |
| 258 | expect(registeredDb.setSetting).not.toHaveBeenCalledWith('proxy_password', expect.anything()) |
| 259 | }) |
| 260 | }) |
| 261 | |
| 262 | describe('registerSettingsHandlers model list fetching', () => { |
| 263 | beforeEach(() => { |
| 264 | settingsHandlersState.handlers.clear() |
| 265 | settingsHandlersState.ipcMainMock.handle.mockClear() |
| 266 | settingsHandlersState.localeMock.readAppLocale.mockResolvedValue('zh') |
| 267 | settingsHandlersState.logMock.error.mockClear() |
| 268 | settingsHandlersState.logMock.info.mockClear() |
| 269 | }) |
| 270 | |
| 271 | afterEach(() => { |
| 272 | vi.unstubAllGlobals() |
| 273 | }) |
| 274 | |
| 275 | it('fetches the model list from the provider endpoint', async () => { |
| 276 | const fetchMock = vi.fn( |
| 277 | async () => |
| 278 | new Response( |
| 279 | JSON.stringify({ data: [{ id: 'glm-5.3' }, { id: 'deepseek-v4-flash' }] }), |
| 280 | { status: 200 } |
| 281 | ) |
| 282 | ) |
| 283 | vi.stubGlobal('fetch', fetchMock) |
| 284 | const { getHandler } = await registerWithDb() |
| 285 | |
| 286 | const fetchModelList = getHandler('settings:fetchModelList') |
| 287 | const result = await fetchModelList?.(undefined, { |
| 288 | provider: 'openai', |
| 289 | apiKey: 'secret', |
| 290 | baseUrl: 'https://llm.corp.internal/v1' |
| 291 | }) |
| 292 | |
| 293 | expect(result).toEqual({ models: ['deepseek-v4-flash', 'glm-5.3'], message: null }) |
| 294 | expect(fetchMock).toHaveBeenCalledWith( |
| 295 | 'https://llm.corp.internal/v1/models', |
| 296 | expect.objectContaining({ |
| 297 | headers: { Authorization: 'Bearer secret' } |
| 298 | }) |
| 299 | ) |
| 300 | }) |
| 301 | |
| 302 | it('requires the api key before fetching', async () => { |
| 303 | const fetchMock = vi.fn() |
| 304 | vi.stubGlobal('fetch', fetchMock) |
| 305 | const { getHandler } = await registerWithDb() |
| 306 | |
| 307 | const fetchModelList = getHandler('settings:fetchModelList') |
| 308 | const result = await fetchModelList?.(undefined, { |
| 309 | provider: 'openai', |
| 310 | apiKey: ' ', |
| 311 | baseUrl: '' |
| 312 | }) |
| 313 | |
| 314 | expect(result).toEqual({ models: [], message: '请先填写 api_key。' }) |
| 315 | expect(fetchMock).not.toHaveBeenCalled() |
| 316 | }) |
| 317 | |
| 318 | it('reports http failures without throwing', async () => { |
| 319 | vi.stubGlobal('fetch', vi.fn(async () => new Response('not found', { status: 404 }))) |
| 320 | const { getHandler } = await registerWithDb() |
| 321 | |
| 322 | const fetchModelList = getHandler('settings:fetchModelList') |
| 323 | const result = await fetchModelList?.(undefined, { |
| 324 | provider: 'anthropic', |
| 325 | apiKey: 'secret', |
| 326 | baseUrl: 'https://gw.corp.cn' |
| 327 | }) |
| 328 | |
| 329 | expect(result).toEqual({ |
| 330 | models: [], |
| 331 | message: expect.stringContaining('HTTP 404') |
| 332 | }) |
| 333 | }) |
| 334 | |
| 335 | it('reports empty model lists with a hint', async () => { |
| 336 | vi.stubGlobal( |
| 337 | 'fetch', |
| 338 | vi.fn(async () => new Response(JSON.stringify({ data: [] }), { status: 200 })) |
| 339 | ) |
| 340 | const { getHandler } = await registerWithDb() |
| 341 | |
| 342 | const fetchModelList = getHandler('settings:fetchModelList') |
| 343 | const result = await fetchModelList?.(undefined, { |
| 344 | provider: 'openai', |
| 345 | apiKey: 'secret', |
| 346 | baseUrl: '' |
| 347 | }) |
| 348 | |
| 349 | expect(result).toEqual({ models: [], message: expect.stringContaining('未返回任何模型') }) |
| 350 | }) |
| 351 | }) |
| 352 | |
| 353 | describe('registerSettingsHandlers model temperature settings', () => { |
| 354 | beforeEach(() => { |
| 355 | settingsHandlersState.handlers.clear() |
| 356 | settingsHandlersState.ipcMainMock.handle.mockClear() |
| 357 | settingsHandlersState.localeMock.readAppLocale.mockResolvedValue('zh') |
| 358 | settingsHandlersState.resolveModelMock.mockReset() |
| 359 | }) |
| 360 | |
| 361 | it('returns disableTemperature in the model config list', async () => { |
| 362 | const { getHandler } = await registerWithDb({ |
| 363 | listModelConfigs: vi.fn(async () => [ |
| 364 | { |
| 365 | id: 'model-1', |
| 366 | name: 'Reasoning model', |
| 367 | provider: 'openai', |
| 368 | model: 'reasoner', |
| 369 | apiKey: 'secret', |
| 370 | baseUrl: '', |
| 371 | maxTokens: 4096, |
| 372 | disableTemperature: 1, |
| 373 | thinkingParameterMode: 'omit', |
| 374 | active: 1, |
| 375 | createdAt: 1, |
| 376 | updatedAt: 2 |
| 377 | } |
| 378 | ]) |
| 379 | }) |
| 380 | |
| 381 | const listModelConfigs = getHandler('settings:listModelConfigs') |
| 382 | await expect(listModelConfigs?.()).resolves.toEqual([ |
| 383 | expect.objectContaining({ |
| 384 | id: 'model-1', |
| 385 | disableTemperature: true, |
| 386 | thinkingParameterMode: 'omit' |
| 387 | }) |
| 388 | ]) |
| 389 | }) |
| 390 | |
| 391 | it('persists parameter controls when saving a model config', async () => { |
| 392 | const upsertModelConfig = vi.fn(async () => 'model-1') |
| 393 | const { getHandler } = await registerWithDb({ upsertModelConfig }) |
| 394 | |
| 395 | const saveModelConfig = getHandler('settings:upsertModelConfig') |
| 396 | await saveModelConfig?.(undefined, { |
| 397 | name: 'Reasoning model', |
| 398 | provider: 'openai', |
| 399 | model: 'reasoner', |
| 400 | apiKey: 'secret', |
| 401 | baseUrl: '', |
| 402 | maxTokens: 4096, |
| 403 | disableTemperature: true, |
| 404 | thinkingParameterMode: 'omit' |
| 405 | }) |
| 406 | |
| 407 | expect(upsertModelConfig).toHaveBeenCalledWith( |
| 408 | expect.objectContaining({ |
| 409 | disableTemperature: true, |
| 410 | thinkingParameterMode: 'omit' |
| 411 | }) |
| 412 | ) |
| 413 | }) |
| 414 | |
| 415 | it('accepts the OpenAI Responses provider when saving a model config', async () => { |
| 416 | const upsertModelConfig = vi.fn(async () => 'model-1') |
| 417 | const { getHandler } = await registerWithDb({ upsertModelConfig }) |
| 418 | |
| 419 | const saveModelConfig = getHandler('settings:upsertModelConfig') |
| 420 | await saveModelConfig?.(undefined, { |
| 421 | name: 'Responses model', |
| 422 | provider: 'openai-responses', |
| 423 | model: 'gpt-5.1', |
| 424 | apiKey: 'secret', |
| 425 | baseUrl: 'https://api.openai.com/v1', |
| 426 | maxTokens: 4096, |
| 427 | disableTemperature: false, |
| 428 | thinkingParameterMode: 'not-valid' |
| 429 | }) |
| 430 | |
| 431 | expect(upsertModelConfig).toHaveBeenCalledWith( |
| 432 | expect.objectContaining({ |
| 433 | provider: 'openai-responses', |
| 434 | thinkingParameterMode: 'auto' |
| 435 | }) |
| 436 | ) |
| 437 | }) |
| 438 | |
| 439 | it('explains invalid Responses API payloads during model verification', async () => { |
| 440 | settingsHandlersState.localeMock.readAppLocale.mockResolvedValue('zh') |
| 441 | settingsHandlersState.resolveModelMock.mockReturnValue({ |
| 442 | invoke: vi.fn(async () => { |
| 443 | throw new TypeError("Cannot read properties of undefined (reading 'map')") |
| 444 | }) |
| 445 | }) |
| 446 | const { getHandler } = await registerWithDb() |
| 447 | |
| 448 | const verifyApiKey = getHandler('settings:verifyApiKey') |
| 449 | const result = await verifyApiKey?.(undefined, { |
| 450 | provider: 'openai-responses', |
| 451 | model: 'gpt-5.5', |
| 452 | apiKey: 'secret', |
| 453 | baseUrl: 'https://www.toumingren.xyz/v1', |
| 454 | maxTokens: 4096, |
| 455 | timeoutMs: 60000 |
| 456 | }) |
| 457 | |
| 458 | expect(result).toEqual({ |
| 459 | valid: false, |
| 460 | message: expect.stringContaining('不是 OpenAI Responses API 格式') |
| 461 | }) |
| 462 | }) |
| 463 | |
| 464 | it('explains unsupported thinking parameter errors during model verification', async () => { |
| 465 | settingsHandlersState.localeMock.readAppLocale.mockResolvedValue('zh') |
| 466 | settingsHandlersState.resolveModelMock.mockReturnValue({ |
| 467 | invoke: vi.fn(async () => { |
| 468 | throw new Error('Unsupported parameter: thinking') |
| 469 | }) |
| 470 | }) |
| 471 | const { getHandler } = await registerWithDb() |
| 472 | |
| 473 | const verifyApiKey = getHandler('settings:verifyApiKey') |
| 474 | const result = await verifyApiKey?.(undefined, { |
| 475 | provider: 'openai', |
| 476 | model: 'compatible-model', |
| 477 | apiKey: 'secret', |
| 478 | baseUrl: 'https://api.example-compatible.com/v1', |
| 479 | maxTokens: 4096, |
| 480 | thinkingParameterMode: 'auto', |
| 481 | timeoutMs: 60000 |
| 482 | }) |
| 483 | |
| 484 | expect(result).toEqual({ |
| 485 | valid: false, |
| 486 | message: expect.stringContaining('不发送 thinking 参数') |
| 487 | }) |
| 488 | }) |
| 489 | |
| 490 | it('explains unrecognized thinking argument errors during model verification', async () => { |
| 491 | settingsHandlersState.localeMock.readAppLocale.mockResolvedValue('zh') |
| 492 | settingsHandlersState.resolveModelMock.mockReturnValue({ |
| 493 | invoke: vi.fn(async () => { |
| 494 | throw new Error('Unrecognized request argument supplied: thinking') |
| 495 | }) |
| 496 | }) |
| 497 | const { getHandler } = await registerWithDb() |
| 498 | |
| 499 | const verifyApiKey = getHandler('settings:verifyApiKey') |
| 500 | const result = await verifyApiKey?.(undefined, { |
| 501 | provider: 'openai', |
| 502 | model: 'compatible-model', |
| 503 | apiKey: 'secret', |
| 504 | baseUrl: 'https://api.example-compatible.com/v1', |
| 505 | maxTokens: 4096, |
| 506 | thinkingParameterMode: 'auto', |
| 507 | timeoutMs: 60000 |
| 508 | }) |
| 509 | |
| 510 | expect(result).toEqual({ |
| 511 | valid: false, |
| 512 | message: expect.stringContaining('不发送 thinking 参数') |
| 513 | }) |
| 514 | }) |
| 515 | |
| 516 | it('explains older undefined map errors during Responses API verification', async () => { |
| 517 | settingsHandlersState.localeMock.readAppLocale.mockResolvedValue('zh') |
| 518 | settingsHandlersState.resolveModelMock.mockReturnValue({ |
| 519 | invoke: vi.fn(async () => { |
| 520 | throw new TypeError("Cannot read property 'map' of undefined") |
| 521 | }) |
| 522 | }) |
| 523 | const { getHandler } = await registerWithDb() |
| 524 | |
| 525 | const verifyApiKey = getHandler('settings:verifyApiKey') |
| 526 | const result = await verifyApiKey?.(undefined, { |
| 527 | provider: 'openai-responses', |
| 528 | model: 'gpt-5.5', |
| 529 | apiKey: 'secret', |
| 530 | baseUrl: 'https://www.toumingren.xyz/v1', |
| 531 | maxTokens: 4096, |
| 532 | timeoutMs: 60000 |
| 533 | }) |
| 534 | |
| 535 | expect(result).toEqual({ |
| 536 | valid: false, |
| 537 | message: expect.stringContaining('OpenAI Responses API') |
| 538 | }) |
| 539 | }) |
| 540 | }) |
| 541 | |
| 542 | describe('registerSettingsHandlers model usage', () => { |
| 543 | beforeEach(() => { |
| 544 | settingsHandlersState.handlers.clear() |
| 545 | settingsHandlersState.ipcMainMock.handle.mockClear() |
| 546 | }) |
| 547 | |
| 548 | it('delegates the selected usage period to the database', async () => { |
| 549 | const stats = { |
| 550 | period: '7d', |
| 551 | startedAt: 1, |
| 552 | totals: { |
| 553 | callCount: 2, |
| 554 | exactCallCount: 1, |
| 555 | estimatedCallCount: 1, |
| 556 | inputTokens: 100, |
| 557 | outputTokens: 20, |
| 558 | totalTokens: 120 |
| 559 | }, |
| 560 | byModel: [], |
| 561 | byDay: [] |
| 562 | } |
| 563 | const getModelUsageStats = vi.fn(async () => stats) |
| 564 | const { getHandler } = await registerWithDb({ getModelUsageStats }) |
| 565 | |
| 566 | await expect(getHandler('settings:getModelUsage')?.(undefined, '7d')).resolves.toBe(stats) |
| 567 | expect(getModelUsageStats).toHaveBeenCalledWith('7d') |
| 568 | }) |
| 569 | |
| 570 | it('falls back to 30 days for an invalid usage period', async () => { |
| 571 | const getModelUsageStats = vi.fn(async () => ({ period: '30d' })) |
| 572 | const { getHandler } = await registerWithDb({ getModelUsageStats }) |
| 573 | |
| 574 | await getHandler('settings:getModelUsage')?.(undefined, 'invalid') |
| 575 | expect(getModelUsageStats).toHaveBeenCalledWith('30d') |
| 576 | }) |
| 577 | }) |
| 578 |