File size: 6,661 Bytes
b8370f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | import Foundation
import Testing
@testable import OpenClawChatUI
private func pickerModel(_ selectionID: String, manualSelectionAllowed: Bool? = nil) -> OpenClawChatModelChoice {
let parts = selectionID.split(separator: "/", maxSplits: 1).map(String.init)
return OpenClawChatModelChoice(
modelID: parts.count == 2 ? parts[1] : selectionID,
name: selectionID,
provider: parts.count == 2 ? parts[0] : "test",
manualSelectionAllowed: manualSelectionAllowed,
contextWindow: nil)
}
@MainActor
private func withPickerStore(_ body: (ChatModelPickerStore, UserDefaults) throws -> Void) throws {
let suiteName = "ChatModelPickerStoreTests.\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suiteName))
defer { defaults.removePersistentDomain(forName: suiteName) }
try body(ChatModelPickerStore(defaults: defaults), defaults)
}
@MainActor
@Suite struct ChatModelPickerStoreTests {
@Test func `favorites and recents round trip through defaults`() throws {
try withPickerStore { store, defaults in
store.toggleFavorite("anthropic/opus")
store.recordRecent("openai/gpt")
let reloaded = ChatModelPickerStore(defaults: defaults)
#expect(reloaded.favorites == ["anthropic/opus"])
#expect(reloaded.recents == ["openai/gpt"])
}
}
@Test func `recents dedupe move to front cap and skip invalid ids`() throws {
try withPickerStore { store, _ in
store.recordRecent("")
store.recordRecent(OpenClawChatViewModel.defaultModelSelectionID)
for id in ["one", "two", "three", "four", "five", "six"] {
store.recordRecent(id)
}
#expect(store.recents == ["six", "five", "four", "three", "two"])
store.recordRecent("four")
#expect(store.recents == ["four", "six", "five", "three", "two"])
}
}
@Test func `favorites preserve pin order and remove unpinned ids`() throws {
try withPickerStore { store, _ in
store.toggleFavorite("one")
store.toggleFavorite("two")
store.toggleFavorite("three")
#expect(store.favorites == ["one", "two", "three"])
store.toggleFavorite("two")
#expect(store.favorites == ["one", "three"])
store.toggleFavorite("two")
#expect(store.favorites == ["one", "three", "two"])
}
}
@Test func `separate stores preserve each others updates`() throws {
try withPickerStore { first, defaults in
let second = ChatModelPickerStore(defaults: defaults)
first.toggleFavorite("one")
second.toggleFavorite("two")
#expect(second.favorites == ["one", "two"])
first.recordRecent("one")
second.recordRecent("two")
#expect(second.recents == ["two", "one"])
}
}
@Test func `ordering preserves sections and skips missing models`() {
let choices = [
pickerModel("a/one"), pickerModel("b/two"), pickerModel("c/three"), pickerModel("d/four"),
pickerModel("e/automatic", manualSelectionAllowed: false),
]
let sections = ChatModelPickerStore.sections(
choices: choices,
favorites: ["e/automatic", "c/three", "missing/model", "a/one"],
recents: ["a/one", "e/automatic", "d/four", "missing/recent"],
defaultProvider: "b")
#expect(sections.pinned.map(\.selectionID) == ["c/three", "a/one"])
#expect(sections.recent.map(\.selectionID) == ["d/four"])
#expect(sections.providers.flatMap(\.models).map(\.selectionID) == ["b/two"])
#expect(sections.providers.map(\.id) == ["b"])
#expect(sections.providers.first?.isDefaultProvider == true)
}
@Test func `ordering handles empty inputs`() {
let sections = ChatModelPickerStore.sections(choices: [], favorites: [], recents: [])
#expect(sections.pinned.isEmpty)
#expect(sections.recent.isEmpty)
#expect(sections.providers.flatMap(\.models).isEmpty)
}
@Test func `missing default provider does not mark other as default`() {
let unqualified = OpenClawChatModelChoice(
modelID: "local-model",
name: "Local",
provider: "",
contextWindow: nil)
let sections = ChatModelPickerStore.sections(choices: [unqualified], favorites: [], recents: [])
#expect(sections.providers.map(\.id) == ["other"])
#expect(sections.providers.first?.isDefaultProvider == false)
}
@Test func `remaining models group by provider with default provider first`() {
let choices = [
pickerModel("xai/grok"),
pickerModel("openai/gpt"),
pickerModel("anthropic/opus"),
pickerModel("openai/o3"),
]
let sections = ChatModelPickerStore.sections(
choices: choices,
favorites: [],
recents: [],
defaultProvider: "openai")
#expect(sections.providers.map(\.id) == ["openai", "anthropic", "xai"])
#expect(sections.providers[0].models.map(\.selectionID) == ["openai/gpt", "openai/o3"])
#expect(sections.providers.map(\.displayName) == ["OpenAI", "Anthropic", "xAI"])
}
@Test func `qualified global default matches provider model pair`() {
let model = pickerModel("openai/gpt")
let qualifiedModelID = OpenClawChatModelChoice(
modelID: "openai/gpt",
name: "GPT",
provider: "openai",
contextWindow: nil)
#expect(ChatModelPickerStore.isDefaultModel(
model,
defaultProvider: nil,
defaultModel: "openai/gpt"))
#expect(ChatModelPickerStore.isDefaultModel(
model,
defaultProvider: "openai",
defaultModel: "gpt"))
#expect(!ChatModelPickerStore.isDefaultModel(
model,
defaultProvider: "anthropic",
defaultModel: "gpt"))
#expect(ChatModelPickerStore.isDefaultModel(
qualifiedModelID,
defaultProvider: "openai",
defaultModel: "gpt"))
}
@Test func `qualified default model supplies missing default provider`() {
#expect(ChatModelPickerStore.resolvedDefaultProvider(
provider: nil,
model: "openai/gpt") == "openai")
#expect(ChatModelPickerStore.resolvedDefaultProvider(
provider: "anthropic",
model: "openai/gpt") == "anthropic")
}
}
|