66 new tests across 8 files (93 total in the suite now): - GitSyncServiceTests: convertToSSH/injectCredentials/sanitizeFilename/ detectSecretsInText, including a below-threshold false-positive check on the secret regex. - ChatViewModelPureLogicTests: detectGoodbyePhrase, inferProvider, calculateCost -- including the cache-read (0.1x) / cache-write (1.25x) pricing multipliers, which is real billing-affecting logic. - EmbeddingServiceTests / ContextSelectionServiceTests: embedding (de)serialization round-trip, importance-score weighting, and the token-estimate fallback (content.count / 4) when no real count exists. - OpenRouterProviderTests / OllamaProviderTests / OpenAIProviderTests / AnthropicProviderTests: request-building (attachments, online mode, cache_control breakpoints, o1/o3 temperature omission, tool schema conversion) and response-parsing (text, tool_use blocks, empty-choices fallback behavior) for all four providers, with no network involved. Also marks calculateCost/inferProvider/detectGoodbyePhrase (ChatViewModel) and serializeEmbedding/deserializeEmbedding (EmbeddingService) as `nonisolated` -- discovered via the actual test failures, not speculation: the project's `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` setting isolates the classes that carry an explicit `@MainActor` (ChatViewModel), so calling their static members from a plain synchronous @Test needs the pure ones marked `nonisolated`. Matches the existing convention already used elsewhere in EmbeddingService (cosineSimilarity was already nonisolated before this change). Phase 2 of the test-suite rollout plan (peaceful-baking-kurzweil).
104 lines
4.5 KiB
Swift
104 lines
4.5 KiB
Swift
//
|
|
// OpenRouterProviderTests.swift
|
|
// oAITests
|
|
//
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
|
// Copyright (C) 2026 Rune Olsen
|
|
|
|
import Testing
|
|
import Foundation
|
|
@testable import oAI
|
|
|
|
@Suite("OpenRouterProvider request/response conversion")
|
|
struct OpenRouterProviderTests {
|
|
|
|
private var provider: OpenRouterProvider { OpenRouterProvider(apiKey: "test-key") }
|
|
|
|
// MARK: - buildAPIRequest
|
|
|
|
@Test("Plain text message with no attachments uses simple string content")
|
|
func buildRequestPlainText() throws {
|
|
let request = ChatRequest(messages: [Message(role: .user, content: "hello")], model: "openai/gpt-4o")
|
|
let apiRequest = try provider.buildAPIRequest(from: request)
|
|
guard case .string(let text) = apiRequest.messages.first?.content else {
|
|
Issue.record("Expected .string content for a message with no attachments")
|
|
return
|
|
}
|
|
#expect(text == "hello")
|
|
}
|
|
|
|
@Test("Message with an image attachment uses array content with a base64 data URL")
|
|
func buildRequestWithImageAttachment() throws {
|
|
let attachment = FileAttachment(path: "photo.png", type: .image, data: Data([0x01, 0x02]))
|
|
let message = Message(role: .user, content: "check this out", attachments: [attachment])
|
|
let request = ChatRequest(messages: [message], model: "openai/gpt-4o")
|
|
let apiRequest = try provider.buildAPIRequest(from: request)
|
|
guard case .array(let items) = apiRequest.messages.first?.content else {
|
|
Issue.record("Expected .array content for a message with attachments")
|
|
return
|
|
}
|
|
#expect(items.count == 2) // text + image
|
|
}
|
|
|
|
@Test("Online mode appends :online suffix, but not for image generation requests")
|
|
func onlineModeSuffix() throws {
|
|
let base = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "openai/gpt-4o", onlineMode: true)
|
|
let request = try provider.buildAPIRequest(from: base)
|
|
#expect(request.model == "openai/gpt-4o:online")
|
|
|
|
let imageGenRequest = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "openai/gpt-4o", onlineMode: true, imageGeneration: true)
|
|
let noSuffix = try provider.buildAPIRequest(from: imageGenRequest)
|
|
#expect(noSuffix.model == "openai/gpt-4o")
|
|
}
|
|
|
|
@Test("Online mode does not double-append :online if the model already has it")
|
|
func onlineModeNoDoubleSuffix() throws {
|
|
let request = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "openai/gpt-4o:online", onlineMode: true)
|
|
let apiRequest = try provider.buildAPIRequest(from: request)
|
|
#expect(apiRequest.model == "openai/gpt-4o:online")
|
|
}
|
|
|
|
@Test("Anthropic models get an explicit cache_control opt-in; others don't")
|
|
func cacheControlOnlyForAnthropic() throws {
|
|
let anthropicRequest = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "anthropic/claude-sonnet-4-5")
|
|
let anthropicAPI = try provider.buildAPIRequest(from: anthropicRequest)
|
|
#expect(anthropicAPI.cacheControl?.type == "ephemeral")
|
|
|
|
let openAIRequest = ChatRequest(messages: [Message(role: .user, content: "hi")], model: "openai/gpt-4o")
|
|
let openAIAPI = try provider.buildAPIRequest(from: openAIRequest)
|
|
#expect(openAIAPI.cacheControl == nil)
|
|
}
|
|
|
|
// MARK: - convertToChatResponse
|
|
|
|
@Test("Throws invalidResponse when there are no choices")
|
|
func convertToChatResponseThrowsOnEmptyChoices() {
|
|
let apiResponse = OpenRouterChatResponse(id: "x", model: "m", choices: [], usage: nil, created: 0)
|
|
#expect(throws: (any Error).self) {
|
|
try provider.convertToChatResponse(apiResponse)
|
|
}
|
|
}
|
|
|
|
// MARK: - decodeImageOutputs
|
|
|
|
@Test("Decodes a base64 data URL into raw Data")
|
|
func decodeImageOutputsValidDataURL() {
|
|
let payload = Data([0xDE, 0xAD, 0xBE, 0xEF])
|
|
let dataURL = "data:image/png;base64,\(payload.base64EncodedString())"
|
|
let output = OpenRouterChatResponse.ImageOutput(imageUrl: .init(url: dataURL))
|
|
let decoded = provider.decodeImageOutputs([output])
|
|
#expect(decoded?.first == payload)
|
|
}
|
|
|
|
@Test("Returns nil for an empty outputs array")
|
|
func decodeImageOutputsEmpty() {
|
|
#expect(provider.decodeImageOutputs([]) == nil)
|
|
}
|
|
|
|
@Test("Skips a malformed URL with no comma separator")
|
|
func decodeImageOutputsMalformedURL() {
|
|
let output = OpenRouterChatResponse.ImageOutput(imageUrl: .init(url: "not-a-data-url"))
|
|
#expect(provider.decodeImageOutputs([output]) == nil)
|
|
}
|
|
}
|