Phase 4 seam: in-memory DatabaseService + DB injection for ContextSelectionService
DatabaseService gains a testable init(dbQueue:) plus a makeInMemory() convenience and schema-introspection helpers (tableExists/columnNames), so migration tests don't need the test target to import GRDB directly. ContextSelectionService now takes an injected DatabaseService (defaults to .shared) and its DB-coupled methods (smartSelection, getSummariesForExcludedRange, isMessageStarred) are bumped to internal. No logic changes to existing call sites.
This commit is contained in:
@@ -47,7 +47,12 @@ enum SelectionStrategy {
|
||||
final class ContextSelectionService {
|
||||
static let shared = ContextSelectionService()
|
||||
|
||||
private init() {}
|
||||
private let db: DatabaseService
|
||||
|
||||
/// `db` defaults to the shared on-disk instance; tests inject an in-memory `DatabaseService`.
|
||||
init(db: DatabaseService = .shared) {
|
||||
self.db = db
|
||||
}
|
||||
|
||||
/// Select context messages using the specified strategy
|
||||
func selectContext(
|
||||
@@ -98,7 +103,7 @@ final class ContextSelectionService {
|
||||
|
||||
// MARK: - Smart Selection Algorithm
|
||||
|
||||
private func smartSelection(allMessages: [Message], maxTokens: Int, conversationId: UUID? = nil) -> ContextWindow {
|
||||
func smartSelection(allMessages: [Message], maxTokens: Int, conversationId: UUID? = nil) -> ContextWindow {
|
||||
guard !allMessages.isEmpty else {
|
||||
return ContextWindow(messages: [], summaries: [], totalTokens: 0, excludedCount: 0)
|
||||
}
|
||||
@@ -191,12 +196,12 @@ final class ContextSelectionService {
|
||||
}
|
||||
|
||||
/// Get summaries for excluded message ranges
|
||||
private func getSummariesForExcludedRange(
|
||||
func getSummariesForExcludedRange(
|
||||
conversationId: UUID,
|
||||
totalMessages: Int,
|
||||
selectedCount: Int
|
||||
) -> [String] {
|
||||
guard let summaryRecords = try? DatabaseService.shared.getConversationSummaries(conversationId: conversationId) else {
|
||||
guard let summaryRecords = try? db.getConversationSummaries(conversationId: conversationId) else {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -238,8 +243,8 @@ final class ContextSelectionService {
|
||||
}
|
||||
|
||||
/// Check if a message is starred by the user
|
||||
private func isMessageStarred(_ message: Message) -> Bool {
|
||||
guard let metadata = try? DatabaseService.shared.getMessageMetadata(messageId: message.id) else {
|
||||
func isMessageStarred(_ message: Message) -> Bool {
|
||||
guard let metadata = try? db.getMessageMetadata(messageId: message.id) else {
|
||||
return false
|
||||
}
|
||||
return metadata.user_starred == 1
|
||||
|
||||
@@ -154,7 +154,14 @@ final class DatabaseService: Sendable {
|
||||
(try? isoStyle.parse(string)) ?? (try? Date(string, strategy: .iso8601))
|
||||
}
|
||||
|
||||
nonisolated private init() {
|
||||
/// Test seam: build a DatabaseService around a caller-provided queue (e.g. an in-memory `DatabaseQueue()`).
|
||||
/// Each test should create its own fresh instance — never mutate `.shared` from a test.
|
||||
nonisolated init(dbQueue: DatabaseQueue) {
|
||||
self.dbQueue = dbQueue
|
||||
try! migrator.migrate(dbQueue)
|
||||
}
|
||||
|
||||
nonisolated private convenience init() {
|
||||
let fileManager = FileManager.default
|
||||
let appSupport = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||
let dbDirectory = appSupport.appendingPathComponent("oAI", isDirectory: true)
|
||||
@@ -163,12 +170,25 @@ final class DatabaseService: Sendable {
|
||||
|
||||
let dbPath = dbDirectory.appendingPathComponent("oai_conversations.db").path
|
||||
Log.db.info("Opening database at \(dbPath)")
|
||||
dbQueue = try! DatabaseQueue(path: dbPath)
|
||||
|
||||
try! migrator.migrate(dbQueue)
|
||||
self.init(dbQueue: try! DatabaseQueue(path: dbPath))
|
||||
}
|
||||
|
||||
private nonisolated var migrator: DatabaseMigrator {
|
||||
/// Test seam: a throwaway in-memory instance with all migrations applied. Avoids requiring
|
||||
/// `import GRDB` from the test target just to construct a `DatabaseQueue()`.
|
||||
nonisolated static func makeInMemory() -> DatabaseService {
|
||||
DatabaseService(dbQueue: try! DatabaseQueue())
|
||||
}
|
||||
|
||||
/// Test seam: schema introspection, so migration tests don't need `import GRDB` either.
|
||||
nonisolated func tableExists(_ name: String) -> Bool {
|
||||
(try? dbQueue.read { db in try db.tableExists(name) }) ?? false
|
||||
}
|
||||
|
||||
nonisolated func columnNames(in table: String) -> [String] {
|
||||
(try? dbQueue.read { db in try db.columns(in: table).map(\.name) }) ?? []
|
||||
}
|
||||
|
||||
nonisolated var migrator: DatabaseMigrator {
|
||||
var migrator = DatabaseMigrator()
|
||||
|
||||
migrator.registerMigration("v1") { db in
|
||||
|
||||
Reference in New Issue
Block a user