botModelSettings.test.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. // @vitest-environment node
  2. import { afterEach, beforeEach, expect, it, vi } from 'vitest'
  3. import { mkdtempSync, rmSync } from 'node:fs'
  4. import { tmpdir } from 'node:os'
  5. import { join } from 'node:path'
  6. import { WorkspaceStore, botModelSettingsInput } from '../../src/server/workspaceStore'
  7. import { applyBotModelSettings, botModelOptions, resolveBotModelSettings } from '../../src/server/botModelSettings'
  8. import type { GatewayTarget } from '../../src/server/workspaceGateway'
  9. import type { WorkspaceAgent } from '../../src/shared/workspace'
  10. import { inheritedBotModelSettings } from '../../src/shared/botModelSettings'
  11. let home: string, store: WorkspaceStore
  12. const settings = { provider: 'openai', model: 'model-b', reasoningEffort: 'high', fastMode: 'auto' as const }
  13. beforeEach(() => { home = mkdtempSync(join(tmpdir(), 'bot-model-')); store = new WorkspaceStore(home) })
  14. afterEach(() => { store.close(); rmSync(home, { recursive: true, force: true }) })
  15. it('persists per-Bot settings, preserves old-client patches, and rejects stale edits atomically', () => {
  16. const a = store.createAgent('owner', { name: '甲', profile: 'default' }), b = store.createAgent('owner', { name: '乙', profile: 'default' })
  17. const updated = store.updateAgent('owner', a.id, { modelSettings: settings, expectedRevision: a.revision })
  18. store.updateAgent('owner', a.id, { name: '新名字' })
  19. expect(store.require<WorkspaceAgent>('owner','agent',a.id).modelSettings).toEqual(settings)
  20. expect(store.require<WorkspaceAgent>('owner','agent',b.id).modelSettings).toBeUndefined()
  21. expect(() => store.updateAgent('owner', a.id, { modelSettings: null, expectedRevision: updated.revision })).toThrow('已在其他地方更新')
  22. const current = store.require<WorkspaceAgent>('owner','agent',a.id)
  23. store.updateAgent('owner', a.id, { modelSettings: null, expectedRevision: current.revision })
  24. store.close(); store = new WorkspaceStore(home)
  25. expect(store.require<WorkspaceAgent>('owner','agent',a.id).modelSettings).toBeNull()
  26. })
  27. it('distinguishes inherited values, explicit no reasoning, and all four speed modes', () => {
  28. for (const fastMode of ['normal','fast','auto','cold']) expect(botModelSettingsInput.parse({ ...inheritedBotModelSettings(), reasoningEffort:'none', fastMode }).fastMode).toBe(fastMode)
  29. for (const invalid of [{...settings,model:null},{...settings,fastMode:true},{...settings,model:'model --global'}]) expect(botModelSettingsInput.safeParse(invalid).success).toBe(false)
  30. })
  31. it('applies and checks model, reasoning, speed in order using session scope only', async () => {
  32. const rpc = vi.fn(async (_method, params) => ({ value: params.key === 'model' ? 'model-b' : params.value, scope:'session' }))
  33. await applyBotModelSettings(rpc, 'session-a', settings, false)
  34. expect(rpc.mock.calls.map(([,p]) => p.key)).toEqual(['model','reasoning','fast'])
  35. for (const [method, params] of rpc.mock.calls) { expect(method).toBe('config.set'); expect(params).toMatchObject({ session_id:'session-a',scope:'session' }) }
  36. expect(rpc.mock.calls[0]![1].value).toBe('model-b --provider openai --session')
  37. })
  38. it.each([{ value:'wrong',scope:'session' }, { value:'model-b',scope:'global' }, { value:'model-b',deferred:true }, { confirm_required:true }])('never submits downstream changes without an immediate acknowledgement: %j', async response => {
  39. const rpc = vi.fn().mockResolvedValue(response)
  40. await expect(applyBotModelSettings(rpc,'session-a',settings,false)).rejects.toThrow()
  41. expect(rpc).toHaveBeenCalledTimes(1)
  42. })
  43. it('avoids rebuilding an unchanged model and still replaces old reasoning and speed pins', async () => {
  44. const rpc = vi.fn(async (_method, params) => ({value:params.value}))
  45. await applyBotModelSettings(rpc,'session-a',{...settings,reasoningEffort:'none',fastMode:'normal'},false,settings)
  46. expect(rpc.mock.calls.map(([,p])=>[p.key,p.value])).toEqual([['reasoning','none'],['fast','normal']])
  47. })
  48. it('rejects old bridges and malformed catalogues and resolves inherited values through the bridge', async () => {
  49. const request = vi.fn().mockResolvedValue({status:404,body:Buffer.from('{}')})
  50. const target = {session:{request}} as unknown as GatewayTarget
  51. await expect(botModelOptions(target,'default')).rejects.toMatchObject({code:'model_settings_upgrade_required'})
  52. request.mockResolvedValue({status:200,body:Buffer.from(JSON.stringify({version:1,effective:settings,confirmationMessage:null}))})
  53. expect((await resolveBotModelSettings(target,'default',null)).effective).toEqual(settings)
  54. expect(request.mock.lastCall).toEqual(['/api/plugins/yaoyao-bot-bridge/model-settings/resolve',{method:'POST',body:{profile:'default',settings:null},cache:'reload'}])
  55. await expect(botModelOptions(target,'default')).rejects.toMatchObject({code:'model_settings_upgrade_required'})
  56. })