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