Chat with any SQLite database using natural language. Built on AnyLanguageModel (HuggingFace) for LLM-agnostic provider support and GRDB for SQLite access. Core features: - Auto schema introspection from sqlite_master (zero config) - NL → SQL generation via any AnyLanguageModel provider - Three rendering modes: text summary, data table, Swift Charts - Drop-in DataChatView (SwiftUI) and headless ChatEngine - Operation allowlist with read-only default - Mutation policy with per-table control - ToolExecutionDelegate for destructive operation confirmation - Multi-turn conversation context - 352 tests across 24 suites, all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
326 lines
10 KiB
Swift
326 lines
10 KiB
Swift
// ProviderConfigurationTests.swift
|
|
// SwiftDBAI Tests
|
|
//
|
|
// Tests for ProviderConfiguration — verifying all cloud provider configurations
|
|
// produce valid LanguageModel instances with correct settings.
|
|
|
|
import AnyLanguageModel
|
|
import Foundation
|
|
@testable import SwiftDBAI
|
|
import Testing
|
|
|
|
@Suite("ProviderConfiguration")
|
|
struct ProviderConfigurationTests {
|
|
|
|
// MARK: - OpenAI Configuration
|
|
|
|
@Test("OpenAI configuration stores provider and model")
|
|
func openAIBasicConfiguration() {
|
|
let config = ProviderConfiguration.openAI(
|
|
apiKey: "sk-test-key-123",
|
|
model: "gpt-4o"
|
|
)
|
|
|
|
#expect(config.provider == .openAI)
|
|
#expect(config.model == "gpt-4o")
|
|
#expect(config.apiKey == "sk-test-key-123")
|
|
#expect(config.hasValidAPIKey)
|
|
}
|
|
|
|
@Test("OpenAI configuration produces a valid LanguageModel")
|
|
func openAIMakeModel() {
|
|
let config = ProviderConfiguration.openAI(
|
|
apiKey: "sk-test-key",
|
|
model: "gpt-4o-mini"
|
|
)
|
|
|
|
let model = config.makeModel()
|
|
#expect(model is OpenAILanguageModel)
|
|
}
|
|
|
|
@Test("OpenAI with custom base URL for compatible services")
|
|
func openAICustomBaseURL() {
|
|
let customURL = URL(string: "https://my-proxy.example.com/v1/")!
|
|
let config = ProviderConfiguration.openAI(
|
|
apiKey: "sk-proxy-key",
|
|
model: "gpt-4o",
|
|
baseURL: customURL
|
|
)
|
|
|
|
#expect(config.baseURL == customURL)
|
|
let model = config.makeModel()
|
|
#expect(model is OpenAILanguageModel)
|
|
}
|
|
|
|
@Test("OpenAI with Responses API variant")
|
|
func openAIResponsesVariant() {
|
|
let config = ProviderConfiguration.openAI(
|
|
apiKey: "sk-test",
|
|
model: "gpt-4o",
|
|
variant: .responses
|
|
)
|
|
|
|
#expect(config.openAIVariant == .responses)
|
|
let model = config.makeModel()
|
|
#expect(model is OpenAILanguageModel)
|
|
}
|
|
|
|
@Test("OpenAI with dynamic key provider captures key by reference")
|
|
func openAIDynamicKeyProvider() {
|
|
nonisolated(unsafe) var currentKey = "sk-initial"
|
|
let config = ProviderConfiguration.openAI(
|
|
apiKeyProvider: { currentKey },
|
|
model: "gpt-4o"
|
|
)
|
|
|
|
#expect(config.apiKey == "sk-initial")
|
|
currentKey = "sk-rotated"
|
|
#expect(config.apiKey == "sk-rotated")
|
|
}
|
|
|
|
// MARK: - Anthropic Configuration
|
|
|
|
@Test("Anthropic configuration stores provider and model")
|
|
func anthropicBasicConfiguration() {
|
|
let config = ProviderConfiguration.anthropic(
|
|
apiKey: "sk-ant-test-key",
|
|
model: "claude-sonnet-4-20250514"
|
|
)
|
|
|
|
#expect(config.provider == .anthropic)
|
|
#expect(config.model == "claude-sonnet-4-20250514")
|
|
#expect(config.apiKey == "sk-ant-test-key")
|
|
#expect(config.hasValidAPIKey)
|
|
}
|
|
|
|
@Test("Anthropic configuration produces a valid LanguageModel")
|
|
func anthropicMakeModel() {
|
|
let config = ProviderConfiguration.anthropic(
|
|
apiKey: "sk-ant-test",
|
|
model: "claude-sonnet-4-20250514"
|
|
)
|
|
|
|
let model = config.makeModel()
|
|
#expect(model is AnthropicLanguageModel)
|
|
}
|
|
|
|
@Test("Anthropic with API version and betas")
|
|
func anthropicWithVersionAndBetas() {
|
|
let config = ProviderConfiguration.anthropic(
|
|
apiKey: "sk-ant-test",
|
|
model: "claude-sonnet-4-20250514",
|
|
apiVersion: "2024-01-01",
|
|
betas: ["computer-use"]
|
|
)
|
|
|
|
#expect(config.apiVersion == "2024-01-01")
|
|
#expect(config.betas == ["computer-use"])
|
|
let model = config.makeModel()
|
|
#expect(model is AnthropicLanguageModel)
|
|
}
|
|
|
|
@Test("Anthropic with dynamic key provider captures key by reference")
|
|
func anthropicDynamicKeyProvider() {
|
|
nonisolated(unsafe) var currentKey = "sk-ant-initial"
|
|
let config = ProviderConfiguration.anthropic(
|
|
apiKeyProvider: { currentKey },
|
|
model: "claude-sonnet-4-20250514"
|
|
)
|
|
|
|
#expect(config.apiKey == "sk-ant-initial")
|
|
currentKey = "sk-ant-rotated"
|
|
#expect(config.apiKey == "sk-ant-rotated")
|
|
}
|
|
|
|
// MARK: - Gemini Configuration
|
|
|
|
@Test("Gemini configuration stores provider and model")
|
|
func geminiBasicConfiguration() {
|
|
let config = ProviderConfiguration.gemini(
|
|
apiKey: "AIzaSyTest123",
|
|
model: "gemini-2.0-flash"
|
|
)
|
|
|
|
#expect(config.provider == .gemini)
|
|
#expect(config.model == "gemini-2.0-flash")
|
|
#expect(config.apiKey == "AIzaSyTest123")
|
|
#expect(config.hasValidAPIKey)
|
|
}
|
|
|
|
@Test("Gemini configuration produces a valid LanguageModel")
|
|
func geminiMakeModel() {
|
|
let config = ProviderConfiguration.gemini(
|
|
apiKey: "AIzaSyTest",
|
|
model: "gemini-2.0-flash"
|
|
)
|
|
|
|
let model = config.makeModel()
|
|
#expect(model is GeminiLanguageModel)
|
|
}
|
|
|
|
@Test("Gemini with custom API version")
|
|
func geminiCustomVersion() {
|
|
let config = ProviderConfiguration.gemini(
|
|
apiKey: "AIzaSyTest",
|
|
model: "gemini-2.0-flash",
|
|
apiVersion: "v1"
|
|
)
|
|
|
|
#expect(config.apiVersion == "v1")
|
|
let model = config.makeModel()
|
|
#expect(model is GeminiLanguageModel)
|
|
}
|
|
|
|
@Test("Gemini with dynamic key provider captures key by reference")
|
|
func geminiDynamicKeyProvider() {
|
|
nonisolated(unsafe) var currentKey = "AIza-initial"
|
|
let config = ProviderConfiguration.gemini(
|
|
apiKeyProvider: { currentKey },
|
|
model: "gemini-2.0-flash"
|
|
)
|
|
|
|
#expect(config.apiKey == "AIza-initial")
|
|
currentKey = "AIza-rotated"
|
|
#expect(config.apiKey == "AIza-rotated")
|
|
}
|
|
|
|
// MARK: - OpenAI-Compatible Configuration
|
|
|
|
@Test("OpenAI-compatible configuration with custom base URL")
|
|
func openAICompatibleConfiguration() {
|
|
let baseURL = URL(string: "https://api.together.xyz/v1/")!
|
|
let config = ProviderConfiguration.openAICompatible(
|
|
apiKey: "together-key",
|
|
model: "meta-llama/Llama-3.1-70B",
|
|
baseURL: baseURL
|
|
)
|
|
|
|
#expect(config.provider == .openAICompatible)
|
|
#expect(config.model == "meta-llama/Llama-3.1-70B")
|
|
#expect(config.baseURL == baseURL)
|
|
let model = config.makeModel()
|
|
#expect(model is OpenAILanguageModel)
|
|
}
|
|
|
|
@Test("OpenAI-compatible with dynamic key provider")
|
|
func openAICompatibleDynamicKey() {
|
|
let baseURL = URL(string: "http://localhost:1234/v1/")!
|
|
nonisolated(unsafe) var currentKey = "local-key"
|
|
let config = ProviderConfiguration.openAICompatible(
|
|
apiKeyProvider: { currentKey },
|
|
model: "local-model",
|
|
baseURL: baseURL
|
|
)
|
|
|
|
#expect(config.apiKey == "local-key")
|
|
currentKey = "new-local-key"
|
|
#expect(config.apiKey == "new-local-key")
|
|
}
|
|
|
|
// MARK: - API Key Validation
|
|
|
|
@Test("Empty API key reports invalid")
|
|
func emptyAPIKeyInvalid() {
|
|
let config = ProviderConfiguration.openAI(
|
|
apiKey: "",
|
|
model: "gpt-4o"
|
|
)
|
|
|
|
#expect(!config.hasValidAPIKey)
|
|
}
|
|
|
|
@Test("Whitespace-only API key reports invalid")
|
|
func whitespaceAPIKeyInvalid() {
|
|
let config = ProviderConfiguration.openAI(
|
|
apiKey: " \n\t ",
|
|
model: "gpt-4o"
|
|
)
|
|
|
|
#expect(!config.hasValidAPIKey)
|
|
}
|
|
|
|
@Test("Non-empty API key reports valid")
|
|
func nonEmptyAPIKeyValid() {
|
|
let config = ProviderConfiguration.openAI(
|
|
apiKey: "x",
|
|
model: "gpt-4o"
|
|
)
|
|
|
|
#expect(config.hasValidAPIKey)
|
|
}
|
|
|
|
// MARK: - Environment Variable Configuration
|
|
|
|
@Test("fromEnvironment creates configuration for each provider")
|
|
func fromEnvironmentCreatesConfig() {
|
|
let openAI = ProviderConfiguration.fromEnvironment(
|
|
provider: .openAI,
|
|
environmentVariable: "SWIFTDAI_TEST_OPENAI_KEY",
|
|
model: "gpt-4o"
|
|
)
|
|
#expect(openAI.provider == .openAI)
|
|
#expect(openAI.model == "gpt-4o")
|
|
|
|
let anthropic = ProviderConfiguration.fromEnvironment(
|
|
provider: .anthropic,
|
|
environmentVariable: "SWIFTDAI_TEST_ANTHROPIC_KEY",
|
|
model: "claude-sonnet-4-20250514"
|
|
)
|
|
#expect(anthropic.provider == .anthropic)
|
|
|
|
let gemini = ProviderConfiguration.fromEnvironment(
|
|
provider: .gemini,
|
|
environmentVariable: "SWIFTDAI_TEST_GEMINI_KEY",
|
|
model: "gemini-2.0-flash"
|
|
)
|
|
#expect(gemini.provider == .gemini)
|
|
}
|
|
|
|
@Test("fromEnvironment returns empty key when variable not set")
|
|
func fromEnvironmentMissingVariable() {
|
|
let config = ProviderConfiguration.fromEnvironment(
|
|
provider: .openAI,
|
|
environmentVariable: "NONEXISTENT_KEY_VAR_SWIFTDBAI_TEST",
|
|
model: "gpt-4o"
|
|
)
|
|
|
|
#expect(!config.hasValidAPIKey)
|
|
#expect(config.apiKey == "")
|
|
}
|
|
|
|
// MARK: - Provider Enum
|
|
|
|
@Test("Provider enum has all expected cases")
|
|
func providerCases() {
|
|
let cases = ProviderConfiguration.Provider.allCases
|
|
#expect(cases.count == 6)
|
|
#expect(cases.contains(.openAI))
|
|
#expect(cases.contains(.anthropic))
|
|
#expect(cases.contains(.gemini))
|
|
#expect(cases.contains(.openAICompatible))
|
|
#expect(cases.contains(.ollama))
|
|
#expect(cases.contains(.llamaCpp))
|
|
}
|
|
|
|
// MARK: - Cross-Provider Model Creation
|
|
|
|
@Test("All providers produce available models")
|
|
func allProvidersCreateAvailableModels() {
|
|
let configs: [ProviderConfiguration] = [
|
|
.openAI(apiKey: "test", model: "gpt-4o"),
|
|
.anthropic(apiKey: "test", model: "claude-sonnet-4-20250514"),
|
|
.gemini(apiKey: "test", model: "gemini-2.0-flash"),
|
|
.openAICompatible(
|
|
apiKey: "test",
|
|
model: "local",
|
|
baseURL: URL(string: "http://localhost:8080/v1/")!
|
|
),
|
|
]
|
|
|
|
for config in configs {
|
|
let model = config.makeModel()
|
|
#expect(model.isAvailable, "Model for \(config.provider) should be available")
|
|
}
|
|
}
|
|
}
|