Files
goodbuddy/src/main/runtime-settings-store.test.ts
T
mesalogo c037d7fb20 fix: complete runtime and workspace workflows
Restore Ask workspace reads and output paging without granting write access,
and let programming Subagents reuse their parent's browser page. Allocate
browser resources only when used, keep Agent pages aligned with workbar tabs,
recover released bindings, and remove unused automatic screenshot work.

Preserve inherited Runtime selections during remote usage persistence and
manual compression. Keep viable channel model fallbacks, display the actual
scheduled-task mode and Harness model source, and avoid rediscovery delays
when calling healthy MCP tools. Fix directory/history refresh, case-only
renames, rename-aware commit diffs, and FastGPT probe success reporting.

Add regression coverage and align feature documentation. The Agent-side
connection timeout fix requires deploying an updated Agent; existing Ask
read-only and Execute authorization boundaries remain unchanged.

Final validation: 3839 tests passed, 66 skipped; typecheck, lint, development
build, and diff checks passed. Isolated Electron UI/MCP workflows passed with
a loopback model fixture. Current-source Linux x64 Host checks passed with
three successful real model requests. Chromium loadURL rejection was not
reproduced in the UI; released-binding recovery has automated coverage.

Release note: 修复 Ask 读取项目文件、浏览器页面显示与恢复、文件管理及模型配置跟随中的不一致,并减少无用截图和 MCP 等待。定时任务显示实际工作模式,远程会话可正常保存继承 Runtime 的用量;远端连接超时修复需更新 Agent 后生效。
2026-09-10 12:48:30 +08:00

2927 lines
96 KiB
TypeScript

import {
mkdtemp,
readFile,
readdir,
rm,
writeFile
} from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
builtinEmbeddingConnectionId,
legacyEmbeddingConnectionId,
runtimeSettingsInputSchema,
type RuntimeSettingsInput
} from '../shared/contracts'
import {
RuntimeSettingsStore,
type CredentialCipher
} from './runtime-settings-store'
const temporaryDirectories: string[] = []
const cipher: CredentialCipher = {
isAvailable: () => true,
encrypt: (value) => Buffer.from(`encrypted:${value}`),
decrypt: (value) => value.toString().replace(/^encrypted:/, '')
}
function settings(
overrides: Partial<RuntimeSettingsInput> = {}
): RuntimeSettingsInput {
return {
provider: 'model',
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
modelProtocol: 'anthropic-messages',
modelAuthentication: 'api-key',
imageGenerationQuality: 'auto',
opencodeBaseUrl: '',
opencodeEmbedded: false,
opencodeBinaryPath: '',
opencodeConfigPath: '',
continueBinaryPath: '',
continueConfigPath: '',
continueMode: 'chat',
deepseekHarnessModelSource: { kind: 'platform' },
knowledgeEmbeddingEnabled: false,
knowledgeEmbeddingBaseUrl:
'http://127.0.0.1:11434/v1/embeddings',
knowledgeEmbeddingModel: 'nomic-embed-text',
knowledgeRerankEnabled: false,
knowledgeRerankEndpoint: 'https://api.cohere.com/v1/rerank',
knowledgeRerankModel: 'rerank-v3.5',
workspacePath: 'test-workspace',
apiKey: { action: 'keep' },
toolApproval: 'always',
...overrides
}
}
async function createStore(
environment: NodeJS.ProcessEnv = {}
): Promise<{ filePath: string; store: RuntimeSettingsStore }> {
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-settings-'))
temporaryDirectories.push(directory)
const filePath = join(directory, 'runtime-settings.json')
return {
filePath,
store: new RuntimeSettingsStore(filePath, cipher, environment)
}
}
afterEach(async () => {
await Promise.all(
temporaryDirectories.splice(0).map((directory) =>
rm(directory, { recursive: true, force: true })
)
)
})
describe('RuntimeSettingsStore', () => {
it('persists default references separately from fixed profiles and resolves each settings generation', async () => {
const { store, filePath } = await createStore()
const firstId = '00000000-0000-4000-8000-000000000041'
const secondId = '00000000-0000-4000-8000-000000000042'
const modelProfiles = [firstId, secondId].map((id) => ({
id, name: id, baseUrl: 'https://gateway.example/v1', modelName: id,
protocol: 'openai-chat-completions' as const,
authentication: 'api-key' as const, imageGenerationQuality: 'auto' as const,
apiKey: { action: 'keep' as const }
}))
await store.update(settings({ modelProfiles, defaultModelProfileId: firstId,
opencodeModelSource: { kind: 'default' },
continueModelSource: { kind: 'profile', profileId: firstId },
deepseekHarnessModelSource: { kind: 'default' }
}))
const started = await store.getResolvedSettings()
await store.update(settings({ defaultModelProfileId: secondId, deepseekHarnessModelSource: { kind: 'default' } }))
const reloaded = new RuntimeSettingsStore(filePath, cipher, {})
await expect(reloaded.getPublicSettings()).resolves.toMatchObject({
opencodeModelSource: { kind: 'default' },
continueModelSource: { kind: 'profile', profileId: firstId },
deepseekHarnessModelSource: { kind: 'default' }
})
await expect(reloaded.getResolvedSettings()).resolves.toMatchObject({
opencodeModelProfile: { id: secondId },
continueModelProfile: { id: firstId },
deepseekHarnessModelProfile: { id: secondId }
})
expect(started.opencodeModelProfile?.id).toBe(firstId)
})
it('falls back to compatible profiles when the global default is incompatible with a Runtime', async () => {
const { store } = await createStore()
const textId = '00000000-0000-4000-8000-000000000043'
const imageId = '00000000-0000-4000-8000-000000000044'
const anthropicId = '00000000-0000-4000-8000-000000000045'
const modelProfiles = [
{
id: textId, name: 'OpenAI text', baseUrl: 'https://gateway.example/v1', modelName: 'text',
protocol: 'openai-chat-completions' as const, authentication: 'api-key' as const,
imageGenerationQuality: 'auto' as const, apiKey: { action: 'keep' as const }
},
{
id: imageId, name: 'Image', baseUrl: 'https://gateway.example/v1', modelName: 'image',
protocol: 'openai-images-generations' as const, authentication: 'api-key' as const,
imageGenerationQuality: 'auto' as const, apiKey: { action: 'keep' as const }
},
{
id: anthropicId, name: 'Anthropic text', baseUrl: 'https://gateway.example/v1', modelName: 'text',
protocol: 'anthropic-messages' as const, authentication: 'api-key' as const,
imageGenerationQuality: 'auto' as const, apiKey: { action: 'keep' as const }
}
]
const runtimeDefaults = settings({
modelProfiles,
defaultModelProfileId: textId,
opencodeModelSource: { kind: 'default' },
continueModelSource: { kind: 'default' },
deepseekHarnessModelSource: { kind: 'default' }
})
await store.update(runtimeDefaults)
await store.update({
...runtimeDefaults,
defaultModelProfileId: imageId
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
opencodeModelProfile: { id: textId },
continueModelProfile: { id: textId },
deepseekHarnessModelProfile: { id: textId }
})
await store.update({
...runtimeDefaults,
defaultModelProfileId: anthropicId
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
opencodeModelProfile: { id: anthropicId },
continueModelProfile: { id: anthropicId },
deepseekHarnessModelProfile: { id: textId }
})
})
it('round-trips the Harness platform rule without pinning its resolved fallback', async () => {
const { store, filePath } = await createStore()
const firstId = '00000000-0000-4000-8000-000000000041'
const secondId = '00000000-0000-4000-8000-000000000042'
const modelProfiles = [firstId, secondId].map((id) => ({
id, name: id, baseUrl: 'https://gateway.example/v1', modelName: id,
protocol: 'openai-chat-completions' as const,
authentication: 'api-key' as const,
imageGenerationQuality: 'auto' as const,
apiKey: { action: 'keep' as const }
}))
await store.update(settings({ modelProfiles, defaultModelProfileId: firstId }))
const publicSettings = await store.getPublicSettings()
const started = await store.getResolvedSettings()
await store.update(settings({
defaultModelProfileId: secondId,
deepseekHarnessModelSource: publicSettings.deepseekHarnessModelSource
}))
const reloaded = new RuntimeSettingsStore(filePath, cipher, {})
expect((await reloaded.getPublicSettings()).deepseekHarnessModelSource).toEqual({ kind: 'platform' })
expect((await reloaded.getResolvedSettings()).deepseekHarnessModelProfile?.id).toBe(secondId)
expect(started.deepseekHarnessModelProfile?.id).toBe(firstId)
})
it('seeds a local provider-neutral model connection', async () => {
const { store } = await createStore()
await expect(store.getPublicSettings()).resolves.toMatchObject({
modelBaseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
modelProtocol: 'openai-chat-completions',
modelAuthentication: 'none',
modelProfiles: [
expect.objectContaining({
id: '00000000-0000-4000-8000-000000000001',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
authentication: 'none'
})
]
})
})
it('migrates version 20 model profiles with empty request customization', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const version20 = JSON.parse(
await readFile(filePath, 'utf8')
) as {
version: number
modelProfiles: Array<Record<string, unknown>>
}
version20.version = 20
for (const profile of version20.modelProfiles) {
delete profile.requestHeaders
delete profile.requestBody
}
await writeFile(filePath, JSON.stringify(version20), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getPublicSettings()).resolves.toMatchObject({
modelProfiles: [
expect.objectContaining({
requestHeaders: {},
requestBody: {}
})
]
})
await expect(migrated.getResolvedSettings()).resolves.toMatchObject({
modelProfiles: [
expect.objectContaining({
requestHeaders: {},
requestBody: {}
})
]
})
})
it('round-trips model request headers and body without treating them as credentials', async () => {
const { filePath, store } = await createStore()
const profileId = '00000000-0000-4000-8000-000000000010'
const updated = await store.update(
settings({
modelProfiles: [
{
id: profileId,
name: 'Customized model',
baseUrl: 'https://provider.example/v1',
modelName: 'model-1',
protocol: 'openai-responses',
authentication: 'none',
supportsImageInput: false,
imageGenerationQuality: 'auto',
requestHeaders: {
'x-tenant-id': 'tenant-a'
},
requestBody: {
temperature: 0.2,
metadata: { source: 'goodbuddy' }
},
apiKey: { action: 'keep' }
}
],
defaultModelProfileId: profileId,
opencodeModelSource: { kind: 'profile', profileId },
continueModelSource: { kind: 'profile', profileId }
})
)
expect(updated.modelProfiles[0]).toMatchObject({
requestHeaders: {
'x-tenant-id': 'tenant-a'
},
requestBody: {
temperature: 0.2,
metadata: { source: 'goodbuddy' }
},
apiKeyConfigured: false
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelProfiles: [
expect.objectContaining({
requestHeaders: {
'x-tenant-id': 'tenant-a'
},
requestBody: {
temperature: 0.2,
metadata: { source: 'goodbuddy' }
},
apiKey: undefined
})
]
})
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
modelProfiles: Array<Record<string, unknown>>
}
expect(persisted.version).toBe(21)
expect(persisted.modelProfiles[0]).toMatchObject({
requestHeaders: { 'x-tenant-id': 'tenant-a' },
requestBody: { temperature: 0.2 }
})
expect(persisted.modelProfiles[0]).not.toHaveProperty('apiKey')
})
it('replaces only the untouched legacy BigToken seed', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const version19 = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
version19.version = 19
await writeFile(filePath, JSON.stringify(version19), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getPublicSettings()).resolves.toMatchObject({
modelBaseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
modelAuthentication: 'none'
})
})
it('preserves an explicitly credentialed BigToken connection', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
apiKey: { action: 'replace', value: 'explicit-key' }
})
)
const version19 = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
version19.version = 19
await writeFile(filePath, JSON.stringify(version19), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getResolvedSettings()).resolves.toMatchObject({
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
apiKey: 'explicit-key'
})
})
it('preserves the historical connection for a legacy environment key', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const version19 = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
version19.version = 19
await writeFile(filePath, JSON.stringify(version19), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {
GOODBUDDY_BIGTOKEN_API_KEY: 'legacy-key'
})
await expect(migrated.getResolvedSettings()).resolves.toMatchObject({
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
modelProtocol: 'anthropic-messages',
modelAuthentication: 'api-key',
apiKey: 'legacy-key'
})
})
it('restores an exact credential-bearing snapshot after a failed activation', async () => {
const { store } = await createStore()
await store.update(
settings({
apiKey: { action: 'replace', value: 'previous-key' }
})
)
const rollback = await store.captureRollback()
await store.update(
settings({
modelBaseUrl: 'https://candidate.example/v1',
modelName: 'candidate',
apiKey: { action: 'replace', value: 'candidate-key' }
})
)
await expect(rollback.restore()).resolves.toMatchObject({
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
apiKeyConfigured: true
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
apiKey: 'previous-key'
})
})
it('migrates version 17 to empty Runtime customization', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const previous = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
previous.version = 17
Reflect.deleteProperty(previous, 'runtimeCustomization')
await writeFile(filePath, JSON.stringify(previous))
const migrated = new RuntimeSettingsStore(
filePath,
cipher
)
await expect(migrated.getRuntimeCustomization()).resolves.toEqual({
opencode: {},
continue: { presets: [] }
})
})
it('persists validated Runtime customization independently', async () => {
const { store } = await createStore()
const presetId = '00000000-0000-4000-8000-000000000071'
const ruleId = '00000000-0000-4000-8000-000000000072'
const promptId = '00000000-0000-4000-8000-000000000073'
const customization = {
opencode: { defaultAgent: 'build' },
continue: {
defaultPresetId: presetId,
presets: [
{
id: presetId,
name: '代码审查',
rules: [
{
id: ruleId,
name: '只报告可复现问题',
content: 'Only report reproducible findings.',
enabled: true
}
],
prompts: [
{
id: promptId,
name: '审查变更',
prompt: 'Review the current changes.'
}
]
}
]
}
}
await expect(
store.updateRuntimeCustomization(customization)
).resolves.toEqual(customization)
await expect(store.getRuntimeCustomization()).resolves.toEqual(
customization
)
})
it('migrates version 16 to disabled default context compression', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const previous = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
previous.version = 16
delete previous.contextCompression
await writeFile(filePath, JSON.stringify(previous), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getPublicSettings()).resolves.toMatchObject({
contextCompression: {
enabled: false,
triggerTokens: 200_000,
recentRawTokens: 32_000,
modelSource: { kind: 'current' }
}
})
})
it('persists context compression and optional model context windows', async () => {
const { store } = await createStore()
const profileId = '00000000-0000-4000-8000-000000000061'
const updated = await store.update(
settings({
modelProfiles: [
{
id: profileId,
name: 'Long context',
baseUrl: 'https://model.example/v1',
modelName: 'long-model',
protocol: 'openai-chat-completions',
authentication: 'none',
supportsImageInput: false,
contextWindowTokens: 256_000,
maximumOutputTokens: 48_000,
imageGenerationQuality: 'auto',
apiKey: { action: 'keep' }
}
],
defaultModelProfileId: profileId,
contextCompression: {
enabled: true,
triggerTokens: 200_000,
recentRawTokens: 32_000,
modelSource: { kind: 'profile', profileId },
summaryPrompt: 'Keep exact decisions and unresolved work.'
}
})
)
expect(updated).toMatchObject({
modelProfiles: [
{
id: profileId,
contextWindowTokens: 256_000,
maximumOutputTokens: 48_000
}
],
contextCompression: {
enabled: true,
triggerTokens: 200_000,
recentRawTokens: 32_000,
modelSource: { kind: 'profile', profileId }
}
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelProfiles: [
{
id: profileId,
contextWindowTokens: 256_000,
maximumOutputTokens: 48_000
}
],
contextCompression: {
enabled: true
}
})
})
it('rejects undersized model context windows and repairs legacy values', async () => {
const { filePath, store } = await createStore()
const profileId = '00000000-0000-4000-8000-000000000062'
const profile = {
id: profileId,
name: 'Small context',
baseUrl: 'https://model.example/v1',
modelName: 'small-model',
protocol: 'openai-responses' as const,
authentication: 'api-key' as const,
supportsImageInput: false,
contextWindowTokens: 10_000,
imageGenerationQuality: 'auto' as const,
apiKey: { action: 'keep' as const }
}
expect(() =>
runtimeSettingsInputSchema.parse(
settings({
modelProfiles: [profile],
defaultModelProfileId: profileId
})
)
).toThrow()
await store.update(
settings({
modelProfiles: [
{
...profile,
contextWindowTokens: 32_000
}
],
defaultModelProfileId: profileId
})
)
const persisted = JSON.parse(
await readFile(filePath, 'utf8')
) as {
modelProfiles: Array<{ contextWindowTokens?: number }>
}
persisted.modelProfiles[0]!.contextWindowTokens = 10_000
await writeFile(filePath, JSON.stringify(persisted), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getPublicSettings()).resolves.toMatchObject({
modelProfiles: [
expect.objectContaining({
id: profileId,
contextWindowTokens: undefined
})
]
})
})
it('configures bundled runtimes from the default model profile', async () => {
const { store } = await createStore()
await expect(store.getPublicSettings()).resolves.toMatchObject({
opencodeEmbedded: true,
opencodeModelSource: {
kind: 'default'
},
continueModelSource: {
kind: 'default'
}
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
opencodeEmbedded: true,
opencodeModelProfile: {
id: '00000000-0000-4000-8000-000000000001'
},
continueModelProfile: {
id: '00000000-0000-4000-8000-000000000001'
}
})
})
it('uses a compatible GoodBuddy model for Harness when no platform model is configured', async () => {
const profileId = '00000000-0000-4000-8000-000000000044'
const { store } = await createStore()
await store.update(
settings({
modelProfiles: [
{
id: profileId,
name: 'OpenAI-compatible gateway',
baseUrl: 'https://gateway.example/openai/v1',
modelName: 'qwen-plus',
protocol: 'openai-chat-completions',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: { action: 'replace', value: 'deepseek-secret' }
}
],
defaultModelProfileId: profileId
})
)
await expect(store.getPublicSettings()).resolves.toMatchObject({
deepseekHarnessModelSource: { kind: 'platform' },
configured: {
deepseekHarnessModelSource: { kind: 'platform' }
}
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
deepseekHarnessModelProfile: {
id: profileId,
apiKey: 'deepseek-secret'
}
})
})
it('uses the first compatible Harness model when the default model is incompatible', async () => {
const defaultId = '00000000-0000-4000-8000-000000000043'
const harnessId = '00000000-0000-4000-8000-000000000044'
const { store } = await createStore()
await store.update(
settings({
modelProfiles: [
{
id: defaultId,
name: 'Anthropic model',
baseUrl: 'https://anthropic.example',
modelName: 'claude',
protocol: 'anthropic-messages',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: { action: 'replace', value: 'anthropic-secret' }
},
{
id: harnessId,
name: 'OpenAI-compatible gateway',
baseUrl: 'https://gateway.example/openai/v1',
modelName: 'qwen-plus',
protocol: 'openai-chat-completions',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: { action: 'replace', value: 'deepseek-secret' }
}
],
defaultModelProfileId: defaultId
})
)
await expect(store.getResolvedSettings()).resolves.toMatchObject({
deepseekHarnessModelProfile: { id: harnessId }
})
})
it('migrates DeepSeek Harness to controlled platform mode and stores a compatible profile', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const versionFourteen = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
versionFourteen.version = 14
delete versionFourteen.deepseekHarnessModelSource
delete versionFourteen.deepseekHarnessBinaryPath
await writeFile(filePath, JSON.stringify(versionFourteen), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getResolvedSettings()).resolves.toMatchObject({
deepseekHarnessModelProfile: undefined
})
const profileId = '00000000-0000-4000-8000-000000000044'
await migrated.update(
settings({
provider: 'deepseek-harness',
modelProfiles: [
{
id: profileId,
name: 'OpenAI-compatible gateway',
baseUrl: 'https://gateway.example/openai/v1',
modelName: 'qwen-plus',
protocol: 'openai-chat-completions',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: { action: 'replace', value: 'deepseek-secret' }
}
],
defaultModelProfileId: profileId,
deepseekHarnessModelSource: { kind: 'profile', profileId }
})
)
await expect(migrated.getResolvedSettings()).resolves.toMatchObject({
provider: 'deepseek-harness',
deepseekHarnessModelProfile: {
id: profileId,
apiKey: 'deepseek-secret'
}
})
})
it.each([
{ name: 'complete administrator override', environment: {
GOODBUDDY_MODEL_API_KEY: 'administrator-secret',
GOODBUDDY_MODEL_BASE_URL: 'https://administrator.example/v1?private=marker',
GOODBUDDY_MODEL_NAME: 'administrator-model'
}, compatibleDefault: true, expected: 'environment' },
{ name: 'API key only with incompatible default', environment: {
GOODBUDDY_MODEL_API_KEY: 'administrator-secret'
}, compatibleDefault: false, expected: 'first' },
{ name: 'API key only with compatible default', environment: {
GOODBUDDY_MODEL_API_KEY: 'administrator-secret'
}, compatibleDefault: true, expected: 'default' },
{ name: 'address and model without API key', environment: {
GOODBUDDY_MODEL_BASE_URL: 'https://administrator.example/v1',
GOODBUDDY_MODEL_NAME: 'administrator-model'
}, compatibleDefault: false, expected: 'first' },
{ name: 'legacy environment credentials', environment: {
GOODBUDDY_BIGTOKEN_API_KEY: 'legacy-secret',
GOODBUDDY_BIGTOKEN_BASE_URL: 'https://legacy.example/v1',
GOODBUDDY_BIGTOKEN_MODEL: 'legacy-model'
}, compatibleDefault: false, expected: 'first' },
{ name: 'compatible saved default', environment: {},
compatibleDefault: true, expected: 'default' },
{ name: 'first compatible saved connection', environment: {},
compatibleDefault: false, expected: 'first' },
{ name: 'no compatible connection', environment: {},
compatibleDefault: false, expected: 'unavailable' }
])('projects the actual Harness platform source: $name', async ({
environment, compatibleDefault, expected
}) => {
const { store, filePath } = await createStore(environment)
const firstId = '00000000-0000-4000-8000-000000000061'
const defaultId = '00000000-0000-4000-8000-000000000062'
const profile = {
name: 'First compatible', baseUrl: 'https://saved.example/v1?private=marker',
modelName: 'saved-model', protocol: 'openai-chat-completions' as const,
authentication: 'api-key' as const, imageGenerationQuality: 'auto' as const,
apiKey: { action: 'replace' as const, value: 'saved-secret' },
requestHeaders: { 'X-Private-Header': 'header-marker' },
requestBody: { privateMetadata: 'body-marker' }
}
const input = settings({
modelProfiles: [
...(expected === 'unavailable' ? [] : [{ ...profile, id: firstId }]),
{
...profile, id: defaultId, name: 'Preferred connection',
protocol: compatibleDefault ? 'openai-chat-completions' : 'anthropic-messages'
}
],
defaultModelProfileId: defaultId
})
const publicSettings = await store.update(input)
const resolved = (await store.getResolvedSettings()).deepseekHarnessModelProfile
const projection = publicSettings.deepseekHarnessPlatformModel
if (expected === 'unavailable') {
expect(resolved).toBeUndefined()
expect(projection).toEqual({ source: 'unavailable' })
} else {
expect(resolved).toBeDefined()
expect(projection).toEqual({
source: expected === 'environment' ? 'environment' : 'profile',
...(expected === 'environment'
? {}
: { profileId: expected === 'default' ? defaultId : firstId }),
name: resolved!.name,
modelName: resolved!.modelName
})
if (expected === 'environment') {
expect(projection?.source).toBe('environment')
expect(resolved?.modelName).toBe('administrator-model')
}
}
const serialized = JSON.stringify(projection)
for (const excluded of [
'https://', 'apiKey', 'baseUrl', 'requestHeaders', 'requestBody',
'administrator-secret', 'saved-secret', 'legacy-secret', 'header-marker', 'body-marker'
]) {
expect(serialized).not.toContain(excluded)
}
expect(await readFile(filePath, 'utf8')).not.toContain('deepseekHarnessPlatformModel')
expect((await store.getPublicSettings()).deepseekHarnessPlatformModel).toEqual(projection)
// Merely inspecting the platform option must not change a selected default rule.
if (expected !== 'unavailable') {
const followingDefault = await store.update({
...input, deepseekHarnessModelSource: { kind: 'default' }
})
expect(followingDefault.deepseekHarnessModelSource).toEqual({ kind: 'default' })
expect(followingDefault.deepseekHarnessPlatformModel).toEqual(projection)
}
})
it('resolves a controlled platform Harness profile without exposing its credential', async () => {
const apiKey = 'platform-harness-secret'
const { store } = await createStore({
GOODBUDDY_MODEL_API_KEY: apiKey,
GOODBUDDY_MODEL_BASE_URL:
'http://gateway.example/openai/v1?api-version=1',
GOODBUDDY_MODEL_NAME: 'qwen-plus'
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelProtocol: 'openai-chat-completions',
deepseekHarnessModelProfile: {
id: 'goodbuddy-platform-harness',
name: '管理员预置模型',
baseUrl: 'http://gateway.example/openai/v1?api-version=1',
modelName: 'qwen-plus',
protocol: 'openai-chat-completions',
authentication: 'api-key',
supportsImageInput: false,
imageGenerationQuality: 'auto',
apiKey
}
})
const publicSettings = await store.getPublicSettings()
expect(JSON.stringify(publicSettings)).not.toContain(apiKey)
expect(publicSettings.modelProtocol).toBe(
'openai-chat-completions'
)
})
it.each([
[
'an unsupported endpoint protocol',
{
GOODBUDDY_MODEL_API_KEY: 'platform-key',
GOODBUDDY_MODEL_BASE_URL:
'ftp://gateway.example/v1',
GOODBUDDY_MODEL_NAME: 'qwen-plus'
}
],
[
'a missing API key',
{
GOODBUDDY_MODEL_BASE_URL: 'https://gateway.example/v1',
GOODBUDDY_MODEL_NAME: 'qwen-plus'
}
]
])('does not resolve platform Harness from %s', async (_, environment) => {
const { store } = await createStore(environment)
await expect(store.getResolvedSettings()).resolves.toMatchObject({
deepseekHarnessModelProfile: undefined
})
})
it('drops the legacy custom Harness Host path and ignores its environment override', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const versionFifteen = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
versionFifteen.version = 15
versionFifteen.deepseekHarnessBinaryPath =
'C:\\untrusted\\custom-harness.js'
versionFifteen.runtimeSandboxMode = 'strict'
await writeFile(filePath, JSON.stringify(versionFifteen), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {
GOODBUDDY_DEEPSEEK_HARNESS_BINARY:
'C:\\environment\\custom-harness.js'
})
const publicSettings = await migrated.getPublicSettings()
const resolvedSettings = await migrated.getResolvedSettings()
expect(publicSettings).not.toHaveProperty(
'deepseekHarnessBinaryPath'
)
expect(publicSettings.configured).not.toHaveProperty(
'deepseekHarnessBinaryPath'
)
expect(resolvedSettings).not.toHaveProperty(
'deepseekHarnessBinaryPath'
)
expect(publicSettings).not.toHaveProperty('runtimeSandboxMode')
expect(publicSettings.configured).not.toHaveProperty(
'runtimeSandboxMode'
)
expect(resolvedSettings).not.toHaveProperty(
'runtimeSandboxMode'
)
await migrated.update(settings())
const persisted = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
expect(persisted.version).toBe(21)
expect(persisted).not.toHaveProperty(
'deepseekHarnessBinaryPath'
)
expect(persisted).not.toHaveProperty('runtimeSandboxMode')
})
it('accepts compatible gateways and rejects incompatible Harness profiles', () => {
const profileId = '00000000-0000-4000-8000-000000000045'
expect(
runtimeSettingsInputSchema.safeParse(
settings({
modelProfiles: [
{
id: profileId,
name: 'Compatible API',
baseUrl: 'https://other.example/v1',
modelName: 'qwen-plus',
protocol: 'openai-chat-completions',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: { action: 'keep' }
}
],
defaultModelProfileId: profileId,
deepseekHarnessModelSource: { kind: 'profile', profileId }
})
).success
).toBe(true)
expect(
runtimeSettingsInputSchema.safeParse(
settings({
modelProfiles: [
{
id: profileId,
name: 'Gateway without API key',
baseUrl: 'https://other.example/v1',
modelName: 'qwen-plus',
protocol: 'openai-chat-completions',
authentication: 'none',
imageGenerationQuality: 'auto',
apiKey: { action: 'clear' }
}
],
defaultModelProfileId: profileId,
deepseekHarnessModelSource: { kind: 'profile', profileId }
})
).success
).toBe(false)
})
it('always enables bundled OpenCode when the Server address is blank', async () => {
const { filePath, store } = await createStore({
GOODBUDDY_OPENCODE_EMBEDDED: 'false'
})
await expect(
store.update(
settings({
opencodeBaseUrl: '',
opencodeEmbedded: false
})
)
).resolves.toMatchObject({
opencodeBaseUrl: '',
opencodeEmbedded: true
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
opencodeBaseUrl: '',
opencodeEmbedded: true
})
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as {
opencodeEmbedded: boolean
}
expect(persisted.opencodeEmbedded).toBe(true)
})
it('repairs version 11 embedded state and normalizes an external Server to platform mode', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const versionEleven = JSON.parse(
await readFile(filePath, 'utf8')
) as {
opencodeEmbedded: boolean
}
versionEleven.opencodeEmbedded = false
await writeFile(filePath, JSON.stringify(versionEleven), 'utf8')
const repaired = new RuntimeSettingsStore(filePath, cipher, {})
await expect(repaired.getPublicSettings()).resolves.toMatchObject({
opencodeEmbedded: true
})
await expect(
repaired.update(
settings({
opencodeBaseUrl: 'https://opencode.example',
opencodeEmbedded: true,
opencodeModelSource: {
kind: 'profile',
profileId: '00000000-0000-4000-8000-000000000001'
}
})
)
).resolves.toMatchObject({
opencodeBaseUrl: 'https://opencode.example',
opencodeEmbedded: false,
opencodeModelSource: { kind: 'platform' }
})
})
it('rejects an explicit Runtime source that is missing after input merge', async () => {
const { store } = await createStore()
await expect(
store.update(
settings({
opencodeModelSource: {
kind: 'profile',
profileId: '00000000-0000-4000-8000-000000000099'
}
})
)
).rejects.toThrow('OpenCode 引用的模型连接不存在')
})
it('migrates untouched version 10 platform sources to the first compatible text profile', async () => {
const { filePath, store } = await createStore()
const imageId = '00000000-0000-4000-8000-000000000031'
const textId = '00000000-0000-4000-8000-000000000032'
await store.update(
settings({
provider: 'auto',
modelProfiles: [
{
id: imageId,
name: '默认图像模型',
baseUrl: 'https://images.example/v1',
modelName: 'image-model',
protocol: 'openai-images-generations',
authentication: 'api-key',
imageGenerationQuality: 'high',
apiKey: { action: 'clear' }
},
{
id: textId,
name: '文本模型',
baseUrl: 'https://text.example/v1',
modelName: 'text-model',
protocol: 'openai-chat-completions',
authentication: 'none',
imageGenerationQuality: 'auto',
apiKey: { action: 'clear' }
}
],
defaultModelProfileId: imageId,
opencodeModelSource: { kind: 'platform' },
continueModelSource: { kind: 'platform' },
opencodeEmbedded: false
})
)
const versionTen = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
intranetCompatibilityEnabled?: boolean
}
versionTen.version = 10
versionTen.intranetCompatibilityEnabled = false
await writeFile(filePath, JSON.stringify(versionTen), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getPublicSettings()).resolves.toMatchObject({
provider: 'model',
opencodeEmbedded: true,
opencodeModelSource: { kind: 'profile', profileId: textId },
continueModelSource: { kind: 'profile', profileId: textId }
})
await expect(migrated.getResolvedSettings()).resolves.toMatchObject({
opencodeModelProfile: { id: textId },
continueModelProfile: { id: textId }
})
})
it('preserves explicit and intentionally native version 10 Runtime sources', async () => {
const { filePath, store } = await createStore()
const profileId = '00000000-0000-4000-8000-000000000033'
await store.update(
settings({
modelProfiles: [
{
id: profileId,
name: '文本模型',
baseUrl: 'https://text.example/v1',
modelName: 'text-model',
protocol: 'openai-responses',
authentication: 'none',
imageGenerationQuality: 'auto',
apiKey: { action: 'clear' }
}
],
defaultModelProfileId: profileId,
opencodeModelSource: { kind: 'profile', profileId },
continueModelSource: { kind: 'platform' },
continueConfigPath: ''
})
)
const versionTen = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
continueConfigPath: string
intranetCompatibilityEnabled?: boolean
}
versionTen.version = 10
versionTen.continueConfigPath = 'C:\\Users\\test\\.continue\\config.yaml'
versionTen.intranetCompatibilityEnabled = false
await writeFile(filePath, JSON.stringify(versionTen), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getPublicSettings()).resolves.toMatchObject({
opencodeModelSource: { kind: 'profile', profileId },
continueModelSource: { kind: 'platform' }
})
})
it('retains version 10 platform sources when no text profile exists', async () => {
const { filePath, store } = await createStore()
const imageId = '00000000-0000-4000-8000-000000000034'
await store.update(
settings({
modelProfiles: [
{
id: imageId,
name: '图像模型',
baseUrl: 'https://images.example/v1',
modelName: 'image-model',
protocol: 'openai-images-generations',
authentication: 'api-key',
imageGenerationQuality: 'medium',
apiKey: { action: 'clear' }
}
],
defaultModelProfileId: imageId,
opencodeModelSource: { kind: 'platform' },
continueModelSource: { kind: 'platform' },
opencodeEmbedded: false
})
)
const versionTen = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
intranetCompatibilityEnabled?: boolean
}
versionTen.version = 10
versionTen.intranetCompatibilityEnabled = false
await writeFile(filePath, JSON.stringify(versionTen), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getPublicSettings()).resolves.toMatchObject({
opencodeEmbedded: true,
opencodeModelSource: { kind: 'platform' },
continueModelSource: { kind: 'platform' }
})
})
it('migrates version 8 settings with smart routing disabled', async () => {
const { filePath, store } = await createStore()
await store.update(settings({ subagentSmartRoutingEnabled: true }))
const versionEight = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
subagentSmartRoutingEnabled?: boolean
}
versionEight.version = 8
delete versionEight.subagentSmartRoutingEnabled
await writeFile(filePath, JSON.stringify(versionEight), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getPublicSettings()).resolves.toMatchObject({
subagentSmartRoutingEnabled: false
})
await migrated.update(settings())
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
}
expect(persisted.version).toBe(21)
})
it('migrates version 11 and removes the obsolete intranet toggle', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const versionEleven = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
intranetCompatibilityEnabled?: boolean
}
versionEleven.version = 11
versionEleven.intranetCompatibilityEnabled = false
await writeFile(filePath, JSON.stringify(versionEleven), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await migrated.update(settings())
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
intranetCompatibilityEnabled?: boolean
}
expect(persisted.version).toBe(21)
expect(persisted).not.toHaveProperty('intranetCompatibilityEnabled')
})
it('keeps image input disabled when migrating version 12 profiles', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const versionTwelve = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
modelProfiles: Array<Record<string, unknown>>
}
versionTwelve.version = 12
for (const profile of versionTwelve.modelProfiles) {
delete profile.supportsImageInput
}
await writeFile(filePath, JSON.stringify(versionTwelve), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getPublicSettings()).resolves.toMatchObject({
supportsImageInput: false,
modelProfiles: [
expect.objectContaining({ supportsImageInput: false })
]
})
})
it('persists enabled image input for a model profile', async () => {
const { filePath, store } = await createStore()
const profileId = '00000000-0000-4000-8000-000000000035'
await store.update(
settings({
modelProfiles: [
{
id: profileId,
name: '视觉模型',
baseUrl: 'https://model.example/v1',
modelName: 'vision-model',
protocol: 'openai-responses',
authentication: 'none',
supportsImageInput: true,
imageGenerationQuality: 'auto',
apiKey: { action: 'clear' }
}
],
defaultModelProfileId: profileId
})
)
await expect(store.getPublicSettings()).resolves.toMatchObject({
supportsImageInput: true,
modelProfiles: [
expect.objectContaining({ supportsImageInput: true })
]
})
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as {
modelProfiles: Array<Record<string, unknown>>
}
expect(persisted.modelProfiles[0]).toMatchObject({
supportsImageInput: true
})
})
it('accepts only supported image quality values', () => {
for (const imageGenerationQuality of [
'auto',
'low',
'medium',
'high'
] as const) {
expect(
runtimeSettingsInputSchema.safeParse(
settings({ imageGenerationQuality })
).success
).toBe(true)
}
expect(
runtimeSettingsInputSchema.safeParse({
...settings(),
imageGenerationQuality: 'ultra'
}).success
).toBe(false)
})
it('allows HTTP embedding endpoints on any host', () => {
expect(
runtimeSettingsInputSchema.safeParse(
settings({
knowledgeEmbeddingEnabled: true,
knowledgeEmbeddingBaseUrl:
'http://10.7.0.23:11434/v1/embeddings',
knowledgeEmbeddingModel: 'bge-m3'
})
).success
).toBe(true)
expect(
runtimeSettingsInputSchema.safeParse(
settings({
knowledgeEmbeddingEnabled: true,
knowledgeEmbeddingBaseUrl:
'http://example.com:11434/v1/embeddings'
})
).success
).toBe(true)
})
it('rejects non-HTTP model profile URLs during legacy migration', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
modelProfiles: Array<{ baseUrl: string }>
}
persisted.version = 6
persisted.modelProfiles[0]!.baseUrl = 'file:///tmp/model'
await writeFile(filePath, JSON.stringify(persisted), 'utf8')
const migratedStore = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migratedStore.getPublicSettings()).resolves.toMatchObject({
provider: 'model',
warnings: [{ code: 'runtime-settings-recovered' }]
})
expect(
(await readdir(join(filePath, '..'))).some((name) =>
name.startsWith('runtime-settings.json.corrupt-')
)
).toBe(true)
})
it('keeps an encrypted embedding API key when its endpoint changes', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
knowledgeEmbeddingEnabled: true,
knowledgeEmbeddingBaseUrl:
'https://vectors.example/custom/embeddings',
knowledgeEmbeddingModel: 'vendor/embed-large',
knowledgeEmbeddingApiKey: {
action: 'replace',
value: 'vector-secret-value'
}
})
)
const contents = await readFile(filePath, 'utf8')
expect(contents).not.toContain('vector-secret-value')
await expect(store.getResolvedSettings()).resolves.toMatchObject({
knowledgeEmbeddingBaseUrl:
'https://vectors.example/custom/embeddings',
knowledgeEmbeddingModel: 'vendor/embed-large',
knowledgeEmbeddingApiKey: 'vector-secret-value'
})
await expect(store.getPublicSettings()).resolves.toMatchObject({
knowledgeEmbeddingApiKeyConfigured: true,
knowledgeEmbeddingCredentialSource: 'encrypted'
})
await store.update(
settings({
knowledgeEmbeddingBaseUrl:
'https://vectors.example/v1/embeddings',
knowledgeEmbeddingApiKey: { action: 'keep' }
})
)
await expect(store.getResolvedSettings()).resolves.toMatchObject({
knowledgeEmbeddingBaseUrl:
'https://vectors.example/v1/embeddings',
knowledgeEmbeddingApiKey: 'vector-secret-value'
})
})
it('migrates version 18 embedding settings into a separate user connection', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
knowledgeEmbeddingEnabled: true,
knowledgeEmbeddingBaseUrl:
'https://legacy-vectors.example/v1/embeddings',
knowledgeEmbeddingModel: 'legacy/embed-large',
knowledgeEmbeddingApiKey: {
action: 'replace',
value: 'legacy-vector-secret'
}
})
)
const previous = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
previous.version = 18
delete previous.embeddingConnections
delete previous.activeEmbeddingConnectionId
await writeFile(filePath, JSON.stringify(previous), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getPublicSettings()).resolves.toMatchObject({
activeEmbeddingConnectionId: legacyEmbeddingConnectionId,
embeddingConnections: [
{
id: builtinEmbeddingConnectionId,
kind: 'builtin',
apiKeyConfigured: false,
credentialSource: 'none'
},
{
id: legacyEmbeddingConnectionId,
kind: 'openai-compatible',
baseUrl: 'https://legacy-vectors.example/v1/embeddings',
modelName: 'legacy/embed-large',
apiKeyConfigured: true,
credentialSource: 'encrypted'
}
],
knowledgeEmbeddingBaseUrl:
'https://legacy-vectors.example/v1/embeddings',
knowledgeEmbeddingModel: 'legacy/embed-large'
})
await expect(migrated.getResolvedSettings()).resolves.toMatchObject({
activeEmbeddingConnection: {
id: legacyEmbeddingConnectionId,
apiKey: 'legacy-vector-secret'
},
knowledgeEmbeddingApiKey: 'legacy-vector-secret'
})
})
it('round-trips embedding connections without assigning them to Runtime profiles', async () => {
const { filePath, store } = await createStore()
const connectionId = '00000000-0000-4000-8000-000000000081'
await store.update(
settings({
embeddingConnections: [
{
id: builtinEmbeddingConnectionId,
name: 'GoodBuddy 内置向量模型',
kind: 'builtin'
},
{
id: connectionId,
name: '团队向量模型',
kind: 'openai-compatible',
baseUrl: 'https://team-vectors.example/v1/embeddings',
modelName: 'team/embed-v2',
authentication: 'api-key',
apiKey: {
action: 'replace',
value: 'team-vector-secret'
}
}
],
activeEmbeddingConnectionId: connectionId
})
)
const reloaded = new RuntimeSettingsStore(filePath, cipher, {})
await expect(reloaded.getResolvedSettings()).resolves.toMatchObject({
activeEmbeddingConnectionId: connectionId,
activeEmbeddingConnection: {
id: connectionId,
kind: 'openai-compatible',
apiKey: 'team-vector-secret'
},
modelProfiles: [
expect.not.objectContaining({ id: connectionId })
]
})
const publicSettings = await reloaded.getPublicSettings()
expect(JSON.stringify(publicSettings)).not.toContain(
'team-vector-secret'
)
expect(publicSettings.embeddingConnections).toContainEqual(
expect.objectContaining({
id: connectionId,
apiKeyConfigured: true,
credentialSource: 'encrypted'
})
)
expect(
(JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
}).version
).toBe(21)
})
it('changes the active embedding connection without changing model profiles', async () => {
const { store } = await createStore()
const profileId = '00000000-0000-4000-8000-000000000091'
const connectionId = '00000000-0000-4000-8000-000000000092'
await store.update(
settings({
modelProfiles: [
{
id: profileId,
name: 'Tenant model',
baseUrl: 'https://gateway.example/v1',
modelName: 'tenant-model',
protocol: 'anthropic-messages',
authentication: 'api-key',
supportsImageInput: false,
contextWindowTokens: 200_000,
maximumOutputTokens: 48_000,
imageGenerationQuality: 'auto',
requestHeaders: { 'x-tenant-id': 'tenant-a' },
requestBody: { temperature: 0.2 },
apiKey: { action: 'replace', value: 'tenant-secret' }
}
],
defaultModelProfileId: profileId,
embeddingConnections: [
{
id: builtinEmbeddingConnectionId,
name: 'GoodBuddy 内置向量模型',
kind: 'builtin'
},
{
id: connectionId,
name: '团队向量模型',
kind: 'openai-compatible',
baseUrl: 'https://team-vectors.example/v1/embeddings',
modelName: 'team/embed-v2',
authentication: 'none',
apiKey: { action: 'keep' }
}
],
activeEmbeddingConnectionId: builtinEmbeddingConnectionId
})
)
const selection =
await store.setActiveEmbeddingConnection(connectionId)
expect(selection).toMatchObject({
changed: true,
previousConnectionId: builtinEmbeddingConnectionId
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
activeEmbeddingConnectionId: connectionId,
modelProfiles: [
expect.objectContaining({
id: profileId,
contextWindowTokens: 200_000,
maximumOutputTokens: 48_000,
requestHeaders: { 'x-tenant-id': 'tenant-a' },
requestBody: { temperature: 0.2 },
apiKey: 'tenant-secret'
})
]
})
})
it('preserves configured model fields when embedding changes under an environment override', async () => {
const { filePath, store } = await createStore()
const profileId = '00000000-0000-4000-8000-000000000093'
await store.update(
settings({
modelProfiles: [
{
id: profileId,
name: 'Stored OpenAI model',
baseUrl: 'https://stored.example/v1',
modelName: 'stored-model',
protocol: 'openai-chat-completions',
authentication: 'none',
supportsImageInput: false,
imageGenerationQuality: 'auto',
requestHeaders: { 'x-tenant-id': 'tenant-a' },
requestBody: { temperature: 0.2 },
apiKey: { action: 'keep' }
}
],
defaultModelProfileId: profileId
})
)
const environmentStore = new RuntimeSettingsStore(
filePath,
cipher,
{ GOODBUDDY_BIGTOKEN_API_KEY: 'environment-key' }
)
const effective = await environmentStore.getPublicSettings()
expect(effective.modelProfiles[0]).toMatchObject({
protocol: 'anthropic-messages',
authentication: 'api-key',
credentialSource: 'environment'
})
await environmentStore.setActiveEmbeddingConnection(
legacyEmbeddingConnectionId
)
const withoutEnvironment = new RuntimeSettingsStore(
filePath,
cipher,
{}
)
await expect(
withoutEnvironment.getResolvedSettings()
).resolves.toMatchObject({
activeEmbeddingConnectionId: legacyEmbeddingConnectionId,
modelProfiles: [
expect.objectContaining({
id: profileId,
baseUrl: 'https://stored.example/v1',
modelName: 'stored-model',
protocol: 'openai-chat-completions',
authentication: 'none',
requestHeaders: { 'x-tenant-id': 'tenant-a' },
requestBody: { temperature: 0.2 }
})
]
})
})
it('serializes embedding selection with full settings updates', async () => {
const { store } = await createStore()
const settingsUpdate = store.update(
settings({ modelName: 'updated-after-selection-read' })
)
const selectionUpdate =
store.setActiveEmbeddingConnection(
legacyEmbeddingConnectionId
)
await Promise.all([settingsUpdate, selectionUpdate])
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelName: 'updated-after-selection-read',
activeEmbeddingConnectionId: legacyEmbeddingConnectionId
})
})
it('rolls back only the embedding selection after later settings updates', async () => {
const { store } = await createStore()
const selection =
await store.setActiveEmbeddingConnection(
legacyEmbeddingConnectionId
)
await store.update(
settings({
modelName: 'saved-while-embedding-activated',
activeEmbeddingConnectionId: legacyEmbeddingConnectionId
})
)
await selection.restore()
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelName: 'saved-while-embedding-activated',
activeEmbeddingConnectionId: builtinEmbeddingConnectionId
})
})
it('migrates version 13 with reranking disabled by default', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as Record<
string,
unknown
>
persisted.version = 13
delete persisted.knowledgeRerankEnabled
delete persisted.knowledgeRerankEndpoint
delete persisted.knowledgeRerankModel
delete persisted.knowledgeRerankCredential
await writeFile(filePath, JSON.stringify(persisted), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getPublicSettings()).resolves.toMatchObject({
knowledgeRerankEnabled: false,
knowledgeRerankEndpoint: 'https://api.cohere.com/v1/rerank',
knowledgeRerankModel: 'rerank-v3.5',
knowledgeRerankApiKeyConfigured: false,
knowledgeRerankCredentialSource: 'none'
})
})
it('keeps an encrypted rerank API key when its endpoint changes', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
knowledgeRerankEnabled: true,
knowledgeRerankEndpoint: 'https://rerank.example/v1/rerank',
knowledgeRerankModel: 'vendor/rerank-large',
knowledgeRerankApiKey: {
action: 'replace',
value: 'rerank-secret-value'
}
})
)
expect(await readFile(filePath, 'utf8')).not.toContain(
'rerank-secret-value'
)
await expect(store.getResolvedSettings()).resolves.toMatchObject({
knowledgeRerankEnabled: true,
knowledgeRerankEndpoint: 'https://rerank.example/v1/rerank',
knowledgeRerankModel: 'vendor/rerank-large',
knowledgeRerankApiKey: 'rerank-secret-value'
})
await expect(store.getPublicSettings()).resolves.toMatchObject({
knowledgeRerankApiKeyConfigured: true,
knowledgeRerankCredentialSource: 'encrypted'
})
await store.update(
settings({
knowledgeRerankEndpoint: 'https://other.example/v1/rerank',
knowledgeRerankApiKey: { action: 'keep' }
})
)
await expect(store.getResolvedSettings()).resolves.toMatchObject({
knowledgeRerankEndpoint: 'https://other.example/v1/rerank',
knowledgeRerankApiKey: 'rerank-secret-value'
})
})
it('prefers the rerank environment API key without exposing it', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
knowledgeRerankApiKey: {
action: 'replace',
value: 'stored-rerank-secret'
}
})
)
const environmentStore = new RuntimeSettingsStore(filePath, cipher, {
GOODBUDDY_RERANK_API_KEY: 'environment-rerank-secret'
})
await expect(environmentStore.getResolvedSettings()).resolves.toMatchObject({
knowledgeRerankApiKey: 'environment-rerank-secret'
})
const publicSettings = await environmentStore.getPublicSettings()
expect(publicSettings).toMatchObject({
knowledgeRerankApiKeyConfigured: true,
knowledgeRerankCredentialSource: 'environment'
})
expect(JSON.stringify(publicSettings)).not.toContain(
'environment-rerank-secret'
)
})
it('does not warn about unreadable stored credentials shadowed by environment keys', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
apiKey: { action: 'replace', value: 'stored-model-secret' },
knowledgeEmbeddingApiKey: {
action: 'replace',
value: 'stored-embedding-secret'
},
knowledgeRerankApiKey: {
action: 'replace',
value: 'stored-rerank-secret'
}
})
)
const environmentStore = new RuntimeSettingsStore(
filePath,
{
...cipher,
decrypt: () => {
throw new Error('stored credential is unreadable')
}
},
{
GOODBUDDY_MODEL_API_KEY: 'environment-model-secret',
GOODBUDDY_EMBEDDING_API_KEY: 'environment-embedding-secret',
GOODBUDDY_RERANK_API_KEY: 'environment-rerank-secret'
}
)
const publicSettings = await environmentStore.getPublicSettings()
expect(publicSettings).toMatchObject({
credentialSource: 'environment',
knowledgeEmbeddingCredentialSource: 'environment',
knowledgeRerankCredentialSource: 'environment'
})
expect(publicSettings.warnings ?? []).toEqual([])
await expect(environmentStore.getResolvedSettings()).resolves.toMatchObject({
apiKey: 'environment-model-secret',
knowledgeEmbeddingApiKey: 'environment-embedding-secret',
knowledgeRerankApiKey: 'environment-rerank-secret'
})
})
it('reads Runtime policy without decrypting stored credentials', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
subagentSmartRoutingEnabled: true,
toolApproval: 'policy',
apiKey: { action: 'replace', value: 'stored-model-secret' },
knowledgeEmbeddingApiKey: {
action: 'replace',
value: 'stored-embedding-secret'
},
knowledgeRerankApiKey: {
action: 'replace',
value: 'stored-rerank-secret'
}
})
)
const decrypt = vi.fn(cipher.decrypt)
const policyStore = new RuntimeSettingsStore(
filePath,
{ ...cipher, decrypt },
{}
)
await expect(policyStore.getPolicySettings()).resolves.toEqual({
subagentSmartRoutingEnabled: true,
toolApproval: 'policy'
})
expect(decrypt).not.toHaveBeenCalled()
})
it('clears rerank credentials and rejects replacement without secure storage', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
knowledgeRerankApiKey: {
action: 'replace',
value: 'rerank-secret-to-clear'
}
})
)
await store.update(
settings({
knowledgeRerankApiKey: { action: 'clear' }
})
)
expect(await readFile(filePath, 'utf8')).not.toContain(
'rerank-secret-to-clear'
)
await expect(store.getPublicSettings()).resolves.toMatchObject({
knowledgeRerankApiKeyConfigured: false,
knowledgeRerankCredentialSource: 'none'
})
const unavailable = new RuntimeSettingsStore(filePath, {
...cipher,
isAvailable: () => false
})
await expect(
unavailable.update(
settings({
knowledgeRerankApiKey: {
action: 'replace',
value: 'must-not-be-persisted'
}
})
)
).rejects.toThrow('安全存储不可用')
expect(await readFile(filePath, 'utf8')).not.toContain(
'must-not-be-persisted'
)
})
it('migrates version 6 Ollama origins to OpenAI-compatible embedding endpoints', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as Record<
string,
unknown
>
persisted.version = 6
persisted.knowledgeEmbeddingBaseUrl = 'http://127.0.0.1:11434'
delete persisted.knowledgeEmbeddingCredential
await writeFile(filePath, JSON.stringify(persisted), 'utf8')
const migratedStore = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migratedStore.getPublicSettings()).resolves.toMatchObject({
knowledgeEmbeddingBaseUrl:
'http://127.0.0.1:11434/v1/embeddings',
knowledgeEmbeddingApiKeyConfigured: false,
imageGenerationQuality: 'auto',
modelProfiles: [
expect.objectContaining({ imageGenerationQuality: 'auto' })
]
})
})
it('keeps an already complete version 6 embedding endpoint unchanged', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as Record<
string,
unknown
>
persisted.version = 6
persisted.knowledgeEmbeddingBaseUrl =
'https://vectors.example/custom/v1/embeddings'
delete persisted.knowledgeEmbeddingCredential
await writeFile(filePath, JSON.stringify(persisted), 'utf8')
const migratedStore = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migratedStore.getPublicSettings()).resolves.toMatchObject({
knowledgeEmbeddingBaseUrl:
'https://vectors.example/custom/v1/embeddings'
})
})
it('repairs only an invalid version 6 embedding endpoint', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
provider: 'continue',
workspacePath: 'preserve-this-workspace'
})
)
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as Record<
string,
unknown
>
persisted.version = 6
persisted.knowledgeEmbeddingBaseUrl = 'not a URL'
delete persisted.knowledgeEmbeddingCredential
await writeFile(filePath, JSON.stringify(persisted), 'utf8')
const migratedStore = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migratedStore.getPublicSettings()).resolves.toMatchObject({
provider: 'continue',
workspacePath: 'preserve-this-workspace',
knowledgeEmbeddingBaseUrl:
'http://127.0.0.1:11434/v1/embeddings'
})
expect(
(await readdir(join(filePath, '..'))).some((name) =>
name.startsWith('runtime-settings.json.corrupt-')
)
).toBe(false)
})
it('defaults image quality when migrating version 7 settings', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({ imageGenerationQuality: 'high' })
)
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
modelProfiles: Array<Record<string, unknown>>
}
persisted.version = 7
for (const profile of persisted.modelProfiles) {
delete profile.imageGenerationQuality
}
await writeFile(filePath, JSON.stringify(persisted), 'utf8')
const migratedStore = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migratedStore.getResolvedSettings()).resolves.toMatchObject({
imageGenerationQuality: 'auto'
})
await expect(migratedStore.getPublicSettings()).resolves.toMatchObject({
imageGenerationQuality: 'auto',
modelProfiles: [
expect.objectContaining({ imageGenerationQuality: 'auto' })
]
})
})
it('keeps an encrypted API key with its model connection when the URL changes', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
apiKey: { action: 'replace', value: 'test-secret-value' }
})
)
const contents = await readFile(filePath, 'utf8')
expect(contents).not.toContain('test-secret-value')
await expect(store.getResolvedSettings()).resolves.toMatchObject({
apiKey: 'test-secret-value',
modelBaseUrl: 'https://bigtoken.ai'
})
await store.update(
settings({
modelBaseUrl: 'https://other.example',
apiKey: { action: 'keep' }
})
)
await expect(store.getResolvedSettings()).resolves.toMatchObject({
apiKey: 'test-secret-value',
modelBaseUrl: 'https://other.example'
})
await expect(store.getPublicSettings()).resolves.toMatchObject({
apiKeyConfigured: true,
credentialSource: 'encrypted'
})
})
it('keeps a model API key while authentication is disabled and restores it when re-enabled', async () => {
const { store } = await createStore()
await store.update(
settings({
apiKey: {
action: 'replace',
value: 'connection-scoped-secret'
}
})
)
await store.update(
settings({
modelAuthentication: 'none',
apiKey: { action: 'keep' }
})
)
await expect(store.getPublicSettings()).resolves.toMatchObject({
modelAuthentication: 'none',
modelProfiles: [
expect.objectContaining({
authentication: 'none',
apiKeyConfigured: true,
credentialSource: 'encrypted'
})
]
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelAuthentication: 'none',
apiKey: undefined
})
await store.update(
settings({
modelAuthentication: 'api-key',
apiKey: { action: 'keep' }
})
)
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelAuthentication: 'api-key',
apiKey: 'connection-scoped-secret'
})
})
it('reads a legacy API key payload after the model connection URL changes', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as {
modelProfiles: Array<Record<string, unknown>>
}
persisted.modelProfiles[0]!.baseUrl = 'https://new.example/v1'
persisted.modelProfiles[0]!.credential = {
formatVersion: 1,
scheme: 'electron-safe-storage',
ciphertextBase64: cipher
.encrypt(
JSON.stringify({
version: 1,
apiKey: 'legacy-connection-secret',
origin: 'https://old.example'
})
)
.toString('base64')
}
await writeFile(filePath, JSON.stringify(persisted), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getResolvedSettings()).resolves.toMatchObject({
modelBaseUrl: 'https://new.example/v1',
apiKey: 'legacy-connection-secret'
})
})
it('does not infer image capability from the model name', async () => {
const { store } = await createStore()
await store.update(
settings({
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'gpt-image-2',
modelProtocol: 'anthropic-messages',
apiKey: { action: 'replace', value: 'image-secret' }
})
)
await expect(store.getPublicSettings()).resolves.toMatchObject({
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'gpt-image-2',
modelProtocol: 'anthropic-messages',
apiKeyConfigured: true,
credentialSource: 'encrypted',
modelProfiles: [
expect.objectContaining({
baseUrl: 'https://bigtoken.ai',
modelName: 'gpt-image-2',
protocol: 'anthropic-messages'
})
]
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'gpt-image-2',
modelProtocol: 'anthropic-messages',
apiKey: 'image-secret'
})
})
it('uses the explicit protocol as the image-generation capability marker', async () => {
const { filePath, store } = await createStore()
const chatId = crypto.randomUUID()
const imageId = crypto.randomUUID()
await store.update(
settings({
modelProfiles: [
{
id: chatId,
name: 'Chat',
baseUrl: 'https://chat.example/v1',
modelName: 'chat-model',
protocol: 'openai-chat-completions',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: { action: 'replace', value: 'chat-secret' }
},
{
id: imageId,
name: 'Custom Image',
baseUrl: 'https://images.example/custom/v2',
modelName: 'vendor/custom-renderer',
protocol: 'openai-images-generations',
authentication: 'api-key',
imageGenerationQuality: 'high',
apiKey: { action: 'replace', value: 'image-secret' }
}
],
defaultModelProfileId: imageId
})
)
await expect(store.getPublicSettings()).resolves.toMatchObject({
modelBaseUrl: 'https://images.example/custom/v2',
modelName: 'vendor/custom-renderer',
modelProtocol: 'openai-images-generations',
modelProfiles: [
expect.objectContaining({ id: chatId }),
expect.objectContaining({
id: imageId,
baseUrl: 'https://images.example/custom/v2',
modelName: 'vendor/custom-renderer',
protocol: 'openai-images-generations',
imageGenerationQuality: 'high'
})
]
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelBaseUrl: 'https://images.example/custom/v2',
modelName: 'vendor/custom-renderer',
modelProtocol: 'openai-images-generations',
imageGenerationQuality: 'high',
apiKey: 'image-secret'
})
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
modelProfiles: Array<Record<string, unknown>>
}
expect(persisted.version).toBe(21)
expect(persisted.modelProfiles).toContainEqual(
expect.objectContaining({
id: imageId,
imageGenerationQuality: 'high'
})
)
})
it('stores multiple encrypted model profiles and resolves runtime sources', async () => {
const { filePath, store } = await createStore()
const firstId = '00000000-0000-4000-8000-000000000011'
const secondId = '00000000-0000-4000-8000-000000000012'
await store.update(
settings({
modelProfiles: [
{
id: firstId,
name: '工作模型',
baseUrl: 'https://work.example',
modelName: 'work-model',
protocol: 'openai-responses',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: { action: 'replace', value: 'work-secret' }
},
{
id: secondId,
name: '默认模型',
baseUrl: 'https://default.example',
modelName: 'default-model',
protocol: 'openai-chat-completions',
authentication: 'none',
imageGenerationQuality: 'auto',
apiKey: { action: 'keep' }
}
],
defaultModelProfileId: secondId,
opencodeModelSource: { kind: 'profile', profileId: firstId },
continueModelSource: { kind: 'profile', profileId: secondId }
})
)
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelBaseUrl: 'https://default.example',
modelName: 'default-model',
modelAuthentication: 'none',
opencodeModelProfile: {
id: firstId,
protocol: 'openai-responses',
apiKey: 'work-secret'
},
continueModelProfile: {
id: secondId,
protocol: 'openai-chat-completions',
authentication: 'none'
}
})
const persisted = await readFile(filePath, 'utf8')
expect(persisted).not.toContain('work-secret')
const publicSettings = await store.getPublicSettings()
expect(publicSettings.modelProfiles).toHaveLength(2)
expect(JSON.stringify(publicSettings)).not.toContain('work-secret')
await store.update(
settings({
modelBaseUrl: 'https://default.example',
modelName: 'updated-default-model'
})
)
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelName: 'updated-default-model',
opencodeModelProfile: {
id: firstId,
apiKey: 'work-secret'
}
})
await expect(store.getPublicSettings()).resolves.toMatchObject({
modelProfiles: [
expect.objectContaining({ id: firstId }),
expect.objectContaining({
id: secondId,
modelName: 'updated-default-model'
})
]
})
})
it('keeps provider-specific environment aliases for existing deployments', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
modelBaseUrl: 'https://custom.example',
apiKey: { action: 'replace', value: 'stored-test-key' }
})
)
const environmentStore = new RuntimeSettingsStore(filePath, cipher, {
GOODBUDDY_BIGTOKEN_API_KEY: 'legacy-key',
GOODBUDDY_BIGTOKEN_BASE_URL: 'https://legacy.example',
GOODBUDDY_BIGTOKEN_MODEL: 'legacy-model'
})
await expect(environmentStore.getResolvedSettings()).resolves.toMatchObject({
apiKey: 'legacy-key',
modelBaseUrl: 'https://legacy.example',
modelName: 'legacy-model'
})
})
it('uses the default environment key only for the default model connection', async () => {
const { filePath, store } = await createStore()
const defaultId = '00000000-0000-4000-8000-000000000031'
const secondaryId = '00000000-0000-4000-8000-000000000032'
await store.update(
settings({
modelProfiles: [
{
id: defaultId,
name: 'Default',
baseUrl: 'https://default.example/v1',
modelName: 'default-model',
protocol: 'openai-chat-completions',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: { action: 'replace', value: 'stored-default-key' }
},
{
id: secondaryId,
name: 'Secondary',
baseUrl: 'https://secondary.example/v1',
modelName: 'secondary-model',
protocol: 'openai-chat-completions',
authentication: 'api-key',
imageGenerationQuality: 'auto',
apiKey: { action: 'replace', value: 'secondary-key' }
}
],
defaultModelProfileId: defaultId
})
)
const environmentStore = new RuntimeSettingsStore(filePath, cipher, {
GOODBUDDY_MODEL_API_KEY: 'environment-default-key',
GOODBUDDY_MODEL_BASE_URL: 'https://environment.example/v1',
GOODBUDDY_MODEL_NAME: 'environment-model'
})
await expect(
environmentStore.getResolvedSettings()
).resolves.toMatchObject({
modelProfiles: [
expect.objectContaining({
id: defaultId,
apiKey: 'environment-default-key'
}),
expect.objectContaining({
id: secondaryId,
apiKey: 'secondary-key'
})
]
})
})
it('prefers generic model environment variables over legacy aliases', async () => {
const { filePath } = await createStore()
const store = new RuntimeSettingsStore(filePath, cipher, {
GOODBUDDY_MODEL_API_KEY: 'generic-key',
GOODBUDDY_MODEL_BASE_URL: 'https://generic.example',
GOODBUDDY_MODEL_NAME: 'generic-model',
GOODBUDDY_BIGTOKEN_API_KEY: 'legacy-key',
GOODBUDDY_BIGTOKEN_BASE_URL: 'https://legacy.example',
GOODBUDDY_BIGTOKEN_MODEL: 'legacy-model'
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
apiKey: 'generic-key',
modelBaseUrl: 'https://generic.example',
modelName: 'generic-model'
})
})
it('keeps configured model values when environment values are effective', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
modelBaseUrl: 'https://stored.example/v1',
modelName: 'stored-model',
apiKey: { action: 'replace', value: 'stored-key' }
})
)
const environmentStore = new RuntimeSettingsStore(filePath, cipher, {
GOODBUDDY_MODEL_API_KEY: 'environment-key',
GOODBUDDY_MODEL_BASE_URL: 'https://environment.example/v1',
GOODBUDDY_MODEL_NAME: 'environment-model'
})
const publicSettings = await environmentStore.getPublicSettings()
expect(publicSettings).toMatchObject({
modelBaseUrl: 'https://environment.example/v1',
modelName: 'environment-model',
credentialSource: 'environment',
modelProfiles: [
expect.objectContaining({
baseUrl: 'https://environment.example/v1',
modelName: 'environment-model',
credentialSource: 'environment'
})
],
configured: {
modelProfiles: [
expect.objectContaining({
baseUrl: 'https://stored.example/v1',
modelName: 'stored-model',
credentialSource: 'environment'
})
]
}
})
const defaultProfile = publicSettings.configured!.modelProfiles[0]!
await environmentStore.update(
settings({
modelBaseUrl: defaultProfile.baseUrl,
modelName: defaultProfile.modelName,
modelProtocol: defaultProfile.protocol,
modelAuthentication: defaultProfile.authentication,
imageGenerationQuality: defaultProfile.imageGenerationQuality,
modelProfiles: publicSettings.configured!.modelProfiles.map(
(profile) => ({
id: profile.id,
name: profile.name,
baseUrl: profile.baseUrl,
modelName: profile.modelName,
protocol: profile.protocol,
authentication: profile.authentication,
supportsImageInput: profile.supportsImageInput,
imageGenerationQuality: profile.imageGenerationQuality,
apiKey: { action: 'keep' }
})
),
defaultModelProfileId: publicSettings.defaultModelProfileId
})
)
await expect(
new RuntimeSettingsStore(filePath, cipher, {}).getResolvedSettings()
).resolves.toMatchObject({
modelBaseUrl: 'https://stored.example/v1',
modelName: 'stored-model',
apiKey: 'stored-key'
})
})
it('migrates version 1 settings without losing the encrypted API key', async () => {
const { filePath, store } = await createStore()
const encryptedCredential = cipher
.encrypt(
JSON.stringify({
version: 1,
apiKey: 'legacy-secret',
origin: 'https://legacy.example'
})
)
.toString('base64')
await writeFile(
filePath,
JSON.stringify({
version: 1,
provider: 'bigtoken',
bigtokenBaseUrl: 'https://legacy.example',
bigtokenModel: 'legacy-model',
opencodeBaseUrl: '',
opencodeEmbedded: false,
continueCommand: 'cn',
workspacePath: 'legacy-workspace',
credential: {
formatVersion: 1,
scheme: 'electron-safe-storage',
ciphertextBase64: encryptedCredential
},
toolApproval: 'always'
}),
'utf8'
)
await expect(store.getResolvedSettings()).resolves.toMatchObject({
provider: 'model',
modelBaseUrl: 'https://legacy.example',
modelName: 'legacy-model',
apiKey: 'legacy-secret'
})
await store.update(
settings({
modelBaseUrl: 'https://legacy.example',
modelName: 'legacy-model'
})
)
const saved = JSON.parse(await readFile(filePath, 'utf8')) as Record<
string,
unknown
>
expect(saved).toMatchObject({
version: 21,
provider: 'model',
continueBinaryPath: '',
continueMode: 'chat',
modelProfiles: [
expect.objectContaining({
baseUrl: 'https://legacy.example',
modelName: 'legacy-model'
})
]
})
expect(saved).not.toHaveProperty('bigtokenBaseUrl')
await expect(store.getResolvedSettings()).resolves.toMatchObject({
apiKey: 'legacy-secret'
})
})
it('migrates version 2 Continue commands to binary paths', async () => {
const { filePath, store } = await createStore()
await writeFile(
filePath,
JSON.stringify({
version: 2,
provider: 'continue',
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
opencodeBaseUrl: '',
opencodeEmbedded: false,
continueCommand: 'C:\\Tools\\continue.exe',
workspacePath: 'legacy-workspace',
toolApproval: 'always'
}),
'utf8'
)
const publicSettings = await store.getPublicSettings()
expect(publicSettings).toMatchObject({
continueBinaryPath: 'C:\\Tools\\continue.exe',
continueConfigPath: '',
opencodeBinaryPath: '',
opencodeConfigPath: ''
})
expect(publicSettings).not.toHaveProperty('continueCommand')
})
it('migrates version 3 settings to read-only Continue chat mode', async () => {
const { filePath, store } = await createStore()
await writeFile(
filePath,
JSON.stringify({
version: 3,
provider: 'continue',
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
opencodeBaseUrl: '',
opencodeEmbedded: false,
opencodeBinaryPath: '',
opencodeConfigPath: '',
continueBinaryPath: '',
continueConfigPath: '',
workspacePath: 'legacy-workspace',
toolApproval: 'always'
}),
'utf8'
)
await expect(store.getPublicSettings()).resolves.toMatchObject({
provider: 'continue',
continueMode: 'chat'
})
})
it('treats the legacy default cn command as automatic detection', async () => {
const { filePath, store } = await createStore()
await writeFile(
filePath,
JSON.stringify({
version: 2,
provider: 'continue',
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
continueCommand: 'cn',
workspacePath: 'legacy-workspace',
toolApproval: 'always'
}),
'utf8'
)
await expect(store.getPublicSettings()).resolves.toMatchObject({
continueBinaryPath: ''
})
})
it('canonicalizes runtime paths and only accepts regular files', async () => {
const { filePath, store } = await createStore()
const directory = join(filePath, '..')
const binaryPath = join(directory, 'continue-test-binary')
const configPath = join(directory, 'continue-test-config.json')
await Promise.all([
writeFile(binaryPath, 'binary', 'utf8'),
writeFile(configPath, '{}', 'utf8')
])
await expect(
store.update(
settings({
continueBinaryPath: binaryPath,
continueConfigPath: configPath
})
)
).resolves.toMatchObject({
continueBinaryPath: binaryPath,
continueConfigPath: configPath
})
await expect(
store.update(settings({ opencodeConfigPath: directory }))
).rejects.toThrow('不是普通文件')
})
it('rejects control characters in runtime paths', () => {
expect(
runtimeSettingsInputSchema.safeParse(
settings({ continueBinaryPath: 'C:\\Tools\\continue.exe\n--evil' })
).success
).toBe(false)
expect(
runtimeSettingsInputSchema.safeParse(
settings({ opencodeConfigPath: '' })
).success
).toBe(true)
})
it('allows HTTP, IP literals, credentials, paths and queries', () => {
expect(
runtimeSettingsInputSchema.safeParse(
settings({
modelBaseUrl:
'http://user@10.0.0.25:8000/models/v1?api-version=2024-02-01',
knowledgeEmbeddingEnabled: true,
knowledgeEmbeddingBaseUrl:
'http://vectors.example.com/v1/embeddings?format=float'
})
).success
).toBe(true)
expect(
runtimeSettingsInputSchema.safeParse(
settings({
modelProfiles: [
{
id: crypto.randomUUID(),
name: '内网模型',
baseUrl: 'http://[fd00::25]:8000/api',
modelName: 'corp-model',
protocol: 'openai-chat-completions',
authentication: 'none',
imageGenerationQuality: 'auto',
apiKey: { action: 'clear' }
}
]
})
).success
).toBe(true)
})
it('still rejects endpoint protocols the clients cannot transport', () => {
expect(
runtimeSettingsInputSchema.safeParse(
settings({
modelBaseUrl: 'ftp://models.example.com/v1'
})
).success
).toBe(false)
expect(
runtimeSettingsInputSchema.safeParse(
settings({
knowledgeEmbeddingEnabled: true,
knowledgeEmbeddingBaseUrl:
'file:///tmp/embeddings'
})
).success
).toBe(false)
})
it('migrates version 5 profiles to Anthropic API-key profiles', async () => {
const { filePath, store } = await createStore()
const profileId = '00000000-0000-4000-8000-000000000021'
const encryptedCredential = cipher
.encrypt(
JSON.stringify({
version: 1,
apiKey: 'version-five-secret',
origin: 'https://legacy-v5.example'
})
)
.toString('base64')
await writeFile(
filePath,
JSON.stringify({
version: 5,
provider: 'model',
modelProfiles: [
{
id: profileId,
name: 'V5 模型',
baseUrl: 'https://legacy-v5.example',
modelName: 'legacy-v5-model',
credential: {
formatVersion: 1,
scheme: 'electron-safe-storage',
ciphertextBase64: encryptedCredential
}
}
],
defaultModelProfileId: profileId,
opencodeModelSource: { kind: 'profile', profileId },
continueModelSource: { kind: 'profile', profileId },
opencodeBaseUrl: '',
opencodeEmbedded: false,
opencodeBinaryPath: '',
opencodeConfigPath: '',
continueBinaryPath: '',
continueConfigPath: '',
continueMode: 'chat',
workspacePath: 'legacy-workspace',
toolApproval: 'always'
}),
'utf8'
)
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelProtocol: 'anthropic-messages',
modelAuthentication: 'api-key',
apiKey: 'version-five-secret',
opencodeModelProfile: {
id: profileId,
protocol: 'anthropic-messages',
authentication: 'api-key'
},
continueModelProfile: { id: profileId }
})
})
it('persists an unauthenticated Ollama profile without a credential', async () => {
const { filePath, store } = await createStore()
const profileId = '00000000-0000-4000-8000-000000000022'
await store.update(
settings({
modelBaseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
modelProtocol: 'openai-chat-completions',
modelAuthentication: 'none',
modelProfiles: [
{
id: profileId,
name: 'Ollama',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
protocol: 'openai-chat-completions',
authentication: 'none',
imageGenerationQuality: 'auto',
apiKey: { action: 'clear' }
}
],
defaultModelProfileId: profileId
})
)
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelBaseUrl: 'http://127.0.0.1:11434/v1',
modelProtocol: 'openai-chat-completions',
modelAuthentication: 'none',
apiKey: undefined
})
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
modelProfiles: Array<Record<string, unknown>>
}
expect(persisted.version).toBe(21)
expect(persisted.modelProfiles[0]).not.toHaveProperty('credential')
})
it('resolves new runtime environment variables with legacy fallback', async () => {
const { store } = await createStore({
GOODBUDDY_OPENCODE_BINARY: 'C:\\Tools\\opencode.exe',
GOODBUDDY_OPENCODE_CONFIG: 'C:\\Config\\opencode.json',
GOODBUDDY_CONTINUE_BINARY: 'C:\\Tools\\cn.exe',
GOODBUDDY_CONTINUE_CONFIG: 'C:\\Config\\continue.yaml',
GOODBUDDY_CONTINUE_COMMAND: 'legacy-cn'
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
opencodeBinaryPath: 'C:\\Tools\\opencode.exe',
opencodeConfigPath: 'C:\\Config\\opencode.json',
continueBinaryPath: 'C:\\Tools\\cn.exe',
continueConfigPath: 'C:\\Config\\continue.yaml'
})
const { store: legacyStore } = await createStore({
GOODBUDDY_CONTINUE_COMMAND: 'legacy-cn'
})
await expect(legacyStore.getResolvedSettings()).resolves.toMatchObject({
continueBinaryPath: 'legacy-cn'
})
const { store: defaultLegacyStore } = await createStore({
GOODBUDDY_CONTINUE_COMMAND: 'cn'
})
await expect(defaultLegacyStore.getResolvedSettings()).resolves.toMatchObject({
continueBinaryPath: ''
})
})
it('refuses to persist credentials when secure storage is unavailable', async () => {
const { filePath } = await createStore()
const store = new RuntimeSettingsStore(filePath, {
...cipher,
isAvailable: () => false
})
await expect(
store.update(
settings({
apiKey: { action: 'replace', value: 'test-secret-value' }
})
)
).rejects.toThrow('安全存储不可用')
})
it('preserves settings created by a newer unsupported version', async () => {
const { filePath, store } = await createStore()
const futureSettings = JSON.stringify({
version: 99,
futureField: 'keep-me'
})
await writeFile(filePath, futureSettings, 'utf8')
await expect(store.getPublicSettings()).rejects.toThrow(
'不支持 Runtime 设置版本 99'
)
expect(await readFile(filePath, 'utf8')).toBe(futureSettings)
const files = await readdir(join(filePath, '..'))
expect(
files.some((name) => name.startsWith('runtime-settings.json.corrupt-'))
).toBe(false)
})
it('isolates a corrupt settings file and reports recovery', async () => {
const { filePath, store } = await createStore()
await writeFile(filePath, '{not-valid-json', 'utf8')
await expect(store.getPublicSettings()).resolves.toMatchObject({
provider: 'model',
warnings: [{ code: 'runtime-settings-recovered' }]
})
const files = await readdir(join(filePath, '..'))
expect(
files.some((name) => name.startsWith('runtime-settings.json.corrupt-'))
).toBe(true)
})
it('distinguishes an unreadable saved credential from a missing credential', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
apiKey: {
action: 'replace',
value: 'credential-that-will-become-unreadable'
}
})
)
const unreadable = new RuntimeSettingsStore(
filePath,
{
...cipher,
decrypt: () => {
throw new Error('cannot decrypt')
}
},
{}
)
await expect(unreadable.getPublicSettings()).resolves.toMatchObject({
apiKeyConfigured: false,
credentialSource: 'unreadable',
modelProfiles: [
expect.objectContaining({
credentialSource: 'unreadable'
})
],
warnings: [
expect.objectContaining({
code: 'runtime-model-credential-unreadable',
subject: '默认模型'
})
]
})
})
it('clears credential warnings after secure storage recovers', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
apiKey: {
action: 'replace',
value: 'recoverable-model-credential'
},
knowledgeEmbeddingApiKey: {
action: 'replace',
value: 'recoverable-embedding-credential'
},
knowledgeRerankApiKey: {
action: 'replace',
value: 'recoverable-rerank-credential'
}
})
)
let decryptAvailable = false
const recoveringStore = new RuntimeSettingsStore(
filePath,
{
...cipher,
decrypt: (value) => {
if (!decryptAvailable) {
throw new Error('secure storage is temporarily unavailable')
}
return cipher.decrypt(value)
}
},
{}
)
await expect(recoveringStore.getPublicSettings()).resolves.toMatchObject({
warnings: expect.arrayContaining([
expect.objectContaining({
code: 'runtime-model-credential-unreadable'
}),
{ code: 'runtime-embedding-credential-unreadable' },
{ code: 'runtime-rerank-credential-unreadable' }
])
})
decryptAvailable = true
await expect(recoveringStore.getPublicSettings()).resolves.toMatchObject({
apiKeyConfigured: true,
knowledgeEmbeddingApiKeyConfigured: true,
knowledgeRerankApiKeyConfigured: true
})
expect((await recoveringStore.getPublicSettings()).warnings ?? []).toEqual(
[]
)
})
})