"oAI" reads as easily confused with OpenAI, both visually and in casual conversation. Renamed to "Confab" throughout: Xcode target/scheme/bundle ID (com.oai.Confab), Info.plist and Help Book identity, all user-facing UI text, internal Log subsystem and color identifiers, localization catalogs (6 languages, including a proper reworded/retranslated Intel-deprecation notice), Help Book HTML content, and docs (README/DEVELOPMENT/PRIVACY/SECURITY). Deliberately cosmetic-only: the on-disk data folder (~/Library/Application Support/oAI/), database/backup filenames, Keychain service identifiers, and EncryptionService's key-derivation inputs are all left untouched so existing conversations, settings, and stored API keys survive the update with zero migration and no re-entering credentials. Verified live: a real signed build successfully decrypted a stored API key and loaded an existing conversation database after the bundle ID change. Also includes a small already-completed, previously uncommitted model-release-date feature (ModelInfo/OpenRouterModels/ OpenRouterProvider/ModelInfoView) that happened to share several files with this rename. Gitignored on this branch and updated on disk but not part of this commit: CLAUDE.md, RELEASE_NOTES.md, and the build*.sh scripts.
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 Confab
|
|
|
|
@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)
|
|
}
|
|
}
|