File size: 5,763 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
import Foundation
import Testing
@testable import OpenClawChatUI

private struct SidebarPreviewTransport: OpenClawChatTransport {
    func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload {
        .init(sessionKey: sessionKey, sessionId: nil, messages: [], thinkingLevel: "off")
    }

    func sendMessage(
        sessionKey _: String, message _: String, thinking _: String,
        idempotencyKey: String, attachments _: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse
    {
        .init(runId: idempotencyKey, status: "ok")
    }

    func requestHealth(timeoutMs _: Int) async throws -> Bool {
        false
    }

    func events() -> AsyncStream<OpenClawChatTransportEvent> {
        AsyncStream { $0.finish() }
    }
}

private actor SidebarPreviewCache: OpenClawChatTranscriptCache {
    let text: String
    private var released: Bool
    private var continuation: CheckedContinuation<Void, Never>?
    private(set) var requests: [(String, String?)] = []

    init(text: String, held: Bool = false) {
        self.text = text
        self.released = !held
    }

    func release() {
        self.released = true
        self.continuation?.resume()
        self.continuation = nil
    }

    func loadTranscript(sessionKey: String, agentID: String?) async -> [OpenClawChatMessage] {
        self.requests.append((sessionKey, agentID))
        if !self.released { await withCheckedContinuation { self.continuation = $0 } }
        return [.init(role: "assistant", content: [
            .init(type: "text", text: self.text, mimeType: nil, fileName: nil, content: nil),
        ], timestamp: 1)]
    }

    func loadTranscript(sessionKey: String) async -> [OpenClawChatMessage] {
        await self.loadTranscript(sessionKey: sessionKey, agentID: nil)
    }

    func loadSessions() async -> [OpenClawChatSessionEntry] {
        []
    }

    func storeSessions(_: [OpenClawChatSessionEntry]) async {}
    func storeCanonicalTranscript(
        sessionKey _: String, agentID _: String?, messages _: [OpenClawChatMessage],
        canonicalMessageIdempotencyKeys _: Set<String>) async {}
}

@MainActor
struct ChatSessionSidebarPreviewsTests {
    @Test(arguments: [false, true])
    func `changing Gateway owners never reuses an identical session preview`(oldLoadPending: Bool) async throws {
        let suite = "ChatSessionSidebarPreviewsTests.\(UUID().uuidString)"
        let defaults = try #require(UserDefaults(suiteName: suite))
        let first = self.model(defaults: defaults)
        let second = self.model(defaults: defaults)
        defer {
            first.detachTransport()
            second.detachTransport()
            defaults.removePersistentDomain(forName: suite)
        }
        let row = self.row(key: "global")
        let firstRequest = ChatSessionSidebarPreviews.Request(viewModel: first, sessions: [row])
        let secondRequest = ChatSessionSidebarPreviews.Request(viewModel: second, sessions: [row])
        let store = ChatSessionSidebarPreviews()
        let old = SidebarPreviewCache(text: "Gateway A", held: oldLoadPending)
        let next = SidebarPreviewCache(text: "Gateway B", held: !oldLoadPending)
        if oldLoadPending {
            let pending = Task { await store.refresh(firstRequest, cache: old) }
            try await waitUntil("old cache read starts") { await old.requests.count == 1 }
            await store.refresh(secondRequest, cache: next)
            await old.release()
            await pending.value
        } else {
            await store.refresh(firstRequest, cache: old)
            #expect(store.text(for: row, in: firstRequest) == "Gateway A")
            let pending = Task { await store.refresh(secondRequest, cache: next) }
            try await waitUntil("new cache read starts") { await next.requests.count == 1 }
            #expect(store.text(for: row, in: secondRequest) == nil)
            await next.release()
            await pending.value
        }
        #expect(store.text(for: row, in: secondRequest) == "Gateway B")
        #expect(store.text(for: row, in: firstRequest) == nil)
    }

    @Test func `preview reads are bounded and retain the selected global owner`() async throws {
        let suite = "ChatSessionSidebarPreviewsTests.\(UUID().uuidString)"
        let defaults = try #require(UserDefaults(suiteName: suite))
        let model = self.model(defaults: defaults)
        defer {
            model.detachTransport()
            defaults.removePersistentDomain(forName: suite)
        }
        let rows = [self.row(key: "global")] + (0..<60).map { self.row(key: "agent:research:thread-\($0)") }
        let request = ChatSessionSidebarPreviews.Request(viewModel: model, sessions: rows)
        let cache = SidebarPreviewCache(text: "Preview")
        let store = ChatSessionSidebarPreviews()
        await store.refresh(request, cache: cache)
        let calls = await cache.requests
        #expect(calls.count == 32)
        #expect(calls.first?.0 == "global")
        #expect(calls.first?.1 == "research")
        #expect(calls.dropFirst().allSatisfy { $0.1 == nil })
        #expect(try store.text(for: #require(rows.last), in: request) == nil)
    }

    private func model(defaults: UserDefaults) -> OpenClawChatViewModel {
        OpenClawChatViewModel(
            sessionKey: "global", transport: SidebarPreviewTransport(), activeAgentId: "research",
            modelPickerStore: ChatModelPickerStore(defaults: defaults))
    }

    private func row(key: String) -> OpenClawChatSessionEntry {
        var row = OpenClawChatSessionEntry.placeholder(key: key)
        row.agentId = "research"
        row.sessionId = "same-session"
        row.updatedAt = 1
        return row
    }
}