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 {
|
final class ContextSelectionService {
|
||||||
static let shared = 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
|
/// Select context messages using the specified strategy
|
||||||
func selectContext(
|
func selectContext(
|
||||||
@@ -98,7 +103,7 @@ final class ContextSelectionService {
|
|||||||
|
|
||||||
// MARK: - Smart Selection Algorithm
|
// 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 {
|
guard !allMessages.isEmpty else {
|
||||||
return ContextWindow(messages: [], summaries: [], totalTokens: 0, excludedCount: 0)
|
return ContextWindow(messages: [], summaries: [], totalTokens: 0, excludedCount: 0)
|
||||||
}
|
}
|
||||||
@@ -191,12 +196,12 @@ final class ContextSelectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get summaries for excluded message ranges
|
/// Get summaries for excluded message ranges
|
||||||
private func getSummariesForExcludedRange(
|
func getSummariesForExcludedRange(
|
||||||
conversationId: UUID,
|
conversationId: UUID,
|
||||||
totalMessages: Int,
|
totalMessages: Int,
|
||||||
selectedCount: Int
|
selectedCount: Int
|
||||||
) -> [String] {
|
) -> [String] {
|
||||||
guard let summaryRecords = try? DatabaseService.shared.getConversationSummaries(conversationId: conversationId) else {
|
guard let summaryRecords = try? db.getConversationSummaries(conversationId: conversationId) else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,8 +243,8 @@ final class ContextSelectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a message is starred by the user
|
/// Check if a message is starred by the user
|
||||||
private func isMessageStarred(_ message: Message) -> Bool {
|
func isMessageStarred(_ message: Message) -> Bool {
|
||||||
guard let metadata = try? DatabaseService.shared.getMessageMetadata(messageId: message.id) else {
|
guard let metadata = try? db.getMessageMetadata(messageId: message.id) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return metadata.user_starred == 1
|
return metadata.user_starred == 1
|
||||||
|
|||||||
@@ -154,7 +154,14 @@ final class DatabaseService: Sendable {
|
|||||||
(try? isoStyle.parse(string)) ?? (try? Date(string, strategy: .iso8601))
|
(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 fileManager = FileManager.default
|
||||||
let appSupport = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
let appSupport = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||||
let dbDirectory = appSupport.appendingPathComponent("oAI", isDirectory: true)
|
let dbDirectory = appSupport.appendingPathComponent("oAI", isDirectory: true)
|
||||||
@@ -163,12 +170,25 @@ final class DatabaseService: Sendable {
|
|||||||
|
|
||||||
let dbPath = dbDirectory.appendingPathComponent("oai_conversations.db").path
|
let dbPath = dbDirectory.appendingPathComponent("oai_conversations.db").path
|
||||||
Log.db.info("Opening database at \(dbPath)")
|
Log.db.info("Opening database at \(dbPath)")
|
||||||
dbQueue = try! DatabaseQueue(path: dbPath)
|
self.init(dbQueue: try! DatabaseQueue(path: dbPath))
|
||||||
|
|
||||||
try! migrator.migrate(dbQueue)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
var migrator = DatabaseMigrator()
|
||||||
|
|
||||||
migrator.registerMigration("v1") { db in
|
migrator.registerMigration("v1") { db in
|
||||||
|
|||||||
Reference in New Issue
Block a user