将 v1 schema 中 `providers.llm` 从 Ark 专用结构推广为通用 `CredentialsProviderLLMEntry`(containing displayName / temperature / extraHeaders), 为 OpenAI / DeepSeek / DashScope / Moonshot 等多 LLM provider 共用一套字段。 旧 `CredentialsProviderLLMArk` 保留为 typealias,已迁移过的 v1 文件无需再次迁移即可继续解码。 新增公开 API: - `CredentialsVault.activeLLMProviderId` getter/setter - `CredentialsVault.configuredLLMProviderIds` - `CredentialsVault.llmProviderConfig(for:)` - `CredentialsVault.setLLMProviderConfig(_:)` - `CredentialsVault.removeLLMProvider(_:)`,禁删 active provider `OpenLessCore` 新增 `LLMProviderRegistry`,集中维护 5 家 OpenAI 兼容供应商 预设(baseURL / model / 帮助文案 / docs URL),UI 与 vault 兜底逻辑共用。 `PolishOutputCleaner` 从 DoubaoPolishClient 内部提升为独立 public 类型, 让 `OpenAICompatibleLLMProvider` 输出统一过 boilerplate / markdown 栅栏清理。 DoubaoPolishClient.swift 已无调用方,整个文件删除。 Tests: - LLMProviderRegistryTests:5 家预设健康检查 + 唯一性 / URL 合法性 - CredentialsVaultMultiProviderTests:active 默认 / 增删 / 往返 / 老 `ark.*` 账号 key 路由到 active provider
87 lines
3.4 KiB
Swift
87 lines
3.4 KiB
Swift
import XCTest
|
||
@testable import OpenLessCore
|
||
|
||
/// LLMProviderRegistry 的静态健康检查:5 家供应商各 1 条,id 不重复,URL 合法。
|
||
///
|
||
/// 这些断言看起来很无聊,但 registry 直接被设置 UI 和 vault 兜底逻辑用,
|
||
/// 漏一条都会变成"用户加不了 provider / baseURL 解不出来"的运行期 bug。
|
||
final class LLMProviderRegistryTests: XCTestCase {
|
||
|
||
func test_presets_haveExpectedCount() {
|
||
// Arrange
|
||
let presets = LLMProviderRegistry.presets
|
||
|
||
// Act / Assert
|
||
XCTAssertEqual(presets.count, 5, "B-3 计划列出的预设是 ark / openai / aliyun-dashscope / deepseek / moonshot")
|
||
}
|
||
|
||
func test_presets_includeAllExpectedProviderIds() {
|
||
// Arrange
|
||
let expected: Set<String> = ["ark", "openai", "aliyun-dashscope", "deepseek", "moonshot"]
|
||
|
||
// Act
|
||
let actualIds = Set(LLMProviderRegistry.presets.map(\.providerId))
|
||
|
||
// Assert
|
||
XCTAssertEqual(actualIds, expected)
|
||
}
|
||
|
||
func test_presets_haveUniqueProviderIds() {
|
||
// Arrange
|
||
let ids = LLMProviderRegistry.presets.map(\.providerId)
|
||
|
||
// Act / Assert
|
||
XCTAssertEqual(Set(ids).count, ids.count, "providerId 必须唯一,否则 vault 会被互相覆盖")
|
||
}
|
||
|
||
func test_presets_haveValidBaseURLs() {
|
||
for preset in LLMProviderRegistry.presets {
|
||
// Arrange
|
||
let url = preset.defaultBaseURL
|
||
|
||
// Act / Assert:URL 必须是 https + 有 host
|
||
XCTAssertEqual(url.scheme, "https", "preset \(preset.providerId) 的 baseURL 必须是 https,避免网络层降级")
|
||
XCTAssertNotNil(url.host, "preset \(preset.providerId) 的 baseURL 必须有 host")
|
||
}
|
||
}
|
||
|
||
func test_arkPreset_hasEmptyDefaultModel() {
|
||
// Ark 的 defaultModel 故意留空——endpoint id 是用户在控制台拿到的 hash,
|
||
// 不能预设。如果哪天默认填了一个看起来像 model 的字符串,UI 会误导用户。
|
||
let ark = LLMProviderRegistry.preset(for: "ark")
|
||
XCTAssertNotNil(ark)
|
||
XCTAssertEqual(ark?.defaultModel, "")
|
||
}
|
||
|
||
func test_nonArkPresets_haveDefaultModel() {
|
||
// OpenAI / DeepSeek / Moonshot / DashScope 都给了一个开箱即用的 model。
|
||
for preset in LLMProviderRegistry.presets where preset.providerId != "ark" {
|
||
XCTAssertFalse(preset.defaultModel.isEmpty, "preset \(preset.providerId) 应当有 defaultModel")
|
||
}
|
||
}
|
||
|
||
func test_presets_haveNonEmptyHelpText() {
|
||
for preset in LLMProviderRegistry.presets {
|
||
XCTAssertFalse(preset.helpText.isEmpty, "preset \(preset.providerId) 的 helpText 不能为空")
|
||
}
|
||
}
|
||
|
||
func test_preset_lookup_returnsRegistered() {
|
||
for preset in LLMProviderRegistry.presets {
|
||
let resolved = LLMProviderRegistry.preset(for: preset.providerId)
|
||
XCTAssertEqual(resolved, preset)
|
||
}
|
||
}
|
||
|
||
func test_preset_lookup_unknownReturnsNil() {
|
||
XCTAssertNil(LLMProviderRegistry.preset(for: "no-such-provider-xyz"))
|
||
}
|
||
|
||
func test_customSentinels_areDistinctFromPresetIds() {
|
||
// 添加 sheet 用 customProviderId 区分"预设 vs 自定义"——它必须不在预设列表里,
|
||
// 否则会被点击事件混淆。
|
||
let presetIds = Set(LLMProviderRegistry.presets.map(\.providerId))
|
||
XCTAssertFalse(presetIds.contains(LLMProviderRegistry.customProviderId))
|
||
}
|
||
}
|