Folders and conversation→folder assignments now sync across machines:
- Folder gains updatedAt (v11 migration) to resolve renames/reparents
last-write-wins across machines.
- New folders.json manifest at the sync repo root: folder tree +
conversationId→folderId assignments, imported before conversation
files so new conversations land in the right folder immediately.
- Local folders missing from the manifest are pruned (reparent-safe),
guarded the same way conversation-orphan cleanup already is against
an empty/stale manifest wiping everything.
Three real bugs found and fixed during live multi-machine testing:
- Sidebar never refreshed after Git Sync imported conversations/folders
directly into the database — only reloaded on launch or when the
advanced conversation list closed, with no equivalent hook for the
Settings sheet.
- "Sync Now" exported before pulling, so it could write folders.json
as an untracked file that then collided with the remote's tracked
copy on the next pull ("untracked working tree files would be
overwritten by merge"). Reordered to pull → import → export → push.
- Folder assignment only applied to brand-new conversations during
import, so any conversation already synced to a machine before this
feature existed never got filed — which in practice is every
conversation on a second machine, not an edge case. Now backfills
a folder assignment for existing conversations that aren't filed
anywhere locally yet, without clobbering an already-set folderId.
Also renamed the "Initialize Repository" button to "Clone Repository"
(it's always been a git clone, not new-repo creation) across the UI,
localization catalog, and Help Book.
490 lines
21 KiB
Swift
490 lines
21 KiB
Swift
//
|
|
// DatabaseServiceTests.swift
|
|
// oAITests
|
|
//
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
|
// Copyright (C) 2026 Rune Olsen
|
|
|
|
import Testing
|
|
import Foundation
|
|
@testable import Confab
|
|
|
|
@Suite("DatabaseService migrations, against a throwaway in-memory queue")
|
|
struct DatabaseServiceMigrationTests {
|
|
|
|
@Test("All v1-v8 tables exist after migration")
|
|
func allTablesExist() {
|
|
let db = DatabaseService.makeInMemory()
|
|
let expectedTables = [
|
|
"conversations", "messages", "settings", "command_history",
|
|
"email_logs", "message_metadata", "message_embeddings",
|
|
"conversation_embeddings", "conversation_summaries",
|
|
]
|
|
for table in expectedTables {
|
|
#expect(db.tableExists(table), "expected table \(table) to exist")
|
|
}
|
|
}
|
|
|
|
@Test("v4 adds modelId to messages and primaryModel to conversations")
|
|
func v4AddsModelColumns() {
|
|
let db = DatabaseService.makeInMemory()
|
|
#expect(db.columnNames(in: "messages").contains("modelId"))
|
|
#expect(db.columnNames(in: "conversations").contains("primaryModel"))
|
|
}
|
|
|
|
@Test("messages table has the expected v1 columns")
|
|
func messagesTableColumns() {
|
|
let db = DatabaseService.makeInMemory()
|
|
let columns = Set(db.columnNames(in: "messages"))
|
|
let expected: Set<String> = ["id", "conversationId", "role", "content", "tokens", "cost", "timestamp", "sortOrder"]
|
|
#expect(expected.isSubset(of: columns))
|
|
}
|
|
|
|
@Test("message_metadata table has the expected v6 columns")
|
|
func messageMetadataColumns() {
|
|
let db = DatabaseService.makeInMemory()
|
|
let columns = Set(db.columnNames(in: "message_metadata"))
|
|
#expect(columns == ["message_id", "importance_score", "user_starred", "summary", "chunk_index"])
|
|
}
|
|
|
|
@Test("conversation_summaries table has the expected v8 columns")
|
|
func conversationSummariesColumns() {
|
|
let db = DatabaseService.makeInMemory()
|
|
let columns = Set(db.columnNames(in: "conversation_summaries"))
|
|
#expect(columns == ["id", "conversation_id", "start_message_index", "end_message_index", "summary", "token_count", "created_at", "summary_model"])
|
|
}
|
|
|
|
@Test("A nonexistent table reports as absent, not a crash")
|
|
func nonexistentTableReportsAbsent() {
|
|
let db = DatabaseService.makeInMemory()
|
|
#expect(db.tableExists("not_a_real_table") == false)
|
|
}
|
|
|
|
@Test("Two in-memory instances are isolated from each other")
|
|
func instancesAreIsolated() throws {
|
|
let dbA = DatabaseService.makeInMemory()
|
|
let dbB = DatabaseService.makeInMemory()
|
|
|
|
_ = try dbA.saveConversation(name: "only in A", messages: [Message(role: .user, content: "hi")])
|
|
|
|
#expect(try dbA.listConversations().count == 1)
|
|
#expect(try dbB.listConversations().count == 0)
|
|
}
|
|
}
|
|
|
|
@Suite("DatabaseService settings CRUD, against a throwaway in-memory queue")
|
|
struct DatabaseServiceSettingsTests {
|
|
|
|
@Test("Round-trips a plain setting")
|
|
func roundTripsSetting() {
|
|
let db = DatabaseService.makeInMemory()
|
|
db.setSetting(key: "theme", value: "dark")
|
|
#expect((try? db.loadAllSettings()["theme"]) == "dark")
|
|
}
|
|
|
|
@Test("Deletes a setting")
|
|
func deletesSetting() {
|
|
let db = DatabaseService.makeInMemory()
|
|
db.setSetting(key: "temp", value: "1")
|
|
db.deleteSetting(key: "temp")
|
|
#expect((try? db.loadAllSettings()["temp"]) == nil)
|
|
}
|
|
}
|
|
|
|
@Suite("DatabaseService conversation + message persistence, against a throwaway in-memory queue")
|
|
struct DatabaseServiceConversationTests {
|
|
|
|
@Test("Saving a conversation round-trips its messages via loadConversation")
|
|
func savesAndLoadsConversation() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let messages = [
|
|
Message(role: .user, content: "hello"),
|
|
Message(role: .assistant, content: "hi there"),
|
|
]
|
|
let saved = try db.saveConversation(name: "Test Chat", messages: messages)
|
|
|
|
let loaded = try db.loadConversation(id: saved.id)
|
|
#expect(loaded?.0.name == "Test Chat")
|
|
#expect(loaded?.1.map(\.content) == ["hello", "hi there"])
|
|
}
|
|
|
|
@Test("System messages are excluded from persistence")
|
|
func systemMessagesExcluded() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let messages = [
|
|
Message(role: .system, content: "tool call"),
|
|
Message(role: .user, content: "hello"),
|
|
]
|
|
let saved = try db.saveConversation(name: "Test", messages: messages)
|
|
let loaded = try db.loadConversation(id: saved.id)
|
|
#expect(loaded?.1.count == 1)
|
|
#expect(loaded?.1.first?.content == "hello")
|
|
}
|
|
}
|
|
|
|
@Suite("DatabaseService usage statistics, against a throwaway in-memory queue")
|
|
struct DatabaseServiceUsageStatsTests {
|
|
|
|
@Test("Overall stats aggregate tokens, cost, and message count across conversations")
|
|
func overallStatsAggregate() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
_ = try db.saveConversation(name: "Chat A", messages: [
|
|
Message(role: .user, content: "hi", tokens: 10, modelId: "claude-sonnet"),
|
|
Message(role: .assistant, content: "hello", tokens: 20, cost: 0.01, modelId: "claude-sonnet"),
|
|
])
|
|
_ = try db.saveConversation(name: "Chat B", messages: [
|
|
Message(role: .user, content: "hey", tokens: 5, modelId: "gpt-4"),
|
|
Message(role: .assistant, content: "hi", tokens: 15, cost: 0.02, modelId: "gpt-4"),
|
|
])
|
|
|
|
let stats = try db.getOverallUsageStats()
|
|
#expect(stats.totalMessages == 4)
|
|
#expect(stats.totalTokens == 50)
|
|
#expect(stats.hasCostData == true)
|
|
#expect(abs(stats.totalCost - 0.03) < 0.0001)
|
|
}
|
|
|
|
@Test("Overall stats report no cost data when no message has a cost")
|
|
func overallStatsNoCostData() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
_ = try db.saveConversation(name: "Chat", messages: [
|
|
Message(role: .user, content: "hi", tokens: 10),
|
|
])
|
|
|
|
let stats = try db.getOverallUsageStats()
|
|
#expect(stats.hasCostData == false)
|
|
#expect(stats.totalCost == 0)
|
|
}
|
|
|
|
@Test("Usage by model groups messages by modelId and sums their tokens/cost")
|
|
func usageByModelGroups() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
_ = try db.saveConversation(name: "Chat A", messages: [
|
|
Message(role: .assistant, content: "a1", tokens: 20, cost: 0.01, modelId: "claude-sonnet"),
|
|
])
|
|
_ = try db.saveConversation(name: "Chat B", messages: [
|
|
Message(role: .assistant, content: "b1", tokens: 15, cost: 0.02, modelId: "gpt-4"),
|
|
Message(role: .assistant, content: "b2", tokens: 5, cost: 0.02, modelId: "gpt-4"),
|
|
])
|
|
|
|
let byModel = try db.getUsageByModel()
|
|
#expect(byModel.count == 2)
|
|
|
|
let gpt4 = byModel.first { $0.modelId == "gpt-4" }
|
|
#expect(gpt4?.messageCount == 2)
|
|
#expect(gpt4?.totalTokens == 20)
|
|
#expect(abs((gpt4?.totalCost ?? 0) - 0.04) < 0.0001)
|
|
|
|
// gpt-4 has higher total cost than claude-sonnet, so it should sort first
|
|
#expect(byModel.first?.modelId == "gpt-4")
|
|
}
|
|
|
|
@Test("Usage by model excludes messages with no modelId")
|
|
func usageByModelExcludesNilModelId() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
_ = try db.saveConversation(name: "Chat", messages: [
|
|
Message(role: .user, content: "hi", tokens: 10),
|
|
Message(role: .assistant, content: "hello", tokens: 20, modelId: "claude-sonnet"),
|
|
])
|
|
|
|
let byModel = try db.getUsageByModel()
|
|
#expect(byModel.count == 1)
|
|
#expect(byModel.first?.modelId == "claude-sonnet")
|
|
}
|
|
|
|
@Test("Usage by conversation joins conversation names and sorts by cost descending")
|
|
func usageByConversationSortsByCost() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
_ = try db.saveConversation(name: "Cheap Chat", messages: [
|
|
Message(role: .assistant, content: "a", tokens: 10, cost: 0.001, modelId: "m"),
|
|
])
|
|
_ = try db.saveConversation(name: "Expensive Chat", messages: [
|
|
Message(role: .assistant, content: "b", tokens: 10, cost: 0.05, modelId: "m"),
|
|
])
|
|
|
|
let byConversation = try db.getUsageByConversation()
|
|
#expect(byConversation.count == 2)
|
|
#expect(byConversation.first?.name == "Expensive Chat")
|
|
}
|
|
|
|
@Test("Usage by conversation respects the limit parameter")
|
|
func usageByConversationRespectsLimit() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
for i in 0..<5 {
|
|
_ = try db.saveConversation(name: "Chat \(i)", messages: [
|
|
Message(role: .assistant, content: "a", tokens: 10, cost: Double(i) * 0.01, modelId: "m"),
|
|
])
|
|
}
|
|
|
|
let byConversation = try db.getUsageByConversation(limit: 3)
|
|
#expect(byConversation.count == 3)
|
|
}
|
|
}
|
|
|
|
@Suite("DatabaseService folders, against a throwaway in-memory queue")
|
|
struct DatabaseServiceFolderTests {
|
|
|
|
@Test("v9 adds the folders table and folderId to conversations")
|
|
func v9AddsFolderSupport() {
|
|
let db = DatabaseService.makeInMemory()
|
|
#expect(db.tableExists("folders"))
|
|
#expect(db.columnNames(in: "conversations").contains("folderId"))
|
|
}
|
|
|
|
@Test("v10 adds parentId to folders")
|
|
func v10AddsParentId() {
|
|
let db = DatabaseService.makeInMemory()
|
|
#expect(db.columnNames(in: "folders").contains("parentId"))
|
|
}
|
|
|
|
@Test("v11 adds updatedAt to folders")
|
|
func v11AddsUpdatedAt() {
|
|
let db = DatabaseService.makeInMemory()
|
|
#expect(db.columnNames(in: "folders").contains("updatedAt"))
|
|
}
|
|
|
|
@Test("renameFolder bumps updatedAt")
|
|
func renameFolderBumpsUpdatedAt() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let folder = try db.createFolder(name: "Work")
|
|
// Round-trip through the DB for the "before" value too, so both sides go through the same
|
|
// fractional-seconds truncation as the "after" read below — comparing a raw in-memory
|
|
// Date() (full precision) against a DB-round-tripped one can flake when both timestamps
|
|
// land in the same millisecond window.
|
|
let originalUpdatedAt = try #require(db.listFolders().first(where: { $0.id == folder.id })?.updatedAt)
|
|
|
|
try db.renameFolder(id: folder.id, name: "Projects")
|
|
|
|
let updated = try db.listFolders().first(where: { $0.id == folder.id })
|
|
#expect(updated?.updatedAt ?? .distantPast >= originalUpdatedAt)
|
|
}
|
|
|
|
@Test("moveFolder bumps updatedAt")
|
|
func moveFolderBumpsUpdatedAt() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let parent = try db.createFolder(name: "Work")
|
|
let child = try db.createFolder(name: "Personal")
|
|
// See renameFolderBumpsUpdatedAt's comment: round-trip through the DB for the "before"
|
|
// value so it's truncated the same way as the "after" read.
|
|
let originalUpdatedAt = try #require(db.listFolders().first(where: { $0.id == child.id })?.updatedAt)
|
|
|
|
try db.moveFolder(id: child.id, toParent: parent.id)
|
|
|
|
let updated = try db.listFolders().first(where: { $0.id == child.id })
|
|
#expect(updated?.updatedAt ?? .distantPast >= originalUpdatedAt)
|
|
}
|
|
|
|
@Test("upsertSyncedFolder creates a folder that doesn't exist locally yet")
|
|
func upsertSyncedFolderCreatesNew() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let id = UUID()
|
|
let createdAt = Date(timeIntervalSince1970: 1_000)
|
|
let updatedAt = Date(timeIntervalSince1970: 2_000)
|
|
|
|
try db.upsertSyncedFolder(id: id, name: "Work", parentId: nil, createdAt: createdAt, updatedAt: updatedAt)
|
|
|
|
let folders = try db.listFolders()
|
|
let created = try #require(folders.first(where: { $0.id == id }))
|
|
#expect(created.name == "Work")
|
|
#expect(created.parentId == nil)
|
|
#expect(created.updatedAt == updatedAt)
|
|
}
|
|
|
|
@Test("upsertSyncedFolder is a no-op when the local version is the same age or newer")
|
|
func upsertSyncedFolderNoOpWhenLocalNotOlder() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let id = UUID()
|
|
let createdAt = Date(timeIntervalSince1970: 1_000)
|
|
let localUpdatedAt = Date(timeIntervalSince1970: 5_000)
|
|
try db.upsertSyncedFolder(id: id, name: "Work", parentId: nil, createdAt: createdAt, updatedAt: localUpdatedAt)
|
|
|
|
// Incoming manifest entry is older than what's already local.
|
|
let staleIncomingUpdatedAt = Date(timeIntervalSince1970: 2_000)
|
|
try db.upsertSyncedFolder(id: id, name: "Renamed Elsewhere", parentId: nil, createdAt: createdAt, updatedAt: staleIncomingUpdatedAt)
|
|
|
|
let folders = try db.listFolders()
|
|
let unchanged = try #require(folders.first(where: { $0.id == id }))
|
|
#expect(unchanged.name == "Work")
|
|
#expect(unchanged.updatedAt == localUpdatedAt)
|
|
}
|
|
|
|
@Test("upsertSyncedFolder updates name and parent when the incoming version is newer")
|
|
func upsertSyncedFolderUpdatesWhenIncomingNewer() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let id = UUID()
|
|
let otherParent = try db.createFolder(name: "Other Parent")
|
|
let createdAt = Date(timeIntervalSince1970: 1_000)
|
|
let localUpdatedAt = Date(timeIntervalSince1970: 2_000)
|
|
try db.upsertSyncedFolder(id: id, name: "Work", parentId: nil, createdAt: createdAt, updatedAt: localUpdatedAt)
|
|
|
|
let newerIncomingUpdatedAt = Date(timeIntervalSince1970: 9_000)
|
|
try db.upsertSyncedFolder(
|
|
id: id, name: "Projects", parentId: otherParent.id, createdAt: createdAt, updatedAt: newerIncomingUpdatedAt
|
|
)
|
|
|
|
let folders = try db.listFolders()
|
|
let updated = try #require(folders.first(where: { $0.id == id }))
|
|
#expect(updated.name == "Projects")
|
|
#expect(updated.parentId == otherParent.id)
|
|
#expect(updated.updatedAt == newerIncomingUpdatedAt)
|
|
}
|
|
|
|
@Test("createFolder(parentId:) nests the new folder under its parent")
|
|
func createFolderWithParent() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let parent = try db.createFolder(name: "Work")
|
|
let child = try db.createFolder(name: "Project A", parentId: parent.id)
|
|
#expect(child.parentId == parent.id)
|
|
|
|
let folders = try db.listFolders()
|
|
#expect(folders.first(where: { $0.id == child.id })?.parentId == parent.id)
|
|
}
|
|
|
|
@Test("moveFolder reparents a folder")
|
|
func moveFolderReparents() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let workFolder = try db.createFolder(name: "Work")
|
|
let personalFolder = try db.createFolder(name: "Personal")
|
|
|
|
try db.moveFolder(id: personalFolder.id, toParent: workFolder.id)
|
|
|
|
let folders = try db.listFolders()
|
|
#expect(folders.first(where: { $0.id == personalFolder.id })?.parentId == workFolder.id)
|
|
}
|
|
|
|
@Test("moveFolder promotes a nested folder to top-level when given nil")
|
|
func moveFolderPromotesToTopLevel() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let parent = try db.createFolder(name: "Work")
|
|
let child = try db.createFolder(name: "Project A", parentId: parent.id)
|
|
|
|
try db.moveFolder(id: child.id, toParent: nil)
|
|
|
|
let folders = try db.listFolders()
|
|
#expect(folders.first(where: { $0.id == child.id })?.parentId == nil)
|
|
}
|
|
|
|
@Test("moveFolder throws wouldCreateCycle when reparenting a folder under itself")
|
|
func moveFolderSelfReparentThrows() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let folder = try db.createFolder(name: "Work")
|
|
#expect(throws: DatabaseService.FolderError.wouldCreateCycle) {
|
|
try db.moveFolder(id: folder.id, toParent: folder.id)
|
|
}
|
|
}
|
|
|
|
@Test("moveFolder throws wouldCreateCycle when reparenting an ancestor under its own descendant")
|
|
func moveFolderAncestorUnderDescendantThrows() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let grandparent = try db.createFolder(name: "Work")
|
|
let parent = try db.createFolder(name: "Project A", parentId: grandparent.id)
|
|
let child = try db.createFolder(name: "Sub-task", parentId: parent.id)
|
|
|
|
#expect(throws: DatabaseService.FolderError.wouldCreateCycle) {
|
|
try db.moveFolder(id: grandparent.id, toParent: child.id)
|
|
}
|
|
}
|
|
|
|
@Test("Creating folders assigns increasing sort order")
|
|
func createFolderAssignsSortOrder() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let first = try db.createFolder(name: "Work")
|
|
let second = try db.createFolder(name: "Personal")
|
|
#expect(first.sortOrder == 0)
|
|
#expect(second.sortOrder == 1)
|
|
}
|
|
|
|
@Test("listFolders returns folders sorted alphabetically, case-insensitive, regardless of creation order")
|
|
func listFoldersOrdered() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
_ = try db.createFolder(name: "Work")
|
|
_ = try db.createFolder(name: "apple")
|
|
_ = try db.createFolder(name: "Personal")
|
|
|
|
let folders = try db.listFolders()
|
|
#expect(folders.map(\.name) == ["apple", "Personal", "Work"])
|
|
}
|
|
|
|
@Test("renameFolder updates the stored name")
|
|
func renameFolderUpdatesName() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let folder = try db.createFolder(name: "Work")
|
|
try db.renameFolder(id: folder.id, name: "Projects")
|
|
|
|
let folders = try db.listFolders()
|
|
#expect(folders.first?.name == "Projects")
|
|
}
|
|
|
|
@Test("moveConversation files a conversation into a folder")
|
|
func moveConversationFiles() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let folder = try db.createFolder(name: "Work")
|
|
let conversation = try db.saveConversation(name: "Chat", messages: [Message(role: .user, content: "hi")])
|
|
|
|
try db.moveConversation(id: conversation.id, toFolder: folder.id)
|
|
|
|
let loaded = try db.loadConversation(id: conversation.id)
|
|
#expect(loaded?.0.folderId == folder.id)
|
|
}
|
|
|
|
@Test("moveConversation with nil folder unfiles a conversation")
|
|
func moveConversationUnfiles() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let folder = try db.createFolder(name: "Work")
|
|
let conversation = try db.saveConversation(name: "Chat", messages: [Message(role: .user, content: "hi")])
|
|
try db.moveConversation(id: conversation.id, toFolder: folder.id)
|
|
|
|
try db.moveConversation(id: conversation.id, toFolder: nil)
|
|
|
|
let loaded = try db.loadConversation(id: conversation.id)
|
|
#expect(loaded?.0.folderId == nil)
|
|
}
|
|
|
|
@Test("Deleting a folder unfiles its conversations instead of deleting them")
|
|
func deleteFolderUnfilesConversations() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let folder = try db.createFolder(name: "Work")
|
|
let conversation = try db.saveConversation(name: "Chat", messages: [Message(role: .user, content: "hi")])
|
|
try db.moveConversation(id: conversation.id, toFolder: folder.id)
|
|
|
|
try db.deleteFolder(id: folder.id)
|
|
|
|
let folders = try db.listFolders()
|
|
#expect(folders.isEmpty)
|
|
|
|
let loaded = try db.loadConversation(id: conversation.id)
|
|
#expect(loaded != nil)
|
|
#expect(loaded?.0.folderId == nil)
|
|
}
|
|
|
|
@Test("Deleting a nested folder reparents its children and conversations up one level, not to top-level")
|
|
func deleteNestedFolderReparentsUpOneLevel() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let work = try db.createFolder(name: "Work")
|
|
let projectA = try db.createFolder(name: "Project A", parentId: work.id)
|
|
let subTask = try db.createFolder(name: "Sub-task", parentId: projectA.id)
|
|
let conversation = try db.saveConversation(name: "Chat", messages: [Message(role: .user, content: "hi")])
|
|
try db.moveConversation(id: conversation.id, toFolder: projectA.id)
|
|
|
|
try db.deleteFolder(id: projectA.id)
|
|
|
|
let folders = try db.listFolders()
|
|
#expect(folders.first(where: { $0.id == subTask.id })?.parentId == work.id)
|
|
#expect(folders.contains(where: { $0.id == projectA.id }) == false)
|
|
|
|
let loaded = try db.loadConversation(id: conversation.id)
|
|
#expect(loaded?.0.folderId == work.id)
|
|
}
|
|
|
|
@Test("listConversations reflects folderId")
|
|
func listConversationsReflectsFolderId() throws {
|
|
let db = DatabaseService.makeInMemory()
|
|
let folder = try db.createFolder(name: "Work")
|
|
let conversation = try db.saveConversation(name: "Chat", messages: [Message(role: .user, content: "hi")])
|
|
try db.moveConversation(id: conversation.id, toFolder: folder.id)
|
|
|
|
let conversations = try db.listConversations()
|
|
#expect(conversations.first?.folderId == folder.id)
|
|
}
|
|
}
|