Files
goodbuddy/src/main/agent/runtime-selection.test.ts
T
mesalogo 9c88ca20f3 feat: layer runtime and model selection across conversation, project and global
- Store runtime selection as { provider?, model? } layers and resolve them per
  field (conversation -> project -> global); drop the legacy auto provider.
- Migrate stored selections (database v49) and clear deleted profile ids.
- Re-resolve selections in Main per request and at queue dispatch.
- Add ProjectRuntimeSelector and a two-column RuntimeModelPicker; manage
  link opens model connections; restore per-runtime default model tabs.
- Stop duplicating trusted instructions and history across OpenCode,
  Continue and DeepSeek Harness runtimes.
- Document the layered design, global provider removal plan and planned
  purpose-specific models.
2026-09-29 21:33:06 +08:00

312 lines
10 KiB
TypeScript

import type { ResolvedRuntimeSettings } from '../runtime-settings-store'
import { defaultRuntimeCustomizationSettings } from '../../shared/contracts'
import { describe, expect, it } from 'vitest'
import {
applyRuntimeSelection,
getConfiguredRuntimeTarget,
resolveConfiguredAgentRuntimeSelection
} from './runtime-selection'
const defaultProfileId = '00000000-0000-4000-8000-000000000001'
const secondProfileId = '00000000-0000-4000-8000-000000000002'
const responsesProfileId = '00000000-0000-4000-8000-000000000003'
const imageProfileId = '00000000-0000-4000-8000-000000000004'
const harnessProfileId = '00000000-0000-4000-8000-000000000005'
function settings(
overrides: Partial<ResolvedRuntimeSettings> = {}
): ResolvedRuntimeSettings {
return {
provider: 'auto',
modelBaseUrl: 'https://default.example/v1',
modelName: 'default-model',
modelProtocol: 'anthropic-messages',
modelAuthentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: 'default-key',
modelProfiles: [
{
id: defaultProfileId,
name: '默认模型',
baseUrl: 'https://default.example/v1',
modelName: 'default-model',
protocol: 'anthropic-messages',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: 'default-key'
},
{
id: secondProfileId,
name: '第二模型',
baseUrl: 'https://second.example/v1',
modelName: 'second-model',
protocol: 'openai-chat-completions',
authentication: 'none',
supportsImageInput: true,
imageGenerationQuality: 'auto'
},
{
id: responsesProfileId,
name: 'Responses 模型',
baseUrl: 'https://responses.example/v1',
modelName: 'responses-model',
protocol: 'openai-responses',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: 'responses-key'
},
{
id: imageProfileId,
name: '图像模型',
baseUrl: 'https://images.example/v1',
modelName: 'image-model',
protocol: 'openai-images-generations',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: 'image-key'
},
{
id: harnessProfileId,
name: 'OpenAI-compatible gateway',
baseUrl: 'https://gateway.example/openai/v1',
modelName: 'qwen-plus',
protocol: 'openai-chat-completions',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: 'deepseek-key'
}
],
defaultModelProfileId: defaultProfileId,
opencodeBaseUrl: '',
opencodeEmbedded: true,
opencodeBinaryPath: '',
opencodeConfigPath: '',
continueBinaryPath: '',
continueConfigPath: '',
continueMode: 'chat',
subagentSmartRoutingEnabled: false,
knowledgeEmbeddingEnabled: false,
knowledgeEmbeddingBaseUrl:
'http://127.0.0.1:11434/v1/embeddings',
knowledgeEmbeddingModel: 'embedding',
knowledgeRerankEnabled: false,
knowledgeRerankEndpoint: 'https://api.cohere.com/v1/rerank',
knowledgeRerankModel: 'rerank-v3.5',
runtimeCustomization: defaultRuntimeCustomizationSettings,
workspacePath: process.cwd(),
toolApproval: 'always',
...overrides
}
}
describe('runtime selection', () => {
it('resolves direct defaults per request while fixed selections and started requests stay unchanged', () => {
const before = settings()
const started = applyRuntimeSelection(before, { provider: 'model' })
const after = settings({ defaultModelProfileId: secondProfileId })
expect(applyRuntimeSelection(after, { provider: 'model' }).settings.defaultModelProfileId).toBe(secondProfileId)
expect(applyRuntimeSelection(after, { provider: 'model', profileId: defaultProfileId }).settings.defaultModelProfileId).toBe(defaultProfileId)
expect(started.settings.defaultModelProfileId).toBe(defaultProfileId)
})
it.each(['opencode', 'continue', 'deepseek-harness'] as const)(
'resolves %s references from its current configuration and preserves explicit overrides', (provider) => {
const base = settings()
const first = base.modelProfiles[4]!
const second = { ...first, id: responsesProfileId }
const key = provider === 'opencode' ? 'opencodeModelProfile' : provider === 'continue' ? 'continueModelProfile' : 'deepseekHarnessModelProfile'
const configured = settings({ [key]: first })
const started = applyRuntimeSelection(configured, { provider })
const changed = settings({ [key]: second })
expect(applyRuntimeSelection(changed, { provider }).settings[key]?.id).toBe(second.id)
expect(applyRuntimeSelection(changed, { provider, profileId: first.id }).settings[key]?.id).toBe(first.id)
expect(started.settings[key]?.id).toBe(first.id)
}
)
it('selects an independent direct model profile without changing defaults', () => {
const original = settings()
const selected = applyRuntimeSelection(original, {
provider: 'model',
profileId: secondProfileId
})
expect(selected.target).toBe('model')
expect(selected.settings).toMatchObject({
provider: 'model',
modelBaseUrl: 'https://second.example/v1',
modelName: 'second-model',
modelProtocol: 'openai-chat-completions',
modelAuthentication: 'none',
supportsImageInput: true,
defaultModelProfileId: secondProfileId
})
expect(original.defaultModelProfileId).toBe(defaultProfileId)
})
it.each([
['opencode', defaultProfileId],
['opencode', secondProfileId],
['opencode', responsesProfileId],
['continue', defaultProfileId],
['continue', secondProfileId],
['continue', responsesProfileId]
] as const)(
'selects %s with text profile %s',
(provider, profileId) => {
const selected = applyRuntimeSelection(settings(), {
provider,
profileId
})
expect(
provider === 'opencode'
? selected.settings.opencodeModelProfile?.id
: selected.settings.continueModelProfile?.id
).toBe(profileId)
}
)
it('rejects deleted or incompatible profile selections', () => {
expect(() =>
applyRuntimeSelection(settings(), {
provider: 'model',
profileId: '00000000-0000-4000-8000-000000000099'
})
).toThrow('不存在')
expect(() =>
applyRuntimeSelection(settings(), {
provider: 'opencode',
profileId: imageProfileId
})
).toThrow('不支持图像生成协议')
expect(() =>
applyRuntimeSelection(
settings({ opencodeBaseUrl: 'http://127.0.0.1:4096' }),
{
provider: 'opencode',
profileId: defaultProfileId
}
)
).toThrow('自动启动')
})
it('selects DeepSeek Harness with a compatible gateway profile', () => {
const selected = applyRuntimeSelection(settings(), {
provider: 'deepseek-harness',
profileId: harnessProfileId
})
expect(selected.target).toBe('deepseek-harness')
expect(selected.settings).toMatchObject({
provider: 'deepseek-harness',
deepseekHarnessModelProfile: { id: harnessProfileId }
})
expect(() =>
applyRuntimeSelection(settings(), {
provider: 'deepseek-harness',
profileId: secondProfileId
})
).toThrow('API Key')
})
it('keeps the controlled platform Harness profile when selected without a profile ID', () => {
const base = settings()
const platformProfile = {
...base.modelProfiles[4]!,
id: 'goodbuddy-platform-harness',
name: '管理员预置模型',
modelName: 'qwen-plus'
}
const selected = applyRuntimeSelection(
settings({ deepseekHarnessModelProfile: platformProfile }),
{ provider: 'deepseek-harness' }
)
expect(selected.settings).toMatchObject({
provider: 'deepseek-harness',
deepseekHarnessModelProfile: {
id: 'goodbuddy-platform-harness',
modelName: 'qwen-plus'
}
})
})
it('resolves Agent Runtime backends from the global Runtime configuration', () => {
const base = settings()
const configured = settings({
opencodeModelProfile: base.modelProfiles[1],
continueModelProfile: base.modelProfiles[2],
deepseekHarnessModelProfile: base.modelProfiles[4]
})
expect(
resolveConfiguredAgentRuntimeSelection(configured, {
provider: 'deepseek-harness'
})
).toEqual({
provider: 'deepseek-harness',
profileId: harnessProfileId
})
expect(
resolveConfiguredAgentRuntimeSelection(configured, {
provider: 'opencode',
profileId: defaultProfileId
})
).toEqual({
provider: 'opencode',
profileId: defaultProfileId
})
expect(
resolveConfiguredAgentRuntimeSelection(configured, {
provider: 'continue'
})
).toEqual({
provider: 'continue',
profileId: responsesProfileId
})
expect(
resolveConfiguredAgentRuntimeSelection(configured, {
provider: 'model',
profileId: defaultProfileId
})
).toEqual({
provider: 'model',
profileId: defaultProfileId
})
})
it('keeps the controlled platform Harness source profile-free across configured selection repair', () => {
const base = settings()
const configured = settings({
deepseekHarnessModelProfile: {
...base.modelProfiles[4]!,
id: 'goodbuddy-platform-harness',
name: '管理员预置模型'
}
})
expect(
resolveConfiguredAgentRuntimeSelection(configured, {
provider: 'deepseek-harness'
})
).toEqual({ provider: 'deepseek-harness' })
})
it('treats legacy automatic settings as the direct model, matching settings normalization', () => {
expect(getConfiguredRuntimeTarget(settings())).toBe('model')
expect(
getConfiguredRuntimeTarget(settings({ provider: 'opencode' }))
).toBe('opencode')
})
it('resolves own configuration explicitly instead of the configured profile', () => {
const configured = settings({
provider: 'opencode',
opencodeModelProfile: settings().modelProfiles[0]
})
expect(
applyRuntimeSelection(configured, { provider: 'opencode', runtimeConfig: true })
.settings.opencodeModelProfile
).toBeUndefined()
})
})