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")
    }
}