GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Projects, channels, conversations, and Agent Runtimes now retain dynamic default references while explicit model selections remain pinned. Compatible fallback resolution stays aligned across settings validation, runtime creation, labels, context metrics, DeepSeek Harness browser routing, and remote project saves. Prepare the bilingual Desktop 0.12.11 release metadata and keep Agent 0.11.22 as the published remote baseline. Release note: 修复默认模型与 Runtime 配置变化后项目、通道和对话选择失去跟随关系的问题;显式选择仍保持固定,新请求使用当前兼容配置。
306 lines
9.9 KiB
TypeScript
306 lines
9.9 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('routes legacy automatic settings through local OpenCode when the Server is blank', () => {
|
|
expect(getConfiguredRuntimeTarget(settings())).toBe('opencode')
|
|
expect(
|
|
getConfiguredRuntimeTarget(
|
|
settings({ opencodeEmbedded: false })
|
|
)
|
|
).toBe('opencode')
|
|
expect(
|
|
applyRuntimeSelection(settings(), { provider: 'auto' }).settings
|
|
).toEqual(settings())
|
|
})
|
|
})
|