Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion Packages/OsaurusCore/Managers/Model/ModelManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,27 @@ final class ModelManager: NSObject, ObservableObject {

// Pull the OsaurusAI HF org listing once on launch so newly published
// models surface in the Recommended tab without requiring a code push.
Task { [weak self] in await self?.loadOsaurusAIOrgModels() }
//
// The unit-test runner constructs `ModelManager()` repeatedly to drive
// `applyOsaurusOrgFetch` directly. If the launch-time HF fetch races
// with those test calls, whichever finishes last wins and the merge
// result is non-deterministic — that's the regression class behind
// `ModelManagerSuggestedTests/applyOsaurusOrgFetch_*` flaking in CI.
// Skip the background fetch under XCTest; production launches still
// get it because `XCTestConfigurationFilePath` is only set inside
// a test host.
if !Self.isRunningInTestEnvironment {
Task { [weak self] in await self?.loadOsaurusAIOrgModels() }
}
}

/// True when the current process was launched by xctest. Used to gate
/// network-touching launch-time side effects so tests can drive the
/// affected code paths deterministically.
nonisolated private static var isRunningInTestEnvironment: Bool {
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
|| ProcessInfo.processInfo.environment["XCTestBundlePath"] != nil
|| ProcessInfo.processInfo.environment["XCTestSessionIdentifier"] != nil
}

// MARK: - Public Methods
Expand Down
8 changes: 6 additions & 2 deletions Packages/OsaurusCore/Services/Tool/ToolSearchService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,12 @@ public actor ToolSearchService {
}

let toolIdSet = Set(toolIds)
let entries = try ToolDatabase.shared.loadAllEntries()
.filter { toolIdSet.contains($0.id) && enabledNames.contains($0.name) }
// Pull just the candidate rows from the tool_index table instead
// of paging in every row and filtering in Swift. On a large tool
// catalogue (plugin-heavy workspaces, large MCP bundles) this
// changes per-query cost from O(catalog_size) to O(candidates).
let entries = try ToolDatabase.shared.loadEntries(ids: toolIdSet)
.filter { enabledNames.contains($0.name) }
let entryById = Dictionary(entries.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first })

return Array(
Expand Down
31 changes: 31 additions & 0 deletions Packages/OsaurusCore/Storage/ToolDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,37 @@ public final class ToolDatabase: @unchecked Sendable {
return entries
}

/// Load only the entries whose `id` is in `ids`. Used by `ToolSearchService`
/// which already has a small candidate set returned by VecturaKit; without
/// this method it has to call `loadAllEntries()` and filter in Swift —
/// every RAG-style tool query then pays the cost of reading and decoding
/// every row in `tool_index`, which is O(catalog_size) per query.
///
/// The generated SQL uses `IN (?, ?, ?, …)` with a placeholder per id;
/// SQLite's default `SQLITE_MAX_VARIABLE_NUMBER` (999) bounds the safe
/// batch size, far above any realistic search-candidate count.
public func loadEntries(ids: Set<String>) throws -> [ToolIndexEntry] {
guard !ids.isEmpty else { return [] }
let idArray = Array(ids)
let placeholders = Array(repeating: "?", count: idArray.count).joined(separator: ",")
let sql = "SELECT \(Self.columns) FROM tool_index WHERE id IN (\(placeholders))"
var entries: [ToolIndexEntry] = []
try prepareAndExecute(
sql,
bind: { stmt in
for (i, id) in idArray.enumerated() {
Self.bindText(stmt, index: Int32(i + 1), value: id)
}
},
process: { stmt in
while sqlite3_step(stmt) == SQLITE_ROW {
entries.append(Self.readEntry(from: stmt))
}
}
)
return entries
}

public func entryCount() throws -> Int {
var count = 0
try prepareAndExecute(
Expand Down
29 changes: 29 additions & 0 deletions Packages/OsaurusCore/Tests/Tool/ToolIndexServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,35 @@ struct ToolDatabaseTests {
#expect(all.count == 3)
}

@Test func loadEntriesByIdsReturnsOnlyRequested() throws {
let db = try makeTempDB()
try db.upsertEntry(sampleEntry(id: "a", name: "alpha"))
try db.upsertEntry(sampleEntry(id: "b", name: "beta"))
try db.upsertEntry(sampleEntry(id: "c", name: "gamma"))

let subset = try db.loadEntries(ids: ["a", "c"])
#expect(subset.count == 2)
let ids = Set(subset.map { $0.id })
#expect(ids == ["a", "c"])
}

@Test func loadEntriesByIdsEmptyInputSkipsQuery() throws {
let db = try makeTempDB()
try db.upsertEntry(sampleEntry(id: "a", name: "alpha"))

let result = try db.loadEntries(ids: [])
#expect(result.isEmpty)
}

@Test func loadEntriesByIdsIgnoresUnknownIds() throws {
let db = try makeTempDB()
try db.upsertEntry(sampleEntry(id: "a", name: "alpha"))

let result = try db.loadEntries(ids: ["a", "does-not-exist", "also-not-here"])
#expect(result.count == 1)
#expect(result.first?.id == "a")
}

@Test func entryCountIsAccurate() throws {
let db = try makeTempDB()
#expect(try db.entryCount() == 0)
Expand Down
Loading