This commit is contained in:
@@ -10,7 +10,10 @@ private struct MockClientCallSnapshot: Sendable {
|
||||
var createChat = 0
|
||||
var getChat = 0
|
||||
var updateChatTitle = 0
|
||||
var suggestChatTitle = 0
|
||||
var updateChatStar = 0
|
||||
var lastUpdateChatStarID: String?
|
||||
var lastUpdateChatStarred: Bool?
|
||||
var updateSearchStar = 0
|
||||
var getSearch = 0
|
||||
var getActiveRuns = 0
|
||||
@@ -36,6 +39,7 @@ private actor MockSybilClient: SybilAPIClienting {
|
||||
private let searchDetails: [String: SearchDetail]
|
||||
private let createChatResponse: ChatSummary?
|
||||
private let updateChatTitleResponses: [String: ChatSummary]
|
||||
private let suggestChatTitleResponses: [String: ChatSummary]
|
||||
private let updateChatStarResponses: [String: ChatSummary]
|
||||
private let updateSearchStarResponses: [String: SearchSummary]
|
||||
private let activeRunsResponse: ActiveRunsResponse
|
||||
@@ -64,6 +68,7 @@ private actor MockSybilClient: SybilAPIClienting {
|
||||
searchDetails: [String: SearchDetail] = [:],
|
||||
createChatResponse: ChatSummary? = nil,
|
||||
updateChatTitleResponses: [String: ChatSummary] = [:],
|
||||
suggestChatTitleResponses: [String: ChatSummary] = [:],
|
||||
updateChatStarResponses: [String: ChatSummary] = [:],
|
||||
updateSearchStarResponses: [String: SearchSummary] = [:],
|
||||
activeRunsResponse: ActiveRunsResponse = ActiveRunsResponse(),
|
||||
@@ -76,6 +81,7 @@ private actor MockSybilClient: SybilAPIClienting {
|
||||
self.searchDetails = searchDetails
|
||||
self.createChatResponse = createChatResponse
|
||||
self.updateChatTitleResponses = updateChatTitleResponses
|
||||
self.suggestChatTitleResponses = suggestChatTitleResponses
|
||||
self.updateChatStarResponses = updateChatStarResponses
|
||||
self.updateSearchStarResponses = updateSearchStarResponses
|
||||
self.activeRunsResponse = activeRunsResponse
|
||||
@@ -204,6 +210,8 @@ private actor MockSybilClient: SybilAPIClienting {
|
||||
|
||||
func updateChatStar(chatID: String, starred: Bool) async throws -> ChatSummary {
|
||||
snapshot.updateChatStar += 1
|
||||
snapshot.lastUpdateChatStarID = chatID
|
||||
snapshot.lastUpdateChatStarred = starred
|
||||
guard let summary = updateChatStarResponses[chatID] else {
|
||||
throw UnexpectedClientCall()
|
||||
}
|
||||
@@ -215,7 +223,11 @@ private actor MockSybilClient: SybilAPIClienting {
|
||||
}
|
||||
|
||||
func suggestChatTitle(chatID: String, content: String) async throws -> ChatSummary {
|
||||
throw UnexpectedClientCall()
|
||||
snapshot.suggestChatTitle += 1
|
||||
guard let summary = suggestChatTitleResponses[chatID] else {
|
||||
throw UnexpectedClientCall()
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
func listSearches() async throws -> [SearchSummary] {
|
||||
@@ -420,6 +432,46 @@ private func makeToolCallMessage(id: String, date: Date, summary: String = "Ran
|
||||
)
|
||||
}
|
||||
|
||||
@Test func chatForkMetadataDecodesBackwardCompatiblyAndSurvivesWorkspaceConversions() throws {
|
||||
let decoder = JSONDecoder()
|
||||
let legacySummary = try decoder.decode(
|
||||
ChatSummary.self,
|
||||
from: Data(#"{"id":"legacy-chat","title":"Legacy","createdAt":0,"updatedAt":1}"#.utf8)
|
||||
)
|
||||
let legacyWorkspaceItem = try decoder.decode(
|
||||
WorkspaceItem.self,
|
||||
from: Data(#"{"type":"chat","id":"legacy-chat","title":"Legacy","createdAt":0,"updatedAt":1}"#.utf8)
|
||||
)
|
||||
let legacyDetail = try decoder.decode(
|
||||
ChatDetail.self,
|
||||
from: Data(#"{"id":"legacy-chat","title":"Legacy","createdAt":0,"updatedAt":1,"messages":[]}"#.utf8)
|
||||
)
|
||||
let forkDetail = try decoder.decode(
|
||||
ChatDetail.self,
|
||||
from: Data(#"{"id":"fork-chat","title":"Fork of Legacy","parentChatId":"root-chat","titleGenerationPending":true,"createdAt":0,"updatedAt":1,"messages":[]}"#.utf8)
|
||||
)
|
||||
|
||||
#expect(legacySummary.parentChatId == nil)
|
||||
#expect(!legacySummary.titleGenerationPending)
|
||||
#expect(legacyWorkspaceItem.parentChatId == nil)
|
||||
#expect(!legacyWorkspaceItem.titleGenerationPending)
|
||||
#expect(legacyDetail.parentChatId == nil)
|
||||
#expect(!legacyDetail.titleGenerationPending)
|
||||
#expect(forkDetail.parentChatId == "root-chat")
|
||||
#expect(forkDetail.titleGenerationPending)
|
||||
|
||||
var fork = legacySummary
|
||||
fork.parentChatId = "root-chat"
|
||||
fork.titleGenerationPending = true
|
||||
|
||||
let workspaceItem = WorkspaceItem(chat: fork)
|
||||
let restoredSummary = try #require(workspaceItem.chatSummary)
|
||||
#expect(workspaceItem.parentChatId == "root-chat")
|
||||
#expect(workspaceItem.titleGenerationPending)
|
||||
#expect(restoredSummary.parentChatId == "root-chat")
|
||||
#expect(restoredSummary.titleGenerationPending)
|
||||
}
|
||||
|
||||
@Test func transcriptRenderItemsGroupAdjacentToolCalls() async throws {
|
||||
let date = Date(timeIntervalSince1970: 1_700_000_000)
|
||||
let user = Message(id: "user-1", createdAt: date, role: .user, content: "Search this", name: nil)
|
||||
@@ -567,18 +619,16 @@ private func makeToolCallMessage(id: String, date: Date, summary: String = "Ran
|
||||
@MainActor
|
||||
@Test func renameChatUpdatesSidebarAndSelectedTranscriptTitle() async throws {
|
||||
let date = Date(timeIntervalSince1970: 1_700_000_150)
|
||||
let original = makeChatSummary(id: "chat-rename", date: date)
|
||||
let renamed = ChatSummary(
|
||||
id: "chat-rename",
|
||||
title: "Renamed chat",
|
||||
createdAt: date,
|
||||
updatedAt: date.addingTimeInterval(60),
|
||||
initiatedProvider: .openai,
|
||||
initiatedModel: "gpt-4.1-mini",
|
||||
lastUsedProvider: .openai,
|
||||
lastUsedModel: "gpt-4.1-mini"
|
||||
)
|
||||
let detail = makeChatDetail(id: "chat-rename", date: date, body: "existing transcript")
|
||||
var original = makeChatSummary(id: "chat-rename", date: date)
|
||||
original.parentChatId = "root-chat"
|
||||
original.titleGenerationPending = true
|
||||
var renamed = original
|
||||
renamed.title = "Renamed chat"
|
||||
renamed.titleGenerationPending = false
|
||||
renamed.updatedAt = date.addingTimeInterval(60)
|
||||
var detail = makeChatDetail(id: "chat-rename", date: date, body: "existing transcript")
|
||||
detail.parentChatId = original.parentChatId
|
||||
detail.titleGenerationPending = original.titleGenerationPending
|
||||
let client = MockSybilClient(
|
||||
chatsResponse: [original],
|
||||
updateChatTitleResponses: ["chat-rename": renamed]
|
||||
@@ -596,7 +646,13 @@ private func makeToolCallMessage(id: String, date: Date, summary: String = "Ran
|
||||
let snapshot = await client.currentSnapshot()
|
||||
#expect(snapshot.updateChatTitle == 1)
|
||||
#expect(viewModel.sidebarItems.first?.title == "Renamed chat")
|
||||
#expect(viewModel.chats.first?.parentChatId == "root-chat")
|
||||
#expect(viewModel.chats.first?.titleGenerationPending == false)
|
||||
#expect(viewModel.workspaceItems.first?.parentChatId == "root-chat")
|
||||
#expect(viewModel.workspaceItems.first?.titleGenerationPending == false)
|
||||
#expect(viewModel.selectedChat?.title == "Renamed chat")
|
||||
#expect(viewModel.selectedChat?.parentChatId == "root-chat")
|
||||
#expect(viewModel.selectedChat?.titleGenerationPending == false)
|
||||
#expect(viewModel.errorMessage == nil)
|
||||
}
|
||||
|
||||
@@ -635,6 +691,54 @@ private func makeToolCallMessage(id: String, date: Date, summary: String = "Ran
|
||||
#expect(viewModel.sidebarItems.first(where: { $0.selection == .search("search-star") })?.starred == true)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func unstarringForkTargetsRootWithoutReplacingSelectedChild() async throws {
|
||||
let date = Date(timeIntervalSince1970: 1_700_000_180)
|
||||
var root = makeChatSummary(id: "chat-root", date: date)
|
||||
root.starred = true
|
||||
root.starredAt = date.addingTimeInterval(5)
|
||||
|
||||
var child = makeChatSummary(id: "chat-child", date: date.addingTimeInterval(1))
|
||||
child.parentChatId = root.id
|
||||
child.titleGenerationPending = true
|
||||
|
||||
var childDetail = makeChatDetail(id: child.id, date: date, body: "forked transcript")
|
||||
childDetail.title = child.title
|
||||
childDetail.parentChatId = root.id
|
||||
childDetail.titleGenerationPending = true
|
||||
|
||||
var unstarredRoot = root
|
||||
unstarredRoot.starred = false
|
||||
unstarredRoot.starredAt = nil
|
||||
|
||||
let client = MockSybilClient(
|
||||
chatsResponse: [root, child],
|
||||
updateChatStarResponses: [root.id: unstarredRoot]
|
||||
)
|
||||
let viewModel = SybilViewModel(settings: testSettings(named: #function)) { _ in client }
|
||||
viewModel.isAuthenticated = true
|
||||
viewModel.isCheckingSession = false
|
||||
viewModel.chats = [root, child]
|
||||
viewModel.workspaceItems = [WorkspaceItem(chat: root), WorkspaceItem(chat: child)]
|
||||
viewModel.selectedItem = .chat(child.id)
|
||||
viewModel.selectedChat = childDetail
|
||||
|
||||
#expect(viewModel.sidebarItems.first(where: { $0.selection == .chat(child.id) })?.starred == true)
|
||||
|
||||
await viewModel.setItemStarred(.chat(child.id), starred: false)
|
||||
|
||||
let snapshot = await client.currentSnapshot()
|
||||
#expect(snapshot.updateChatStar == 1)
|
||||
#expect(snapshot.lastUpdateChatStarID == root.id)
|
||||
#expect(snapshot.lastUpdateChatStarred == false)
|
||||
#expect(viewModel.chats.first(where: { $0.id == root.id })?.starred == false)
|
||||
#expect(viewModel.chats.first(where: { $0.id == child.id }) == child)
|
||||
#expect(viewModel.sidebarItems.first(where: { $0.selection == .chat(child.id) })?.starred == false)
|
||||
#expect(viewModel.selectedItem == .chat(child.id))
|
||||
#expect(viewModel.selectedChat == childDetail)
|
||||
#expect(viewModel.errorMessage == nil)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func foregroundSearchRefreshReloadsSelectedSearch() async throws {
|
||||
let date = Date(timeIntervalSince1970: 1_700_000_200)
|
||||
@@ -781,6 +885,72 @@ private func makeToolCallMessage(id: String, date: Date, summary: String = "Ran
|
||||
#expect(viewModel.chatBottomPinRequestID == initialPinRequestID + 1)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func firstPromptInPendingForkRequestsAndAppliesGeneratedTitle() async throws {
|
||||
let date = Date(timeIntervalSince1970: 1_700_000_247)
|
||||
var fork = makeChatSummary(id: "chat-fork", date: date)
|
||||
fork.title = "Fork of Original chat"
|
||||
fork.parentChatId = "root-chat"
|
||||
fork.titleGenerationPending = true
|
||||
|
||||
var forkDetail = makeChatDetail(id: fork.id, date: date, body: "forked transcript")
|
||||
forkDetail.title = fork.title
|
||||
forkDetail.parentChatId = fork.parentChatId
|
||||
forkDetail.titleGenerationPending = true
|
||||
|
||||
var titledFork = fork
|
||||
titledFork.title = "Investigating the follow-up"
|
||||
titledFork.titleGenerationPending = false
|
||||
titledFork.updatedAt = date.addingTimeInterval(1)
|
||||
|
||||
var titledForkDetail = forkDetail
|
||||
titledForkDetail.title = titledFork.title
|
||||
titledForkDetail.titleGenerationPending = false
|
||||
titledForkDetail.updatedAt = titledFork.updatedAt
|
||||
|
||||
let client = MockSybilClient(
|
||||
chatsResponse: [titledFork],
|
||||
chatDetails: [fork.id: titledForkDetail],
|
||||
suggestChatTitleResponses: [fork.id: titledFork]
|
||||
)
|
||||
await client.setCompletionStreamEvents(
|
||||
[.done(CompletionStreamDone(text: "Follow-up answer"))],
|
||||
delayNanoseconds: 100_000_000
|
||||
)
|
||||
|
||||
let viewModel = SybilViewModel(settings: testSettings(named: #function)) { _ in client }
|
||||
viewModel.isAuthenticated = true
|
||||
viewModel.isCheckingSession = false
|
||||
viewModel.chats = [fork]
|
||||
viewModel.workspaceItems = [WorkspaceItem(chat: fork)]
|
||||
viewModel.selectedItem = .chat(fork.id)
|
||||
viewModel.selectedChat = forkDetail
|
||||
viewModel.composer = "Investigate this follow-up"
|
||||
|
||||
let sendTask = Task {
|
||||
await viewModel.sendComposer()
|
||||
}
|
||||
|
||||
for _ in 0..<20 {
|
||||
let snapshot = await client.currentSnapshot()
|
||||
if snapshot.suggestChatTitle == 1,
|
||||
viewModel.selectedChat?.title == titledFork.title,
|
||||
viewModel.selectedChat?.titleGenerationPending == false {
|
||||
break
|
||||
}
|
||||
try await Task.sleep(nanoseconds: 5_000_000)
|
||||
}
|
||||
|
||||
let titleSnapshot = await client.currentSnapshot()
|
||||
#expect(titleSnapshot.suggestChatTitle == 1)
|
||||
#expect(viewModel.chats.first?.title == titledFork.title)
|
||||
#expect(viewModel.workspaceItems.first?.title == titledFork.title)
|
||||
#expect(viewModel.selectedChat?.title == titledFork.title)
|
||||
#expect(viewModel.selectedChat?.titleGenerationPending == false)
|
||||
|
||||
await sendTask.value
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test func quickQuestionRunsNonPersistentCompletionStream() async throws {
|
||||
let client = MockSybilClient()
|
||||
|
||||
Reference in New Issue
Block a user