ios: add multi-polling support
This commit is contained in:
@@ -8,6 +8,9 @@ private struct MockClientCallSnapshot: Sendable {
|
||||
var listSearches = 0
|
||||
var getChat = 0
|
||||
var getSearch = 0
|
||||
var getActiveRuns = 0
|
||||
var attachCompletionStream = 0
|
||||
var attachSearchStream = 0
|
||||
}
|
||||
|
||||
private struct UnexpectedClientCall: Error {}
|
||||
@@ -18,27 +21,34 @@ private actor MockSybilClient: SybilAPIClienting {
|
||||
private let chatDetails: [String: ChatDetail]
|
||||
private let searchDetails: [String: SearchDetail]
|
||||
private let createChatResponse: ChatSummary?
|
||||
private let activeRunsResponse: ActiveRunsResponse
|
||||
|
||||
private var snapshot = MockClientCallSnapshot()
|
||||
private var getChatDelayNanoseconds: UInt64 = 0
|
||||
private var getSearchDelayNanoseconds: UInt64 = 0
|
||||
private var completionStreamNetworkErrorMessage: String?
|
||||
private var completionStreamDelayNanoseconds: UInt64 = 0
|
||||
private var completionAttachEvents: [String: [CompletionStreamEvent]] = [:]
|
||||
private var completionAttachDelayNanoseconds: UInt64 = 0
|
||||
private var searchStreamNetworkErrorMessage: String?
|
||||
private var searchStreamDelayNanoseconds: UInt64 = 0
|
||||
private var searchAttachEvents: [String: [SearchStreamEvent]] = [:]
|
||||
private var searchAttachDelayNanoseconds: UInt64 = 0
|
||||
|
||||
init(
|
||||
chatsResponse: [ChatSummary] = [],
|
||||
searchesResponse: [SearchSummary] = [],
|
||||
chatDetails: [String: ChatDetail] = [:],
|
||||
searchDetails: [String: SearchDetail] = [:],
|
||||
createChatResponse: ChatSummary? = nil
|
||||
createChatResponse: ChatSummary? = nil,
|
||||
activeRunsResponse: ActiveRunsResponse = ActiveRunsResponse()
|
||||
) {
|
||||
self.chatsResponse = chatsResponse
|
||||
self.searchesResponse = searchesResponse
|
||||
self.chatDetails = chatDetails
|
||||
self.searchDetails = searchDetails
|
||||
self.createChatResponse = createChatResponse
|
||||
self.activeRunsResponse = activeRunsResponse
|
||||
}
|
||||
|
||||
func currentSnapshot() -> MockClientCallSnapshot {
|
||||
@@ -63,6 +73,24 @@ private actor MockSybilClient: SybilAPIClienting {
|
||||
searchStreamDelayNanoseconds = delayNanoseconds
|
||||
}
|
||||
|
||||
func setCompletionAttachEvents(
|
||||
chatID: String,
|
||||
events: [CompletionStreamEvent],
|
||||
delayNanoseconds: UInt64 = 0
|
||||
) {
|
||||
completionAttachEvents[chatID] = events
|
||||
completionAttachDelayNanoseconds = delayNanoseconds
|
||||
}
|
||||
|
||||
func setSearchAttachEvents(
|
||||
searchID: String,
|
||||
events: [SearchStreamEvent],
|
||||
delayNanoseconds: UInt64 = 0
|
||||
) {
|
||||
searchAttachEvents[searchID] = events
|
||||
searchAttachDelayNanoseconds = delayNanoseconds
|
||||
}
|
||||
|
||||
func verifySession() async throws -> AuthSession {
|
||||
AuthSession(authenticated: true, mode: "open")
|
||||
}
|
||||
@@ -130,6 +158,11 @@ private actor MockSybilClient: SybilAPIClienting {
|
||||
ModelCatalogResponse(providers: [:])
|
||||
}
|
||||
|
||||
func getActiveRuns() async throws -> ActiveRunsResponse {
|
||||
snapshot.getActiveRuns += 1
|
||||
return activeRunsResponse
|
||||
}
|
||||
|
||||
func runCompletionStream(
|
||||
body: CompletionStreamRequest,
|
||||
onEvent: @escaping @Sendable (CompletionStreamEvent) async -> Void
|
||||
@@ -143,6 +176,20 @@ private actor MockSybilClient: SybilAPIClienting {
|
||||
throw UnexpectedClientCall()
|
||||
}
|
||||
|
||||
func attachCompletionStream(
|
||||
chatID: String,
|
||||
onEvent: @escaping @Sendable (CompletionStreamEvent) async -> Void
|
||||
) async throws {
|
||||
snapshot.attachCompletionStream += 1
|
||||
let events = completionAttachEvents[chatID] ?? []
|
||||
for event in events {
|
||||
await onEvent(event)
|
||||
}
|
||||
if completionAttachDelayNanoseconds > 0 {
|
||||
try await Task.sleep(nanoseconds: completionAttachDelayNanoseconds)
|
||||
}
|
||||
}
|
||||
|
||||
func runSearchStream(
|
||||
searchID: String,
|
||||
body: SearchRunRequest,
|
||||
@@ -156,6 +203,20 @@ private actor MockSybilClient: SybilAPIClienting {
|
||||
}
|
||||
throw UnexpectedClientCall()
|
||||
}
|
||||
|
||||
func attachSearchStream(
|
||||
searchID: String,
|
||||
onEvent: @escaping @Sendable (SearchStreamEvent) async -> Void
|
||||
) async throws {
|
||||
snapshot.attachSearchStream += 1
|
||||
let events = searchAttachEvents[searchID] ?? []
|
||||
for event in events {
|
||||
await onEvent(event)
|
||||
}
|
||||
if searchAttachDelayNanoseconds > 0 {
|
||||
try await Task.sleep(nanoseconds: searchAttachDelayNanoseconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -409,6 +470,59 @@ private func makeSearchDetail(id: String, date: Date, answer: String) -> SearchD
|
||||
await sendTask.value
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func reconnectAttachesSelectedActiveChatStream() async throws {
|
||||
let date = Date(timeIntervalSince1970: 1_700_000_260)
|
||||
let chat = makeChatSummary(id: "chat-active", date: date)
|
||||
let detail = makeChatDetail(id: "chat-active", date: date, body: "existing transcript")
|
||||
let client = MockSybilClient(
|
||||
chatsResponse: [chat],
|
||||
chatDetails: ["chat-active": detail],
|
||||
activeRunsResponse: ActiveRunsResponse(chats: ["chat-active"])
|
||||
)
|
||||
await client.setCompletionAttachEvents(
|
||||
chatID: "chat-active",
|
||||
events: [.delta(CompletionStreamDelta(text: "streaming"))],
|
||||
delayNanoseconds: 100_000_000
|
||||
)
|
||||
let viewModel = SybilViewModel(settings: testSettings(named: #function)) { _ in client }
|
||||
|
||||
await viewModel.reconnect()
|
||||
try await Task.sleep(nanoseconds: 20_000_000)
|
||||
|
||||
let snapshot = await client.currentSnapshot()
|
||||
#expect(snapshot.getActiveRuns >= 1)
|
||||
#expect(snapshot.attachCompletionStream == 1)
|
||||
#expect(viewModel.sidebarItems.first?.isRunning == true)
|
||||
#expect(viewModel.isSendingVisibleChat)
|
||||
#expect(viewModel.displayedMessages.last?.content == "streaming")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func activeRunOnDifferentChatDoesNotDisableComposer() async throws {
|
||||
let date = Date(timeIntervalSince1970: 1_700_000_270)
|
||||
let activeChat = makeChatSummary(id: "chat-active", date: date)
|
||||
let idleChat = makeChatSummary(id: "chat-idle", date: date.addingTimeInterval(1))
|
||||
let client = MockSybilClient(
|
||||
chatsResponse: [idleChat, activeChat],
|
||||
chatDetails: [
|
||||
"chat-active": makeChatDetail(id: "chat-active", date: date, body: "active transcript"),
|
||||
"chat-idle": makeChatDetail(id: "chat-idle", date: date, body: "idle transcript")
|
||||
],
|
||||
activeRunsResponse: ActiveRunsResponse(chats: ["chat-active"])
|
||||
)
|
||||
let viewModel = SybilViewModel(settings: testSettings(named: #function)) { _ in client }
|
||||
viewModel.selectedItem = .chat("chat-idle")
|
||||
viewModel.composer = "new message"
|
||||
|
||||
await viewModel.reconnect()
|
||||
|
||||
#expect(viewModel.selectedItem == .chat("chat-idle"))
|
||||
#expect(viewModel.sidebarItems.first(where: { $0.selection == .chat("chat-active") })?.isRunning == true)
|
||||
#expect(!viewModel.isActiveSelectionSending)
|
||||
#expect(viewModel.canSendComposer)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func backgroundChatStreamInterruptionIsSuppressedUntilForegroundRefresh() async throws {
|
||||
let date = Date(timeIntervalSince1970: 1_700_000_300)
|
||||
|
||||
Reference in New Issue
Block a user