style: run swiftformat
This commit is contained in:
parent
8465b4e598
commit
46a5405d40
@ -301,8 +301,7 @@ struct AgentCLI: AsyncParsableCommand {
|
||||
private func loadMessages(from path: String) async throws -> [ModelMessage] {
|
||||
let url = URL(fileURLWithPath: path)
|
||||
let data = try Data(contentsOf: url)
|
||||
let messages = try JSONDecoder().decode([ModelMessage].self, from: data)
|
||||
return messages
|
||||
return try JSONDecoder().decode([ModelMessage].self, from: data)
|
||||
}
|
||||
|
||||
private func saveMessages(_ messages: [ModelMessage], to path: String) async throws {
|
||||
@ -546,7 +545,7 @@ enum CLIError: LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
// Extension to add provider helpers
|
||||
/// Extension to add provider helpers
|
||||
extension Provider {
|
||||
static var allStandard: [Provider] {
|
||||
[.openai, .anthropic, .google, .mistral, .groq, .grok, .ollama]
|
||||
|
||||
@ -4,14 +4,14 @@ import Foundation
|
||||
|
||||
// swift-sh Tachikoma ~> 1.0.0
|
||||
|
||||
/// This demo script shows how to use Tachikoma in a real application
|
||||
/// Run with: swift-sh DemoScript.swift
|
||||
/// Or: swift run DemoScript (if added to Package.swift)
|
||||
// This demo script shows how to use Tachikoma in a real application
|
||||
// Run with: swift-sh DemoScript.swift
|
||||
// Or: swift run DemoScript (if added to Package.swift)
|
||||
|
||||
print("🕷️ Tachikoma Demo Script")
|
||||
print("=" * 40)
|
||||
|
||||
// Check environment variables directly
|
||||
/// Check environment variables directly
|
||||
let env = ProcessInfo.processInfo.environment
|
||||
print("Environment check:")
|
||||
for key in ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "X_AI_API_KEY"] {
|
||||
|
||||
@ -106,7 +106,7 @@ func testRealtimeConfiguration() async throws {
|
||||
print(" swift run RealtimeVoiceAssistant --basic")
|
||||
}
|
||||
|
||||
// Extension for string multiplication
|
||||
/// Extension for string multiplication
|
||||
extension String {
|
||||
static func * (string: String, count: Int) -> String {
|
||||
String(repeating: string, count: count)
|
||||
|
||||
@ -485,7 +485,6 @@ extension ProviderFactory {
|
||||
-> any ModelProvider
|
||||
{
|
||||
// Create a provider with configuration
|
||||
let provider = try createProvider(for: model, configuration: configuration)
|
||||
return provider
|
||||
try createProvider(for: model, configuration: configuration)
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ public final class CustomProviderRegistry: @unchecked Sendable {
|
||||
|
||||
private init() {}
|
||||
|
||||
// Load from ~/.<profile>/config.json customProviders
|
||||
/// Load from ~/.<profile>/config.json customProviders
|
||||
public func loadFromProfile() {
|
||||
guard let json = Self.loadRawConfigJSON() else { return }
|
||||
guard let cp = json["customProviders"] as? [String: Any] else { return }
|
||||
@ -60,7 +60,9 @@ public final class CustomProviderRegistry: @unchecked Sendable {
|
||||
return "\(home)/\(TachikomaConfiguration.profileDirectoryName)"
|
||||
}
|
||||
|
||||
private static func profileConfigPath() -> String { "\(self.profileDirectoryPath())/config.json" }
|
||||
private static func profileConfigPath() -> String {
|
||||
"\(self.profileDirectoryPath())/config.json"
|
||||
}
|
||||
|
||||
private static func stripJSONComments(from json: String) -> String {
|
||||
var result = ""
|
||||
|
||||
@ -220,7 +220,7 @@ protocol EmbeddingProvider: Sendable {
|
||||
|
||||
/// Request for embedding generation
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
struct EmbeddingRequest: Sendable {
|
||||
struct EmbeddingRequest {
|
||||
let input: EmbeddingInput
|
||||
let settings: EmbeddingSettings
|
||||
}
|
||||
|
||||
@ -172,7 +172,7 @@ extension Provider {
|
||||
let key = Self.processEnvironmentValue(for: self.environmentVariable),
|
||||
!key.isEmpty
|
||||
{
|
||||
return key
|
||||
return key
|
||||
}
|
||||
|
||||
// Check alternative environment variables
|
||||
|
||||
@ -74,7 +74,7 @@ public struct ProviderConfiguration: Sendable {
|
||||
self.customHeaders = customHeaders
|
||||
}
|
||||
|
||||
// Common configurations for providers
|
||||
/// Common configurations for providers
|
||||
public static let openAI = ProviderConfiguration(
|
||||
maxTokens: 4096,
|
||||
maxContextLength: 128_000,
|
||||
@ -111,10 +111,21 @@ public final class ProviderAdapter: EnhancedModelProvider {
|
||||
private let baseProvider: ModelProvider
|
||||
public let configuration: ProviderConfiguration
|
||||
|
||||
public var modelId: String { self.baseProvider.modelId }
|
||||
public var baseURL: String? { self.baseProvider.baseURL }
|
||||
public var apiKey: String? { self.baseProvider.apiKey }
|
||||
public var capabilities: ModelCapabilities { self.baseProvider.capabilities }
|
||||
public var modelId: String {
|
||||
self.baseProvider.modelId
|
||||
}
|
||||
|
||||
public var baseURL: String? {
|
||||
self.baseProvider.baseURL
|
||||
}
|
||||
|
||||
public var apiKey: String? {
|
||||
self.baseProvider.apiKey
|
||||
}
|
||||
|
||||
public var capabilities: ModelCapabilities {
|
||||
self.baseProvider.capabilities
|
||||
}
|
||||
|
||||
public init(provider: ModelProvider, configuration: ProviderConfiguration) {
|
||||
self.baseProvider = provider
|
||||
|
||||
@ -27,9 +27,13 @@ public enum AgentValueType: String, Sendable, Codable, CaseIterable {
|
||||
// MARK: - Built-in Type Conformances
|
||||
|
||||
extension String: AgentToolValue {
|
||||
public static var agentValueType: AgentValueType { .string }
|
||||
public static var agentValueType: AgentValueType {
|
||||
.string
|
||||
}
|
||||
|
||||
public func toJSON() throws -> Any { self }
|
||||
public func toJSON() throws -> Any {
|
||||
self
|
||||
}
|
||||
|
||||
public static func fromJSON(_ json: Any) throws -> Self {
|
||||
guard let string = json as? String else {
|
||||
@ -40,9 +44,13 @@ extension String: AgentToolValue {
|
||||
}
|
||||
|
||||
extension Int: AgentToolValue {
|
||||
public static var agentValueType: AgentValueType { .integer }
|
||||
public static var agentValueType: AgentValueType {
|
||||
.integer
|
||||
}
|
||||
|
||||
public func toJSON() throws -> Any { self }
|
||||
public func toJSON() throws -> Any {
|
||||
self
|
||||
}
|
||||
|
||||
public static func fromJSON(_ json: Any) throws -> Self {
|
||||
if let int = json as? Int {
|
||||
@ -56,9 +64,13 @@ extension Int: AgentToolValue {
|
||||
}
|
||||
|
||||
extension Double: AgentToolValue {
|
||||
public static var agentValueType: AgentValueType { .number }
|
||||
public static var agentValueType: AgentValueType {
|
||||
.number
|
||||
}
|
||||
|
||||
public func toJSON() throws -> Any { self }
|
||||
public func toJSON() throws -> Any {
|
||||
self
|
||||
}
|
||||
|
||||
public static func fromJSON(_ json: Any) throws -> Self {
|
||||
if let double = json as? Double {
|
||||
@ -72,9 +84,13 @@ extension Double: AgentToolValue {
|
||||
}
|
||||
|
||||
extension Bool: AgentToolValue {
|
||||
public static var agentValueType: AgentValueType { .boolean }
|
||||
public static var agentValueType: AgentValueType {
|
||||
.boolean
|
||||
}
|
||||
|
||||
public func toJSON() throws -> Any { self }
|
||||
public func toJSON() throws -> Any {
|
||||
self
|
||||
}
|
||||
|
||||
public static func fromJSON(_ json: Any) throws -> Self {
|
||||
guard let bool = json as? Bool else {
|
||||
@ -87,11 +103,15 @@ extension Bool: AgentToolValue {
|
||||
/// Null value type
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
public struct AgentNullValue: AgentToolValue, Equatable {
|
||||
public static var agentValueType: AgentValueType { .null }
|
||||
public static var agentValueType: AgentValueType {
|
||||
.null
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public func toJSON() throws -> Any { NSNull() }
|
||||
public func toJSON() throws -> Any {
|
||||
NSNull()
|
||||
}
|
||||
|
||||
public static func fromJSON(_ json: Any) throws -> Self {
|
||||
if json is NSNull {
|
||||
@ -103,7 +123,9 @@ public struct AgentNullValue: AgentToolValue, Equatable {
|
||||
|
||||
/// Array conformance
|
||||
extension Array: AgentToolValue where Element: AgentToolValue {
|
||||
public static var agentValueType: AgentValueType { .array }
|
||||
public static var agentValueType: AgentValueType {
|
||||
.array
|
||||
}
|
||||
|
||||
public func toJSON() throws -> Any {
|
||||
try map { try $0.toJSON() }
|
||||
@ -119,7 +141,9 @@ extension Array: AgentToolValue where Element: AgentToolValue {
|
||||
|
||||
/// Dictionary conformance
|
||||
extension Dictionary: AgentToolValue where Key == String, Value: AgentToolValue {
|
||||
public static var agentValueType: AgentValueType { .object }
|
||||
public static var agentValueType: AgentValueType {
|
||||
.object
|
||||
}
|
||||
|
||||
public func toJSON() throws -> Any {
|
||||
var result: [String: Any] = [:]
|
||||
@ -158,7 +182,9 @@ public struct AnyAgentToolValue: AgentToolValue, Equatable, Codable {
|
||||
case object([String: AnyAgentToolValue])
|
||||
}
|
||||
|
||||
public static var agentValueType: AgentValueType { .object } // Generic type
|
||||
public static var agentValueType: AgentValueType {
|
||||
.object
|
||||
} // Generic type
|
||||
|
||||
public init(_ value: some AgentToolValue) throws {
|
||||
let json = try value.toJSON()
|
||||
|
||||
@ -246,7 +246,7 @@ public struct ModelMessage: Sendable, Codable, Equatable {
|
||||
self.metadata = metadata
|
||||
}
|
||||
|
||||
// Convenience initializers
|
||||
/// Convenience initializers
|
||||
public static func system(_ text: String) -> ModelMessage {
|
||||
ModelMessage(role: .system, content: [.text(text)])
|
||||
}
|
||||
@ -366,7 +366,7 @@ public struct GenerationSettings: Sendable {
|
||||
public static let `default` = GenerationSettings()
|
||||
}
|
||||
|
||||
// Manual Codable conformance excluding non-codable stopConditions
|
||||
/// Manual Codable conformance excluding non-codable stopConditions
|
||||
extension GenerationSettings: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case maxTokens
|
||||
@ -508,7 +508,7 @@ public struct TextStreamDelta: Sendable {
|
||||
self.finishReason = finishReason
|
||||
}
|
||||
|
||||
// Convenience constructors
|
||||
/// Convenience constructors
|
||||
public static func text(_ content: String, channel: ResponseChannel? = nil) -> TextStreamDelta {
|
||||
TextStreamDelta(type: .textDelta, content: content, channel: channel)
|
||||
}
|
||||
|
||||
@ -151,7 +151,7 @@ public struct RecoverySuggestion: Sendable {
|
||||
self.helpURL = helpURL
|
||||
}
|
||||
|
||||
// Common recovery suggestions
|
||||
/// Common recovery suggestions
|
||||
public static let checkAPIKey = RecoverySuggestion(
|
||||
suggestion: "Check that your API key is valid and has the necessary permissions",
|
||||
actions: [.validateAPIKey, .regenerateAPIKey],
|
||||
|
||||
@ -31,13 +31,13 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
// MARK: - Provider Sub-Enums
|
||||
|
||||
public enum OpenAI: Sendable, Hashable, CaseIterable {
|
||||
// Latest models (2025)
|
||||
/// Latest models (2025)
|
||||
case o4Mini
|
||||
|
||||
// GPT-5.2 Series
|
||||
/// GPT-5.2 Series
|
||||
case gpt52 // Flagship GPT-5.2
|
||||
|
||||
// GPT-5.1 Series (November 2025)
|
||||
/// GPT-5.1 Series (November 2025)
|
||||
case gpt51 // Flagship GPT-5.1
|
||||
|
||||
// GPT-5 Series (August 2025)
|
||||
@ -63,7 +63,7 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
case gpt4Turbo
|
||||
case gpt35Turbo
|
||||
|
||||
// Fine-tuned models
|
||||
/// Fine-tuned models
|
||||
case custom(String)
|
||||
|
||||
public static var allCases: [OpenAI] {
|
||||
@ -188,7 +188,7 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
case sonnet45
|
||||
case haiku45
|
||||
|
||||
// Fine-tuned models
|
||||
/// Fine-tuned models
|
||||
case custom(String)
|
||||
|
||||
public static var allCases: [Anthropic] {
|
||||
@ -223,7 +223,9 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public var supportsTools: Bool { true } // All Claude models support tools
|
||||
public var supportsTools: Bool {
|
||||
true
|
||||
} // All Claude models support tools
|
||||
|
||||
public var supportsAudioInput: Bool {
|
||||
// Anthropic has voice features in mobile apps but limited API support as of 2025
|
||||
@ -251,7 +253,9 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
case gemini25Flash = "gemini-2.5-flash"
|
||||
case gemini25FlashLite = "gemini-2.5-flash-lite"
|
||||
|
||||
public var apiModelId: String { self.rawValue }
|
||||
public var apiModelId: String {
|
||||
self.rawValue
|
||||
}
|
||||
|
||||
public var userFacingModelId: String {
|
||||
switch self {
|
||||
@ -262,8 +266,13 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public var supportsVision: Bool { true }
|
||||
public var supportsTools: Bool { true }
|
||||
public var supportsVision: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
public var supportsTools: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
public var supportsAudioInput: Bool {
|
||||
switch self {
|
||||
@ -274,7 +283,9 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public var supportsAudioOutput: Bool { false }
|
||||
public var supportsAudioOutput: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
public var contextLength: Int {
|
||||
switch self {
|
||||
@ -301,10 +312,16 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public var supportsTools: Bool { true }
|
||||
public var supportsTools: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
public var supportsAudioInput: Bool { false } // Mistral doesn't support audio yet
|
||||
public var supportsAudioOutput: Bool { false }
|
||||
public var supportsAudioInput: Bool {
|
||||
false
|
||||
} // Mistral doesn't support audio yet
|
||||
public var supportsAudioOutput: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
public var contextLength: Int {
|
||||
switch self {
|
||||
@ -326,11 +343,19 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
case mixtral8x7b = "mixtral-8x7b"
|
||||
case gemma29b = "gemma2-9b"
|
||||
|
||||
public var supportsVision: Bool { false } // Groq models don't support vision yet
|
||||
public var supportsTools: Bool { true }
|
||||
public var supportsVision: Bool {
|
||||
false
|
||||
} // Groq models don't support vision yet
|
||||
public var supportsTools: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
public var supportsAudioInput: Bool { false } // Groq focuses on text inference speed
|
||||
public var supportsAudioOutput: Bool { false }
|
||||
public var supportsAudioInput: Bool {
|
||||
false
|
||||
} // Groq focuses on text inference speed
|
||||
public var supportsAudioOutput: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
public var contextLength: Int {
|
||||
switch self {
|
||||
@ -356,7 +381,7 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
case grokVisionBeta
|
||||
case grokBeta
|
||||
|
||||
// Custom models
|
||||
/// Custom models
|
||||
case custom(String)
|
||||
|
||||
public static var allCases: [Grok] {
|
||||
@ -401,7 +426,9 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public var supportsTools: Bool { true }
|
||||
public var supportsTools: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
public var supportsAudioInput: Bool {
|
||||
// Grok has voice support but limited API access as of 2025
|
||||
@ -472,7 +499,7 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
case firefunction
|
||||
case commandR
|
||||
|
||||
// Custom/other models
|
||||
/// Custom/other models
|
||||
case custom(String)
|
||||
|
||||
public static var allCases: [Ollama] {
|
||||
@ -587,9 +614,12 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public var supportsAudioInput: Bool { false
|
||||
public var supportsAudioInput: Bool {
|
||||
false
|
||||
} // Ollama models run locally and don't support native audio processing
|
||||
public var supportsAudioOutput: Bool { false }
|
||||
public var supportsAudioOutput: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
public var contextLength: Int {
|
||||
switch self {
|
||||
@ -630,10 +660,10 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
|
||||
case mistral7B
|
||||
case phi3Mini
|
||||
|
||||
// Currently loaded model
|
||||
/// Currently loaded model
|
||||
case current
|
||||
|
||||
// Custom model path
|
||||
/// Custom model path
|
||||
case custom(String)
|
||||
|
||||
public static var allCases: [LMStudio] {
|
||||
@ -1227,7 +1257,7 @@ extension LanguageModel {
|
||||
return .anthropic(.haiku45)
|
||||
}
|
||||
|
||||
let genericClaudeIdentifiers: Set<String> = [
|
||||
let genericClaudeIdentifiers: Set = [
|
||||
"claude",
|
||||
"claudelatest",
|
||||
"claude-latest",
|
||||
@ -1265,7 +1295,7 @@ extension LanguageModel {
|
||||
return .google(.gemini25Flash)
|
||||
}
|
||||
|
||||
let genericGeminiIdentifiers: Set<String> = [
|
||||
let genericGeminiIdentifiers: Set = [
|
||||
"gemini",
|
||||
"geminiflash",
|
||||
"gemini-flash",
|
||||
|
||||
@ -194,7 +194,7 @@ enum AnthropicContent: Codable {
|
||||
try Self.encodeAnyDict(self.input, to: inputEncoder)
|
||||
}
|
||||
|
||||
// Helper methods for encoding/decoding [String: Any]
|
||||
/// Helper methods for encoding/decoding [String: Any]
|
||||
private static func decodeAnyDict(from decoder: Decoder) throws -> [String: Any] {
|
||||
let container = try decoder.singleValueContainer()
|
||||
return try container.decode([String: JSONValue].self).mapValues { $0.value }
|
||||
|
||||
@ -9,8 +9,13 @@ public final class AzureOpenAIProvider: ModelProvider {
|
||||
public let modelId: String
|
||||
public let apiVersion: String
|
||||
public let capabilities: ModelCapabilities
|
||||
public var baseURL: String? { self.resolvedBaseURL }
|
||||
public var apiKey: String? { self.resolvedAPIKey }
|
||||
public var baseURL: String? {
|
||||
self.resolvedBaseURL
|
||||
}
|
||||
|
||||
public var apiKey: String? {
|
||||
self.resolvedAPIKey
|
||||
}
|
||||
|
||||
private let authHeaderName: String
|
||||
private let authHeaderValuePrefix: String
|
||||
|
||||
@ -12,7 +12,9 @@ public final class GoogleProvider: ModelProvider {
|
||||
public let capabilities: ModelCapabilities
|
||||
|
||||
private let model: LanguageModel.Google
|
||||
private var apiModelName: String { self.model.apiModelId }
|
||||
private var apiModelName: String {
|
||||
self.model.apiModelId
|
||||
}
|
||||
|
||||
public init(model: LanguageModel.Google, configuration: TachikomaConfiguration) throws {
|
||||
self.model = model
|
||||
|
||||
@ -16,11 +16,22 @@ public actor LMStudioProvider: ModelProvider {
|
||||
private let configuredModelId: String
|
||||
private let configuredCapabilities: ModelCapabilities
|
||||
|
||||
// Expose as optional for protocol conformance, but it's never actually nil
|
||||
public nonisolated var baseURL: String? { self.actualBaseURL }
|
||||
public nonisolated var apiKey: String? { self.configuredApiKey }
|
||||
public nonisolated var modelId: String { self.configuredModelId }
|
||||
public nonisolated var capabilities: ModelCapabilities { self.configuredCapabilities }
|
||||
/// Expose as optional for protocol conformance, but it's never actually nil
|
||||
public nonisolated var baseURL: String? {
|
||||
self.actualBaseURL
|
||||
}
|
||||
|
||||
public nonisolated var apiKey: String? {
|
||||
self.configuredApiKey
|
||||
}
|
||||
|
||||
public nonisolated var modelId: String {
|
||||
self.configuredModelId
|
||||
}
|
||||
|
||||
public nonisolated var capabilities: ModelCapabilities {
|
||||
self.configuredCapabilities
|
||||
}
|
||||
|
||||
private let session: URLSession
|
||||
private let encoder = JSONEncoder()
|
||||
|
||||
@ -12,10 +12,15 @@ struct OpenAIEmbeddingProvider: EmbeddingProvider, ModelProvider {
|
||||
let apiKey: String?
|
||||
let baseURL: String?
|
||||
|
||||
var modelId: String { self.model.rawValue }
|
||||
var capabilities: ModelCapabilities { ModelCapabilities() }
|
||||
var modelId: String {
|
||||
self.model.rawValue
|
||||
}
|
||||
|
||||
// ModelProvider conformance (not used for embeddings)
|
||||
var capabilities: ModelCapabilities {
|
||||
ModelCapabilities()
|
||||
}
|
||||
|
||||
/// ModelProvider conformance (not used for embeddings)
|
||||
func generateText(request _: ProviderRequest) async throws -> ProviderResponse {
|
||||
throw TachikomaError.unsupportedOperation("Text generation not supported for embedding models")
|
||||
}
|
||||
@ -92,17 +97,25 @@ struct OpenAIEmbeddingProvider: EmbeddingProvider, ModelProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder providers for other services
|
||||
/// Placeholder providers for other services
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
struct CohereEmbeddingProvider: EmbeddingProvider, ModelProvider {
|
||||
let model: EmbeddingModel.CohereEmbedding
|
||||
let apiKey: String?
|
||||
|
||||
var modelId: String { self.model.rawValue }
|
||||
var baseURL: String? { nil }
|
||||
var capabilities: ModelCapabilities { ModelCapabilities() }
|
||||
var modelId: String {
|
||||
self.model.rawValue
|
||||
}
|
||||
|
||||
// ModelProvider conformance (not used for embeddings)
|
||||
var baseURL: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var capabilities: ModelCapabilities {
|
||||
ModelCapabilities()
|
||||
}
|
||||
|
||||
/// ModelProvider conformance (not used for embeddings)
|
||||
func generateText(request _: ProviderRequest) async throws -> ProviderResponse {
|
||||
throw TachikomaError.unsupportedOperation("Text generation not supported for embedding models")
|
||||
}
|
||||
@ -121,11 +134,19 @@ struct VoyageEmbeddingProvider: EmbeddingProvider, ModelProvider {
|
||||
let model: EmbeddingModel.VoyageEmbedding
|
||||
let apiKey: String?
|
||||
|
||||
var modelId: String { self.model.rawValue }
|
||||
var baseURL: String? { nil }
|
||||
var capabilities: ModelCapabilities { ModelCapabilities() }
|
||||
var modelId: String {
|
||||
self.model.rawValue
|
||||
}
|
||||
|
||||
// ModelProvider conformance (not used for embeddings)
|
||||
var baseURL: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var capabilities: ModelCapabilities {
|
||||
ModelCapabilities()
|
||||
}
|
||||
|
||||
/// ModelProvider conformance (not used for embeddings)
|
||||
func generateText(request _: ProviderRequest) async throws -> ProviderResponse {
|
||||
throw TachikomaError.unsupportedOperation("Text generation not supported for embedding models")
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ struct OpenAIResponsesRequest: Encodable {
|
||||
let topP: Double?
|
||||
let maxOutputTokens: Int?
|
||||
|
||||
// Response format and text configuration
|
||||
/// Response format and text configuration
|
||||
let text: TextConfig? // GPT-5 text configuration with verbosity
|
||||
|
||||
// Tool configuration
|
||||
@ -28,13 +28,13 @@ struct OpenAIResponsesRequest: Encodable {
|
||||
let serviceTier: String?
|
||||
let include: [String]?
|
||||
|
||||
// Reasoning configuration (for o3/o4/GPT-5)
|
||||
/// Reasoning configuration (for o3/o4/GPT-5)
|
||||
let reasoning: ReasoningConfig?
|
||||
|
||||
// Truncation for long inputs
|
||||
/// Truncation for long inputs
|
||||
let truncation: String?
|
||||
|
||||
// Streaming support
|
||||
/// Streaming support
|
||||
let stream: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
@ -62,7 +62,7 @@ struct OpenAIResponsesRequest: Encodable {
|
||||
|
||||
/// Text verbosity levels for GPT-5
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
enum TextVerbosity: String, Codable, Sendable {
|
||||
enum TextVerbosity: String, Codable {
|
||||
case low
|
||||
case medium
|
||||
case high
|
||||
@ -70,7 +70,7 @@ enum TextVerbosity: String, Codable, Sendable {
|
||||
|
||||
/// Internal reasoning effort levels for OpenAI responses provider
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
enum OpenAIReasoningEffort: String, Codable, Sendable {
|
||||
enum OpenAIReasoningEffort: String, Codable {
|
||||
case minimal
|
||||
case low
|
||||
case medium
|
||||
@ -79,7 +79,7 @@ enum OpenAIReasoningEffort: String, Codable, Sendable {
|
||||
|
||||
/// Reasoning summary modes for reasoning models
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
enum ReasoningSummary: String, Codable, Sendable {
|
||||
enum ReasoningSummary: String, Codable {
|
||||
case concise
|
||||
case detailed
|
||||
case auto
|
||||
@ -87,13 +87,13 @@ enum ReasoningSummary: String, Codable, Sendable {
|
||||
|
||||
/// Text configuration for GPT-5 models
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
struct TextConfig: Codable, Sendable {
|
||||
struct TextConfig: Codable {
|
||||
let verbosity: TextVerbosity?
|
||||
}
|
||||
|
||||
/// Reasoning configuration for reasoning models
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
struct ReasoningConfig: Codable, Sendable {
|
||||
struct ReasoningConfig: Codable {
|
||||
let effort: OpenAIReasoningEffort?
|
||||
let summary: ReasoningSummary?
|
||||
}
|
||||
@ -186,11 +186,11 @@ struct JSONSchemaFormat: Codable {
|
||||
|
||||
/// Message format for Responses API
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
struct ResponsesMessage: Codable, Sendable {
|
||||
struct ResponsesMessage: Codable {
|
||||
let role: String
|
||||
let content: ResponsesContent
|
||||
|
||||
enum ResponsesContent: Codable, Sendable {
|
||||
enum ResponsesContent: Codable {
|
||||
case text(String)
|
||||
case parts([ResponsesContentPart])
|
||||
|
||||
@ -218,7 +218,7 @@ struct ResponsesMessage: Codable, Sendable {
|
||||
|
||||
/// Content part for multimodal messages
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
struct ResponsesContentPart: Codable, Sendable {
|
||||
struct ResponsesContentPart: Codable {
|
||||
let type: String
|
||||
let text: String?
|
||||
/// OpenAI Responses API (GPT‑5.x) accepts `image_url` only as a string (URL or data URL).
|
||||
@ -226,7 +226,7 @@ struct ResponsesContentPart: Codable, Sendable {
|
||||
/// avoid 400s ("expected an image URL, but got an object instead").
|
||||
let imageUrl: ImageURL?
|
||||
|
||||
struct ImageURL: Codable, Sendable {
|
||||
struct ImageURL: Codable {
|
||||
let url: String
|
||||
let detail: String?
|
||||
}
|
||||
@ -271,12 +271,12 @@ struct ResponsesContentPart: Codable, Sendable {
|
||||
|
||||
/// Heterogeneous input entries supported by the Responses API
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
enum ResponsesInputItem: Encodable, Sendable {
|
||||
enum ResponsesInputItem: Encodable {
|
||||
case message(ResponsesMessage)
|
||||
case functionCall(FunctionCall)
|
||||
case functionCallOutput(FunctionCallOutput)
|
||||
|
||||
struct FunctionCall: Encodable, Sendable {
|
||||
struct FunctionCall: Encodable {
|
||||
let type: String = "function_call"
|
||||
let callId: String
|
||||
let name: String
|
||||
@ -290,7 +290,7 @@ enum ResponsesInputItem: Encodable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
struct FunctionCallOutput: Encodable, Sendable {
|
||||
struct FunctionCallOutput: Encodable {
|
||||
let type: String = "function_call_output"
|
||||
let callId: String
|
||||
let output: String
|
||||
@ -473,7 +473,7 @@ struct ResponsesTool: Codable {
|
||||
|
||||
/// Response from OpenAI Responses API (GPT-5 format)
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
struct OpenAIResponsesResponse: Codable, Sendable {
|
||||
struct OpenAIResponsesResponse: Codable {
|
||||
let id: String
|
||||
let object: String?
|
||||
let createdAt: Int? // GPT-5 uses created_at
|
||||
@ -491,8 +491,8 @@ struct OpenAIResponsesResponse: Codable, Sendable {
|
||||
case created
|
||||
}
|
||||
|
||||
// GPT-5 output format
|
||||
struct ResponsesOutput: Codable, Sendable {
|
||||
/// GPT-5 output format
|
||||
struct ResponsesOutput: Codable {
|
||||
let id: String
|
||||
let type: String
|
||||
let status: String?
|
||||
@ -510,7 +510,7 @@ struct OpenAIResponsesResponse: Codable, Sendable {
|
||||
case toolCall = "tool_call"
|
||||
}
|
||||
|
||||
struct OutputContent: Codable, Sendable {
|
||||
struct OutputContent: Codable {
|
||||
let type: String
|
||||
let text: String?
|
||||
let toolCall: ResponsesToolCall?
|
||||
@ -523,8 +523,8 @@ struct OpenAIResponsesResponse: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
// O3 choices format (kept for compatibility)
|
||||
struct ResponsesChoice: Codable, Sendable {
|
||||
/// O3 choices format (kept for compatibility)
|
||||
struct ResponsesChoice: Codable {
|
||||
let index: Int
|
||||
let message: ResponsesOutputMessage
|
||||
let finishReason: String?
|
||||
@ -538,7 +538,7 @@ struct OpenAIResponsesResponse: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
struct ResponsesOutputMessage: Codable, Sendable {
|
||||
struct ResponsesOutputMessage: Codable {
|
||||
let role: String
|
||||
let content: String?
|
||||
let toolCalls: [ResponsesToolCall]?
|
||||
@ -552,18 +552,18 @@ struct OpenAIResponsesResponse: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
struct ResponsesToolCall: Codable, Sendable {
|
||||
struct ResponsesToolCall: Codable {
|
||||
let id: String
|
||||
let type: String
|
||||
let function: ResponsesToolFunction
|
||||
|
||||
struct ResponsesToolFunction: Codable, Sendable {
|
||||
struct ResponsesToolFunction: Codable {
|
||||
let name: String
|
||||
let arguments: String
|
||||
}
|
||||
}
|
||||
|
||||
struct ResponsesUsage: Codable, Sendable {
|
||||
struct ResponsesUsage: Codable {
|
||||
let inputTokens: Int?
|
||||
let outputTokens: Int?
|
||||
let totalTokens: Int?
|
||||
@ -584,7 +584,7 @@ struct OpenAIResponsesResponse: Codable, Sendable {
|
||||
case outputTokensDetails = "output_tokens_details"
|
||||
}
|
||||
|
||||
struct TokenDetails: Codable, Sendable {
|
||||
struct TokenDetails: Codable {
|
||||
let cachedTokens: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
@ -592,7 +592,7 @@ struct OpenAIResponsesResponse: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
struct OutputTokenDetails: Codable, Sendable {
|
||||
struct OutputTokenDetails: Codable {
|
||||
let reasoningTokens: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
@ -601,7 +601,7 @@ struct OpenAIResponsesResponse: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
struct ResponsesMetadata: Codable, Sendable {
|
||||
struct ResponsesMetadata: Codable {
|
||||
let responseId: String?
|
||||
let reasoningItemIds: [String]?
|
||||
|
||||
@ -616,14 +616,14 @@ struct OpenAIResponsesResponse: Codable, Sendable {
|
||||
|
||||
/// Server-sent event for streaming responses (O3 and older models)
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
struct OpenAIResponsesStreamChunk: Codable, Sendable {
|
||||
struct OpenAIResponsesStreamChunk: Codable {
|
||||
let id: String
|
||||
let object: String
|
||||
let created: Int
|
||||
let model: String
|
||||
let choices: [StreamChoice]
|
||||
|
||||
struct StreamChoice: Codable, Sendable {
|
||||
struct StreamChoice: Codable {
|
||||
let index: Int
|
||||
let delta: StreamDelta
|
||||
let finishReason: String?
|
||||
@ -635,7 +635,7 @@ struct OpenAIResponsesStreamChunk: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
struct StreamDelta: Codable, Sendable {
|
||||
struct StreamDelta: Codable {
|
||||
let role: String?
|
||||
let content: String?
|
||||
let toolCalls: [StreamToolCall]?
|
||||
@ -647,13 +647,13 @@ struct OpenAIResponsesStreamChunk: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
struct StreamToolCall: Codable, Sendable {
|
||||
struct StreamToolCall: Codable {
|
||||
let index: Int
|
||||
let id: String?
|
||||
let type: String?
|
||||
let function: StreamToolFunction?
|
||||
|
||||
struct StreamToolFunction: Codable, Sendable {
|
||||
struct StreamToolFunction: Codable {
|
||||
let name: String?
|
||||
let arguments: String?
|
||||
}
|
||||
|
||||
@ -140,7 +140,7 @@ enum OpenAIChatMessageContent: Codable {
|
||||
let url: String
|
||||
}
|
||||
|
||||
// Provide custom Codable to match OpenAI schema (flattened objects with type field)
|
||||
/// Provide custom Codable to match OpenAI schema (flattened objects with type field)
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case text
|
||||
@ -326,8 +326,8 @@ struct OpenAIErrorResponse: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper type for Either content
|
||||
enum Either<Left, Right>: Codable where Left: Codable, Right: Codable {
|
||||
/// Helper type for Either content
|
||||
enum Either<Left: Codable, Right: Codable>: Codable {
|
||||
case left(Left)
|
||||
case right(Right)
|
||||
|
||||
|
||||
@ -97,9 +97,17 @@ public struct AnyAgentTool: Sendable {
|
||||
private let _schema: AgentToolSchema
|
||||
private let _execute: @Sendable (AnyAgentToolValue, ToolExecutionContext) async throws -> AnyAgentToolValue
|
||||
|
||||
public var name: String { self._name }
|
||||
public var description: String { self._description }
|
||||
public var schema: AgentToolSchema { self._schema }
|
||||
public var name: String {
|
||||
self._name
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
self._description
|
||||
}
|
||||
|
||||
public var schema: AgentToolSchema {
|
||||
self._schema
|
||||
}
|
||||
|
||||
public init<T: AgentToolProtocol>(_ tool: T) {
|
||||
self._name = tool.name
|
||||
|
||||
@ -547,10 +547,21 @@ public struct CacheAwareProvider<Base: ModelProvider>: ModelProvider {
|
||||
let provider: Base
|
||||
let cache: ResponseCache
|
||||
|
||||
public var modelId: String { self.provider.modelId }
|
||||
public var baseURL: String? { self.provider.baseURL }
|
||||
public var apiKey: String? { self.provider.apiKey }
|
||||
public var capabilities: ModelCapabilities { self.provider.capabilities }
|
||||
public var modelId: String {
|
||||
self.provider.modelId
|
||||
}
|
||||
|
||||
public var baseURL: String? {
|
||||
self.provider.baseURL
|
||||
}
|
||||
|
||||
public var apiKey: String? {
|
||||
self.provider.apiKey
|
||||
}
|
||||
|
||||
public var capabilities: ModelCapabilities {
|
||||
self.provider.capabilities
|
||||
}
|
||||
|
||||
public func generateText(request: ProviderRequest) async throws -> ProviderResponse {
|
||||
// Check cache with smart TTL based on request type
|
||||
|
||||
@ -267,7 +267,7 @@ public struct UsageOperation: Sendable, Codable {
|
||||
self.type = type
|
||||
}
|
||||
|
||||
// For backward compatibility, provide a computed property to reconstruct model info
|
||||
/// For backward compatibility, provide a computed property to reconstruct model info
|
||||
public var modelDescription: String {
|
||||
"\(self.providerName)/\(self.modelId)"
|
||||
}
|
||||
@ -393,10 +393,21 @@ public struct UsageReport: Sendable {
|
||||
public let endDate: Date
|
||||
public let sessions: [UsageSession]
|
||||
|
||||
public var totalSessions: Int { self.sessions.count }
|
||||
public var totalOperations: Int { self.sessions.reduce(0) { $0 + $1.operations.count } }
|
||||
public var totalTokens: Int { self.sessions.reduce(0) { $0 + $1.totalTokens } }
|
||||
public var totalCost: Double { self.sessions.reduce(0) { $0 + $1.totalCost } }
|
||||
public var totalSessions: Int {
|
||||
self.sessions.count
|
||||
}
|
||||
|
||||
public var totalOperations: Int {
|
||||
self.sessions.reduce(0) { $0 + $1.operations.count }
|
||||
}
|
||||
|
||||
public var totalTokens: Int {
|
||||
self.sessions.reduce(0) { $0 + $1.totalTokens }
|
||||
}
|
||||
|
||||
public var totalCost: Double {
|
||||
self.sessions.reduce(0) { $0 + $1.totalCost }
|
||||
}
|
||||
|
||||
public let providerBreakdown: [String: ProviderUsage]
|
||||
public let modelBreakdown: [String: ModelUsage]
|
||||
|
||||
@ -124,7 +124,7 @@ public final class Agent<Context>: @unchecked Sendable {
|
||||
)
|
||||
|
||||
// Track final message in conversation (this is approximate for streaming)
|
||||
let trackedStream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
return AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
Task {
|
||||
do {
|
||||
var assistantText = ""
|
||||
@ -151,8 +151,6 @@ public final class Agent<Context>: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return trackedStream
|
||||
}
|
||||
|
||||
/// Reset the agent's conversation history
|
||||
|
||||
@ -126,7 +126,7 @@ public final class Conversation: @unchecked Sendable {
|
||||
)
|
||||
|
||||
// Create a new stream to process the response and update the conversation
|
||||
let processedStream = AsyncThrowingStream<String, Error> { continuation in
|
||||
return AsyncThrowingStream<String, Error> { continuation in
|
||||
Task {
|
||||
var fullResponse = ""
|
||||
do {
|
||||
@ -151,8 +151,6 @@ public final class Conversation: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return processedStream
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -150,7 +150,7 @@ public func createTool(
|
||||
)
|
||||
}
|
||||
|
||||
// Helper struct for type-safe tool creation (moved outside to avoid nesting in generic function)
|
||||
/// Helper struct for type-safe tool creation (moved outside to avoid nesting in generic function)
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
private struct ConcreteAgentTool<I: AgentToolValue, O: AgentToolValue>: AgentToolProtocol {
|
||||
typealias Input = I
|
||||
|
||||
@ -50,7 +50,7 @@ public final class RealtimeAudioManager: NSObject {
|
||||
private let audioSession = AVAudioSession.sharedInstance()
|
||||
#endif
|
||||
|
||||
// Audio format for API (24kHz PCM16)
|
||||
/// Audio format for API (24kHz PCM16)
|
||||
private let apiFormat = AVAudioFormat(
|
||||
commonFormat: .pcmFormatInt16,
|
||||
sampleRate: 24000,
|
||||
@ -297,7 +297,7 @@ public final class RealtimeAudioManager: NSObject {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Set audio input device (macOS only)
|
||||
// Set audio input device (macOS only)
|
||||
#if os(macOS)
|
||||
public func setInputDevice(_: AudioDeviceID) throws {
|
||||
// Note: Setting audio device on macOS requires more complex Core Audio API
|
||||
|
||||
@ -304,9 +304,7 @@ public final class RealtimeAudioProcessor: @unchecked Sendable {
|
||||
}
|
||||
|
||||
let mantissa = UInt8((s >> (exponent + 3)) & 0x0F)
|
||||
let encoded = ~(sign | (exponent << 4) | mantissa)
|
||||
|
||||
return encoded
|
||||
return ~(sign | (exponent << 4) | mantissa)
|
||||
}
|
||||
|
||||
private func decodeUlaw(_ ulaw: UInt8) -> Int16 {
|
||||
|
||||
@ -275,7 +275,7 @@ public protocol AudioStreamPipelineDelegate: AnyObject {
|
||||
func audioStreamPipeline(didEncounterError error: Error) async
|
||||
}
|
||||
|
||||
// Default implementation for optional methods
|
||||
/// Default implementation for optional methods
|
||||
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
||||
extension AudioStreamPipelineDelegate {
|
||||
// Pipeline started
|
||||
|
||||
@ -135,7 +135,7 @@ public final class RealtimeConversation: ObservableObject {
|
||||
private var audioBuffer = Data()
|
||||
private let audioChunkSize = 1024 * 4 // 4KB chunks
|
||||
|
||||
// Background tasks
|
||||
/// Background tasks
|
||||
private var eventProcessingTask: Task<Void, Never>?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
@ -39,7 +39,9 @@ public enum RealtimeClientEvent: RealtimeEventProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
public var eventId: String? { UUID().uuidString }
|
||||
public var eventId: String? {
|
||||
UUID().uuidString
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Server Events (Events sent from server to client)
|
||||
@ -111,7 +113,9 @@ public enum RealtimeServerEvent: RealtimeEventProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
public var eventId: String? { nil }
|
||||
public var eventId: String? {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Session Events
|
||||
|
||||
@ -111,7 +111,7 @@ public struct AgentToolWrapper: RealtimeExecutableTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert Any to AnyAgentToolValue
|
||||
/// Helper function to convert Any to AnyAgentToolValue
|
||||
private func convertAnyToToolArgument(_ value: Any) -> AnyAgentToolValue {
|
||||
do {
|
||||
return try AnyAgentToolValue.fromJSON(value)
|
||||
@ -121,7 +121,7 @@ public struct AgentToolWrapper: RealtimeExecutableTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert AnyAgentToolValue to Any
|
||||
/// Helper function to convert AnyAgentToolValue to Any
|
||||
private func convertToolArgumentToAny(_ arg: AnyAgentToolValue) -> Any {
|
||||
do {
|
||||
return try arg.toJSON()
|
||||
|
||||
@ -88,7 +88,7 @@ public final class AudioRecorder: ObservableObject, AudioRecorderProtocol {
|
||||
private let channels: AVAudioChannelCount = 1
|
||||
private let bitDepth: Int = 16
|
||||
|
||||
// Maximum recording duration (5 minutes by default)
|
||||
/// Maximum recording duration (5 minutes by default)
|
||||
public var maxRecordingDuration: TimeInterval = 300
|
||||
|
||||
// MARK: - Initialization
|
||||
@ -211,8 +211,7 @@ public final class AudioRecorder: ObservableObject, AudioRecorderProtocol {
|
||||
recordingURL = nil
|
||||
}
|
||||
|
||||
let audioData = try AudioData(contentsOf: url)
|
||||
return audioData
|
||||
return try AudioData(contentsOf: url)
|
||||
}
|
||||
|
||||
/// Cancel recording without returning data
|
||||
@ -418,9 +417,17 @@ private let logger = Logger(label: "tachikoma.audio.recorder")
|
||||
public final class AudioRecorder: AudioRecorderProtocol {
|
||||
public init() {}
|
||||
|
||||
public var isRecording: Bool { false }
|
||||
public var isAvailable: Bool { false }
|
||||
public var recordingDuration: TimeInterval { 0 }
|
||||
public var isRecording: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
public var isAvailable: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
public var recordingDuration: TimeInterval {
|
||||
0
|
||||
}
|
||||
|
||||
public func startRecording() async throws {
|
||||
throw AudioRecordingError.audioEngineError("Audio recording is unavailable on this platform")
|
||||
|
||||
@ -39,8 +39,13 @@ public enum TranscriptionModel: Sendable, CustomStringConvertible {
|
||||
case whisperLargeV3Turbo = "whisper-large-v3-turbo"
|
||||
case distilWhisperLargeV3En = "distil-whisper-large-v3-en"
|
||||
|
||||
public var supportsTimestamps: Bool { true }
|
||||
public var supportsLanguageDetection: Bool { true }
|
||||
public var supportsTimestamps: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
public var supportsLanguageDetection: Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
public enum Deepgram: String, CaseIterable, Sendable {
|
||||
@ -49,17 +54,30 @@ public enum TranscriptionModel: Sendable, CustomStringConvertible {
|
||||
case enhanced
|
||||
case base
|
||||
|
||||
public var supportsTimestamps: Bool { true }
|
||||
public var supportsLanguageDetection: Bool { true }
|
||||
public var supportsSummarization: Bool { true }
|
||||
public var supportsTimestamps: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
public var supportsLanguageDetection: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
public var supportsSummarization: Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
public enum ElevenLabs: String, CaseIterable, Sendable {
|
||||
case scribeV1 = "scribe_v1"
|
||||
case scribeV1Experimental = "scribe_v1_experimental"
|
||||
|
||||
public var supportsTimestamps: Bool { false }
|
||||
public var supportsLanguageDetection: Bool { true }
|
||||
public var supportsTimestamps: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
public var supportsLanguageDetection: Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Model Properties
|
||||
@ -160,8 +178,13 @@ public enum SpeechModel: Sendable, CustomStringConvertible {
|
||||
case multilingualV2 = "eleven_multilingual_v2"
|
||||
case englishV1 = "eleven_english_v1"
|
||||
|
||||
public var supportsVoiceCloning: Bool { true }
|
||||
public var supportedFormats: [AudioFormat] { [.mp3, .wav, .pcm] }
|
||||
public var supportsVoiceCloning: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
public var supportedFormats: [AudioFormat] {
|
||||
[.mp3, .wav, .pcm]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Model Properties
|
||||
|
||||
@ -98,7 +98,7 @@ public enum VoiceOption: Sendable, Hashable {
|
||||
case nova
|
||||
case shimmer
|
||||
|
||||
// Custom voice (provider-specific)
|
||||
/// Custom voice (provider-specific)
|
||||
case custom(String)
|
||||
|
||||
public var stringValue: String {
|
||||
|
||||
@ -30,21 +30,21 @@ enum MCPContentBridge {
|
||||
static func convert(_ content: MCP.Tool.Content) -> AnyAgentToolValue {
|
||||
switch content {
|
||||
case let .text(text):
|
||||
return AnyAgentToolValue(string: text)
|
||||
AnyAgentToolValue(string: text)
|
||||
case let .image(data, mimeType, _):
|
||||
return AnyAgentToolValue(object: [
|
||||
AnyAgentToolValue(object: [
|
||||
"type": AnyAgentToolValue(string: "image"),
|
||||
"mimeType": AnyAgentToolValue(string: mimeType),
|
||||
"data": AnyAgentToolValue(string: data),
|
||||
])
|
||||
case let .resource(resource, annotations, meta):
|
||||
return AnyAgentToolValue(object: self.resourceObject(
|
||||
AnyAgentToolValue(object: self.resourceObject(
|
||||
resource: resource,
|
||||
annotations: annotations,
|
||||
meta: meta,
|
||||
))
|
||||
case let .resourceLink(uri, name, title, description, mimeType, annotations):
|
||||
return AnyAgentToolValue(object: self.resourceLinkObject(
|
||||
AnyAgentToolValue(object: self.resourceLinkObject(
|
||||
uri: uri,
|
||||
name: name,
|
||||
title: title,
|
||||
@ -53,7 +53,7 @@ enum MCPContentBridge {
|
||||
annotations: annotations,
|
||||
))
|
||||
case let .audio(data, mimeType):
|
||||
return AnyAgentToolValue(object: [
|
||||
AnyAgentToolValue(object: [
|
||||
"type": AnyAgentToolValue(string: "audio"),
|
||||
"mimeType": AnyAgentToolValue(string: mimeType),
|
||||
"data": AnyAgentToolValue(string: data),
|
||||
@ -65,7 +65,9 @@ enum MCPContentBridge {
|
||||
resource: Resource.Content,
|
||||
annotations: Resource.Annotations?,
|
||||
meta: Metadata?,
|
||||
) -> [String: AnyAgentToolValue] {
|
||||
)
|
||||
-> [String: AnyAgentToolValue]
|
||||
{
|
||||
var resourceDict: [String: AnyAgentToolValue] = [
|
||||
"type": AnyAgentToolValue(string: "resource"),
|
||||
"uri": AnyAgentToolValue(string: resource.uri),
|
||||
@ -105,7 +107,9 @@ enum MCPContentBridge {
|
||||
description: String?,
|
||||
mimeType: String?,
|
||||
annotations: Resource.Annotations?,
|
||||
) -> [String: AnyAgentToolValue] {
|
||||
)
|
||||
-> [String: AnyAgentToolValue]
|
||||
{
|
||||
var resourceDict: [String: AnyAgentToolValue] = [
|
||||
"type": AnyAgentToolValue(string: "resourceLink"),
|
||||
"uri": AnyAgentToolValue(string: uri),
|
||||
|
||||
@ -4,7 +4,7 @@ import FoundationNetworking
|
||||
#endif
|
||||
import Logging
|
||||
|
||||
// Actor to manage mutable state for Sendable conformance
|
||||
/// Actor to manage mutable state for Sendable conformance
|
||||
private actor HTTPTransportState {
|
||||
var urlSession: URLSession?
|
||||
var baseURL: URL?
|
||||
@ -18,10 +18,21 @@ private actor HTTPTransportState {
|
||||
self.headers = headers
|
||||
}
|
||||
|
||||
func getSession() -> URLSession? { self.urlSession }
|
||||
func getBaseURL() -> URL? { self.baseURL }
|
||||
func getTimeout() -> TimeInterval { self.requestTimeout }
|
||||
func getHeaders() -> [String: String] { self.headers }
|
||||
func getSession() -> URLSession? {
|
||||
self.urlSession
|
||||
}
|
||||
|
||||
func getBaseURL() -> URL? {
|
||||
self.baseURL
|
||||
}
|
||||
|
||||
func getTimeout() -> TimeInterval {
|
||||
self.requestTimeout
|
||||
}
|
||||
|
||||
func getHeaders() -> [String: String] {
|
||||
self.headers
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP transport for MCP communication
|
||||
|
||||
@ -2,7 +2,7 @@ import Foundation
|
||||
import Logging
|
||||
import MCP
|
||||
|
||||
// Shared JSON-RPC types for HTTP transport
|
||||
/// Shared JSON-RPC types for HTTP transport
|
||||
struct HTTPJSONRPCRequest<P: Encodable>: Encodable {
|
||||
let jsonrpc = "2.0"
|
||||
let method: String
|
||||
@ -56,7 +56,7 @@ public struct MCPServerConfig: Sendable, Codable {
|
||||
}
|
||||
}
|
||||
|
||||
// Actor to manage mutable state for Sendable conformance
|
||||
/// Actor to manage mutable state for Sendable conformance
|
||||
private actor MCPClientState {
|
||||
var transport: (any MCPTransport)?
|
||||
var tools: [Tool] = []
|
||||
@ -258,7 +258,7 @@ struct InitializeParams: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
// Some servers use snake_case for protocol_version in initialize
|
||||
/// Some servers use snake_case for protocol_version in initialize
|
||||
struct InitializeParamsSnake: Codable {
|
||||
let protocolVersion: String
|
||||
let clientInfo: ClientInfo
|
||||
|
||||
@ -189,7 +189,7 @@ public final class TachikomaMCPClientManager {
|
||||
return all
|
||||
}
|
||||
|
||||
// Execute a tool on a specific server
|
||||
/// Execute a tool on a specific server
|
||||
public func executeTool(
|
||||
serverName: String,
|
||||
toolName: String,
|
||||
@ -222,7 +222,9 @@ public final class TachikomaMCPClientManager {
|
||||
|
||||
// MARK: Health/Info (lightweight)
|
||||
|
||||
public func getServerNames() -> [String] { Array(self.effectiveConfigs.keys).sorted() }
|
||||
public func getServerNames() -> [String] {
|
||||
Array(self.effectiveConfigs.keys).sorted()
|
||||
}
|
||||
|
||||
public func getServerInfo(name: String) async -> (config: MCPServerConfig, connected: Bool)? {
|
||||
guard let cfg = effectiveConfigs[name] else { return nil }
|
||||
|
||||
@ -5,7 +5,7 @@ import FoundationNetworking
|
||||
import Logging
|
||||
import MCP
|
||||
|
||||
// Internal state for SSE transport
|
||||
/// Internal state for SSE transport
|
||||
private actor SSEState {
|
||||
var transport: HTTPClientTransport?
|
||||
var baseURL: URL?
|
||||
@ -16,29 +16,60 @@ private actor SSEState {
|
||||
var timeoutTasks: [Int: Task<Void, Never>] = [:]
|
||||
var requestTimeoutNs: UInt64 = 30_000_000_000 // default 30s
|
||||
|
||||
func setTransport(_ t: HTTPClientTransport?) { self.transport = t }
|
||||
func getTransport() -> HTTPClientTransport? { self.transport }
|
||||
func setBaseURL(_ url: URL?) { self.baseURL = url }
|
||||
func setHeaders(_ h: [String: String]) { self.headers = h }
|
||||
func setEndpoint(_ url: URL?) { self.endpointURL = url }
|
||||
func getEndpoint() -> URL? { self.endpointURL }
|
||||
func getBaseURL() -> URL? { self.baseURL }
|
||||
func setTransport(_ t: HTTPClientTransport?) {
|
||||
self.transport = t
|
||||
}
|
||||
|
||||
func getNextId() -> Int { defer { nextId += 1 }
|
||||
func getTransport() -> HTTPClientTransport? {
|
||||
self.transport
|
||||
}
|
||||
|
||||
func setBaseURL(_ url: URL?) {
|
||||
self.baseURL = url
|
||||
}
|
||||
|
||||
func setHeaders(_ h: [String: String]) {
|
||||
self.headers = h
|
||||
}
|
||||
|
||||
func setEndpoint(_ url: URL?) {
|
||||
self.endpointURL = url
|
||||
}
|
||||
|
||||
func getEndpoint() -> URL? {
|
||||
self.endpointURL
|
||||
}
|
||||
|
||||
func getBaseURL() -> URL? {
|
||||
self.baseURL
|
||||
}
|
||||
|
||||
func getNextId() -> Int {
|
||||
defer { nextId += 1 }
|
||||
return self.nextId
|
||||
}
|
||||
|
||||
func addPending(_ id: Int, _ cont: CheckedContinuation<Data, Swift.Error>) { self.pendingRequests[id] = cont }
|
||||
func removePending(_ id: Int) -> CheckedContinuation<Data, Swift.Error>? { self.pendingRequests
|
||||
.removeValue(forKey: id)
|
||||
func addPending(_ id: Int, _ cont: CheckedContinuation<Data, Swift.Error>) {
|
||||
self.pendingRequests[id] = cont
|
||||
}
|
||||
|
||||
func removePending(_ id: Int) -> CheckedContinuation<Data, Swift.Error>? {
|
||||
self.pendingRequests
|
||||
.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
func setTimeout(_ seconds: TimeInterval) {
|
||||
self.requestTimeoutNs = UInt64((seconds > 0 ? seconds : 30) * 1_000_000_000)
|
||||
}
|
||||
|
||||
func addTimeoutTask(_ id: Int, _ task: Task<Void, Never>) { self.timeoutTasks[id] = task }
|
||||
func cancelTimeout(_ id: Int) { self.timeoutTasks.removeValue(forKey: id)?.cancel() }
|
||||
func addTimeoutTask(_ id: Int, _ task: Task<Void, Never>) {
|
||||
self.timeoutTasks[id] = task
|
||||
}
|
||||
|
||||
func cancelTimeout(_ id: Int) {
|
||||
self.timeoutTasks.removeValue(forKey: id)?.cancel()
|
||||
}
|
||||
|
||||
func cancelAll(_ error: Swift.Error) {
|
||||
for (_, c) in self.pendingRequests {
|
||||
c.resume(throwing: error)
|
||||
@ -99,7 +130,7 @@ public final class SSETransport: MCPTransport {
|
||||
await self.state.setTransport(nil)
|
||||
}
|
||||
|
||||
// Expose underlying swift-sdk HTTP transport for advanced usage
|
||||
/// Expose underlying swift-sdk HTTP transport for advanced usage
|
||||
public func underlyingSDKTransport() async -> HTTPClientTransport? {
|
||||
await self.state.getTransport()
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import Logging
|
||||
import MCP
|
||||
import Tachikoma
|
||||
|
||||
// Actor to manage mutable state for Sendable conformance
|
||||
/// Actor to manage mutable state for Sendable conformance
|
||||
private actor StdioTransportState {
|
||||
var process: Process?
|
||||
var inputPipe: Pipe?
|
||||
|
||||
@ -19,7 +19,7 @@ public protocol MCPTool: Sendable {
|
||||
|
||||
/// Wrapper for tool arguments received from MCP
|
||||
public struct ToolArguments: Sendable {
|
||||
// Execute the tool with the given arguments
|
||||
/// Execute the tool with the given arguments
|
||||
private let raw: Value
|
||||
|
||||
public init(raw: [String: Any]) {
|
||||
@ -174,7 +174,7 @@ private func ValueToAny(_ value: Value) -> Any {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert Any to Value
|
||||
/// Helper function to convert Any to Value
|
||||
private func convertToValue(_ value: Any) -> Value {
|
||||
switch value {
|
||||
case let string as String:
|
||||
@ -250,5 +250,5 @@ public struct ToolResponse: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
// Type alias for convenience
|
||||
/// Type alias for convenience
|
||||
public typealias MCPContent = MCP.Tool.Content
|
||||
|
||||
@ -4,10 +4,9 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
@testable import TachikomaMCP
|
||||
|
||||
@Suite("MCP Client Tests")
|
||||
struct MCPClientTests {
|
||||
@Test("MCPServerConfig initialization with all parameters")
|
||||
func serverConfigFullInit() {
|
||||
@Test
|
||||
func `MCPServerConfig initialization with all parameters`() {
|
||||
let config = MCPServerConfig(
|
||||
transport: "stdio",
|
||||
command: "npx",
|
||||
@ -29,8 +28,8 @@ struct MCPClientTests {
|
||||
#expect(config.description == "Test server")
|
||||
}
|
||||
|
||||
@Test("MCPServerConfig initialization with minimal parameters")
|
||||
func serverConfigMinimalInit() {
|
||||
@Test
|
||||
func `MCPServerConfig initialization with minimal parameters`() {
|
||||
let config = MCPServerConfig(command: "test")
|
||||
|
||||
#expect(config.transport == "stdio")
|
||||
@ -43,8 +42,8 @@ struct MCPClientTests {
|
||||
#expect(config.description == nil)
|
||||
}
|
||||
|
||||
@Test("MCPClient initialization")
|
||||
func clientInit() {
|
||||
@Test
|
||||
func `MCPClient initialization`() {
|
||||
let config = MCPServerConfig(
|
||||
command: "test-command",
|
||||
args: ["arg1"],
|
||||
@ -54,8 +53,8 @@ struct MCPClientTests {
|
||||
_ = MCPClient(name: "test-client", config: config)
|
||||
}
|
||||
|
||||
@Test("MCPError descriptions")
|
||||
func errorDescriptions() {
|
||||
@Test
|
||||
func `MCPError descriptions`() {
|
||||
#expect(MCPError.serverDisabled.errorDescription == "MCP server is disabled")
|
||||
#expect(MCPError.notConnected.errorDescription == "MCP client is not connected")
|
||||
#expect(MCPError.invalidResponse.errorDescription == "Invalid response from MCP server")
|
||||
@ -64,8 +63,8 @@ struct MCPClientTests {
|
||||
#expect(MCPError.executionFailed("error").errorDescription == "Execution failed: error")
|
||||
}
|
||||
|
||||
@Test("ToolArguments convenience methods")
|
||||
func toolArgumentsConvenienceMethods() {
|
||||
@Test
|
||||
func `ToolArguments convenience methods`() {
|
||||
let args = ToolArguments(raw: [
|
||||
"text": "hello",
|
||||
"number": 42,
|
||||
@ -106,8 +105,8 @@ struct MCPClientTests {
|
||||
#expect(emptyArgs.isEmpty == true)
|
||||
}
|
||||
|
||||
@Test("ToolArguments from Value")
|
||||
func toolArgumentsFromValue() {
|
||||
@Test
|
||||
func `ToolArguments from Value`() {
|
||||
let value = Value.object([
|
||||
"name": .string("test"),
|
||||
"count": .int(42),
|
||||
@ -121,8 +120,8 @@ struct MCPClientTests {
|
||||
#expect(args.getBool("active") == true)
|
||||
}
|
||||
|
||||
@Test("ToolArguments raw dictionary preserves nested structures")
|
||||
func toolArgumentsRawDictionary() {
|
||||
@Test
|
||||
func `ToolArguments raw dictionary preserves nested structures`() {
|
||||
let value = Value.object([
|
||||
"text": .string("hello"),
|
||||
"number": .int(5),
|
||||
@ -167,8 +166,8 @@ struct MCPClientTests {
|
||||
#expect(dictionary["none"] is NSNull)
|
||||
}
|
||||
|
||||
@Test("ToolResponse creation methods")
|
||||
func toolResponseCreation() {
|
||||
@Test
|
||||
func `ToolResponse creation methods`() {
|
||||
// Text response
|
||||
let textResponse = ToolResponse.text("Success")
|
||||
#expect(textResponse.content.count == 1)
|
||||
@ -207,8 +206,8 @@ struct MCPClientTests {
|
||||
#expect(multiResponse.content.count == 2)
|
||||
}
|
||||
|
||||
@Test("ToolResponse with metadata")
|
||||
func toolResponseWithMetadata() {
|
||||
@Test
|
||||
func `ToolResponse with metadata`() {
|
||||
let meta = Value.object([
|
||||
"executionTime": .double(1.5),
|
||||
"status": .string("ok"),
|
||||
@ -225,15 +224,15 @@ struct MCPClientTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("MCPToolProvider initialization")
|
||||
func toolProviderInit() {
|
||||
@Test
|
||||
func `MCPToolProvider initialization`() {
|
||||
let config = MCPServerConfig(command: "test")
|
||||
let client = MCPClient(name: "test", config: config)
|
||||
_ = MCPToolProvider(client: client)
|
||||
}
|
||||
|
||||
@Test("MCPToolProvider as DynamicToolProvider")
|
||||
func toolProviderAsDynamicToolProvider() {
|
||||
@Test
|
||||
func `MCPToolProvider as DynamicToolProvider`() {
|
||||
let config = MCPServerConfig(command: "test")
|
||||
let client = MCPClient(name: "test-server", config: config)
|
||||
let provider = MCPToolProvider(client: client)
|
||||
@ -242,8 +241,8 @@ struct MCPClientTests {
|
||||
_ = provider as DynamicToolProvider
|
||||
}
|
||||
|
||||
@Test("Tool metadata structure")
|
||||
func toolMetadata() {
|
||||
@Test
|
||||
func `Tool metadata structure`() {
|
||||
let tool = MCP.Tool(
|
||||
name: "test-tool",
|
||||
description: "A test tool",
|
||||
@ -269,8 +268,8 @@ struct MCPClientTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Value encoding and decoding")
|
||||
func valueEncodingDecoding() throws {
|
||||
@Test
|
||||
func `Value encoding and decoding`() throws {
|
||||
let originalValue = Value.object([
|
||||
"string": .string("test"),
|
||||
"number": .int(42),
|
||||
@ -292,8 +291,8 @@ struct MCPClientTests {
|
||||
#expect(originalValue == decodedValue)
|
||||
}
|
||||
|
||||
@Test("ToolArguments decoding")
|
||||
func toolArgumentsDecoding() throws {
|
||||
@Test
|
||||
func `ToolArguments decoding`() throws {
|
||||
struct TestArgs: Decodable {
|
||||
let name: String
|
||||
let count: Int
|
||||
@ -341,10 +340,9 @@ private struct MockMCPTool: MCPTool {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Mock Tool Tests")
|
||||
struct MockToolTests {
|
||||
@Test("Mock tool execution with valid arguments")
|
||||
func mockToolValidExecution() async throws {
|
||||
@Test
|
||||
func `Mock tool execution with valid arguments`() async throws {
|
||||
let tool = MockMCPTool()
|
||||
|
||||
let args = ToolArguments(raw: ["message": "Hello World"])
|
||||
@ -358,8 +356,8 @@ struct MockToolTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Mock tool execution with missing arguments")
|
||||
func mockToolMissingArguments() async throws {
|
||||
@Test
|
||||
func `Mock tool execution with missing arguments`() async throws {
|
||||
let tool = MockMCPTool()
|
||||
|
||||
let args = ToolArguments(raw: [:])
|
||||
@ -373,8 +371,8 @@ struct MockToolTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Mock tool schema validation")
|
||||
func mockToolSchema() {
|
||||
@Test
|
||||
func `Mock tool schema validation`() {
|
||||
let tool = MockMCPTool()
|
||||
|
||||
#expect(tool.name == "mock_tool")
|
||||
|
||||
@ -3,10 +3,9 @@ import Tachikoma
|
||||
import Testing
|
||||
@testable import TachikomaMCP
|
||||
|
||||
@Suite("MCP Tool Adapter Tests")
|
||||
struct MCPToolAdapterTests {
|
||||
@Test("ToolArguments getString")
|
||||
func toolArgumentsGetString() {
|
||||
@Test
|
||||
func `ToolArguments getString`() {
|
||||
let args = ToolArguments(raw: [
|
||||
"name": "Alice",
|
||||
"count": 42,
|
||||
@ -21,8 +20,8 @@ struct MCPToolAdapterTests {
|
||||
#expect(args.getString("missing") == nil)
|
||||
}
|
||||
|
||||
@Test("ToolArguments getNumber")
|
||||
func toolArgumentsGetNumber() {
|
||||
@Test
|
||||
func `ToolArguments getNumber`() {
|
||||
let args = ToolArguments(raw: [
|
||||
"int": 42,
|
||||
"double": 3.14,
|
||||
@ -37,8 +36,8 @@ struct MCPToolAdapterTests {
|
||||
#expect(args.getNumber("missing") == nil)
|
||||
}
|
||||
|
||||
@Test("ToolArguments getInt")
|
||||
func toolArgumentsGetInt() {
|
||||
@Test
|
||||
func `ToolArguments getInt`() {
|
||||
let args = ToolArguments(raw: [
|
||||
"int": 42,
|
||||
"double": 3.14,
|
||||
@ -53,8 +52,8 @@ struct MCPToolAdapterTests {
|
||||
#expect(args.getInt("missing") == nil)
|
||||
}
|
||||
|
||||
@Test("ToolArguments getBool")
|
||||
func toolArgumentsGetBool() {
|
||||
@Test
|
||||
func `ToolArguments getBool`() {
|
||||
let args = ToolArguments(raw: [
|
||||
"bool": true,
|
||||
"stringTrue": "true",
|
||||
@ -75,8 +74,8 @@ struct MCPToolAdapterTests {
|
||||
#expect(args.getBool("missing") == nil)
|
||||
}
|
||||
|
||||
@Test("ToolArguments getStringArray")
|
||||
func toolArgumentsGetStringArray() {
|
||||
@Test
|
||||
func `ToolArguments getStringArray`() {
|
||||
let args = ToolArguments(raw: [
|
||||
"array": ["a", "b", "c"],
|
||||
"mixed": ["string", 123, true],
|
||||
@ -89,8 +88,8 @@ struct MCPToolAdapterTests {
|
||||
#expect(args.getStringArray("missing") == nil)
|
||||
}
|
||||
|
||||
@Test("ToolArguments isEmpty")
|
||||
func toolArgumentsIsEmpty() {
|
||||
@Test
|
||||
func `ToolArguments isEmpty`() {
|
||||
let emptyArgs = ToolArguments(raw: [:])
|
||||
#expect(emptyArgs.isEmpty == true)
|
||||
|
||||
@ -98,8 +97,8 @@ struct MCPToolAdapterTests {
|
||||
#expect(nonEmptyArgs.isEmpty == false)
|
||||
}
|
||||
|
||||
@Test("ToolResponse text creation")
|
||||
func toolResponseText() {
|
||||
@Test
|
||||
func `ToolResponse text creation`() {
|
||||
let response = ToolResponse.text("Hello, world!")
|
||||
|
||||
#expect(response.isError == false)
|
||||
@ -111,8 +110,8 @@ struct MCPToolAdapterTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("ToolResponse error creation")
|
||||
func toolResponseError() {
|
||||
@Test
|
||||
func `ToolResponse error creation`() {
|
||||
let response = ToolResponse.error("Something went wrong")
|
||||
|
||||
#expect(response.isError == true)
|
||||
@ -124,8 +123,8 @@ struct MCPToolAdapterTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("ToolResponse image creation")
|
||||
func toolResponseImage() {
|
||||
@Test
|
||||
func `ToolResponse image creation`() {
|
||||
let imageData = Data([0xFF, 0xD8, 0xFF]) // JPEG header
|
||||
let response = ToolResponse.image(data: imageData, mimeType: "image/jpeg")
|
||||
|
||||
|
||||
@ -2,10 +2,9 @@ import MCP
|
||||
import Testing
|
||||
@testable import TachikomaMCP
|
||||
|
||||
@Suite("Schema Builder Tests")
|
||||
struct SchemaBuilderTests {
|
||||
@Test("Build string schema")
|
||||
func stringSchema() {
|
||||
@Test
|
||||
func `Build string schema`() {
|
||||
let schema = SchemaBuilder.string(
|
||||
description: "User name",
|
||||
enum: ["Alice", "Bob"],
|
||||
@ -32,8 +31,8 @@ struct SchemaBuilderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Build boolean schema")
|
||||
func booleanSchema() {
|
||||
@Test
|
||||
func `Build boolean schema`() {
|
||||
let schema = SchemaBuilder.boolean(
|
||||
description: "Is active",
|
||||
default: true,
|
||||
@ -49,8 +48,8 @@ struct SchemaBuilderTests {
|
||||
#expect(dict["default"] == .bool(true))
|
||||
}
|
||||
|
||||
@Test("Build number schema")
|
||||
func numberSchema() {
|
||||
@Test
|
||||
func `Build number schema`() {
|
||||
let schema = SchemaBuilder.number(
|
||||
description: "Temperature",
|
||||
minimum: 0.0,
|
||||
@ -70,8 +69,8 @@ struct SchemaBuilderTests {
|
||||
#expect(dict["default"] == .double(25.0))
|
||||
}
|
||||
|
||||
@Test("Build integer schema")
|
||||
func integerSchema() {
|
||||
@Test
|
||||
func `Build integer schema`() {
|
||||
let schema = SchemaBuilder.integer(
|
||||
description: "Count",
|
||||
minimum: 1,
|
||||
@ -91,8 +90,8 @@ struct SchemaBuilderTests {
|
||||
#expect(dict["default"] == .int(10))
|
||||
}
|
||||
|
||||
@Test("Build array schema")
|
||||
func arraySchema() {
|
||||
@Test
|
||||
func `Build array schema`() {
|
||||
let itemSchema = SchemaBuilder.string()
|
||||
let schema = SchemaBuilder.array(
|
||||
items: itemSchema,
|
||||
@ -115,8 +114,8 @@ struct SchemaBuilderTests {
|
||||
#expect(dict["uniqueItems"] == .bool(true))
|
||||
}
|
||||
|
||||
@Test("Build object schema")
|
||||
func objectSchema() {
|
||||
@Test
|
||||
func `Build object schema`() {
|
||||
let schema = SchemaBuilder.object(
|
||||
properties: [
|
||||
"name": SchemaBuilder.string(description: "User name"),
|
||||
@ -151,8 +150,8 @@ struct SchemaBuilderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Build nullable schema")
|
||||
func testNullableSchema() {
|
||||
@Test
|
||||
func `Build nullable schema`() {
|
||||
let baseSchema = SchemaBuilder.string(description: "Optional value")
|
||||
let nullableSchema = SchemaBuilder.nullable(baseSchema)
|
||||
|
||||
@ -168,8 +167,8 @@ struct SchemaBuilderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Build oneOf schema")
|
||||
func oneOfSchema() {
|
||||
@Test
|
||||
func `Build oneOf schema`() {
|
||||
let schemas = [
|
||||
SchemaBuilder.string(),
|
||||
SchemaBuilder.integer(),
|
||||
@ -191,8 +190,8 @@ struct SchemaBuilderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Build anyOf schema")
|
||||
func anyOfSchema() {
|
||||
@Test
|
||||
func `Build anyOf schema`() {
|
||||
let schemas = [
|
||||
SchemaBuilder.string(),
|
||||
SchemaBuilder.integer(),
|
||||
|
||||
@ -4,10 +4,9 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
@testable import TachikomaMCP
|
||||
|
||||
@Suite("Type Conversion Tests")
|
||||
struct TypeConversionTests {
|
||||
@Test("AnyAgentToolValue to JSON conversion")
|
||||
func anyAgentToolValueToJSON() throws {
|
||||
@Test
|
||||
func `AnyAgentToolValue to JSON conversion`() throws {
|
||||
// String
|
||||
let stringVal = AnyAgentToolValue(string: "hello")
|
||||
let stringJSON = try stringVal.toJSON()
|
||||
@ -50,8 +49,8 @@ struct TypeConversionTests {
|
||||
#expect(objectJSON?["count"] as? Int == 5)
|
||||
}
|
||||
|
||||
@Test("Any to AnyAgentToolValue conversion")
|
||||
func anyToAnyAgentToolValue() throws {
|
||||
@Test
|
||||
func `Any to AnyAgentToolValue conversion`() {
|
||||
// String
|
||||
let stringVal = AnyAgentToolValue.from("hello")
|
||||
#expect(stringVal.stringValue == "hello")
|
||||
@ -99,8 +98,8 @@ struct TypeConversionTests {
|
||||
#expect(dateVal.stringValue != nil)
|
||||
}
|
||||
|
||||
@Test("AnyAgentToolValue to Value conversion")
|
||||
func anyAgentToolValueToValue() {
|
||||
@Test
|
||||
func `AnyAgentToolValue to Value conversion`() {
|
||||
// String
|
||||
let stringVal = AnyAgentToolValue(string: "hello")
|
||||
let stringValue = stringVal.toValue()
|
||||
@ -151,8 +150,8 @@ struct TypeConversionTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Value to AnyAgentToolValue conversion")
|
||||
func valueToAnyAgentToolValue() {
|
||||
@Test
|
||||
func `Value to AnyAgentToolValue conversion`() {
|
||||
// String
|
||||
let stringValue = Value.string("hello")
|
||||
let stringVal = stringValue.toAnyAgentToolValue()
|
||||
@ -203,8 +202,8 @@ struct TypeConversionTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("ToolArguments initialization from AgentToolArguments")
|
||||
func toolArgumentsFromAgentToolArguments() throws {
|
||||
@Test
|
||||
func `ToolArguments initialization from AgentToolArguments`() {
|
||||
let agentArgs = AgentToolArguments([
|
||||
"text": AnyAgentToolValue(string: "hello"),
|
||||
"number": AnyAgentToolValue(int: 42),
|
||||
@ -229,8 +228,8 @@ struct TypeConversionTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("ToolResponse to AnyAgentToolValue conversion via toAgentToolResult")
|
||||
func toolResponseToAgentToolResult() {
|
||||
@Test
|
||||
func `ToolResponse to AnyAgentToolValue conversion via toAgentToolResult`() {
|
||||
// Text response
|
||||
let textResponse = ToolResponse.text("Success message")
|
||||
let textResult = textResponse.toAgentToolResult()
|
||||
@ -257,8 +256,8 @@ struct TypeConversionTests {
|
||||
#expect(emptyResult.stringValue == "Success")
|
||||
}
|
||||
|
||||
@Test("ToolResponse to AnyAgentToolValue conversion")
|
||||
func toolResponseToAnyAgentToolValue() {
|
||||
@Test
|
||||
func `ToolResponse to AnyAgentToolValue conversion`() {
|
||||
// Single text content
|
||||
let textResponse = ToolResponse.text("Hello")
|
||||
let textVal = textResponse.toAnyAgentToolValue()
|
||||
@ -298,7 +297,7 @@ struct TypeConversionTests {
|
||||
"content",
|
||||
uri: "https://example.com",
|
||||
mimeType: "text/html",
|
||||
)
|
||||
),
|
||||
),
|
||||
])
|
||||
let resourceVal = resourceResponse.toAnyAgentToolValue()
|
||||
@ -334,8 +333,8 @@ struct TypeConversionTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Round-trip conversions")
|
||||
func roundTripConversions() throws {
|
||||
@Test
|
||||
func `Round-trip conversions`() throws {
|
||||
// AnyAgentToolValue -> Value -> AnyAgentToolValue
|
||||
let originalVal = AnyAgentToolValue(object: [
|
||||
"string": AnyAgentToolValue(string: "test"),
|
||||
|
||||
@ -2,12 +2,11 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("AgentToolValue Protocol System Tests")
|
||||
struct AgentToolValueTests {
|
||||
// MARK: - Basic Type Conformance Tests
|
||||
|
||||
@Test("String conforms to AgentToolValue")
|
||||
func stringConformance() throws {
|
||||
@Test
|
||||
func `String conforms to AgentToolValue`() throws {
|
||||
let value = "Hello, World!"
|
||||
let json = try value.toJSON()
|
||||
#expect(json as? String == "Hello, World!")
|
||||
@ -17,8 +16,8 @@ struct AgentToolValueTests {
|
||||
#expect(String.agentValueType == .string)
|
||||
}
|
||||
|
||||
@Test("Int conforms to AgentToolValue")
|
||||
func intConformance() throws {
|
||||
@Test
|
||||
func `Int conforms to AgentToolValue`() throws {
|
||||
let value = 42
|
||||
let json = try value.toJSON()
|
||||
#expect(json as? Int == 42)
|
||||
@ -32,8 +31,8 @@ struct AgentToolValueTests {
|
||||
#expect(fromDouble == 42)
|
||||
}
|
||||
|
||||
@Test("Double conforms to AgentToolValue")
|
||||
func doubleConformance() throws {
|
||||
@Test
|
||||
func `Double conforms to AgentToolValue`() throws {
|
||||
let value = 3.141_59
|
||||
let json = try value.toJSON()
|
||||
#expect(json as? Double == 3.141_59)
|
||||
@ -47,8 +46,8 @@ struct AgentToolValueTests {
|
||||
#expect(fromInt == 42.0)
|
||||
}
|
||||
|
||||
@Test("Bool conforms to AgentToolValue")
|
||||
func boolConformance() throws {
|
||||
@Test
|
||||
func `Bool conforms to AgentToolValue`() throws {
|
||||
let value = true
|
||||
let json = try value.toJSON()
|
||||
#expect(json as? Bool == true)
|
||||
@ -58,8 +57,8 @@ struct AgentToolValueTests {
|
||||
#expect(Bool.agentValueType == .boolean)
|
||||
}
|
||||
|
||||
@Test("AgentNullValue works correctly")
|
||||
func testNullValue() throws {
|
||||
@Test
|
||||
func `AgentNullValue works correctly`() throws {
|
||||
let value = AgentNullValue()
|
||||
let json = try value.toJSON()
|
||||
#expect(json is NSNull)
|
||||
@ -69,8 +68,8 @@ struct AgentToolValueTests {
|
||||
#expect(AgentNullValue.agentValueType == .null)
|
||||
}
|
||||
|
||||
@Test("Array conforms to AgentToolValue")
|
||||
func arrayConformance() throws {
|
||||
@Test
|
||||
func `Array conforms to AgentToolValue`() throws {
|
||||
let value = ["apple", "banana", "cherry"]
|
||||
let json = try value.toJSON()
|
||||
let jsonArray = json as? [Any]
|
||||
@ -82,8 +81,8 @@ struct AgentToolValueTests {
|
||||
#expect([String].agentValueType == .array)
|
||||
}
|
||||
|
||||
@Test("Dictionary conforms to AgentToolValue")
|
||||
func dictionaryConformance() throws {
|
||||
@Test
|
||||
func `Dictionary conforms to AgentToolValue`() throws {
|
||||
let value = ["name": "John", "city": "NYC"]
|
||||
let json = try value.toJSON()
|
||||
let jsonDict = json as? [String: Any]
|
||||
@ -97,8 +96,8 @@ struct AgentToolValueTests {
|
||||
|
||||
// MARK: - AnyAgentToolValue Tests
|
||||
|
||||
@Test("AnyAgentToolValue wraps basic types")
|
||||
func anyAgentToolValueBasicTypes() throws {
|
||||
@Test
|
||||
func `AnyAgentToolValue wraps basic types`() {
|
||||
// String
|
||||
let stringValue = AnyAgentToolValue(string: "test")
|
||||
#expect(stringValue.stringValue == "test")
|
||||
@ -126,8 +125,8 @@ struct AgentToolValueTests {
|
||||
#expect(nullValue.stringValue == nil)
|
||||
}
|
||||
|
||||
@Test("AnyAgentToolValue wraps complex types")
|
||||
func anyAgentToolValueComplexTypes() throws {
|
||||
@Test
|
||||
func `AnyAgentToolValue wraps complex types`() {
|
||||
// Array
|
||||
let array = [
|
||||
AnyAgentToolValue(string: "a"),
|
||||
@ -152,8 +151,8 @@ struct AgentToolValueTests {
|
||||
#expect(objectValue.objectValue?["active"]?.boolValue == true)
|
||||
}
|
||||
|
||||
@Test("AnyAgentToolValue JSON conversion")
|
||||
func anyAgentToolValueJSON() throws {
|
||||
@Test
|
||||
func `AnyAgentToolValue JSON conversion`() throws {
|
||||
// Test fromJSON with various types
|
||||
let stringValue = try AnyAgentToolValue.fromJSON("hello")
|
||||
#expect(stringValue.stringValue == "hello")
|
||||
@ -180,8 +179,8 @@ struct AgentToolValueTests {
|
||||
#expect(dictValue.objectValue?["num"]?.intValue == 123)
|
||||
}
|
||||
|
||||
@Test("AnyAgentToolValue Codable conformance")
|
||||
func anyAgentToolValueCodable() throws {
|
||||
@Test
|
||||
func `AnyAgentToolValue Codable conformance`() throws {
|
||||
struct TestContainer: Codable {
|
||||
let value: AnyAgentToolValue
|
||||
}
|
||||
@ -223,8 +222,8 @@ struct AgentToolValueTests {
|
||||
|
||||
// MARK: - AgentToolCall and AgentToolResult Tests
|
||||
|
||||
@Test("AgentToolCall uses AnyAgentToolValue")
|
||||
func agentToolCall() throws {
|
||||
@Test
|
||||
func `AgentToolCall uses AnyAgentToolValue`() {
|
||||
let arguments = [
|
||||
"prompt": AnyAgentToolValue(string: "Hello"),
|
||||
"temperature": AnyAgentToolValue(double: 0.7),
|
||||
@ -244,8 +243,8 @@ struct AgentToolValueTests {
|
||||
#expect(toolCall.arguments["maxTokens"]?.intValue == 100)
|
||||
}
|
||||
|
||||
@Test("AgentToolCall legacy init with Any")
|
||||
func agentToolCallLegacyInit() throws {
|
||||
@Test
|
||||
func `AgentToolCall legacy init with Any`() throws {
|
||||
let arguments: [String: Any] = [
|
||||
"text": "Hello",
|
||||
"count": 42,
|
||||
@ -263,8 +262,8 @@ struct AgentToolValueTests {
|
||||
#expect(toolCall.arguments["enabled"]?.boolValue == true)
|
||||
}
|
||||
|
||||
@Test("AgentToolResult uses AnyAgentToolValue")
|
||||
func agentToolResult() {
|
||||
@Test
|
||||
func `AgentToolResult uses AnyAgentToolValue`() {
|
||||
let successResult = AgentToolResult.success(
|
||||
toolCallId: "call_123",
|
||||
result: AnyAgentToolValue(string: "Success!"),
|
||||
@ -286,8 +285,8 @@ struct AgentToolValueTests {
|
||||
|
||||
// MARK: - AgentToolArguments Tests
|
||||
|
||||
@Test("AgentToolArguments accessor methods")
|
||||
func agentToolArgumentsAccessors() throws {
|
||||
@Test
|
||||
func `AgentToolArguments accessor methods`() throws {
|
||||
let args = AgentToolArguments([
|
||||
"string": AnyAgentToolValue(string: "text"),
|
||||
"number": AnyAgentToolValue(double: 42.5),
|
||||
@ -323,8 +322,8 @@ struct AgentToolValueTests {
|
||||
#expect(args.optionalBooleanValue("missing") == nil)
|
||||
}
|
||||
|
||||
@Test("AgentToolArguments error handling")
|
||||
func agentToolArgumentsErrors() throws {
|
||||
@Test
|
||||
func `AgentToolArguments error handling`() throws {
|
||||
let args = AgentToolArguments([
|
||||
"text": AnyAgentToolValue(string: "hello"),
|
||||
])
|
||||
@ -342,15 +341,17 @@ struct AgentToolValueTests {
|
||||
|
||||
// MARK: - Type-Safe Tool Protocol Tests
|
||||
|
||||
@Test("AgentToolProtocol implementation")
|
||||
func agentToolProtocol() async throws {
|
||||
@Test
|
||||
func `AgentToolProtocol implementation`() async throws {
|
||||
// Define a concrete tool
|
||||
struct WeatherTool: AgentToolProtocol {
|
||||
struct Input: AgentToolValue, Equatable {
|
||||
let location: String
|
||||
let units: String
|
||||
|
||||
static var agentValueType: AgentValueType { .object }
|
||||
static var agentValueType: AgentValueType {
|
||||
.object
|
||||
}
|
||||
|
||||
func toJSON() throws -> Any {
|
||||
["location": self.location, "units": self.units]
|
||||
@ -372,7 +373,9 @@ struct AgentToolValueTests {
|
||||
let temperature: Double
|
||||
let conditions: String
|
||||
|
||||
static var agentValueType: AgentValueType { .object }
|
||||
static var agentValueType: AgentValueType {
|
||||
.object
|
||||
}
|
||||
|
||||
func toJSON() throws -> Any {
|
||||
["temperature": self.temperature, "conditions": self.conditions]
|
||||
@ -390,8 +393,14 @@ struct AgentToolValueTests {
|
||||
}
|
||||
}
|
||||
|
||||
var name: String { "get_weather" }
|
||||
var description: String { "Get current weather" }
|
||||
var name: String {
|
||||
"get_weather"
|
||||
}
|
||||
|
||||
var description: String {
|
||||
"Get current weather"
|
||||
}
|
||||
|
||||
var schema: AgentToolSchema {
|
||||
AgentToolSchema(
|
||||
properties: [
|
||||
@ -434,8 +443,8 @@ struct AgentToolValueTests {
|
||||
|
||||
// MARK: - Edge Cases and Error Handling
|
||||
|
||||
@Test("Handle integer/double ambiguity")
|
||||
func integerDoubleAmbiguity() throws {
|
||||
@Test
|
||||
func `Handle integer/double ambiguity`() throws {
|
||||
// Test that whole numbers can be treated as integers
|
||||
let wholeDouble = try AnyAgentToolValue.fromJSON(42.0)
|
||||
#expect(wholeDouble.intValue == 42)
|
||||
@ -452,8 +461,8 @@ struct AgentToolValueTests {
|
||||
#expect(largeInt.intValue == safeInteger)
|
||||
}
|
||||
|
||||
@Test("Handle nested structures")
|
||||
func nestedStructures() throws {
|
||||
@Test
|
||||
func `Handle nested structures`() throws {
|
||||
let nested = [
|
||||
"level1": [
|
||||
"level2": [
|
||||
@ -469,8 +478,8 @@ struct AgentToolValueTests {
|
||||
#expect(level3?["value"]?.stringValue == "deep")
|
||||
}
|
||||
|
||||
@Test("Handle mixed-type arrays")
|
||||
func mixedTypeArrays() throws {
|
||||
@Test
|
||||
func `Handle mixed-type arrays`() throws {
|
||||
let mixed: [Any] = ["string", 42, true, NSNull(), ["nested": "object"]]
|
||||
let value = try AnyAgentToolValue.fromJSON(mixed)
|
||||
|
||||
|
||||
@ -2,16 +2,15 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Async Ergonomics Tests")
|
||||
struct AsyncErgonomicsTests {
|
||||
@Test("Timeout error description")
|
||||
func timeoutErrorDescription() throws {
|
||||
@Test
|
||||
func `Timeout error description`() {
|
||||
let error = TimeoutError(timeout: 5.5)
|
||||
#expect(error.errorDescription == "Operation timed out after 5.5 seconds")
|
||||
}
|
||||
|
||||
@Test("Cancellation token basic operations")
|
||||
func cancellationTokenBasic() async throws {
|
||||
@Test
|
||||
func `Cancellation token basic operations`() async {
|
||||
let token = CancellationToken()
|
||||
|
||||
#expect(await token.cancelled == false)
|
||||
@ -25,8 +24,8 @@ struct AsyncErgonomicsTests {
|
||||
#expect(await token.cancelled == true)
|
||||
}
|
||||
|
||||
@Test("Cancellation token with handlers")
|
||||
func cancellationTokenHandlers() async throws {
|
||||
@Test
|
||||
func `Cancellation token with handlers`() async throws {
|
||||
let token = CancellationToken()
|
||||
|
||||
class Flag: @unchecked Sendable {
|
||||
@ -49,8 +48,8 @@ struct AsyncErgonomicsTests {
|
||||
#expect(flag.value == true)
|
||||
}
|
||||
|
||||
@Test("Retry configuration defaults")
|
||||
func retryConfigurationDefaults() throws {
|
||||
@Test
|
||||
func `Retry configuration defaults`() {
|
||||
let config = RetryConfiguration.default
|
||||
|
||||
#expect(config.maxAttempts == 3)
|
||||
@ -60,8 +59,8 @@ struct AsyncErgonomicsTests {
|
||||
#expect(config.timeout == nil)
|
||||
}
|
||||
|
||||
@Test("Retry configuration presets")
|
||||
func retryConfigurationPresets() throws {
|
||||
@Test
|
||||
func `Retry configuration presets`() {
|
||||
let aggressive = RetryConfiguration.aggressive
|
||||
#expect(aggressive.maxAttempts == 5)
|
||||
#expect(aggressive.delay == 0.5)
|
||||
@ -71,8 +70,8 @@ struct AsyncErgonomicsTests {
|
||||
#expect(conservative.delay == 2.0)
|
||||
}
|
||||
|
||||
@Test("Retry with cancellation - immediate success")
|
||||
func retryWithCancellationImmediateSuccess() async throws {
|
||||
@Test
|
||||
func `Retry with cancellation - immediate success`() async throws {
|
||||
let result = try await retryWithCancellation(
|
||||
configuration: .init(maxAttempts: 3, delay: 0.01),
|
||||
) {
|
||||
@ -82,8 +81,8 @@ struct AsyncErgonomicsTests {
|
||||
#expect(result == "Success")
|
||||
}
|
||||
|
||||
@Test("With timeout basic functionality")
|
||||
func withTimeoutBasic() async throws {
|
||||
@Test
|
||||
func `With timeout basic functionality`() async throws {
|
||||
let result = try await withTimeout(0.1) {
|
||||
"Quick result"
|
||||
}
|
||||
@ -91,8 +90,8 @@ struct AsyncErgonomicsTests {
|
||||
#expect(result == "Quick result")
|
||||
}
|
||||
|
||||
@Test("With timeout throws on timeout")
|
||||
func withTimeoutThrows() async throws {
|
||||
@Test
|
||||
func `With timeout throws on timeout`() async throws {
|
||||
do {
|
||||
_ = try await withTimeout(0.01) {
|
||||
try await Task<Never, Never>.sleep(nanoseconds: 1_000_000_000) // 1 second
|
||||
@ -104,8 +103,8 @@ struct AsyncErgonomicsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Async stream collect basic")
|
||||
func asyncStreamCollectBasic() async throws {
|
||||
@Test
|
||||
func `Async stream collect basic`() async throws {
|
||||
let stream = AsyncThrowingStream<Int, Error> { continuation in
|
||||
continuation.yield(1)
|
||||
continuation.yield(2)
|
||||
@ -117,8 +116,8 @@ struct AsyncErgonomicsTests {
|
||||
#expect(results == [1, 2, 3])
|
||||
}
|
||||
|
||||
@Test("Task group with auto cancellation")
|
||||
func taskGroupAutoCancellation() async throws {
|
||||
@Test
|
||||
func `Task group with auto cancellation`() async throws {
|
||||
class Flag: @unchecked Sendable {
|
||||
var cancelled = false
|
||||
}
|
||||
|
||||
@ -3,14 +3,12 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
@testable import TachikomaAudio
|
||||
|
||||
@Suite("Audio Functions Tests")
|
||||
struct AudioFunctionsTests {
|
||||
enum AudioFunctionsTests {
|
||||
// MARK: - Basic Transcription Function Tests
|
||||
|
||||
@Suite("Basic Transcription Functions Tests")
|
||||
struct BasicTranscriptionFunctionsTests {
|
||||
@Test("transcribe() convenience function works")
|
||||
func transcribeConvenienceFunctionWorks() async throws {
|
||||
@Test
|
||||
func `transcribe() convenience function works`() async throws {
|
||||
try await TestHelpers.withMockProviderEnvironment {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let audioData = TestHelpers.sampleAudioData(configuration: config)
|
||||
@ -20,8 +18,8 @@ struct AudioFunctionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("transcribe() with full model specification works")
|
||||
func transcribeWithFullModelSpecificationWorks() async throws {
|
||||
@Test
|
||||
func `transcribe() with full model specification works`() async throws {
|
||||
try await TestHelpers.withMockProviderEnvironment {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let audioData = TestHelpers.sampleAudioData(configuration: config)
|
||||
@ -43,8 +41,8 @@ struct AudioFunctionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("transcribe() from file URL works")
|
||||
func transcribeFromFileURLWorks() async throws {
|
||||
@Test
|
||||
func `transcribe() from file URL works`() async throws {
|
||||
try await TestHelpers.withMockProviderEnvironment {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let audioData = TestHelpers.sampleAudioData(configuration: config)
|
||||
@ -68,8 +66,8 @@ struct AudioFunctionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("transcribe() with timestamps")
|
||||
func transcribeWithTimestamps() async throws {
|
||||
@Test
|
||||
func `transcribe() with timestamps`() async {
|
||||
await TestHelpers.withMockProviderEnvironment {
|
||||
await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let audioData = TestHelpers.sampleAudioData(configuration: config)
|
||||
@ -95,8 +93,8 @@ struct AudioFunctionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("transcribe() with abort signal")
|
||||
func transcribeWithAbortSignal() async throws {
|
||||
@Test
|
||||
func `transcribe() with abort signal`() async {
|
||||
await TestHelpers.withMockProviderEnvironment {
|
||||
await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let audioData = TestHelpers.sampleAudioData(configuration: config)
|
||||
@ -120,10 +118,9 @@ struct AudioFunctionsTests {
|
||||
|
||||
// MARK: - Basic Speech Generation Function Tests
|
||||
|
||||
@Suite("Basic Speech Generation Functions Tests")
|
||||
struct BasicSpeechGenerationFunctionsTests {
|
||||
@Test("generateSpeech() convenience function works")
|
||||
func generateSpeechConvenienceFunctionWorks() async throws {
|
||||
@Test
|
||||
func `generateSpeech() convenience function works`() async throws {
|
||||
try await TestHelpers.withMockProviderEnvironment {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let audioData = try await generateSpeech("Hello, world!", configuration: config)
|
||||
@ -133,8 +130,8 @@ struct AudioFunctionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("generateSpeech() with voice selection")
|
||||
func generateSpeechWithVoiceSelection() async throws {
|
||||
@Test
|
||||
func `generateSpeech() with voice selection`() async throws {
|
||||
try await TestHelpers.withMockProviderEnvironment {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let audioData = try await generateSpeech("Hello, world!", voice: .nova, configuration: config)
|
||||
@ -144,8 +141,8 @@ struct AudioFunctionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("generateSpeech() with full model specification works")
|
||||
func generateSpeechWithFullModelSpecificationWorks() async throws {
|
||||
@Test
|
||||
func `generateSpeech() with full model specification works`() async throws {
|
||||
try await TestHelpers.withMockProviderEnvironment {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let result = try await generateSpeech(
|
||||
@ -162,8 +159,8 @@ struct AudioFunctionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("generateSpeech() direct to file using convenience function")
|
||||
func generateSpeechDirectToFile() async throws {
|
||||
@Test
|
||||
func `generateSpeech() direct to file using convenience function`() async throws {
|
||||
try await TestHelpers.withMockProviderEnvironment {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
@ -187,8 +184,8 @@ struct AudioFunctionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("generateSpeech() with abort signal")
|
||||
func generateSpeechWithAbortSignal() async throws {
|
||||
@Test
|
||||
func `generateSpeech() with abort signal`() async {
|
||||
await TestHelpers.withMockProviderEnvironment {
|
||||
await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let abortSignal = AbortSignal()
|
||||
@ -211,10 +208,9 @@ struct AudioFunctionsTests {
|
||||
|
||||
// MARK: - Batch Operations Tests
|
||||
|
||||
@Suite("Batch Operations Tests")
|
||||
struct BatchOperationsTests {
|
||||
@Test("transcribeBatch() function works")
|
||||
func transcribeBatchFunctionWorks() async throws {
|
||||
@Test
|
||||
func `transcribeBatch() function works`() async throws {
|
||||
try await TestHelpers.withMockProviderEnvironment {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let audioData = TestHelpers.sampleAudioData(configuration: config)
|
||||
@ -246,8 +242,8 @@ struct AudioFunctionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("generateSpeechBatch() function works")
|
||||
func generateSpeechBatchFunctionWorks() async throws {
|
||||
@Test
|
||||
func `generateSpeechBatch() function works`() async throws {
|
||||
try await TestHelpers.withMockProviderEnvironment {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let texts = ["Hello", "World"]
|
||||
@ -267,24 +263,23 @@ struct AudioFunctionsTests {
|
||||
|
||||
// MARK: - Utility Functions Tests
|
||||
|
||||
@Suite("Utility Functions Tests")
|
||||
struct UtilityFunctionsTests {
|
||||
@Test("availableTranscriptionModels() returns models")
|
||||
func availableTranscriptionModelsReturnsModels() {
|
||||
@Test
|
||||
func `availableTranscriptionModels() returns models`() {
|
||||
let models = availableTranscriptionModels()
|
||||
#expect(!models.isEmpty)
|
||||
#expect(models.contains { $0.description == TranscriptionModel.openai(.whisper1).description })
|
||||
}
|
||||
|
||||
@Test("availableSpeechModels() returns models")
|
||||
func availableSpeechModelsReturnsModels() {
|
||||
@Test
|
||||
func `availableSpeechModels() returns models`() {
|
||||
let models = availableSpeechModels()
|
||||
#expect(!models.isEmpty)
|
||||
#expect(models.contains { $0.description == SpeechModel.openai(.tts1).description })
|
||||
}
|
||||
|
||||
@Test("capabilities() for transcription models")
|
||||
func capabilitiesForTranscriptionModels() throws {
|
||||
@Test
|
||||
func `capabilities() for transcription models`() throws {
|
||||
let config = TestHelpers.createTestConfiguration(apiKeys: ["openai": "test-key"])
|
||||
let capabilities = try capabilities(for: TranscriptionModel.openai(.whisper1), configuration: config)
|
||||
#expect(capabilities.supportsTimestamps == true)
|
||||
@ -292,8 +287,8 @@ struct AudioFunctionsTests {
|
||||
#expect(capabilities.supportedFormats.contains(.wav))
|
||||
}
|
||||
|
||||
@Test("capabilities() for speech models")
|
||||
func capabilitiesForSpeechModels() throws {
|
||||
@Test
|
||||
func `capabilities() for speech models`() throws {
|
||||
let config = TestHelpers.createTestConfiguration(apiKeys: ["openai": "test-key"])
|
||||
let capabilities = try capabilities(for: SpeechModel.openai(.tts1), configuration: config)
|
||||
#expect(capabilities.supportsSpeedControl == true)
|
||||
@ -304,10 +299,9 @@ struct AudioFunctionsTests {
|
||||
|
||||
// MARK: - Error Handling Tests
|
||||
|
||||
@Suite("Error Handling Tests")
|
||||
struct ErrorHandlingTests {
|
||||
@Test("transcribe() handles empty audio data")
|
||||
func transcribeHandlesEmptyAudioData() async throws {
|
||||
@Test
|
||||
func `transcribe() handles empty audio data`() async {
|
||||
await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let emptyAudioData = AudioData(data: Data(), format: .wav)
|
||||
|
||||
@ -317,8 +311,8 @@ struct AudioFunctionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("generateSpeech() handles empty text")
|
||||
func generateSpeechHandlesEmptyText() async throws {
|
||||
@Test
|
||||
func `generateSpeech() handles empty text`() async {
|
||||
await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
await #expect(throws: TachikomaError.self) {
|
||||
_ = try await generateSpeech("", using: .openai(.tts1), configuration: config)
|
||||
@ -326,8 +320,8 @@ struct AudioFunctionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("functions handle missing API keys")
|
||||
func functionsHandleMissingAPIKeys() async throws {
|
||||
@Test
|
||||
func `functions handle missing API keys`() async {
|
||||
guard TestHelpers.isMockAPIKey(ProcessInfo.processInfo.environment["OPENAI_API_KEY"]) else { return }
|
||||
await TestHelpers.withEmptyTestConfiguration { config in
|
||||
let audioData = AudioData(data: Data([0x01, 0x02]), format: .wav)
|
||||
@ -345,10 +339,9 @@ struct AudioFunctionsTests {
|
||||
|
||||
// MARK: - Integration Tests
|
||||
|
||||
@Suite("Integration Tests")
|
||||
struct IntegrationTests {
|
||||
@Test("transcribe and generate speech pipeline")
|
||||
func transcribeAndGenerateSpeechPipeline() async throws {
|
||||
@Test
|
||||
func `transcribe and generate speech pipeline`() async {
|
||||
await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let originalAudioData = TestHelpers.sampleAudioData(configuration: config)
|
||||
|
||||
@ -368,8 +361,8 @@ struct AudioFunctionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("multiple provider integration")
|
||||
func multipleProviderIntegration() async throws {
|
||||
@Test
|
||||
func `multiple provider integration`() async throws {
|
||||
try await TestHelpers.withStandardTestConfiguration { config in
|
||||
let audioData = TestHelpers.sampleAudioData(configuration: config)
|
||||
|
||||
|
||||
@ -7,10 +7,10 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
@testable import TachikomaAudio
|
||||
|
||||
@Suite("Audio Provider Factories", .serialized)
|
||||
@Suite(.serialized)
|
||||
struct AudioProviderFactoryTests {
|
||||
@Test("TranscriptionProviderFactory returns mock provider in test mode")
|
||||
func transcriptionFactoryReturnsMockInTestMode() throws {
|
||||
@Test
|
||||
func `TranscriptionProviderFactory returns mock provider in test mode`() throws {
|
||||
let previousTestMode = getenv("TACHIKOMA_TEST_MODE").flatMap { String(cString: $0) }
|
||||
setenv("TACHIKOMA_TEST_MODE", "mock", 1)
|
||||
defer {
|
||||
@ -32,8 +32,8 @@ struct AudioProviderFactoryTests {
|
||||
#expect(provider is MockTranscriptionProvider)
|
||||
}
|
||||
|
||||
@Test("TranscriptionProviderFactory requires API key in test mode")
|
||||
func transcriptionFactoryRequiresAPIKeyInTestMode() {
|
||||
@Test
|
||||
func `TranscriptionProviderFactory requires API key in test mode`() {
|
||||
let previousTestMode = getenv("TACHIKOMA_TEST_MODE").flatMap { String(cString: $0) }
|
||||
setenv("TACHIKOMA_TEST_MODE", "mock", 1)
|
||||
defer {
|
||||
@ -57,8 +57,8 @@ struct AudioProviderFactoryTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("SpeechProviderFactory returns mock provider in test mode")
|
||||
func speechFactoryReturnsMockInTestMode() throws {
|
||||
@Test
|
||||
func `SpeechProviderFactory returns mock provider in test mode`() throws {
|
||||
let previousTestMode = getenv("TACHIKOMA_TEST_MODE").flatMap { String(cString: $0) }
|
||||
setenv("TACHIKOMA_TEST_MODE", "mock", 1)
|
||||
defer {
|
||||
@ -80,8 +80,8 @@ struct AudioProviderFactoryTests {
|
||||
#expect(provider is MockSpeechProvider)
|
||||
}
|
||||
|
||||
@Test("AudioConfiguration reads configuration keys before environment")
|
||||
func audioConfigurationPrefersExplicitConfiguration() {
|
||||
@Test
|
||||
func `AudioConfiguration reads configuration keys before environment`() {
|
||||
let previousOpenAIKey = getenv("OPENAI_API_KEY").flatMap { String(cString: $0) }
|
||||
unsetenv("OPENAI_API_KEY")
|
||||
defer {
|
||||
@ -97,8 +97,8 @@ struct AudioProviderFactoryTests {
|
||||
#expect(resolvedKey == "configured-key")
|
||||
}
|
||||
|
||||
@Test("AudioConfiguration falls back to environment variable")
|
||||
func audioConfigurationFallsBackToEnvironment() {
|
||||
@Test
|
||||
func `AudioConfiguration falls back to environment variable`() {
|
||||
let previousOpenAIKey = getenv("OPENAI_API_KEY").flatMap { String(cString: $0) }
|
||||
setenv("OPENAI_API_KEY", "env-key-123", 1)
|
||||
defer {
|
||||
|
||||
@ -3,14 +3,12 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
@testable import TachikomaAudio
|
||||
|
||||
@Suite("Audio Types Tests")
|
||||
struct AudioTypesTests {
|
||||
// MARK: - AudioData Tests
|
||||
|
||||
@Suite("AudioData Tests")
|
||||
struct AudioDataTests {
|
||||
@Test("AudioData initialization with basic properties")
|
||||
func audioDataBasicInit() {
|
||||
@Test
|
||||
func `AudioData initialization with basic properties`() {
|
||||
let testData = Data([0x01, 0x02, 0x03, 0x04])
|
||||
let audioData = AudioData(
|
||||
data: testData,
|
||||
@ -28,8 +26,8 @@ struct AudioTypesTests {
|
||||
#expect(audioData.size == 4)
|
||||
}
|
||||
|
||||
@Test("AudioData initialization with defaults")
|
||||
func audioDataDefaultInit() {
|
||||
@Test
|
||||
func `AudioData initialization with defaults`() {
|
||||
let testData = Data([0x01, 0x02])
|
||||
let audioData = AudioData(data: testData)
|
||||
|
||||
@ -41,8 +39,8 @@ struct AudioTypesTests {
|
||||
#expect(audioData.size == 2)
|
||||
}
|
||||
|
||||
@Test("AudioData file URL initialization")
|
||||
func audioDataFileInit() throws {
|
||||
@Test
|
||||
func `AudioData file URL initialization`() throws {
|
||||
// Create a temporary file
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
let testFile = tempDir.appendingPathComponent("test_audio-\(UUID().uuidString).mp3")
|
||||
@ -61,8 +59,8 @@ struct AudioTypesTests {
|
||||
try? FileManager.default.removeItem(at: testFile)
|
||||
}
|
||||
|
||||
@Test("AudioData file URL with unknown extension")
|
||||
func audioDataUnknownExtension() throws {
|
||||
@Test
|
||||
func `AudioData file URL with unknown extension`() throws {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
let testFile = tempDir.appendingPathComponent("test_audio-\(UUID().uuidString).unknown")
|
||||
let testData = Data([0x01, 0x02])
|
||||
@ -76,8 +74,8 @@ struct AudioTypesTests {
|
||||
try? FileManager.default.removeItem(at: testFile)
|
||||
}
|
||||
|
||||
@Test("AudioData write to file")
|
||||
func audioDataWrite() throws {
|
||||
@Test
|
||||
func `AudioData write to file`() throws {
|
||||
let testData = Data([0x01, 0x02, 0x03, 0x04])
|
||||
let audioData = AudioData(data: testData, format: .flac)
|
||||
|
||||
@ -96,10 +94,9 @@ struct AudioTypesTests {
|
||||
|
||||
// MARK: - AudioFormat Tests
|
||||
|
||||
@Suite("AudioFormat Tests")
|
||||
struct AudioFormatTests {
|
||||
@Test("AudioFormat MIME types")
|
||||
func audioFormatMimeTypes() {
|
||||
@Test
|
||||
func `AudioFormat MIME types`() {
|
||||
#expect(AudioFormat.wav.mimeType == "audio/wav")
|
||||
#expect(AudioFormat.mp3.mimeType == "audio/mpeg")
|
||||
#expect(AudioFormat.flac.mimeType == "audio/flac")
|
||||
@ -110,8 +107,8 @@ struct AudioTypesTests {
|
||||
#expect(AudioFormat.ogg.mimeType == "audio/ogg")
|
||||
}
|
||||
|
||||
@Test("AudioFormat lossless property")
|
||||
func audioFormatLossless() {
|
||||
@Test
|
||||
func `AudioFormat lossless property`() {
|
||||
// Lossless formats
|
||||
#expect(AudioFormat.wav.isLossless == true)
|
||||
#expect(AudioFormat.flac.isLossless == true)
|
||||
@ -125,8 +122,8 @@ struct AudioTypesTests {
|
||||
#expect(AudioFormat.ogg.isLossless == false)
|
||||
}
|
||||
|
||||
@Test("AudioFormat all cases completeness")
|
||||
func audioFormatAllCases() {
|
||||
@Test
|
||||
func `AudioFormat all cases completeness`() {
|
||||
let allCases = AudioFormat.allCases
|
||||
let expectedCases: [AudioFormat] = [.wav, .mp3, .flac, .opus, .m4a, .aac, .pcm, .ogg]
|
||||
|
||||
@ -139,10 +136,9 @@ struct AudioTypesTests {
|
||||
|
||||
// MARK: - VoiceOption Tests
|
||||
|
||||
@Suite("VoiceOption Tests")
|
||||
struct VoiceOptionTests {
|
||||
@Test("VoiceOption string values")
|
||||
func voiceOptionStringValues() {
|
||||
@Test
|
||||
func `VoiceOption string values`() {
|
||||
#expect(VoiceOption.alloy.stringValue == "alloy")
|
||||
#expect(VoiceOption.echo.stringValue == "echo")
|
||||
#expect(VoiceOption.fable.stringValue == "fable")
|
||||
@ -154,8 +150,8 @@ struct AudioTypesTests {
|
||||
#expect(customVoice.stringValue == "my-custom-voice")
|
||||
}
|
||||
|
||||
@Test("VoiceOption defaults and recommendations")
|
||||
func voiceOptionDefaults() {
|
||||
@Test
|
||||
func `VoiceOption defaults and recommendations`() {
|
||||
#expect(VoiceOption.default == .alloy)
|
||||
|
||||
let femaleVoices = VoiceOption.female
|
||||
@ -171,8 +167,8 @@ struct AudioTypesTests {
|
||||
#expect(maleVoices.count == 3)
|
||||
}
|
||||
|
||||
@Test("VoiceOption hashable and equatable")
|
||||
func voiceOptionHashableEquatable() {
|
||||
@Test
|
||||
func `VoiceOption hashable and equatable`() {
|
||||
let voice1 = VoiceOption.alloy
|
||||
let voice2 = VoiceOption.alloy
|
||||
let voice3 = VoiceOption.echo
|
||||
@ -191,10 +187,9 @@ struct AudioTypesTests {
|
||||
|
||||
// MARK: - Result Types Tests
|
||||
|
||||
@Suite("Result Types Tests")
|
||||
struct ResultTypesTests {
|
||||
@Test("TranscriptionResult initialization")
|
||||
func transcriptionResultInit() {
|
||||
@Test
|
||||
func `TranscriptionResult initialization`() {
|
||||
let segments = [
|
||||
TranscriptionSegment(text: "Hello", start: 0.0, end: 1.0),
|
||||
TranscriptionSegment(text: "world", start: 1.0, end: 2.0),
|
||||
@ -218,14 +213,14 @@ struct AudioTypesTests {
|
||||
#expect(result.warnings?.first == "Test warning")
|
||||
}
|
||||
|
||||
@Test("TranscriptionSegment duration calculation")
|
||||
func transcriptionSegmentDuration() {
|
||||
@Test
|
||||
func `TranscriptionSegment duration calculation`() {
|
||||
let segment = TranscriptionSegment(text: "test", start: 1.5, end: 3.7)
|
||||
#expect(segment.duration == 2.2)
|
||||
}
|
||||
|
||||
@Test("SpeechResult initialization")
|
||||
func speechResultInit() {
|
||||
@Test
|
||||
func `SpeechResult initialization`() {
|
||||
let audioData = AudioData(data: Data([0x01, 0x02]), format: .mp3)
|
||||
let usage = SpeechUsage(charactersProcessed: 10, cost: 0.05)
|
||||
|
||||
@ -243,10 +238,9 @@ struct AudioTypesTests {
|
||||
|
||||
// MARK: - Request Types Tests
|
||||
|
||||
@Suite("Request Types Tests")
|
||||
struct RequestTypesTests {
|
||||
@Test("TranscriptionRequest initialization")
|
||||
func transcriptionRequestInit() {
|
||||
@Test
|
||||
func `TranscriptionRequest initialization`() {
|
||||
let audioData = AudioData(data: Data([0x01]), format: .wav)
|
||||
let abortSignal = AbortSignal()
|
||||
|
||||
@ -269,8 +263,8 @@ struct AudioTypesTests {
|
||||
#expect(request.headers["Custom-Header"] == "value")
|
||||
}
|
||||
|
||||
@Test("SpeechRequest initialization")
|
||||
func speechRequestInit() {
|
||||
@Test
|
||||
func `SpeechRequest initialization`() {
|
||||
let abortSignal = AbortSignal()
|
||||
|
||||
let request = SpeechRequest(
|
||||
@ -293,8 +287,8 @@ struct AudioTypesTests {
|
||||
#expect(request.headers["API-Version"] == "v1")
|
||||
}
|
||||
|
||||
@Test("Request default values")
|
||||
func requestDefaults() {
|
||||
@Test
|
||||
func `Request default values`() {
|
||||
let audioData = AudioData(data: Data(), format: .wav)
|
||||
let transcriptionRequest = TranscriptionRequest(audio: audioData)
|
||||
|
||||
@ -317,10 +311,9 @@ struct AudioTypesTests {
|
||||
|
||||
// MARK: - AbortSignal Tests
|
||||
|
||||
@Suite("AbortSignal Tests")
|
||||
struct AbortSignalTests {
|
||||
@Test("AbortSignal basic functionality")
|
||||
func abortSignalBasic() {
|
||||
@Test
|
||||
func `AbortSignal basic functionality`() {
|
||||
let signal = AbortSignal()
|
||||
|
||||
#expect(signal.cancelled == false)
|
||||
@ -330,8 +323,8 @@ struct AudioTypesTests {
|
||||
#expect(signal.cancelled == true)
|
||||
}
|
||||
|
||||
@Test("AbortSignal throwIfCancelled")
|
||||
func abortSignalThrowIfCancelled() throws {
|
||||
@Test
|
||||
func `AbortSignal throwIfCancelled`() throws {
|
||||
let signal = AbortSignal()
|
||||
|
||||
// Should not throw when not cancelled
|
||||
@ -345,8 +338,8 @@ struct AudioTypesTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("AbortSignal timeout functionality")
|
||||
func abortSignalTimeout() async throws {
|
||||
@Test
|
||||
func `AbortSignal timeout functionality`() async throws {
|
||||
let signal = AbortSignal.timeout(0.1) // 100ms timeout
|
||||
|
||||
#expect(signal.cancelled == false)
|
||||
@ -357,8 +350,8 @@ struct AudioTypesTests {
|
||||
#expect(signal.cancelled == true)
|
||||
}
|
||||
|
||||
@Test("AbortSignal thread safety")
|
||||
func abortSignalThreadSafety() async {
|
||||
@Test
|
||||
func `AbortSignal thread safety`() async {
|
||||
let signal = AbortSignal()
|
||||
|
||||
// Start multiple concurrent tasks that check and cancel the signal
|
||||
@ -379,8 +372,8 @@ struct AudioTypesTests {
|
||||
|
||||
// MARK: - Enum Tests
|
||||
|
||||
@Test("TimestampGranularity enum")
|
||||
func timestampGranularityEnum() {
|
||||
@Test
|
||||
func `TimestampGranularity enum`() {
|
||||
#expect(TimestampGranularity.word.rawValue == "word")
|
||||
#expect(TimestampGranularity.segment.rawValue == "segment")
|
||||
|
||||
@ -390,8 +383,8 @@ struct AudioTypesTests {
|
||||
#expect(allCases.count == 2)
|
||||
}
|
||||
|
||||
@Test("TranscriptionResponseFormat enum")
|
||||
func transcriptionResponseFormatEnum() {
|
||||
@Test
|
||||
func `TranscriptionResponseFormat enum`() {
|
||||
#expect(TranscriptionResponseFormat.json.rawValue == "json")
|
||||
#expect(TranscriptionResponseFormat.text.rawValue == "text")
|
||||
#expect(TranscriptionResponseFormat.srt.rawValue == "srt")
|
||||
@ -406,8 +399,8 @@ struct AudioTypesTests {
|
||||
|
||||
// MARK: - Error Types Tests
|
||||
|
||||
@Test("Audio error types")
|
||||
func audioErrorTypes() {
|
||||
@Test
|
||||
func `Audio error types`() {
|
||||
let operationCancelled = TachikomaError.operationCancelled
|
||||
let noAudioData = TachikomaError.noAudioData
|
||||
let unsupportedFormat = TachikomaError.unsupportedAudioFormat
|
||||
|
||||
@ -3,14 +3,13 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
@testable import TachikomaAudio
|
||||
|
||||
@Suite("OpenAI Audio Provider Tests", .serialized)
|
||||
@Suite(.serialized)
|
||||
struct OpenAIAudioProviderTests {
|
||||
// MARK: - OpenAI Transcription Provider Tests
|
||||
|
||||
@Suite("OpenAI Transcription Provider Tests")
|
||||
struct OpenAITranscriptionProviderTests {
|
||||
@Test("OpenAI transcription provider initialization")
|
||||
func openAITranscriptionProviderInit() async throws {
|
||||
@Test
|
||||
func `OpenAI transcription provider initialization`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-api-key"]) { config in
|
||||
let provider = try TranscriptionProviderFactory.createProvider(
|
||||
for: .openai(.whisper1),
|
||||
@ -25,8 +24,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI transcription provider initialization fails without API key")
|
||||
func openAITranscriptionProviderInitFailsWithoutAPIKey() async throws {
|
||||
@Test
|
||||
func `OpenAI transcription provider initialization fails without API key`() async {
|
||||
guard TestHelpers.isMockAPIKey(ProcessInfo.processInfo.environment["OPENAI_API_KEY"]) else { return }
|
||||
await TestHelpers.withEmptyTestConfiguration { config in
|
||||
#expect(throws: TachikomaError.self) {
|
||||
@ -35,8 +34,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI transcription provider different models")
|
||||
func openAITranscriptionProviderDifferentModels() async throws {
|
||||
@Test
|
||||
func `OpenAI transcription provider different models`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let whisperProvider = try TranscriptionProviderFactory.createProvider(
|
||||
for: .openai(.whisper1),
|
||||
@ -52,8 +51,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI transcription provider supported formats")
|
||||
func openAITranscriptionProviderSupportedFormats() async throws {
|
||||
@Test
|
||||
func `OpenAI transcription provider supported formats`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try TranscriptionProviderFactory.createProvider(
|
||||
for: .openai(.whisper1),
|
||||
@ -69,8 +68,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI transcription provider transcribe function")
|
||||
func openAITranscriptionProviderTranscribe() async throws {
|
||||
@Test
|
||||
func `OpenAI transcription provider transcribe function`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try TranscriptionProviderFactory.createProvider(
|
||||
for: .openai(.whisper1),
|
||||
@ -90,8 +89,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI transcription provider with timestamps")
|
||||
func openAITranscriptionProviderTimestamps() async throws {
|
||||
@Test
|
||||
func `OpenAI transcription provider with timestamps`() async throws {
|
||||
try await TestHelpers.withMockProviderEnvironment {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try TranscriptionProviderFactory.createProvider(
|
||||
@ -113,8 +112,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI transcription provider with abort signal")
|
||||
func openAITranscriptionProviderAbortSignal() async throws {
|
||||
@Test
|
||||
func `OpenAI transcription provider with abort signal`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try TranscriptionProviderFactory.createProvider(
|
||||
for: .openai(.whisper1),
|
||||
@ -133,8 +132,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI transcription provider request validation")
|
||||
func openAITranscriptionProviderRequestValidation() async throws {
|
||||
@Test
|
||||
func `OpenAI transcription provider request validation`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try TranscriptionProviderFactory.createProvider(
|
||||
for: .openai(.whisper1),
|
||||
@ -163,10 +162,9 @@ struct OpenAIAudioProviderTests {
|
||||
|
||||
// MARK: - OpenAI Speech Provider Tests
|
||||
|
||||
@Suite("OpenAI Speech Provider Tests")
|
||||
struct OpenAISpeechProviderTests {
|
||||
@Test("OpenAI speech provider initialization")
|
||||
func openAISpeechProviderInit() async throws {
|
||||
@Test
|
||||
func `OpenAI speech provider initialization`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-api-key"]) { config in
|
||||
let provider = try SpeechProviderFactory.createProvider(for: .openai(.tts1), configuration: config)
|
||||
|
||||
@ -177,8 +175,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI speech provider initialization fails without API key")
|
||||
func openAISpeechProviderInitFailsWithoutAPIKey() async throws {
|
||||
@Test
|
||||
func `OpenAI speech provider initialization fails without API key`() async {
|
||||
guard TestHelpers.isMockAPIKey(ProcessInfo.processInfo.environment["OPENAI_API_KEY"]) else { return }
|
||||
await TestHelpers.withEmptyTestConfiguration { config in
|
||||
#expect(throws: TachikomaError.self) {
|
||||
@ -187,8 +185,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI speech provider different models")
|
||||
func openAISpeechProviderDifferentModels() async throws {
|
||||
@Test
|
||||
func `OpenAI speech provider different models`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let tts1Provider = try SpeechProviderFactory.createProvider(for: .openai(.tts1), configuration: config)
|
||||
let tts1HDProvider = try SpeechProviderFactory.createProvider(
|
||||
@ -211,8 +209,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI speech provider supported voices")
|
||||
func openAISpeechProviderSupportedVoices() async throws {
|
||||
@Test
|
||||
func `OpenAI speech provider supported voices`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try SpeechProviderFactory.createProvider(for: .openai(.tts1), configuration: config)
|
||||
|
||||
@ -223,8 +221,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI speech provider generate speech function")
|
||||
func openAISpeechProviderGenerateSpeech() async throws {
|
||||
@Test
|
||||
func `OpenAI speech provider generate speech function`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try SpeechProviderFactory.createProvider(for: .openai(.tts1), configuration: config)
|
||||
let request = SpeechRequest(
|
||||
@ -244,8 +242,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI speech provider with different voices")
|
||||
func openAISpeechProviderDifferentVoices() async throws {
|
||||
@Test
|
||||
func `OpenAI speech provider with different voices`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try SpeechProviderFactory.createProvider(for: .openai(.tts1), configuration: config)
|
||||
|
||||
@ -269,8 +267,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI speech provider with speed control")
|
||||
func openAISpeechProviderSpeedControl() async throws {
|
||||
@Test
|
||||
func `OpenAI speech provider with speed control`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try SpeechProviderFactory.createProvider(for: .openai(.tts1), configuration: config)
|
||||
|
||||
@ -295,8 +293,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI speech provider with voice instructions")
|
||||
func openAISpeechProviderVoiceInstructions() async throws {
|
||||
@Test
|
||||
func `OpenAI speech provider with voice instructions`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try SpeechProviderFactory.createProvider(for: .openai(.tts1HD), configuration: config)
|
||||
let request = SpeechRequest(
|
||||
@ -315,8 +313,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI speech provider with abort signal")
|
||||
func openAISpeechProviderAbortSignal() async throws {
|
||||
@Test
|
||||
func `OpenAI speech provider with abort signal`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try SpeechProviderFactory.createProvider(for: .openai(.tts1), configuration: config)
|
||||
let abortSignal = AbortSignal()
|
||||
@ -335,8 +333,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI speech provider request validation")
|
||||
func openAISpeechProviderRequestValidation() async throws {
|
||||
@Test
|
||||
func `OpenAI speech provider request validation`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try SpeechProviderFactory.createProvider(
|
||||
for: .openai(.tts1),
|
||||
@ -367,8 +365,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI speech provider different output formats")
|
||||
func openAISpeechProviderDifferentFormats() async throws {
|
||||
@Test
|
||||
func `OpenAI speech provider different output formats`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try SpeechProviderFactory.createProvider(
|
||||
for: .openai(.tts1),
|
||||
@ -393,10 +391,9 @@ struct OpenAIAudioProviderTests {
|
||||
|
||||
// MARK: - OpenAI API Configuration Tests
|
||||
|
||||
@Suite("OpenAI API Configuration Tests")
|
||||
struct OpenAIAPIConfigurationTests {
|
||||
@Test("OpenAI API key configuration from environment")
|
||||
func openAIAPIKeyFromEnvironment() async throws {
|
||||
@Test
|
||||
func `OpenAI API key configuration from environment`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "env-test-key"]) { config in
|
||||
let provider = try TranscriptionProviderFactory.createProvider(
|
||||
for: .openai(.whisper1),
|
||||
@ -407,8 +404,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI custom base URL configuration")
|
||||
func openAICustomBaseURL() async throws {
|
||||
@Test
|
||||
func `OpenAI custom base URL configuration`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
// Set custom base URL via environment
|
||||
setenv("OPENAI_BASE_URL", "https://custom-openai-api.example.com", 1)
|
||||
@ -424,8 +421,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI organization ID configuration")
|
||||
func openAIOrganizationID() async throws {
|
||||
@Test
|
||||
func `OpenAI organization ID configuration`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
// Set organization ID via environment
|
||||
setenv("OPENAI_ORGANIZATION", "org-test-12345", 1)
|
||||
@ -441,8 +438,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI request timeout configuration")
|
||||
func openAIRequestTimeout() async throws {
|
||||
@Test
|
||||
func `OpenAI request timeout configuration`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try TranscriptionProviderFactory.createProvider(
|
||||
for: .openai(.whisper1),
|
||||
@ -469,10 +466,9 @@ struct OpenAIAudioProviderTests {
|
||||
|
||||
// MARK: - OpenAI Error Handling Tests
|
||||
|
||||
@Suite("OpenAI Error Handling Tests")
|
||||
struct OpenAIErrorHandlingTests {
|
||||
@Test("OpenAI provider handles network errors")
|
||||
func openAIProviderNetworkErrors() async throws {
|
||||
@Test
|
||||
func `OpenAI provider handles network errors`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "invalid-key"]) { config in
|
||||
let provider = try TranscriptionProviderFactory.createProvider(
|
||||
for: .openai(.whisper1),
|
||||
@ -493,8 +489,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI provider handles unsupported formats gracefully")
|
||||
func openAIProviderUnsupportedFormats() async throws {
|
||||
@Test
|
||||
func `OpenAI provider handles unsupported formats gracefully`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try TranscriptionProviderFactory.createProvider(
|
||||
for: .openai(.whisper1),
|
||||
@ -517,8 +513,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI provider handles rate limiting")
|
||||
func openAIProviderRateLimiting() async throws {
|
||||
@Test
|
||||
func `OpenAI provider handles rate limiting`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let provider = try TranscriptionProviderFactory.createProvider(
|
||||
for: .openai(.whisper1),
|
||||
@ -549,8 +545,8 @@ struct OpenAIAudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI provider error message formatting")
|
||||
func openAIProviderErrorMessageFormatting() async throws {
|
||||
@Test
|
||||
func `OpenAI provider error message formatting`() async {
|
||||
await TestHelpers.withEmptyTestConfiguration { config in
|
||||
let originalKey = getenv("OPENAI_API_KEY").flatMap { String(cString: $0) }
|
||||
unsetenv("OPENAI_API_KEY")
|
||||
|
||||
@ -3,14 +3,12 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Audio System Integration Tests")
|
||||
struct AudioSystemIntegrationTests {
|
||||
enum AudioSystemIntegrationTests {
|
||||
// MARK: - Basic Audio Function Tests
|
||||
|
||||
@Suite("Basic Audio Function Tests")
|
||||
struct BasicAudioFunctionTests {
|
||||
@Test("Audio transcription functions are available")
|
||||
func audioTranscriptionFunctionsAvailable() async throws {
|
||||
@Test
|
||||
func `Audio transcription functions are available`() async throws {
|
||||
// Test that core audio transcription functions exist and work
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { _ in
|
||||
let audioData = AudioData(data: Data([0x01, 0x02, 0x03, 0x04]), format: .wav)
|
||||
@ -23,8 +21,8 @@ struct AudioSystemIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Audio speech generation functions are available")
|
||||
func audioSpeechGenerationFunctionsAvailable() async throws {
|
||||
@Test
|
||||
func `Audio speech generation functions are available`() async throws {
|
||||
// Test that core speech generation functions exist and work
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { _ in
|
||||
// Test basic speech generation function exists
|
||||
@ -36,8 +34,8 @@ struct AudioSystemIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Audio model types work correctly")
|
||||
func audioModelTypesWork() {
|
||||
@Test
|
||||
func `Audio model types work correctly`() {
|
||||
// Test that audio model enums work as expected
|
||||
let transcriptionModel = TranscriptionModel.openai(.whisper1)
|
||||
#expect(transcriptionModel.modelId == "whisper-1")
|
||||
@ -50,8 +48,8 @@ struct AudioSystemIntegrationTests {
|
||||
#expect(speechModel.supportedFormats.contains(.mp3))
|
||||
}
|
||||
|
||||
@Test("Audio data types work correctly")
|
||||
func audioDataTypesWork() throws {
|
||||
@Test
|
||||
func `Audio data types work correctly`() {
|
||||
// Test that audio data types work as expected
|
||||
let testData = Data([0x01, 0x02, 0x03, 0x04])
|
||||
let audioData = AudioData(data: testData, format: .wav, sampleRate: 44100, channels: 2, duration: 5.0)
|
||||
@ -73,8 +71,8 @@ struct AudioSystemIntegrationTests {
|
||||
#expect(VoiceOption.default == .alloy)
|
||||
}
|
||||
|
||||
@Test("AbortSignal functionality works")
|
||||
func abortSignalFunctionality() async throws {
|
||||
@Test
|
||||
func `AbortSignal functionality works`() async throws {
|
||||
let signal = AbortSignal()
|
||||
|
||||
#expect(signal.cancelled == false)
|
||||
@ -96,10 +94,9 @@ struct AudioSystemIntegrationTests {
|
||||
|
||||
// MARK: - Provider Integration Tests
|
||||
|
||||
@Suite("Provider Integration Tests")
|
||||
struct ProviderIntegrationTests {
|
||||
@Test("Transcription provider factory works")
|
||||
func transcriptionProviderFactoryWorks() async throws {
|
||||
@Test
|
||||
func `Transcription provider factory works`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { _ in
|
||||
let provider = try TranscriptionProviderFactory.createProvider(for: .openai(.whisper1))
|
||||
|
||||
@ -109,8 +106,8 @@ struct AudioSystemIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Speech provider factory works")
|
||||
func speechProviderFactoryWorks() async throws {
|
||||
@Test
|
||||
func `Speech provider factory works`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { _ in
|
||||
let provider = try SpeechProviderFactory.createProvider(for: .openai(.tts1))
|
||||
|
||||
@ -120,8 +117,8 @@ struct AudioSystemIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Provider factory fails without API key")
|
||||
func providerFactoryFailsWithoutAPIKey() async throws {
|
||||
@Test
|
||||
func `Provider factory fails without API key`() async throws {
|
||||
try await TestHelpers.withEmptyTestConfiguration { _ in
|
||||
#expect(throws: TachikomaError.self) {
|
||||
_ = try TranscriptionProviderFactory.createProvider(for: .openai(.whisper1))
|
||||
@ -136,10 +133,9 @@ struct AudioSystemIntegrationTests {
|
||||
|
||||
// MARK: - Error Handling Tests
|
||||
|
||||
@Suite("Error Handling Tests")
|
||||
struct ErrorHandlingTests {
|
||||
@Test("Audio error types are defined")
|
||||
func audioErrorTypesAreDefined() {
|
||||
@Test
|
||||
func `Audio error types are defined`() {
|
||||
let operationCancelled = TachikomaError.operationCancelled
|
||||
let noAudioData = TachikomaError.noAudioData
|
||||
let unsupportedFormat = TachikomaError.unsupportedAudioFormat
|
||||
@ -154,8 +150,8 @@ struct AudioSystemIntegrationTests {
|
||||
#expect(speechFailed.localizedDescription.contains("Speech"))
|
||||
}
|
||||
|
||||
@Test("Empty audio data is handled")
|
||||
func emptyAudioDataIsHandled() async throws {
|
||||
@Test
|
||||
func `Empty audio data is handled`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { _ in
|
||||
let emptyAudioData = AudioData(data: Data(), format: .wav)
|
||||
|
||||
@ -165,8 +161,8 @@ struct AudioSystemIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Empty text for speech generation is handled")
|
||||
func emptyTextForSpeechGenerationIsHandled() async throws {
|
||||
@Test
|
||||
func `Empty text for speech generation is handled`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { _ in
|
||||
await #expect(throws: TachikomaError.self) {
|
||||
_ = try await generateSpeech("", using: .openai(.tts1))
|
||||
@ -177,10 +173,9 @@ struct AudioSystemIntegrationTests {
|
||||
|
||||
// MARK: - File I/O Tests
|
||||
|
||||
@Suite("File I/O Tests")
|
||||
struct FileIOTests {
|
||||
@Test("AudioData file operations work")
|
||||
func audioDataFileOperationsWork() throws {
|
||||
@Test
|
||||
func `AudioData file operations work`() throws {
|
||||
// Test creating AudioData from file
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
let testFile = tempDir.appendingPathComponent("test_audio.wav")
|
||||
@ -204,8 +199,8 @@ struct AudioSystemIntegrationTests {
|
||||
try? FileManager.default.removeItem(at: outputFile)
|
||||
}
|
||||
|
||||
@Test("AudioData handles unknown file extensions")
|
||||
func audioDataHandlesUnknownExtensions() throws {
|
||||
@Test
|
||||
func `AudioData handles unknown file extensions`() throws {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
let testFile = tempDir.appendingPathComponent("test_audio.unknown")
|
||||
let testData = Data([0x01, 0x02])
|
||||
@ -221,10 +216,9 @@ struct AudioSystemIntegrationTests {
|
||||
|
||||
// MARK: - SpeechRecognizer Integration Tests
|
||||
|
||||
@Suite("SpeechRecognizer Integration Tests")
|
||||
struct SpeechRecognizerIntegrationTests {
|
||||
@Test("SpeechRecognizer can be created with Tachikoma integration")
|
||||
func speechRecognizerCanBeCreatedWithTachikomaIntegration() async throws {
|
||||
@Test
|
||||
func `SpeechRecognizer can be created with Tachikoma integration`() async throws {
|
||||
try await TestHelpers.withStandardTestConfiguration { _ in
|
||||
// Test creating SpeechRecognizer with Tachikoma backend
|
||||
let recognizer = SpeechRecognizer()
|
||||
@ -242,8 +236,8 @@ struct AudioSystemIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("SpeechRecognizer modes have correct properties")
|
||||
func speechRecognizerModesHaveCorrectProperties() {
|
||||
@Test
|
||||
func `SpeechRecognizer modes have correct properties`() {
|
||||
// Test that recognition modes have proper descriptions and requirements
|
||||
#expect(RecognitionMode.native.description.contains("Native"))
|
||||
#expect(RecognitionMode.whisper.description.contains("Whisper"))
|
||||
@ -257,8 +251,8 @@ struct AudioSystemIntegrationTests {
|
||||
#expect(RecognitionMode.direct.requiresOpenAIKey == true)
|
||||
}
|
||||
|
||||
@Test("SpeechRecognizer native mode works without API key")
|
||||
func speechRecognizerNativeModeWorksWithoutAPIKey() async throws {
|
||||
@Test
|
||||
func `SpeechRecognizer native mode works without API key`() async throws {
|
||||
try await TestHelpers.withEmptyTestConfiguration { _ in
|
||||
let recognizer = SpeechRecognizer()
|
||||
recognizer.recognitionMode = .native
|
||||
@ -283,8 +277,8 @@ struct AudioSystemIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("SpeechRecognizer Whisper mode requires API key")
|
||||
func speechRecognizerWhisperModeRequiresAPIKey() async throws {
|
||||
@Test
|
||||
func `SpeechRecognizer Whisper mode requires API key`() async throws {
|
||||
try await TestHelpers.withEmptyTestConfiguration { _ in
|
||||
let recognizer = SpeechRecognizer()
|
||||
recognizer.recognitionMode = .whisper
|
||||
@ -304,8 +298,8 @@ struct AudioSystemIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("SpeechRecognizer Tachikoma mode integrates with audio system")
|
||||
func speechRecognizerTachikomaModeIntegratesWithAudioSystem() async throws {
|
||||
@Test
|
||||
func `SpeechRecognizer Tachikoma mode integrates with audio system`() async throws {
|
||||
try await TestHelpers.withStandardTestConfiguration { _ in
|
||||
let recognizer = SpeechRecognizer()
|
||||
recognizer.recognitionMode = .tachikoma
|
||||
@ -330,8 +324,8 @@ struct AudioSystemIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("SpeechRecognizer direct mode works with audio processing")
|
||||
func speechRecognizerDirectModeWorksWithAudioProcessing() async throws {
|
||||
@Test
|
||||
func `SpeechRecognizer direct mode works with audio processing`() async throws {
|
||||
try await TestHelpers.withStandardTestConfiguration { _ in
|
||||
let recognizer = SpeechRecognizer()
|
||||
recognizer.recognitionMode = .direct
|
||||
@ -364,8 +358,8 @@ struct AudioSystemIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("SpeechRecognizer error handling works correctly")
|
||||
func speechRecognizerErrorHandlingWorksCorrectly() async throws {
|
||||
@Test
|
||||
func `SpeechRecognizer error handling works correctly`() async throws {
|
||||
let recognizer = SpeechRecognizer()
|
||||
|
||||
// Test initial state
|
||||
@ -385,8 +379,8 @@ struct AudioSystemIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("SpeechRecognizer integrates with Tachikoma transcription")
|
||||
func speechRecognizerIntegratesWithTachikomaTranscription() async throws {
|
||||
@Test
|
||||
func `SpeechRecognizer integrates with Tachikoma transcription`() async throws {
|
||||
try await TestHelpers.withStandardTestConfiguration { _ in
|
||||
// Test that we can use the same audio data with both systems
|
||||
let testAudioData = AudioData(data: Data([0x01, 0x02, 0x03, 0x04]), format: .wav, duration: 1.0)
|
||||
@ -405,8 +399,8 @@ struct AudioSystemIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("SpeechRecognizer state management works correctly")
|
||||
func speechRecognizerStateManagementWorksCorrectly() async throws {
|
||||
@Test
|
||||
func `SpeechRecognizer state management works correctly`() {
|
||||
let recognizer = SpeechRecognizer()
|
||||
|
||||
// Test initial state
|
||||
@ -430,8 +424,8 @@ struct AudioSystemIntegrationTests {
|
||||
#expect(recognizer.recognitionMode == .direct)
|
||||
}
|
||||
|
||||
@Test("SpeechRecognizer handles multiple start/stop cycles")
|
||||
func speechRecognizerHandlesMultipleStartStopCycles() async throws {
|
||||
@Test
|
||||
func `SpeechRecognizer handles multiple start/stop cycles`() async throws {
|
||||
try await TestHelpers.withStandardTestConfiguration { _ in
|
||||
let recognizer = SpeechRecognizer()
|
||||
recognizer.recognitionMode = .native
|
||||
@ -458,8 +452,8 @@ struct AudioSystemIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("SpeechRecognizer works with different audio formats")
|
||||
func speechRecognizerWorksWithDifferentAudioFormats() async throws {
|
||||
@Test
|
||||
func `SpeechRecognizer works with different audio formats`() async throws {
|
||||
try await TestHelpers.withStandardTestConfiguration { _ in
|
||||
// Test that audio system supports formats that SpeechRecognizer might use
|
||||
let wavAudio = AudioData(data: Data([0x01, 0x02]), format: .wav)
|
||||
|
||||
@ -2,15 +2,14 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Tachikoma config guidance snapshot")
|
||||
struct TachikomaConfigGuidanceSnapshotTests {
|
||||
@Test("init guidance matches snapshot")
|
||||
func initGuidanceMatchesSnapshot() {
|
||||
@Test
|
||||
func `init guidance matches snapshot`() throws {
|
||||
let rendered = TKConfigMessages.initGuidance
|
||||
.map { $0.replacingOccurrences(of: "{path}", with: "/tmp/config.json") }
|
||||
.joined(separator: "\n")
|
||||
|
||||
let snapshot = try! String(contentsOfFile: "Tests/TachikomaTests/CLITests/__snapshots__/config_init.txt")
|
||||
let snapshot = try String(contentsOfFile: "Tests/TachikomaTests/CLITests/__snapshots__/config_init.txt")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
#expect(rendered.trimmingCharacters(in: .whitespacesAndNewlines) == snapshot)
|
||||
}
|
||||
|
||||
@ -1,23 +1,22 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite
|
||||
struct AnthropicMessageEncodingTests {
|
||||
@Test
|
||||
func encodesStringWithoutQuotes() {
|
||||
func `encodes string without quotes`() {
|
||||
let value = AnyAgentToolValue(string: "hello")
|
||||
#expect(AnthropicMessageEncoding.encodeToolResult(value) == "hello")
|
||||
}
|
||||
|
||||
@Test
|
||||
func encodesBooleansAndNumbers() {
|
||||
func `encodes booleans and numbers`() {
|
||||
#expect(AnthropicMessageEncoding.encodeToolResult(AnyAgentToolValue(bool: true)) == "true")
|
||||
#expect(AnthropicMessageEncoding.encodeToolResult(AnyAgentToolValue(int: 42)) == "42")
|
||||
#expect(AnthropicMessageEncoding.encodeToolResult(AnyAgentToolValue(double: 3.5)) == "3.5")
|
||||
}
|
||||
|
||||
@Test
|
||||
func encodesObjectsAsJSON() {
|
||||
func `encodes objects as JSON`() {
|
||||
let object = AnyAgentToolValue(object: [
|
||||
"name": AnyAgentToolValue(string: "Peekaboo"),
|
||||
"count": AnyAgentToolValue(int: 2),
|
||||
@ -26,7 +25,7 @@ struct AnthropicMessageEncodingTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
func encodesArraysAndNullValues() {
|
||||
func `encodes arrays and null values`() {
|
||||
let array = AnyAgentToolValue(array: [AnyAgentToolValue(int: 1), AnyAgentToolValue(int: 2)])
|
||||
#expect(AnthropicMessageEncoding.encodeToolResult(array) == "[1,2]")
|
||||
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("AsyncSequence Conformance Tests")
|
||||
struct AsyncSequenceTests {
|
||||
@Test("StreamTextResult conforms to AsyncSequence")
|
||||
func streamTextResultAsyncSequence() async throws {
|
||||
@Test
|
||||
func `StreamTextResult conforms to AsyncSequence`() async throws {
|
||||
// Create a test stream
|
||||
let testStream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
Task {
|
||||
@ -33,8 +32,8 @@ struct AsyncSequenceTests {
|
||||
#expect(contents == ["Hello", " ", "World"])
|
||||
}
|
||||
|
||||
@Test("StreamTextResult can be iterated multiple ways")
|
||||
func multipleIterationStyles() async throws {
|
||||
@Test
|
||||
func `StreamTextResult can be iterated multiple ways`() async throws {
|
||||
let testStream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
Task {
|
||||
for i in 1...3 {
|
||||
@ -59,8 +58,8 @@ struct AsyncSequenceTests {
|
||||
#expect(count == 4) // 3 text deltas + 1 done
|
||||
}
|
||||
|
||||
@Test("StreamObjectResult conforms to AsyncSequence")
|
||||
func streamObjectResultAsyncSequence() async throws {
|
||||
@Test
|
||||
func `StreamObjectResult conforms to AsyncSequence`() async throws {
|
||||
struct TestData: Codable, Sendable, Equatable {
|
||||
let value: Int
|
||||
}
|
||||
@ -101,8 +100,8 @@ struct AsyncSequenceTests {
|
||||
#expect(deltaTypes == [.start, .partial, .partial, .complete, .done])
|
||||
}
|
||||
|
||||
@Test("AsyncSequence works with standard operators")
|
||||
func asyncSequenceOperators() async throws {
|
||||
@Test
|
||||
func `AsyncSequence works with standard operators`() async throws {
|
||||
let testStream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
Task {
|
||||
for i in 1...5 {
|
||||
@ -137,8 +136,8 @@ struct AsyncSequenceTests {
|
||||
#expect(numbers == 15) // 1+2+3+4+5
|
||||
}
|
||||
|
||||
@Test("AsyncSequence handles errors properly")
|
||||
func asyncSequenceErrorHandling() async throws {
|
||||
@Test
|
||||
func `AsyncSequence handles errors properly`() async throws {
|
||||
struct TestError: Error {}
|
||||
|
||||
let testStream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
@ -166,8 +165,8 @@ struct AsyncSequenceTests {
|
||||
#expect(receivedError)
|
||||
}
|
||||
|
||||
@Test("AsyncSequence can be cancelled mid-iteration")
|
||||
func asyncSequenceCancellation() async throws {
|
||||
@Test
|
||||
func `AsyncSequence can be cancelled mid-iteration`() async {
|
||||
let testStream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
Task {
|
||||
for i in 1...100 {
|
||||
@ -204,8 +203,8 @@ struct AsyncSequenceTests {
|
||||
#expect(count == 5)
|
||||
}
|
||||
|
||||
@Test("StreamTextResult extension methods work with AsyncSequence")
|
||||
func streamTextResultExtensions() async throws {
|
||||
@Test
|
||||
func `StreamTextResult extension methods work with AsyncSequence`() async throws {
|
||||
let testStream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
Task {
|
||||
continuation.yield(TextStreamDelta(type: .textDelta, content: "Hello"))
|
||||
@ -234,8 +233,8 @@ struct AsyncSequenceTests {
|
||||
#expect(texts.contains(" World")) // From thinking channel
|
||||
}
|
||||
|
||||
@Test("StreamObjectResult extension methods work with AsyncSequence")
|
||||
func streamObjectResultExtensions() async throws {
|
||||
@Test
|
||||
func `StreamObjectResult extension methods work with AsyncSequence`() async throws {
|
||||
struct TestItem: Codable, Sendable, Equatable {
|
||||
let id: Int
|
||||
let name: String
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite("Basic Tests")
|
||||
struct BasicTests {
|
||||
@Test("Simple math test")
|
||||
func simpleMath() {
|
||||
@Test
|
||||
func `Simple math test`() {
|
||||
#expect(2 + 2 == 4)
|
||||
}
|
||||
|
||||
@Test("String test")
|
||||
func stringTest() {
|
||||
@Test
|
||||
func `String test`() {
|
||||
#expect("hello".uppercased() == "HELLO")
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,10 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
@testable import TachikomaAgent
|
||||
|
||||
@Suite("Configuration Architecture Tests", .serialized)
|
||||
@Suite(.serialized)
|
||||
struct ConfigurationArchitectureTests {
|
||||
@Test("Auto instance is a true singleton")
|
||||
func verifyAutoInstanceSingleton() async throws {
|
||||
@Test
|
||||
func `Auto instance is a true singleton`() throws {
|
||||
// Clear any existing default
|
||||
TachikomaConfiguration.default = nil
|
||||
|
||||
@ -16,14 +16,14 @@ struct ConfigurationArchitectureTests {
|
||||
}
|
||||
|
||||
// All should be exactly the same instance
|
||||
let first = instances.first!
|
||||
let first = try #require(instances.first)
|
||||
for instance in instances {
|
||||
#expect(instance === first, "All auto instances must be identical")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Configuration priority chain")
|
||||
func verifyConfigPriorityChain() async throws {
|
||||
@Test
|
||||
func `Configuration priority chain`() {
|
||||
// Clear default
|
||||
TachikomaConfiguration.default = nil
|
||||
|
||||
@ -50,8 +50,8 @@ struct ConfigurationArchitectureTests {
|
||||
TachikomaConfiguration.default = nil
|
||||
}
|
||||
|
||||
@Test("README examples work correctly")
|
||||
func verifyREADMEExamples() async throws {
|
||||
@Test
|
||||
func `README examples work correctly`() {
|
||||
// Example 1: Zero configuration
|
||||
_ = {
|
||||
Task {
|
||||
@ -95,8 +95,8 @@ struct ConfigurationArchitectureTests {
|
||||
TachikomaConfiguration.default = nil
|
||||
}
|
||||
|
||||
@Test("Thread safety under high concurrency")
|
||||
func verifyThreadSafety() async throws {
|
||||
@Test
|
||||
func `Thread safety under high concurrency`() async {
|
||||
TachikomaConfiguration.default = nil
|
||||
|
||||
// Concurrent writes and reads
|
||||
@ -128,8 +128,8 @@ struct ConfigurationArchitectureTests {
|
||||
TachikomaConfiguration.default = nil
|
||||
}
|
||||
|
||||
@Test("Conversation maintains its configuration")
|
||||
func verifyConversationConfig() async throws {
|
||||
@Test
|
||||
func `Conversation maintains its configuration`() {
|
||||
TachikomaConfiguration.default = nil
|
||||
|
||||
let config1 = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
@ -2,10 +2,10 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Configuration environment loading", .serialized)
|
||||
@Suite(.serialized)
|
||||
struct ConfigurationEnvironmentTests {
|
||||
@Test("Provider.environmentValue falls back to process environment")
|
||||
func providerEnvironmentValueFallback() {
|
||||
@Test
|
||||
func `Provider.environmentValue falls back to process environment`() {
|
||||
let key = "TACHIKOMA_ENV_TEST_VALUE"
|
||||
setenv(key, "env-success", 1)
|
||||
defer { unsetenv(key) }
|
||||
@ -14,8 +14,8 @@ struct ConfigurationEnvironmentTests {
|
||||
#expect(value == "env-success")
|
||||
}
|
||||
|
||||
@Test("TachikomaConfiguration picks up base URLs from environment")
|
||||
func configurationLoadsBaseURLFromEnvironment() {
|
||||
@Test
|
||||
func `TachikomaConfiguration picks up base URLs from environment`() {
|
||||
let key = "OPENAI_BASE_URL"
|
||||
setenv(key, "https://env.example.com", 1)
|
||||
defer { unsetenv(key) }
|
||||
|
||||
@ -9,10 +9,9 @@ import Darwin
|
||||
import Glibc
|
||||
#endif
|
||||
|
||||
@Suite("Custom Provider Registry")
|
||||
struct CustomProviderRegistryTests {
|
||||
@Test("Loads providers from profile config with comments and env vars")
|
||||
func loadProvidersFromProfile() throws {
|
||||
@Test
|
||||
func `Loads providers from profile config with comments and env vars`() throws {
|
||||
let fm = FileManager.default
|
||||
let tempHome = fm.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
try fm.createDirectory(at: tempHome, withIntermediateDirectories: true)
|
||||
|
||||
@ -2,10 +2,9 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Debug Configuration Tests")
|
||||
struct DebugConfigTests {
|
||||
@Test("Debug configuration behavior")
|
||||
func debugConfiguration() async {
|
||||
@Test
|
||||
func `Debug configuration behavior`() async {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
print("=== Testing instance-based configuration ===")
|
||||
@ -31,8 +30,8 @@ struct DebugConfigTests {
|
||||
#expect(Bool(true)) // Just to complete the test
|
||||
}
|
||||
|
||||
@Test("Multiple configurations isolation")
|
||||
func multipleConfigurationsIsolation() {
|
||||
@Test
|
||||
func `Multiple configurations isolation`() {
|
||||
let config1 = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
let config2 = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
|
||||
@ -2,10 +2,9 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Debug Exact Test")
|
||||
struct DebugExactTests {
|
||||
@Test("Replicate failing test exactly")
|
||||
func replicateFailingTest() async throws {
|
||||
@Test
|
||||
func `Replicate failing test exactly`() async {
|
||||
print("=== Starting exact replication ===")
|
||||
|
||||
await TestHelpers.withEmptyTestConfiguration { config in
|
||||
|
||||
@ -2,10 +2,9 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Direct Configuration Tests")
|
||||
struct DirectConfigTests {
|
||||
@Test("Direct configuration access")
|
||||
func directConfigAccess() {
|
||||
@Test
|
||||
func `Direct configuration access`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
// Just test basic access without test helpers
|
||||
@ -14,15 +13,15 @@ struct DirectConfigTests {
|
||||
#expect(Bool(true)) // If we get here, no infinite loop
|
||||
}
|
||||
|
||||
@Test("Provider enum direct access")
|
||||
func providerEnumDirect() {
|
||||
@Test
|
||||
func `Provider enum direct access`() {
|
||||
let provider = Provider.openai
|
||||
#expect(provider.identifier == "openai")
|
||||
#expect(provider.displayName == "OpenAI")
|
||||
}
|
||||
|
||||
@Test("Configuration instance creation")
|
||||
func configurationInstanceCreation() {
|
||||
@Test
|
||||
func `Configuration instance creation`() {
|
||||
let config1 = TachikomaConfiguration()
|
||||
let config2 = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
let config3 = TachikomaConfiguration(apiKeys: ["openai": "test"])
|
||||
|
||||
@ -2,12 +2,11 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Generation Function Tests")
|
||||
struct GenerationTests {
|
||||
// MARK: - Basic Generation Tests (Placeholder Providers)
|
||||
|
||||
@Test("Generate Function - OpenAI Provider")
|
||||
func generateFunctionOpenAI() async throws {
|
||||
@Test
|
||||
func `Generate Function - OpenAI Provider`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let result = try await generate(
|
||||
"What is 2+2?",
|
||||
@ -25,8 +24,8 @@ struct GenerationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Generate Function - Anthropic Provider")
|
||||
func generateFunctionAnthropic() async throws {
|
||||
@Test
|
||||
func `Generate Function - Anthropic Provider`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["anthropic": "test-key"]) { config in
|
||||
let result = try await generate(
|
||||
"Explain quantum physics",
|
||||
@ -42,8 +41,8 @@ struct GenerationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Generate Function - Default Model")
|
||||
func generateFunctionDefaultModel() async throws {
|
||||
@Test
|
||||
func `Generate Function - Default Model`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["anthropic": "test-key"]) { config in
|
||||
let result = try await generate("Hello world", configuration: config)
|
||||
|
||||
@ -52,8 +51,8 @@ struct GenerationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Generate Function - With System Prompt")
|
||||
func generateFunctionWithSystem() async throws {
|
||||
@Test
|
||||
func `Generate Function - With System Prompt`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let result = try await generate(
|
||||
"Tell me a joke",
|
||||
@ -73,8 +72,8 @@ struct GenerationTests {
|
||||
|
||||
// MARK: - Streaming Tests
|
||||
|
||||
@Test("Stream Function - Basic Streaming")
|
||||
func streamFunctionBasic() async throws {
|
||||
@Test
|
||||
func `Stream Function - Basic Streaming`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let stream = try await stream(
|
||||
"Count to 5",
|
||||
@ -101,8 +100,8 @@ struct GenerationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Stream Function - Anthropic Streaming")
|
||||
func streamFunctionAnthropic() async throws {
|
||||
@Test
|
||||
func `Stream Function - Anthropic Streaming`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["anthropic": "test-key"]) { config in
|
||||
let stream = try await stream(
|
||||
"Write a haiku",
|
||||
@ -135,8 +134,8 @@ struct GenerationTests {
|
||||
|
||||
// MARK: - Image Analysis Tests
|
||||
|
||||
@Test("Analyze Function - Vision Model")
|
||||
func analyzeFunctionVision() async throws {
|
||||
@Test
|
||||
func `Analyze Function - Vision Model`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let testImageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
|
||||
let result = try await analyze(
|
||||
@ -154,8 +153,8 @@ struct GenerationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Analyze Function - Non-Vision Model Error")
|
||||
func analyzeFunctionNonVisionError() async {
|
||||
@Test
|
||||
func `Analyze Function - Non-Vision Model Error`() async {
|
||||
_ = await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
// GPT-4.1 doesn't support vision
|
||||
await #expect(throws: TachikomaError.self) {
|
||||
@ -169,8 +168,8 @@ struct GenerationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Analyze Function - Default Vision Model")
|
||||
func analyzeFunctionDefaultVision() async throws {
|
||||
@Test
|
||||
func `Analyze Function - Default Vision Model`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
// Use base64 encoded test image (1x1 pixel PNG)
|
||||
let testImageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
|
||||
@ -191,8 +190,8 @@ struct GenerationTests {
|
||||
|
||||
// MARK: - Error Handling Tests
|
||||
|
||||
@Test("Generate Function - Missing API Key")
|
||||
func generateFunctionMissingAPIKey() async {
|
||||
@Test
|
||||
func `Generate Function - Missing API Key`() async {
|
||||
_ = await TestHelpers.withEmptyTestConfiguration { config in
|
||||
await #expect(throws: TachikomaError.self) {
|
||||
try await generate("Test", using: .openai(.gpt4o), configuration: config)
|
||||
@ -200,8 +199,8 @@ struct GenerationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Generate Function - Invalid Configuration")
|
||||
func generateFunctionInvalidConfig() async throws {
|
||||
@Test
|
||||
func `Generate Function - Invalid Configuration`() async {
|
||||
await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
// Test with invalid base URL format
|
||||
config.setBaseURL("not-a-url", for: .openai)
|
||||
@ -221,8 +220,8 @@ struct GenerationTests {
|
||||
|
||||
// MARK: - Tool Integration Tests
|
||||
|
||||
@Test("Generate Function - Without Tools")
|
||||
func generateFunctionWithoutTools() async throws {
|
||||
@Test
|
||||
func `Generate Function - Without Tools`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
// Test generation without tools
|
||||
let result = try await generate(
|
||||
@ -235,8 +234,8 @@ struct GenerationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Generate Function - With Custom Tools")
|
||||
func generateFunctionWithCustomTools() async throws {
|
||||
@Test
|
||||
func `Generate Function - With Custom Tools`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["anthropic": "test-key"]) { config in
|
||||
// Create a simple test tool
|
||||
let testTool = createTool(
|
||||
@ -262,8 +261,8 @@ struct GenerationTests {
|
||||
|
||||
// MARK: - Image Input Type Tests
|
||||
|
||||
@Test("Image Input Types")
|
||||
func imageInputTypes() {
|
||||
@Test
|
||||
func `Image Input Types`() {
|
||||
let base64Image = ImageInput.base64("test-data")
|
||||
let urlImage = ImageInput.url("https://example.com/image.jpg")
|
||||
let fileImage = ImageInput.filePath("/path/to/image.png")
|
||||
|
||||
@ -2,7 +2,6 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Grok Model Catalog Tests")
|
||||
struct GrokModelCatalogTests {
|
||||
private static let catalog: [Model.Grok] = [
|
||||
.grok4,
|
||||
@ -26,15 +25,15 @@ struct GrokModelCatalogTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("CaseIterable reflects the official Grok catalog")
|
||||
func catalogAlignment() {
|
||||
@Test
|
||||
func `CaseIterable reflects the official Grok catalog`() {
|
||||
self.requireModernPlatforms {
|
||||
#expect(Model.Grok.allCases == Self.catalog)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("ModelSelector parses every Grok model identifier")
|
||||
func selectorParsesCatalog() throws {
|
||||
@Test
|
||||
func `ModelSelector parses every Grok model identifier`() throws {
|
||||
try self.requireModernPlatforms {
|
||||
for model in Self.catalog {
|
||||
let parsed = try ModelSelector.parseModel(model.modelId)
|
||||
@ -43,8 +42,8 @@ struct GrokModelCatalogTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Available-model CLI listing matches catalog IDs")
|
||||
func availableModelListingMatchesCatalog() {
|
||||
@Test
|
||||
func `Available-model CLI listing matches catalog IDs`() {
|
||||
self.requireModernPlatforms {
|
||||
let listed = Set(ModelSelector.availableModels(for: "grok"))
|
||||
let expected = Set(Self.catalog.map(\.modelId))
|
||||
@ -52,8 +51,8 @@ struct GrokModelCatalogTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Vision capability only flips on for vision/image Grok models")
|
||||
func visionCapabilityMatchesModelType() {
|
||||
@Test
|
||||
func `Vision capability only flips on for vision/image Grok models`() {
|
||||
self.requireModernPlatforms {
|
||||
let visionModels: Set<Model.Grok> = [.grok2Vision, .grok2Image, .grokVisionBeta]
|
||||
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("LanguageModel enums")
|
||||
struct LanguageModelCoverageTests {
|
||||
@Test("OpenAI enum exposes properties")
|
||||
func openAIProperties() {
|
||||
@Test
|
||||
func `OpenAI enum exposes properties`() {
|
||||
let models = LanguageModel.OpenAI.allCases
|
||||
#expect(!models.isEmpty)
|
||||
for model in models {
|
||||
@ -18,8 +17,8 @@ struct LanguageModelCoverageTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Anthropic enum exposes properties")
|
||||
func anthropicProperties() {
|
||||
@Test
|
||||
func `Anthropic enum exposes properties`() {
|
||||
for model in LanguageModel.Anthropic.allCases {
|
||||
#expect(!model.modelId.isEmpty)
|
||||
_ = model.supportsVision
|
||||
@ -27,8 +26,8 @@ struct LanguageModelCoverageTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Remaining provider enums expose properties")
|
||||
func otherProviders() {
|
||||
@Test
|
||||
func `Remaining provider enums expose properties`() {
|
||||
for model in LanguageModel.Google.allCases {
|
||||
#expect(!model.rawValue.isEmpty)
|
||||
_ = model.supportsVision
|
||||
@ -66,8 +65,8 @@ struct LanguageModelCoverageTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("LanguageModel top level switches")
|
||||
func languageModelDescriptions() {
|
||||
@Test
|
||||
func `LanguageModel top level switches`() {
|
||||
let baseModels: [LanguageModel] = [
|
||||
.openai(.gpt51),
|
||||
.anthropic(.opus45),
|
||||
@ -100,10 +99,21 @@ struct LanguageModelCoverageTests {
|
||||
}
|
||||
|
||||
private struct DummyProvider: ModelProvider {
|
||||
var modelId: String { "dummy" }
|
||||
var baseURL: String? { nil }
|
||||
var apiKey: String? { nil }
|
||||
var capabilities: ModelCapabilities { .init() }
|
||||
var modelId: String {
|
||||
"dummy"
|
||||
}
|
||||
|
||||
var baseURL: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var apiKey: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var capabilities: ModelCapabilities {
|
||||
.init()
|
||||
}
|
||||
|
||||
func generateText(request _: ProviderRequest) async throws -> ProviderResponse {
|
||||
.init(text: "dummy")
|
||||
|
||||
@ -3,12 +3,11 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
@testable import TachikomaAgent
|
||||
|
||||
@Suite("Minimal Modern API Tests")
|
||||
struct MinimalModernAPITests {
|
||||
// MARK: - Model Tests
|
||||
|
||||
@Test("Model enum construction")
|
||||
func modelEnumConstruction() {
|
||||
@Test
|
||||
func `Model enum construction`() {
|
||||
// Test that model enums can be constructed
|
||||
let openaiModel = Model.openai(.gpt4o)
|
||||
let anthropicModel = Model.anthropic(.opus45)
|
||||
@ -31,8 +30,8 @@ struct MinimalModernAPITests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Model default value")
|
||||
func modelDefaultValue() {
|
||||
@Test
|
||||
func `Model default value`() {
|
||||
let defaultModel = Model.default
|
||||
// Should compile without errors
|
||||
switch defaultModel {
|
||||
@ -45,8 +44,8 @@ struct MinimalModernAPITests {
|
||||
|
||||
// MARK: - Tool System Tests
|
||||
|
||||
@Test("AgentTool creation")
|
||||
func agentToolCreation() {
|
||||
@Test
|
||||
func `AgentTool creation`() {
|
||||
let tool = Tachikoma.createTool(
|
||||
name: "test_tool",
|
||||
description: "A test tool",
|
||||
@ -60,8 +59,8 @@ struct MinimalModernAPITests {
|
||||
#expect(tool.description == "A test tool")
|
||||
}
|
||||
|
||||
@Test("AgentToolArguments parsing")
|
||||
func agentToolArgumentsParsing() throws {
|
||||
@Test
|
||||
func `AgentToolArguments parsing`() throws {
|
||||
let args = AgentToolArguments([
|
||||
"name": AnyAgentToolValue(string: "test"),
|
||||
"value": AnyAgentToolValue(int: 42),
|
||||
@ -73,8 +72,8 @@ struct MinimalModernAPITests {
|
||||
#expect(args.optionalStringValue("missing") ?? "default" == "default")
|
||||
}
|
||||
|
||||
@Test("Built-in tools exist")
|
||||
func builtInToolsExist() {
|
||||
@Test
|
||||
func `Built-in tools exist`() {
|
||||
// Test that built-in tools are available
|
||||
#expect(weatherTool.name == "get_weather")
|
||||
#expect(timeTool.name == "get_current_time")
|
||||
@ -87,8 +86,8 @@ struct MinimalModernAPITests {
|
||||
extension MinimalModernAPITests {
|
||||
// MARK: - Error Types
|
||||
|
||||
@Test("Tool error types")
|
||||
func toolErrorTypes() {
|
||||
@Test
|
||||
func `Tool error types`() {
|
||||
let toolError = AgentToolError.invalidInput("test")
|
||||
#expect(toolError.errorDescription != nil)
|
||||
|
||||
@ -98,8 +97,8 @@ extension MinimalModernAPITests {
|
||||
|
||||
// MARK: - Conversation Tests
|
||||
|
||||
@Test("Conversation basic functionality")
|
||||
func conversationBasic() {
|
||||
@Test
|
||||
func `Conversation basic functionality`() {
|
||||
let conversation = Conversation()
|
||||
#expect(conversation.messages.isEmpty)
|
||||
|
||||
@ -114,8 +113,8 @@ extension MinimalModernAPITests {
|
||||
|
||||
// MARK: - Basic Type Tests
|
||||
|
||||
@Test("ConversationMessage basic properties")
|
||||
func conversationMessageBasic() {
|
||||
@Test
|
||||
func `ConversationMessage basic properties`() {
|
||||
let message = ConversationMessage(
|
||||
id: "test",
|
||||
role: .user,
|
||||
|
||||
@ -2,12 +2,10 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Model Capabilities Tests")
|
||||
struct ModelCapabilitiesTests {
|
||||
@Suite("Capability Detection")
|
||||
enum ModelCapabilitiesTests {
|
||||
struct CapabilityDetectionTests {
|
||||
@Test("GPT-5 models exclude temperature and topP")
|
||||
func gPT5ExcludesTemperature() {
|
||||
@Test
|
||||
func `GPT-5 models exclude temperature and topP`() {
|
||||
let models: [LanguageModel] = [
|
||||
.openai(.gpt52),
|
||||
.openai(.gpt51),
|
||||
@ -28,16 +26,16 @@ struct ModelCapabilitiesTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Gemini 3 Flash supports thinking config options")
|
||||
func gemini3FlashThinkingConfig() {
|
||||
@Test
|
||||
func `Gemini 3 Flash supports thinking config options`() {
|
||||
let capabilities = ModelCapabilityRegistry.shared.capabilities(for: .google(.gemini3Flash))
|
||||
#expect(capabilities.supportsTopK)
|
||||
#expect(capabilities.supportedProviderOptions.supportsThinkingConfig)
|
||||
#expect(capabilities.supportedProviderOptions.supportsSafetySettings)
|
||||
}
|
||||
|
||||
@Test("Reasoning models have forced temperature")
|
||||
func reasoningModelsFixedTemperature() {
|
||||
@Test
|
||||
func `Reasoning models have forced temperature`() {
|
||||
let model = LanguageModel.openai(.o4Mini)
|
||||
let capabilities = ModelCapabilityRegistry.shared.capabilities(for: model)
|
||||
|
||||
@ -50,8 +48,8 @@ struct ModelCapabilitiesTests {
|
||||
#expect(capabilities.supportedProviderOptions.supportsPreviousResponseId)
|
||||
}
|
||||
|
||||
@Test("GPT-4 models support standard parameters")
|
||||
func gPT4StandardParameters() {
|
||||
@Test
|
||||
func `GPT-4 models support standard parameters`() {
|
||||
let models: [LanguageModel] = [
|
||||
.openai(.gpt4o),
|
||||
.openai(.gpt4oMini),
|
||||
@ -73,8 +71,8 @@ struct ModelCapabilitiesTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Claude models support thinking")
|
||||
func claudeThinkingSupport() {
|
||||
@Test
|
||||
func `Claude models support thinking`() {
|
||||
let models: [LanguageModel] = [
|
||||
.anthropic(.opus4),
|
||||
.anthropic(.sonnet4),
|
||||
@ -90,8 +88,8 @@ struct ModelCapabilitiesTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Google models support topK and thinking")
|
||||
func googleCapabilities() {
|
||||
@Test
|
||||
func `Google models support topK and thinking`() {
|
||||
let models: [LanguageModel] = [
|
||||
.google(.gemini25Pro),
|
||||
.google(.gemini25Flash),
|
||||
@ -107,8 +105,8 @@ struct ModelCapabilitiesTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Mistral models support safe mode")
|
||||
func mistralCapabilities() {
|
||||
@Test
|
||||
func `Mistral models support safe mode`() {
|
||||
let models: [LanguageModel] = [
|
||||
.mistral(.large2),
|
||||
.mistral(.codestral),
|
||||
@ -121,8 +119,8 @@ struct ModelCapabilitiesTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Groq models support speed level")
|
||||
func groqCapabilities() {
|
||||
@Test
|
||||
func `Groq models support speed level`() {
|
||||
let models: [LanguageModel] = [
|
||||
.groq(.llama3170b),
|
||||
.groq(.llama370b),
|
||||
@ -135,8 +133,8 @@ struct ModelCapabilitiesTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Grok models support fun mode")
|
||||
func grokCapabilities() {
|
||||
@Test
|
||||
func `Grok models support fun mode`() {
|
||||
let models: [LanguageModel] = [
|
||||
.grok(.grok4),
|
||||
.grok(.grok3),
|
||||
@ -151,10 +149,9 @@ struct ModelCapabilitiesTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Settings Validation")
|
||||
struct SettingsValidationTests {
|
||||
@Test("Validate settings for GPT-5.1")
|
||||
func validateGPT51Settings() {
|
||||
@Test
|
||||
func `Validate settings for GPT-5.1`() {
|
||||
let settings = GenerationSettings(
|
||||
maxTokens: 1000,
|
||||
temperature: 0.7,
|
||||
@ -180,8 +177,8 @@ struct ModelCapabilitiesTests {
|
||||
#expect(validated.providerOptions.openai?.previousResponseId == "test-123") // Kept
|
||||
}
|
||||
|
||||
@Test("Validate settings for O3 with forced temperature")
|
||||
func validateO3Settings() {
|
||||
@Test
|
||||
func `Validate settings for O3 with forced temperature`() {
|
||||
let settings = GenerationSettings(
|
||||
temperature: 0.5,
|
||||
topP: 0.8,
|
||||
@ -201,8 +198,8 @@ struct ModelCapabilitiesTests {
|
||||
#expect(validated.providerOptions.openai?.verbosity == nil) // Removed
|
||||
}
|
||||
|
||||
@Test("Validate settings for GPT-4")
|
||||
func validateGPT4Settings() {
|
||||
@Test
|
||||
func `Validate settings for GPT-4`() {
|
||||
let settings = GenerationSettings(
|
||||
maxTokens: 2000,
|
||||
temperature: 0.8,
|
||||
@ -232,8 +229,8 @@ struct ModelCapabilitiesTests {
|
||||
#expect(validated.providerOptions.openai?.topLogprobs == 3)
|
||||
}
|
||||
|
||||
@Test("Validate Anthropic options")
|
||||
func validateAnthropicOptions() {
|
||||
@Test
|
||||
func `Validate Anthropic options`() {
|
||||
let settings = GenerationSettings(
|
||||
temperature: 0.7,
|
||||
providerOptions: .init(
|
||||
@ -257,10 +254,9 @@ struct ModelCapabilitiesTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Custom Model Registration")
|
||||
struct CustomModelTests {
|
||||
@Test("Register custom model capabilities")
|
||||
func registerCustomCapabilities() {
|
||||
@Test
|
||||
func `Register custom model capabilities`() {
|
||||
let customCaps = ModelParameterCapabilities(
|
||||
supportsTemperature: false,
|
||||
supportsTopP: false,
|
||||
@ -282,8 +278,8 @@ struct ModelCapabilitiesTests {
|
||||
#expect(retrieved.excludedParameters.contains("temperature"))
|
||||
}
|
||||
|
||||
@Test("OpenAI-compatible model registration")
|
||||
func openAICompatibleRegistration() {
|
||||
@Test
|
||||
func `OpenAI-compatible model registration`() {
|
||||
let capabilities = ModelParameterCapabilities(
|
||||
supportsTemperature: true,
|
||||
supportsTopK: true,
|
||||
@ -307,10 +303,9 @@ struct ModelCapabilitiesTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Thread Safety")
|
||||
struct ThreadSafetyTests {
|
||||
@Test("Concurrent capability access")
|
||||
func concurrentAccess() async {
|
||||
@Test
|
||||
func `Concurrent capability access`() async {
|
||||
let models: [LanguageModel] = [
|
||||
.openai(.gpt51),
|
||||
.openai(.gpt4o),
|
||||
@ -347,7 +342,7 @@ struct ModelCapabilitiesTests {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for testing custom models
|
||||
/// Helper for testing custom models
|
||||
private struct TestModelProvider: ModelProvider {
|
||||
let modelId: String
|
||||
let baseURL: String? = nil
|
||||
|
||||
@ -1,52 +1,51 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("LanguageModel parsing")
|
||||
struct ModelParsingTests {
|
||||
@Test("parse GPT-5 mini alias")
|
||||
func parseGPT5Mini() {
|
||||
@Test
|
||||
func `parse GPT-5 mini alias`() {
|
||||
let parsed = LanguageModel.parse(from: "gpt-5-mini")
|
||||
#expect(parsed == .openai(.gpt5Mini))
|
||||
}
|
||||
|
||||
@Test("parse GPT-5.1 base model")
|
||||
func parseGPT51() {
|
||||
@Test
|
||||
func `parse GPT-5.1 base model`() {
|
||||
let parsed = LanguageModel.parse(from: "gpt-5.1")
|
||||
#expect(parsed == .openai(.gpt51))
|
||||
}
|
||||
|
||||
@Test("parse GPT-5.2 base model")
|
||||
func parseGPT52() {
|
||||
@Test
|
||||
func `parse GPT-5.2 base model`() {
|
||||
let parsed = LanguageModel.parse(from: "gpt-5.2")
|
||||
#expect(parsed == .openai(.gpt52))
|
||||
}
|
||||
|
||||
@Test("parse GPT-5.1 nano alias")
|
||||
func parseGPT51Nano() {
|
||||
@Test
|
||||
func `parse GPT-5.1 nano alias`() {
|
||||
let parsed = LanguageModel.parse(from: "gpt51-nano")
|
||||
#expect(parsed == .openai(.gpt5Nano))
|
||||
}
|
||||
|
||||
@Test("parse Claude Sonnet 4.5 snapshot id")
|
||||
func parseClaudeSonnetSnapshot() {
|
||||
@Test
|
||||
func `parse Claude Sonnet 4.5 snapshot id`() {
|
||||
let parsed = LanguageModel.parse(from: "claude-sonnet-4-5-20250929")
|
||||
#expect(parsed == .anthropic(.sonnet45))
|
||||
}
|
||||
|
||||
@Test("parse shorthand Claude alias")
|
||||
func parseClaudeAlias() {
|
||||
@Test
|
||||
func `parse shorthand Claude alias`() {
|
||||
let parsed = LanguageModel.parse(from: "claude")
|
||||
#expect(parsed == .anthropic(.sonnet45))
|
||||
}
|
||||
|
||||
@Test("parse Gemini 3 Flash model id")
|
||||
func parseGemini3Flash() {
|
||||
@Test
|
||||
func `parse Gemini 3 Flash model id`() {
|
||||
let parsed = LanguageModel.parse(from: "gemini-3-flash")
|
||||
#expect(parsed == .google(.gemini3Flash))
|
||||
}
|
||||
|
||||
@Test("parse shorthand Gemini alias")
|
||||
func parseGeminiAlias() {
|
||||
@Test
|
||||
func `parse shorthand Gemini alias`() {
|
||||
let parsed = LanguageModel.parse(from: "gemini")
|
||||
#expect(parsed == .google(.gemini3Flash))
|
||||
}
|
||||
|
||||
@ -6,14 +6,14 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
#if os(Linux)
|
||||
@Suite("OpenAICompatibleHelper Tests", .disabled("URLProtocol mocking unavailable on Linux"))
|
||||
@Suite(.disabled("URLProtocol mocking unavailable on Linux"))
|
||||
struct OpenAICompatibleHelperTests {}
|
||||
#else
|
||||
|
||||
@Suite("OpenAICompatibleHelper Tests", .serialized)
|
||||
@Suite(.serialized)
|
||||
struct OpenAICompatibleHelperTests {
|
||||
@Test("generateText encodes stop sequences, headers, and tool definitions")
|
||||
func generateTextEncodesPayload() async throws {
|
||||
@Test
|
||||
func `generateText encodes stop sequences, headers, and tool definitions`() async throws {
|
||||
let tool = AgentTool(
|
||||
name: "lookup",
|
||||
description: "Lookup a value",
|
||||
@ -75,8 +75,8 @@ struct OpenAICompatibleHelperTests {
|
||||
#expect(required == ["query"])
|
||||
}
|
||||
|
||||
@Test("streamText emits deltas as SSE chunks arrive")
|
||||
func streamTextEmitsDeltas() async throws {
|
||||
@Test
|
||||
func `streamText emits deltas as SSE chunks arrive`() async throws {
|
||||
let request = ProviderRequest(
|
||||
messages: [ModelMessage(role: .user, content: [.text("stream")])],
|
||||
)
|
||||
@ -121,8 +121,8 @@ struct OpenAICompatibleHelperTests {
|
||||
#expect(deltas == "Hello world")
|
||||
}
|
||||
|
||||
@Test("non-200 responses surface TachikomaError.apiError")
|
||||
func apiErrorsSurface() async throws {
|
||||
@Test
|
||||
func `non-200 responses surface TachikomaError.apiError`() async {
|
||||
await self.withMockedSession { urlRequest in
|
||||
let errorJSON = """
|
||||
{"error":{"message":"bad request","type":"invalid_request_error"}}
|
||||
|
||||
@ -2,12 +2,10 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Provider Options Tests")
|
||||
struct ProviderOptionsTests {
|
||||
@Suite("Provider Options Creation")
|
||||
enum ProviderOptionsTests {
|
||||
struct CreationTests {
|
||||
@Test("Create OpenAI options")
|
||||
func openAIOptions() {
|
||||
@Test
|
||||
func `Create OpenAI options`() {
|
||||
let options = OpenAIOptions(
|
||||
parallelToolCalls: false,
|
||||
responseFormat: .json,
|
||||
@ -35,8 +33,8 @@ struct ProviderOptionsTests {
|
||||
#expect(options.topLogprobs == 5)
|
||||
}
|
||||
|
||||
@Test("Create Anthropic options")
|
||||
func anthropicOptions() {
|
||||
@Test
|
||||
func `Create Anthropic options`() {
|
||||
let options = AnthropicOptions(
|
||||
thinking: .enabled(budgetTokens: 5000),
|
||||
cacheControl: .persistent,
|
||||
@ -57,8 +55,8 @@ struct ProviderOptionsTests {
|
||||
#expect(options.metadata?["key"] == "value")
|
||||
}
|
||||
|
||||
@Test("Create Google options")
|
||||
func googleOptions() {
|
||||
@Test
|
||||
func `Create Google options`() {
|
||||
let options = GoogleOptions(
|
||||
thinkingConfig: .init(budgetTokens: 3000, includeThoughts: true),
|
||||
safetySettings: .moderate,
|
||||
@ -73,8 +71,8 @@ struct ProviderOptionsTests {
|
||||
#expect(options.stopSequences == ["###"])
|
||||
}
|
||||
|
||||
@Test("Create Mistral options")
|
||||
func mistralOptions() {
|
||||
@Test
|
||||
func `Create Mistral options`() {
|
||||
let options = MistralOptions(
|
||||
safeMode: true,
|
||||
randomSeed: 12345,
|
||||
@ -84,14 +82,14 @@ struct ProviderOptionsTests {
|
||||
#expect(options.randomSeed == 12345)
|
||||
}
|
||||
|
||||
@Test("Create Groq options")
|
||||
func groqOptions() {
|
||||
@Test
|
||||
func `Create Groq options`() {
|
||||
let options = GroqOptions(speed: .ultraFast)
|
||||
#expect(options.speed == .ultraFast)
|
||||
}
|
||||
|
||||
@Test("Create Grok options")
|
||||
func grokOptions() {
|
||||
@Test
|
||||
func `Create Grok options`() {
|
||||
let options = GrokOptions(
|
||||
funMode: true,
|
||||
includeCurrentEvents: true,
|
||||
@ -102,10 +100,9 @@ struct ProviderOptionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Provider Options Container")
|
||||
struct ContainerTests {
|
||||
@Test("Create provider options container")
|
||||
func providerOptionsContainer() {
|
||||
@Test
|
||||
func `Create provider options container`() {
|
||||
let options = ProviderOptions(
|
||||
openai: .init(verbosity: .high),
|
||||
anthropic: .init(thinking: .disabled),
|
||||
@ -123,8 +120,8 @@ struct ProviderOptionsTests {
|
||||
#expect(options.grok?.funMode == false)
|
||||
}
|
||||
|
||||
@Test("Empty provider options")
|
||||
func emptyProviderOptions() {
|
||||
@Test
|
||||
func `Empty provider options`() {
|
||||
let options = ProviderOptions()
|
||||
|
||||
#expect(options.openai == nil)
|
||||
@ -136,10 +133,9 @@ struct ProviderOptionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("GenerationSettings Integration")
|
||||
struct SettingsIntegrationTests {
|
||||
@Test("Settings with provider options")
|
||||
func settingsWithProviderOptions() {
|
||||
@Test
|
||||
func `Settings with provider options`() {
|
||||
let settings = GenerationSettings(
|
||||
maxTokens: 1000,
|
||||
temperature: 0.7,
|
||||
@ -157,8 +153,8 @@ struct ProviderOptionsTests {
|
||||
#expect(settings.providerOptions.openai?.parallelToolCalls == true)
|
||||
}
|
||||
|
||||
@Test("Settings with empty provider options")
|
||||
func settingsWithEmptyProviderOptions() {
|
||||
@Test
|
||||
func `Settings with empty provider options`() {
|
||||
let settings = GenerationSettings(
|
||||
maxTokens: 500,
|
||||
temperature: 0.5,
|
||||
@ -170,10 +166,9 @@ struct ProviderOptionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Codable Conformance")
|
||||
struct CodableTests {
|
||||
@Test("Encode and decode OpenAI options")
|
||||
func openAIOptionsCodable() throws {
|
||||
@Test
|
||||
func `Encode and decode OpenAI options`() throws {
|
||||
let original = OpenAIOptions(
|
||||
parallelToolCalls: true,
|
||||
seed: 42,
|
||||
@ -193,8 +188,8 @@ struct ProviderOptionsTests {
|
||||
#expect(decoded.seed == original.seed)
|
||||
}
|
||||
|
||||
@Test("Encode and decode Anthropic thinking mode")
|
||||
func anthropicThinkingCodable() throws {
|
||||
@Test
|
||||
func `Encode and decode Anthropic thinking mode`() throws {
|
||||
let original = AnthropicOptions(
|
||||
thinking: .enabled(budgetTokens: 3000),
|
||||
)
|
||||
@ -212,8 +207,8 @@ struct ProviderOptionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Encode and decode provider options container")
|
||||
func providerOptionsCodable() throws {
|
||||
@Test
|
||||
func `Encode and decode provider options container`() throws {
|
||||
let original = ProviderOptions(
|
||||
openai: .init(verbosity: .low),
|
||||
anthropic: .init(cacheControl: .ephemeral),
|
||||
@ -231,8 +226,8 @@ struct ProviderOptionsTests {
|
||||
#expect(decoded.google?.safetySettings == .relaxed)
|
||||
}
|
||||
|
||||
@Test("Encode and decode GenerationSettings with provider options")
|
||||
func generationSettingsCodable() throws {
|
||||
@Test
|
||||
func `Encode and decode GenerationSettings with provider options`() throws {
|
||||
let original = GenerationSettings(
|
||||
maxTokens: 2000,
|
||||
temperature: 0.8,
|
||||
@ -257,45 +252,44 @@ struct ProviderOptionsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Enum Value Tests")
|
||||
struct EnumValueTests {
|
||||
@Test("OpenAI verbosity values")
|
||||
func openAIVerbosityValues() {
|
||||
@Test
|
||||
func `OpenAI verbosity values`() {
|
||||
#expect(OpenAIOptions.Verbosity.low.rawValue == "low")
|
||||
#expect(OpenAIOptions.Verbosity.medium.rawValue == "medium")
|
||||
#expect(OpenAIOptions.Verbosity.high.rawValue == "high")
|
||||
}
|
||||
|
||||
@Test("OpenAI reasoning effort values")
|
||||
func openAIReasoningEffortValues() {
|
||||
@Test
|
||||
func `OpenAI reasoning effort values`() {
|
||||
#expect(OpenAIOptions.ReasoningEffort.minimal.rawValue == "minimal")
|
||||
#expect(OpenAIOptions.ReasoningEffort.low.rawValue == "low")
|
||||
#expect(OpenAIOptions.ReasoningEffort.medium.rawValue == "medium")
|
||||
#expect(OpenAIOptions.ReasoningEffort.high.rawValue == "high")
|
||||
}
|
||||
|
||||
@Test("OpenAI response format values")
|
||||
func openAIResponseFormatValues() {
|
||||
@Test
|
||||
func `OpenAI response format values`() {
|
||||
#expect(OpenAIOptions.ResponseFormat.text.rawValue == "text")
|
||||
#expect(OpenAIOptions.ResponseFormat.json.rawValue == "json_object")
|
||||
#expect(OpenAIOptions.ResponseFormat.jsonSchema.rawValue == "json_schema")
|
||||
}
|
||||
|
||||
@Test("Anthropic cache control values")
|
||||
func anthropicCacheControlValues() {
|
||||
@Test
|
||||
func `Anthropic cache control values`() {
|
||||
#expect(AnthropicOptions.CacheControl.ephemeral.rawValue == "ephemeral")
|
||||
#expect(AnthropicOptions.CacheControl.persistent.rawValue == "persistent")
|
||||
}
|
||||
|
||||
@Test("Google safety settings values")
|
||||
func googleSafetySettingsValues() {
|
||||
@Test
|
||||
func `Google safety settings values`() {
|
||||
#expect(GoogleOptions.SafetySettings.strict.rawValue == "strict")
|
||||
#expect(GoogleOptions.SafetySettings.moderate.rawValue == "moderate")
|
||||
#expect(GoogleOptions.SafetySettings.relaxed.rawValue == "relaxed")
|
||||
}
|
||||
|
||||
@Test("Groq speed level values")
|
||||
func groqSpeedLevelValues() {
|
||||
@Test
|
||||
func `Groq speed level values`() {
|
||||
#expect(GroqOptions.SpeedLevel.normal.rawValue == "normal")
|
||||
#expect(GroqOptions.SpeedLevel.fast.rawValue == "fast")
|
||||
#expect(GroqOptions.SpeedLevel.ultraFast.rawValue == "ultra_fast")
|
||||
|
||||
@ -10,7 +10,9 @@ import Testing
|
||||
private func withTemporaryEnvironment<T: Sendable>(
|
||||
_ updates: [String: String?],
|
||||
_ body: @Sendable () throws -> T,
|
||||
) async rethrows -> T {
|
||||
) async rethrows
|
||||
-> T
|
||||
{
|
||||
try await TestEnvironmentMutex.shared.withLock {
|
||||
let saved = updates.keys.map { key in
|
||||
(key, getenv(key).map { String(cString: $0) })
|
||||
@ -38,12 +40,10 @@ private func withTemporaryEnvironment<T: Sendable>(
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Provider Enum Tests")
|
||||
struct ProviderTests {
|
||||
@Suite("Provider Properties Tests")
|
||||
enum ProviderTests {
|
||||
struct ProviderPropertiesTests {
|
||||
@Test("Standard providers have correct identifiers")
|
||||
func standardProviderIdentifiers() {
|
||||
@Test
|
||||
func `Standard providers have correct identifiers`() {
|
||||
#expect(Provider.openai.identifier == "openai")
|
||||
#expect(Provider.anthropic.identifier == "anthropic")
|
||||
#expect(Provider.grok.identifier == "grok")
|
||||
@ -54,14 +54,14 @@ struct ProviderTests {
|
||||
#expect(Provider.azureOpenAI.identifier == "azure-openai")
|
||||
}
|
||||
|
||||
@Test("Custom provider has correct identifier")
|
||||
func customProviderIdentifier() {
|
||||
@Test
|
||||
func `Custom provider has correct identifier`() {
|
||||
let customProvider = Provider.custom("my-custom-provider")
|
||||
#expect(customProvider.identifier == "my-custom-provider")
|
||||
}
|
||||
|
||||
@Test("Display names are human-readable")
|
||||
func displayNames() {
|
||||
@Test
|
||||
func `Display names are human-readable`() {
|
||||
#expect(Provider.openai.displayName == "OpenAI")
|
||||
#expect(Provider.anthropic.displayName == "Anthropic")
|
||||
#expect(Provider.grok.displayName == "Grok")
|
||||
@ -73,8 +73,8 @@ struct ProviderTests {
|
||||
#expect(Provider.custom("test").displayName == "Test")
|
||||
}
|
||||
|
||||
@Test("Environment variables are correct")
|
||||
func environmentVariables() {
|
||||
@Test
|
||||
func `Environment variables are correct`() {
|
||||
#expect(Provider.openai.environmentVariable == "OPENAI_API_KEY")
|
||||
#expect(Provider.anthropic.environmentVariable == "ANTHROPIC_API_KEY")
|
||||
#expect(Provider.grok.environmentVariable == "X_AI_API_KEY")
|
||||
@ -86,8 +86,8 @@ struct ProviderTests {
|
||||
#expect(Provider.custom("test").environmentVariable.isEmpty)
|
||||
}
|
||||
|
||||
@Test("Alternative environment variables")
|
||||
func alternativeEnvironmentVariables() {
|
||||
@Test
|
||||
func `Alternative environment variables`() {
|
||||
#expect(Provider.grok.alternativeEnvironmentVariables == ["XAI_API_KEY", "GROK_API_KEY"])
|
||||
#expect(Provider.google.alternativeEnvironmentVariables == ["GOOGLE_API_KEY"])
|
||||
#expect(Provider.openai.alternativeEnvironmentVariables.isEmpty)
|
||||
@ -98,8 +98,8 @@ struct ProviderTests {
|
||||
])
|
||||
}
|
||||
|
||||
@Test("Default base URLs")
|
||||
func defaultBaseURLs() {
|
||||
@Test
|
||||
func `Default base URLs`() {
|
||||
#expect(Provider.openai.defaultBaseURL == "https://api.openai.com/v1")
|
||||
#expect(Provider.anthropic.defaultBaseURL == "https://api.anthropic.com")
|
||||
#expect(Provider.grok.defaultBaseURL == "https://api.x.ai/v1")
|
||||
@ -111,8 +111,8 @@ struct ProviderTests {
|
||||
#expect(Provider.custom("test").defaultBaseURL == nil)
|
||||
}
|
||||
|
||||
@Test("API key requirements")
|
||||
func apiKeyRequirements() {
|
||||
@Test
|
||||
func `API key requirements`() {
|
||||
#expect(Provider.openai.requiresAPIKey == true)
|
||||
#expect(Provider.anthropic.requiresAPIKey == true)
|
||||
#expect(Provider.grok.requiresAPIKey == true)
|
||||
@ -125,10 +125,9 @@ struct ProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Provider Factory Tests")
|
||||
struct ProviderFactoryTests {
|
||||
@Test("Create provider from identifier - standard providers")
|
||||
func createStandardProviders() {
|
||||
@Test
|
||||
func `Create provider from identifier - standard providers`() {
|
||||
#expect(Provider.from(identifier: "openai") == .openai)
|
||||
#expect(Provider.from(identifier: "anthropic") == .anthropic)
|
||||
#expect(Provider.from(identifier: "grok") == .grok)
|
||||
@ -139,15 +138,15 @@ struct ProviderTests {
|
||||
#expect(Provider.from(identifier: "azure-openai") == .azureOpenAI)
|
||||
}
|
||||
|
||||
@Test("Create provider from identifier - case insensitive")
|
||||
func createProvidersCase() {
|
||||
@Test
|
||||
func `Create provider from identifier - case insensitive`() {
|
||||
#expect(Provider.from(identifier: "OpenAI") == .openai)
|
||||
#expect(Provider.from(identifier: "ANTHROPIC") == .anthropic)
|
||||
#expect(Provider.from(identifier: "Grok") == .grok)
|
||||
}
|
||||
|
||||
@Test("Create provider from identifier - custom providers")
|
||||
func createCustomProviders() {
|
||||
@Test
|
||||
func `Create provider from identifier - custom providers`() {
|
||||
let provider1 = Provider.from(identifier: "custom-provider")
|
||||
let provider2 = Provider.from(identifier: "unknown-provider")
|
||||
|
||||
@ -164,8 +163,8 @@ struct ProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Standard providers list")
|
||||
func standardProvidersList() {
|
||||
@Test
|
||||
func `Standard providers list`() {
|
||||
let expected: [Provider] = [
|
||||
.openai,
|
||||
.anthropic,
|
||||
@ -180,31 +179,30 @@ struct ProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Environment Variable Loading Tests")
|
||||
struct EnvironmentVariableTests {
|
||||
@Test("Load API key from primary environment variable")
|
||||
func loadFromPrimaryEnvironment() {
|
||||
@Test
|
||||
func `Load API key from primary environment variable`() {
|
||||
// We can't easily mock ProcessInfo.processInfo.environment in tests,
|
||||
// so we'll test the logic indirectly through TachikomaConfiguration
|
||||
}
|
||||
|
||||
@Test("Load API key from alternative environment variable")
|
||||
func loadFromAlternativeEnvironment() {
|
||||
@Test
|
||||
func `Load API key from alternative environment variable`() {
|
||||
// Test that Grok provider loads from XAI_API_KEY when X_AI_API_KEY is not available
|
||||
let provider = Provider.grok
|
||||
#expect(provider.environmentVariable == "X_AI_API_KEY")
|
||||
#expect(provider.alternativeEnvironmentVariables == ["XAI_API_KEY", "GROK_API_KEY"])
|
||||
}
|
||||
|
||||
@Test("Custom providers don't have environment variables")
|
||||
func customProviderNoEnvironment() {
|
||||
@Test
|
||||
func `Custom providers don't have environment variables`() {
|
||||
let customProvider = Provider.custom("test")
|
||||
#expect(customProvider.environmentVariable.isEmpty)
|
||||
#expect(customProvider.alternativeEnvironmentVariables.isEmpty)
|
||||
}
|
||||
|
||||
@Test("Google ignores ADC credential paths as API keys")
|
||||
func googleIgnoresADCCredentialPaths() async throws {
|
||||
@Test
|
||||
func `Google ignores ADC credential paths as API keys`() async {
|
||||
let resolved = await withTemporaryEnvironment([
|
||||
"GEMINI_API_KEY": nil,
|
||||
"GOOGLE_API_KEY": nil,
|
||||
@ -217,10 +215,9 @@ struct ProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Codable Tests")
|
||||
struct CodableTests {
|
||||
@Test("Provider encodes to identifier string")
|
||||
func providerEncoding() throws {
|
||||
@Test
|
||||
func `Provider encodes to identifier string`() throws {
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
let openaiData = try encoder.encode(Provider.openai)
|
||||
@ -232,8 +229,8 @@ struct ProviderTests {
|
||||
#expect(customString == "\"my-provider\"")
|
||||
}
|
||||
|
||||
@Test("Provider decodes from identifier string")
|
||||
func providerDecoding() throws {
|
||||
@Test
|
||||
func `Provider decodes from identifier string`() throws {
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let openaiData = "\"openai\"".utf8Data()
|
||||
@ -250,17 +247,16 @@ struct ProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Equality Tests")
|
||||
struct EqualityTests {
|
||||
@Test("Standard providers equality")
|
||||
func standardProvidersEquality() {
|
||||
@Test
|
||||
func `Standard providers equality`() {
|
||||
#expect(Provider.openai == Provider.openai)
|
||||
#expect(Provider.anthropic == Provider.anthropic)
|
||||
#expect(Provider.openai != Provider.anthropic)
|
||||
}
|
||||
|
||||
@Test("Custom providers equality")
|
||||
func customProvidersEquality() {
|
||||
@Test
|
||||
func `Custom providers equality`() {
|
||||
let custom1 = Provider.custom("test")
|
||||
let custom2 = Provider.custom("test")
|
||||
let custom3 = Provider.custom("different")
|
||||
@ -271,10 +267,9 @@ struct ProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Hashable Tests")
|
||||
struct HashableTests {
|
||||
@Test("Provider hashable implementation")
|
||||
func providerHashable() {
|
||||
@Test
|
||||
func `Provider hashable implementation`() {
|
||||
let providers: Set<Provider> = [
|
||||
.openai,
|
||||
.anthropic,
|
||||
|
||||
@ -2,23 +2,22 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Simple Configuration Tests")
|
||||
struct SimpleConfigurationTests {
|
||||
@Test("Provider enum basic functionality")
|
||||
func providerEnumBasics() {
|
||||
@Test
|
||||
func `Provider enum basic functionality`() {
|
||||
#expect(Provider.openai.identifier == "openai")
|
||||
#expect(Provider.anthropic.identifier == "anthropic")
|
||||
#expect(Provider.custom("test").identifier == "test")
|
||||
}
|
||||
|
||||
@Test("Provider factory method")
|
||||
func providerFactory() {
|
||||
@Test
|
||||
func `Provider factory method`() {
|
||||
#expect(Provider.from(identifier: "openai") == .openai)
|
||||
#expect(Provider.from(identifier: "custom-provider") == .custom("custom-provider"))
|
||||
}
|
||||
|
||||
@Test("Configuration instance basic functionality")
|
||||
func configurationInstance() {
|
||||
@Test
|
||||
func `Configuration instance basic functionality`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
// Set keys directly
|
||||
@ -32,8 +31,8 @@ struct SimpleConfigurationTests {
|
||||
#expect(config.getAPIKey(for: .anthropic) == nil)
|
||||
}
|
||||
|
||||
@Test("Multiple configuration instances are isolated")
|
||||
func multipleInstancesIsolated() {
|
||||
@Test
|
||||
func `Multiple configuration instances are isolated`() {
|
||||
let config1 = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
let config2 = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
|
||||
@ -2,12 +2,11 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Stop Conditions Integration Tests")
|
||||
struct StopConditionsIntegrationTests {
|
||||
// MARK: - Provider Integration Tests
|
||||
|
||||
@Test("Stop conditions passed to OpenAI as native stop sequences")
|
||||
func openAIProviderStopSequences() async throws {
|
||||
@Test
|
||||
func `Stop conditions passed to OpenAI as native stop sequences`() {
|
||||
// Create a mock provider that can verify the request
|
||||
_ = MockOpenAIProvider()
|
||||
|
||||
@ -37,8 +36,8 @@ struct StopConditionsIntegrationTests {
|
||||
#expect(extractedComposite.isEmpty) // Current behavior
|
||||
}
|
||||
|
||||
@Test("Stop conditions work with generateText end-to-end")
|
||||
func generateTextWithStopConditions() async throws {
|
||||
@Test
|
||||
func `Stop conditions work with generateText end-to-end`() async throws {
|
||||
// Use a mock provider that returns specific text
|
||||
let mockProvider = MockTextProvider(responseText: "Count: 1 2 3 END 4 5 6")
|
||||
|
||||
@ -60,8 +59,8 @@ struct StopConditionsIntegrationTests {
|
||||
#expect(response.finishReason == FinishReason.stop)
|
||||
}
|
||||
|
||||
@Test("Streaming with multiple stop conditions")
|
||||
func streamingWithMultipleStopConditions() async throws {
|
||||
@Test
|
||||
func `Streaming with multiple stop conditions`() async throws {
|
||||
// Create a stream that emits text gradually
|
||||
let stream = self.createMockStream(texts: [
|
||||
"Hello ",
|
||||
@ -94,8 +93,8 @@ struct StopConditionsIntegrationTests {
|
||||
#expect(!result.contains("STOP"))
|
||||
}
|
||||
|
||||
@Test("Token count stop condition with streaming")
|
||||
func tokenCountStopWithStreaming() async throws {
|
||||
@Test
|
||||
func `Token count stop condition with streaming`() async throws {
|
||||
// Create a stream with many tokens
|
||||
let longTexts = (1...100).map { "Token \($0) " }
|
||||
let stream = self.createMockStream(texts: longTexts)
|
||||
@ -118,8 +117,8 @@ struct StopConditionsIntegrationTests {
|
||||
#expect(!result.contains("Token 50"))
|
||||
}
|
||||
|
||||
@Test("Timeout stop condition with streaming")
|
||||
func timeoutStopWithStreaming() async throws {
|
||||
@Test
|
||||
func `Timeout stop condition with streaming`() async throws {
|
||||
// Create a slow stream
|
||||
let stream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
Task {
|
||||
@ -149,8 +148,8 @@ struct StopConditionsIntegrationTests {
|
||||
#expect(chunkCount < 10) // Should not have all chunks
|
||||
}
|
||||
|
||||
@Test("Regex stop condition with complex patterns")
|
||||
func regexStopConditionWithPatterns() async throws {
|
||||
@Test
|
||||
func `Regex stop condition with complex patterns`() async throws {
|
||||
let texts = [
|
||||
"Processing item 1...",
|
||||
"Processing item 2...",
|
||||
@ -173,8 +172,8 @@ struct StopConditionsIntegrationTests {
|
||||
#expect(!result.contains("Should not see this"))
|
||||
}
|
||||
|
||||
@Test("Predicate stop condition with custom logic")
|
||||
func predicateStopCondition() async throws {
|
||||
@Test
|
||||
func `Predicate stop condition with custom logic`() async throws {
|
||||
let stream = self.createMockStream(texts: [
|
||||
"Step 1: Initialize",
|
||||
"Step 2: Process",
|
||||
@ -200,8 +199,8 @@ struct StopConditionsIntegrationTests {
|
||||
#expect(!result.contains("Step 4"))
|
||||
}
|
||||
|
||||
@Test("Composite stop conditions with ALL logic")
|
||||
func allStopCondition() async throws {
|
||||
@Test
|
||||
func `Composite stop conditions with ALL logic`() async {
|
||||
let condition = AllStopCondition([
|
||||
PredicateStopCondition { text, _ in text.count > 20 }, // Length check
|
||||
PredicateStopCondition { text, _ in text.contains("END") }, // Content check
|
||||
@ -214,8 +213,8 @@ struct StopConditionsIntegrationTests {
|
||||
#expect(await condition.shouldStop(text: "This is long enough and has END", delta: nil) == true)
|
||||
}
|
||||
|
||||
@Test("Stop condition state management with reset")
|
||||
func stopConditionReset() async throws {
|
||||
@Test
|
||||
func `Stop condition state management with reset`() async {
|
||||
let condition = TokenCountStopCondition(maxTokens: 10)
|
||||
|
||||
// First use
|
||||
@ -231,8 +230,8 @@ struct StopConditionsIntegrationTests {
|
||||
#expect(await condition.shouldStop(text: "Short", delta: nil) == false)
|
||||
}
|
||||
|
||||
@Test("Native provider stop sequence extraction")
|
||||
func stopSequenceExtraction() async throws {
|
||||
@Test
|
||||
func `Native provider stop sequence extraction`() {
|
||||
// Test single string condition
|
||||
let singleCondition = StringStopCondition("STOP")
|
||||
let singleSequences = extractStopSequences(from: singleCondition)
|
||||
@ -271,10 +270,21 @@ struct StopConditionsIntegrationTests {
|
||||
private struct MockTextProvider: ModelProvider {
|
||||
let responseText: String
|
||||
|
||||
var modelId: String { "mock" }
|
||||
var baseURL: String? { nil }
|
||||
var apiKey: String? { nil }
|
||||
var capabilities: ModelCapabilities { ModelCapabilities() }
|
||||
var modelId: String {
|
||||
"mock"
|
||||
}
|
||||
|
||||
var baseURL: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var apiKey: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var capabilities: ModelCapabilities {
|
||||
ModelCapabilities()
|
||||
}
|
||||
|
||||
func generateText(request: ProviderRequest) async throws -> ProviderResponse {
|
||||
var finalText = self.responseText
|
||||
@ -316,10 +326,21 @@ private struct MockTextProvider: ModelProvider {
|
||||
}
|
||||
|
||||
private struct MockOpenAIProvider: ModelProvider {
|
||||
var modelId: String { "gpt-4" }
|
||||
var baseURL: String? { nil }
|
||||
var apiKey: String? { nil }
|
||||
var capabilities: ModelCapabilities { ModelCapabilities() }
|
||||
var modelId: String {
|
||||
"gpt-4"
|
||||
}
|
||||
|
||||
var baseURL: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var apiKey: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var capabilities: ModelCapabilities {
|
||||
ModelCapabilities()
|
||||
}
|
||||
|
||||
func generateText(request _: ProviderRequest) async throws -> ProviderResponse {
|
||||
ProviderResponse(text: "Mock response", usage: nil, finishReason: .stop)
|
||||
@ -336,7 +357,7 @@ private struct MockOpenAIProvider: ModelProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to extract stop sequences (mimics the internal implementation)
|
||||
/// Helper function to extract stop sequences (mimics the internal implementation)
|
||||
private func extractStopSequences(from stopCondition: (any StopCondition)?) -> [String] {
|
||||
guard let stopCondition else { return [] }
|
||||
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Stop Conditions Tests")
|
||||
struct StopConditionsTests {
|
||||
// MARK: - String Stop Condition Tests
|
||||
|
||||
@Test("StringStopCondition should stop on exact match")
|
||||
func stringStopCondition() async throws {
|
||||
@Test
|
||||
func `StringStopCondition should stop on exact match`() async {
|
||||
let condition = StringStopCondition("STOP")
|
||||
|
||||
// Should not stop before the stop string
|
||||
@ -19,8 +18,8 @@ struct StopConditionsTests {
|
||||
#expect(await condition.shouldStop(text: "Hello", delta: "STOP") == true)
|
||||
}
|
||||
|
||||
@Test("StringStopCondition case-insensitive matching")
|
||||
func stringStopConditionCaseInsensitive() async throws {
|
||||
@Test
|
||||
func `StringStopCondition case-insensitive matching`() async {
|
||||
let condition = StringStopCondition("stop", caseSensitive: false)
|
||||
|
||||
// Should match regardless of case
|
||||
@ -31,8 +30,8 @@ struct StopConditionsTests {
|
||||
|
||||
// MARK: - Regex Stop Condition Tests
|
||||
|
||||
@Test("RegexStopCondition should match patterns")
|
||||
func regexStopCondition() async throws {
|
||||
@Test
|
||||
func `RegexStopCondition should match patterns`() async {
|
||||
let condition = RegexStopCondition(pattern: "END.*SESSION")
|
||||
|
||||
// Should not match without pattern
|
||||
@ -48,8 +47,8 @@ struct StopConditionsTests {
|
||||
|
||||
// MARK: - Token Count Stop Condition Tests
|
||||
|
||||
@Test("TokenCountStopCondition should stop after limit")
|
||||
func tokenCountStopCondition() async throws {
|
||||
@Test
|
||||
func `TokenCountStopCondition should stop after limit`() async {
|
||||
let condition = TokenCountStopCondition(maxTokens: 10)
|
||||
|
||||
// Reset to start fresh
|
||||
@ -70,8 +69,8 @@ struct StopConditionsTests {
|
||||
|
||||
// MARK: - Timeout Stop Condition Tests
|
||||
|
||||
@Test("TimeoutStopCondition should stop after duration")
|
||||
func timeoutStopCondition() async throws {
|
||||
@Test
|
||||
func `TimeoutStopCondition should stop after duration`() async throws {
|
||||
let condition = TimeoutStopCondition(timeout: 0.1) // 100ms
|
||||
|
||||
// Should not stop immediately
|
||||
@ -86,8 +85,8 @@ struct StopConditionsTests {
|
||||
|
||||
// MARK: - Predicate Stop Condition Tests
|
||||
|
||||
@Test("PredicateStopCondition with custom logic")
|
||||
func predicateStopCondition() async throws {
|
||||
@Test
|
||||
func `PredicateStopCondition with custom logic`() async {
|
||||
let condition = PredicateStopCondition { text, _ in
|
||||
text.count > 20
|
||||
}
|
||||
@ -101,8 +100,8 @@ struct StopConditionsTests {
|
||||
|
||||
// MARK: - Composite Stop Conditions Tests
|
||||
|
||||
@Test("AnyStopCondition should stop when any condition is met")
|
||||
func anyStopCondition() async throws {
|
||||
@Test
|
||||
func `AnyStopCondition should stop when any condition is met`() async {
|
||||
let conditions: [any StopCondition] = [
|
||||
StringStopCondition("STOP"),
|
||||
TokenCountStopCondition(maxTokens: 100),
|
||||
@ -117,8 +116,8 @@ struct StopConditionsTests {
|
||||
#expect(await anyCondition.shouldStop(text: "Hello world", delta: nil) == false)
|
||||
}
|
||||
|
||||
@Test("AllStopCondition should stop only when all conditions are met")
|
||||
func allStopCondition() async throws {
|
||||
@Test
|
||||
func `AllStopCondition should stop only when all conditions are met`() async {
|
||||
let conditions: [any StopCondition] = [
|
||||
StringStopCondition("END"),
|
||||
PredicateStopCondition { text, _ in text.count > 10 },
|
||||
@ -135,8 +134,8 @@ struct StopConditionsTests {
|
||||
|
||||
// MARK: - Integration Tests with Generation
|
||||
|
||||
@Test("Stop conditions in generateText")
|
||||
func stopConditionsInGenerateText() async throws {
|
||||
@Test
|
||||
func `Stop conditions in generateText`() {
|
||||
// Create a mock provider that returns text with a stop marker
|
||||
let mockText = "This is the response. STOP HERE. This should be truncated."
|
||||
_ = createMockProvider(responseText: mockText)
|
||||
@ -151,8 +150,8 @@ struct StopConditionsTests {
|
||||
#expect(settings.stopConditions != nil)
|
||||
}
|
||||
|
||||
@Test("Stop conditions with streaming")
|
||||
func stopConditionsInStreamText() async throws {
|
||||
@Test
|
||||
func `Stop conditions with streaming`() async throws {
|
||||
// Create a simple text stream
|
||||
let stream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
Task {
|
||||
@ -183,8 +182,8 @@ struct StopConditionsTests {
|
||||
|
||||
// MARK: - Builder Pattern Tests
|
||||
|
||||
@Test("StopConditionBuilder creates correct conditions")
|
||||
func stopConditionBuilder() async throws {
|
||||
@Test
|
||||
func `StopConditionBuilder creates correct conditions`() async {
|
||||
let condition = StopConditionBuilder()
|
||||
.whenContains("END")
|
||||
.afterTokens(100)
|
||||
@ -198,8 +197,8 @@ struct StopConditionsTests {
|
||||
#expect(await condition.shouldStop(text: "Test END", delta: nilDelta) == true)
|
||||
}
|
||||
|
||||
@Test("GenerationSettings with stop conditions factory")
|
||||
func generationSettingsWithStopConditions() {
|
||||
@Test
|
||||
func `GenerationSettings with stop conditions factory`() {
|
||||
let settings = GenerationSettings.withStopConditions(
|
||||
StringStopCondition("STOP"),
|
||||
TokenCountStopCondition(maxTokens: 100),
|
||||
@ -214,8 +213,8 @@ struct StopConditionsTests {
|
||||
|
||||
// MARK: - Pattern Detection Tests
|
||||
|
||||
@Test("ConsecutivePatternStopCondition detects repeating patterns")
|
||||
func consecutivePatternStopCondition() async throws {
|
||||
@Test
|
||||
func `ConsecutivePatternStopCondition detects repeating patterns`() async {
|
||||
let condition = ConsecutivePatternStopCondition(pattern: "loop", count: 3)
|
||||
|
||||
await condition.reset()
|
||||
@ -230,8 +229,8 @@ struct StopConditionsTests {
|
||||
#expect(await condition.shouldStop(text: "loop loop loop", delta: "loop") == true)
|
||||
}
|
||||
|
||||
@Test("RepetitionStopCondition detects repeating content")
|
||||
func repetitionStopCondition() async throws {
|
||||
@Test
|
||||
func `RepetitionStopCondition detects repeating content`() async {
|
||||
let condition = RepetitionStopCondition(
|
||||
windowSize: 50, // Increased to ensure we don't hit the window limit
|
||||
threshold: 0.8,
|
||||
@ -259,8 +258,8 @@ struct StopConditionsTests {
|
||||
|
||||
// MARK: - Edge Cases
|
||||
|
||||
@Test("Empty text and nil delta handling")
|
||||
func emptyAndNilHandling() async throws {
|
||||
@Test
|
||||
func `Empty text and nil delta handling`() async {
|
||||
let condition = StringStopCondition("STOP")
|
||||
|
||||
// Empty text with nil delta
|
||||
@ -273,8 +272,8 @@ struct StopConditionsTests {
|
||||
#expect(await condition.shouldStop(text: "", delta: "STOP") == true)
|
||||
}
|
||||
|
||||
@Test("Reset functionality")
|
||||
func resetFunctionality() async throws {
|
||||
@Test
|
||||
func `Reset functionality`() async {
|
||||
let condition = TokenCountStopCondition(maxTokens: 5)
|
||||
|
||||
// Add tokens until stop
|
||||
@ -300,14 +299,25 @@ private func createMockProvider(responseText: String) -> ModelProvider {
|
||||
StopConditionTestMockProvider(responseText: responseText)
|
||||
}
|
||||
|
||||
// Mock provider for testing (simplified)
|
||||
/// Mock provider for testing (simplified)
|
||||
private struct StopConditionTestMockProvider: ModelProvider {
|
||||
let responseText: String
|
||||
|
||||
var modelId: String { "mock-model" }
|
||||
var baseURL: String? { nil }
|
||||
var apiKey: String? { nil }
|
||||
var capabilities: ModelCapabilities { ModelCapabilities() }
|
||||
var modelId: String {
|
||||
"mock-model"
|
||||
}
|
||||
|
||||
var baseURL: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var apiKey: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var capabilities: ModelCapabilities {
|
||||
ModelCapabilities()
|
||||
}
|
||||
|
||||
func generateText(request _: ProviderRequest) async throws -> ProviderResponse {
|
||||
ProviderResponse(text: self.responseText)
|
||||
|
||||
@ -1,28 +1,27 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("StreamObject Tests")
|
||||
struct StreamObjectTests {
|
||||
// Test struct for structured output
|
||||
struct TestPerson: Codable, Sendable, Equatable {
|
||||
/// Test struct for structured output
|
||||
struct TestPerson: Codable, Equatable {
|
||||
let name: String
|
||||
let age: Int
|
||||
let email: String?
|
||||
}
|
||||
|
||||
struct TestResponse: Codable, Sendable, Equatable {
|
||||
struct TestResponse: Codable, Equatable {
|
||||
let items: [String]
|
||||
let count: Int
|
||||
let metadata: TestMetadata?
|
||||
|
||||
struct TestMetadata: Codable, Sendable, Equatable {
|
||||
struct TestMetadata: Codable, Equatable {
|
||||
let timestamp: String
|
||||
let version: String
|
||||
}
|
||||
}
|
||||
|
||||
@Test("streamObject basic functionality")
|
||||
func streamObjectBasic() async throws {
|
||||
@Test
|
||||
func `streamObject basic functionality`() {
|
||||
// Since we can't easily mock the provider, we'll test the helper functions
|
||||
// that streamObject uses internally
|
||||
|
||||
@ -39,8 +38,8 @@ struct StreamObjectTests {
|
||||
#expect(fixed2 == "{\"name\":\"Bob\",\"age\":}\"")
|
||||
}
|
||||
|
||||
@Test("ObjectStreamDelta types and structure")
|
||||
func objectStreamDeltaTypes() throws {
|
||||
@Test
|
||||
func `ObjectStreamDelta types and structure`() {
|
||||
// Test the ObjectStreamDelta structure
|
||||
let startDelta = ObjectStreamDelta<TestPerson>(type: .start)
|
||||
#expect(startDelta.type == .start)
|
||||
@ -73,8 +72,8 @@ struct StreamObjectTests {
|
||||
#expect(errorDelta.error != nil)
|
||||
}
|
||||
|
||||
@Test("StreamObjectResult structure")
|
||||
func streamObjectResultStructure() throws {
|
||||
@Test
|
||||
func `StreamObjectResult structure`() {
|
||||
// Test StreamObjectResult initialization
|
||||
let testStream = AsyncThrowingStream<ObjectStreamDelta<TestPerson>, Error> { continuation in
|
||||
Task {
|
||||
@ -99,8 +98,8 @@ struct StreamObjectTests {
|
||||
#expect(result.schema == TestPerson.self)
|
||||
}
|
||||
|
||||
@Test("StreamObjectResult AsyncSequence iteration")
|
||||
func streamObjectResultAsyncSequence() async throws {
|
||||
@Test
|
||||
func `StreamObjectResult AsyncSequence iteration`() async throws {
|
||||
let testStream = AsyncThrowingStream<ObjectStreamDelta<TestPerson>, Error> { continuation in
|
||||
Task {
|
||||
continuation.yield(ObjectStreamDelta(type: .start))
|
||||
@ -136,8 +135,8 @@ struct StreamObjectTests {
|
||||
#expect(deltaCount == 4) // start, partial, complete, done
|
||||
}
|
||||
|
||||
@Test("partialObjects filter method")
|
||||
func partialObjectsFilter() async throws {
|
||||
@Test
|
||||
func `partialObjects filter method`() async throws {
|
||||
let testStream = AsyncThrowingStream<ObjectStreamDelta<TestPerson>, Error> { continuation in
|
||||
Task {
|
||||
continuation.yield(ObjectStreamDelta(type: .start))
|
||||
@ -174,8 +173,8 @@ struct StreamObjectTests {
|
||||
#expect(partialObjects[2].name == "Person 3")
|
||||
}
|
||||
|
||||
@Test("finalObject method")
|
||||
func testFinalObject() async throws {
|
||||
@Test
|
||||
func `finalObject method`() async throws {
|
||||
let testStream = AsyncThrowingStream<ObjectStreamDelta<TestPerson>, Error> { continuation in
|
||||
Task {
|
||||
continuation.yield(ObjectStreamDelta(type: .start))
|
||||
@ -205,8 +204,8 @@ struct StreamObjectTests {
|
||||
#expect(finalObject.email == "complete@test.com")
|
||||
}
|
||||
|
||||
@Test("finalObject throws when no complete object")
|
||||
func finalObjectThrows() async throws {
|
||||
@Test
|
||||
func `finalObject throws when no complete object`() async throws {
|
||||
let testStream = AsyncThrowingStream<ObjectStreamDelta<TestPerson>, Error> { continuation in
|
||||
Task {
|
||||
continuation.yield(ObjectStreamDelta(type: .start))
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Stream Transform Pipeline Tests")
|
||||
struct StreamTransformTests {
|
||||
@Test("FilterTransform filters elements correctly")
|
||||
func testFilterTransform() async throws {
|
||||
@Test
|
||||
func `FilterTransform filters elements correctly`() async throws {
|
||||
let transform = FilterTransform<Int> { $0 % 2 == 0 }
|
||||
|
||||
#expect(try await transform.transform(2) == 2)
|
||||
@ -13,16 +12,16 @@ struct StreamTransformTests {
|
||||
#expect(try await transform.transform(5) == nil)
|
||||
}
|
||||
|
||||
@Test("MapTransform transforms elements")
|
||||
func testMapTransform() async throws {
|
||||
@Test
|
||||
func `MapTransform transforms elements`() async throws {
|
||||
let transform = MapTransform<Int, String> { "\($0 * 2)" }
|
||||
|
||||
#expect(try await transform.transform(5) == "10")
|
||||
#expect(try await transform.transform(3) == "6")
|
||||
}
|
||||
|
||||
@Test("BufferTransform batches elements")
|
||||
func bufferTransform() async throws {
|
||||
@Test
|
||||
func `BufferTransform batches elements`() async throws {
|
||||
let transform = BufferTransform<String>(bufferSize: 3)
|
||||
|
||||
// First two elements shouldn't trigger flush
|
||||
@ -42,8 +41,8 @@ struct StreamTransformTests {
|
||||
#expect(remaining == ["d", "e"])
|
||||
}
|
||||
|
||||
@Test("BufferTransform with time interval")
|
||||
func bufferTransformWithInterval() async throws {
|
||||
@Test
|
||||
func `BufferTransform with time interval`() async throws {
|
||||
let transform = BufferTransform<Int>(bufferSize: 10, flushInterval: 0.1)
|
||||
|
||||
// Add elements
|
||||
@ -61,8 +60,8 @@ struct StreamTransformTests {
|
||||
#expect(batch?.contains(3) == true)
|
||||
}
|
||||
|
||||
@Test("ThrottleTransform limits rate")
|
||||
func throttleTransform() async throws {
|
||||
@Test
|
||||
func `ThrottleTransform limits rate`() async throws {
|
||||
let transform = ThrottleTransform<String>(interval: 0.1)
|
||||
|
||||
// First element passes through
|
||||
@ -78,8 +77,8 @@ struct StreamTransformTests {
|
||||
#expect(try await transform.transform("third") == "third")
|
||||
}
|
||||
|
||||
@Test("TapTransform adds side effects")
|
||||
func tapTransform() async throws {
|
||||
@Test
|
||||
func `TapTransform adds side effects`() async throws {
|
||||
actor SideEffectsCollector {
|
||||
var values: [Int] = []
|
||||
func append(_ value: Int) {
|
||||
@ -97,8 +96,8 @@ struct StreamTransformTests {
|
||||
#expect(await collector.values == [1, 2, 3])
|
||||
}
|
||||
|
||||
@Test("Stream filter extension works")
|
||||
func streamFilterExtension() async throws {
|
||||
@Test
|
||||
func `Stream filter extension works`() async throws {
|
||||
let stream = AsyncThrowingStream<Int, Error> { continuation in
|
||||
Task {
|
||||
for i in 1...5 {
|
||||
@ -118,8 +117,8 @@ struct StreamTransformTests {
|
||||
#expect(results == [2, 4])
|
||||
}
|
||||
|
||||
@Test("Stream map extension works")
|
||||
func streamMapExtension() async throws {
|
||||
@Test
|
||||
func `Stream map extension works`() async throws {
|
||||
let stream = AsyncThrowingStream<Int, Error> { continuation in
|
||||
Task {
|
||||
for i in 1...3 {
|
||||
@ -139,8 +138,8 @@ struct StreamTransformTests {
|
||||
#expect(results == [1, 4, 9])
|
||||
}
|
||||
|
||||
@Test("Stream tap extension works")
|
||||
func streamTapExtension() async throws {
|
||||
@Test
|
||||
func `Stream tap extension works`() async throws {
|
||||
actor TapCollector {
|
||||
var values: [String] = []
|
||||
func append(_ value: String) {
|
||||
@ -170,8 +169,8 @@ struct StreamTransformTests {
|
||||
#expect(await collector.values == ["a", "b", "c"])
|
||||
}
|
||||
|
||||
@Test("Stream buffer extension works")
|
||||
func streamBufferExtension() async throws {
|
||||
@Test
|
||||
func `Stream buffer extension works`() async throws {
|
||||
let stream = AsyncThrowingStream<Int, Error> { continuation in
|
||||
Task {
|
||||
for i in 1...7 {
|
||||
@ -194,8 +193,8 @@ struct StreamTransformTests {
|
||||
#expect(batches[2] == [7]) // Remaining element
|
||||
}
|
||||
|
||||
@Test("Stream throttle extension works")
|
||||
func streamThrottleExtension() async throws {
|
||||
@Test
|
||||
func `Stream throttle extension works`() async throws {
|
||||
let stream = AsyncThrowingStream<Int, Error> { continuation in
|
||||
Task {
|
||||
for i in 1...5 {
|
||||
@ -218,8 +217,8 @@ struct StreamTransformTests {
|
||||
#expect(results.contains(1)) // First element always passes
|
||||
}
|
||||
|
||||
@Test("StreamTextResult filter extension works")
|
||||
func streamTextResultFilter() async throws {
|
||||
@Test
|
||||
func `StreamTextResult filter extension works`() async throws {
|
||||
let stream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
Task {
|
||||
continuation.yield(TextStreamDelta(type: .textDelta, content: "Hello"))
|
||||
@ -251,8 +250,8 @@ struct StreamTransformTests {
|
||||
#expect(count == 2) // Only text deltas
|
||||
}
|
||||
|
||||
@Test("StreamTextResult collectText works")
|
||||
func streamTextResultCollectText() async throws {
|
||||
@Test
|
||||
func `StreamTextResult collectText works`() async throws {
|
||||
let stream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
Task {
|
||||
continuation.yield(TextStreamDelta(type: .textDelta, content: "Hello"))
|
||||
@ -277,8 +276,8 @@ struct StreamTransformTests {
|
||||
#expect(texts == ["Hello", " ", "World"])
|
||||
}
|
||||
|
||||
@Test("StreamTextResult fullText works")
|
||||
func streamTextResultFullText() async throws {
|
||||
@Test
|
||||
func `StreamTextResult fullText works`() async throws {
|
||||
let stream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
Task {
|
||||
continuation.yield(TextStreamDelta(type: .textDelta, content: "The"))
|
||||
@ -300,8 +299,8 @@ struct StreamTransformTests {
|
||||
#expect(fullText == "The quick brown fox")
|
||||
}
|
||||
|
||||
@Test("CombinedTransform chains transforms")
|
||||
func combinedTransform() async throws {
|
||||
@Test
|
||||
func `CombinedTransform chains transforms`() async throws {
|
||||
let filterTransform = FilterTransform<Int> { $0 % 2 == 0 }
|
||||
let mapTransform = MapTransform<Int, String> { "\($0 * 10)" }
|
||||
|
||||
@ -315,8 +314,8 @@ struct StreamTransformTests {
|
||||
#expect(try await combined.transform(4) == "40")
|
||||
}
|
||||
|
||||
@Test("Transform chain with complex pipeline")
|
||||
func complexTransformPipeline() async throws {
|
||||
@Test
|
||||
func `Transform chain with complex pipeline`() async throws {
|
||||
let stream = AsyncThrowingStream<Int, Error> { continuation in
|
||||
Task {
|
||||
for i in 1...10 {
|
||||
|
||||
@ -3,25 +3,24 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
@testable import TachikomaAgent
|
||||
|
||||
@Suite("TachikomaConfiguration Tests", .serialized)
|
||||
@Suite(.serialized)
|
||||
struct TachikomaConfigurationTests {
|
||||
@Suite("Instance Creation Tests")
|
||||
struct InstanceCreationTests {
|
||||
@Test("Default constructor loads from environment")
|
||||
func defaultConstructorLoadsEnvironment() async throws {
|
||||
@Test
|
||||
func `Default constructor loads from environment`() {
|
||||
let config = TachikomaConfiguration()
|
||||
// Should not crash and create valid instance
|
||||
#expect(config.configuredProviders.isEmpty || !config.configuredProviders.isEmpty) // Either is fine
|
||||
}
|
||||
|
||||
@Test("Constructor with loadFromEnvironment=false")
|
||||
func constructorWithoutEnvironmentLoading() async throws {
|
||||
@Test
|
||||
func `Constructor with loadFromEnvironment=false`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
#expect(config.configuredProviders.isEmpty)
|
||||
}
|
||||
|
||||
@Test("Convenience constructor with API keys")
|
||||
func convenienceConstructorWithAPIKeys() async throws {
|
||||
@Test
|
||||
func `Convenience constructor with API keys`() {
|
||||
let config = TachikomaConfiguration(
|
||||
apiKeys: [
|
||||
"openai": "test-openai-key",
|
||||
@ -34,8 +33,8 @@ struct TachikomaConfigurationTests {
|
||||
#expect(config.getAPIKey(for: .groq) == nil)
|
||||
}
|
||||
|
||||
@Test("Convenience constructor with API keys and base URLs")
|
||||
func convenienceConstructorWithAPIKeysAndBaseURLs() async throws {
|
||||
@Test
|
||||
func `Convenience constructor with API keys and base URLs`() {
|
||||
let config = TachikomaConfiguration(
|
||||
apiKeys: ["openai": "test-key"],
|
||||
baseURLs: ["openai": "https://custom.openai.com"],
|
||||
@ -46,10 +45,9 @@ struct TachikomaConfigurationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Type-Safe API Tests")
|
||||
struct TypeSafeAPITests {
|
||||
@Test("Set and get API key with Provider enum")
|
||||
func setAndGetAPIKeyTypeSafe() async throws {
|
||||
@Test
|
||||
func `Set and get API key with Provider enum`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
config.setAPIKey("test-openai-key", for: .openai)
|
||||
@ -62,8 +60,8 @@ struct TachikomaConfigurationTests {
|
||||
#expect(config.getAPIKey(for: .groq) == nil)
|
||||
}
|
||||
|
||||
@Test("Has API key checks")
|
||||
func hasAPIKeyChecks() async throws {
|
||||
@Test
|
||||
func `Has API key checks`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
#expect(!config.hasAPIKey(for: .openai))
|
||||
@ -75,8 +73,8 @@ struct TachikomaConfigurationTests {
|
||||
#expect(config.hasConfiguredAPIKey(for: .openai))
|
||||
}
|
||||
|
||||
@Test("Remove API key")
|
||||
func removeAPIKey() async throws {
|
||||
@Test
|
||||
func `Remove API key`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
config.setAPIKey("test-key", for: .anthropic)
|
||||
@ -86,8 +84,8 @@ struct TachikomaConfigurationTests {
|
||||
#expect(!config.hasAPIKey(for: .anthropic))
|
||||
}
|
||||
|
||||
@Test("Set and get base URL")
|
||||
func setAndGetBaseURL() async throws {
|
||||
@Test
|
||||
func `Set and get base URL`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
// Test custom base URL
|
||||
@ -101,8 +99,8 @@ struct TachikomaConfigurationTests {
|
||||
#expect(config.getBaseURL(for: .custom("test")) == nil)
|
||||
}
|
||||
|
||||
@Test("Remove base URL falls back to default")
|
||||
func removeBaseURLFallback() async throws {
|
||||
@Test
|
||||
func `Remove base URL falls back to default`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
// Set custom URL
|
||||
@ -115,10 +113,9 @@ struct TachikomaConfigurationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("String-Based Compatibility API Tests")
|
||||
struct StringBasedAPITests {
|
||||
@Test("String-based API delegates to type-safe API")
|
||||
func stringBasedAPIDelegation() async throws {
|
||||
@Test
|
||||
func `String-based API delegates to type-safe API`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
// Set via string API
|
||||
@ -135,8 +132,8 @@ struct TachikomaConfigurationTests {
|
||||
#expect(config.hasAPIKey(for: "openai"))
|
||||
}
|
||||
|
||||
@Test("String-based API handles custom providers")
|
||||
func stringBasedCustomProviders() async throws {
|
||||
@Test
|
||||
func `String-based API handles custom providers`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
config.setAPIKey("custom-key", for: "my-custom-provider")
|
||||
@ -147,8 +144,8 @@ struct TachikomaConfigurationTests {
|
||||
#expect(config.hasAPIKey(for: "my-custom-provider"))
|
||||
}
|
||||
|
||||
@Test("String-based API case insensitive")
|
||||
func stringBasedCaseInsensitive() async throws {
|
||||
@Test
|
||||
func `String-based API case insensitive`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
config.setAPIKey("test-key", for: "OpenAI")
|
||||
@ -159,10 +156,9 @@ struct TachikomaConfigurationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Environment Variable Loading Tests")
|
||||
struct EnvironmentVariableTests {
|
||||
@Test("Environment vs configured key priority")
|
||||
func environmentVsConfiguredPriority() async throws {
|
||||
@Test
|
||||
func `Environment vs configured key priority`() {
|
||||
// Create config that loads from environment
|
||||
let config = TachikomaConfiguration()
|
||||
|
||||
@ -177,8 +173,8 @@ struct TachikomaConfigurationTests {
|
||||
// Environment key availability depends on actual environment
|
||||
}
|
||||
|
||||
@Test("Environment detection methods")
|
||||
func environmentDetectionMethods() async throws {
|
||||
@Test
|
||||
func `Environment detection methods`() {
|
||||
_ = TachikomaConfiguration()
|
||||
|
||||
// Provider enum should detect environment independently
|
||||
@ -192,10 +188,9 @@ struct TachikomaConfigurationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Configuration State Tests")
|
||||
struct ConfigurationStateTests {
|
||||
@Test("Configured providers list")
|
||||
func configuredProvidersList() async throws {
|
||||
@Test
|
||||
func `Configured providers list`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
#expect(config.configuredProviders.isEmpty)
|
||||
@ -211,8 +206,8 @@ struct TachikomaConfigurationTests {
|
||||
#expect(providers.contains(.custom("test")))
|
||||
}
|
||||
|
||||
@Test("Clear all configuration")
|
||||
func clearAllConfiguration() async throws {
|
||||
@Test
|
||||
func `Clear all configuration`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
config.setAPIKey("key1", for: .openai)
|
||||
@ -228,8 +223,8 @@ struct TachikomaConfigurationTests {
|
||||
#expect(config.getBaseURL(for: .openai) == "https://api.openai.com/v1") // Falls back to default
|
||||
}
|
||||
|
||||
@Test("Configuration summary")
|
||||
func configurationSummary() async throws {
|
||||
@Test
|
||||
func `Configuration summary`() {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
config.setAPIKey("anthropic-key", for: .anthropic)
|
||||
@ -241,10 +236,9 @@ struct TachikomaConfigurationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Thread Safety Tests")
|
||||
struct ThreadSafetyTests {
|
||||
@Test("Concurrent access is thread-safe")
|
||||
func concurrentAccess() async throws {
|
||||
@Test
|
||||
func `Concurrent access is thread-safe`() async {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
// Test concurrent reads and writes
|
||||
@ -271,10 +265,9 @@ struct TachikomaConfigurationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Default Configuration Tests")
|
||||
struct DefaultConfigurationTests {
|
||||
@Test("Default configuration usage")
|
||||
func defaultConfigurationUsage() async throws {
|
||||
@Test
|
||||
func `Default configuration usage`() {
|
||||
// Clear any existing default
|
||||
TachikomaConfiguration.default = nil
|
||||
|
||||
@ -296,8 +289,8 @@ struct TachikomaConfigurationTests {
|
||||
TachikomaConfiguration.default = nil
|
||||
}
|
||||
|
||||
@Test("Resolve helper function")
|
||||
func resolveHelperFunction() async throws {
|
||||
@Test
|
||||
func `Resolve helper function`() {
|
||||
// Clear any existing default
|
||||
TachikomaConfiguration.default = nil
|
||||
|
||||
@ -325,8 +318,8 @@ struct TachikomaConfigurationTests {
|
||||
TachikomaConfiguration.default = nil
|
||||
}
|
||||
|
||||
@Test("Auto instance is singleton")
|
||||
func autoInstanceIsSingleton() async throws {
|
||||
@Test
|
||||
func `Auto instance is singleton`() async throws {
|
||||
// Clear any existing default
|
||||
TachikomaConfiguration.default = nil
|
||||
|
||||
@ -346,7 +339,7 @@ struct TachikomaConfigurationTests {
|
||||
}
|
||||
|
||||
// All should be the same instance
|
||||
let first = instances.first!
|
||||
let first = try #require(instances.first)
|
||||
for instance in instances {
|
||||
#expect(instance === first)
|
||||
}
|
||||
@ -355,8 +348,8 @@ struct TachikomaConfigurationTests {
|
||||
TachikomaConfiguration.default = nil
|
||||
}
|
||||
|
||||
@Test("Generation functions with different configurations")
|
||||
func generationFunctionsWithConfigs() async throws {
|
||||
@Test
|
||||
func `Generation functions with different configurations`() {
|
||||
// Clear any existing default
|
||||
TachikomaConfiguration.default = nil
|
||||
|
||||
@ -410,8 +403,8 @@ struct TachikomaConfigurationTests {
|
||||
TachikomaConfiguration.default = nil
|
||||
}
|
||||
|
||||
@Test("Conversation uses provided configuration")
|
||||
func conversationUsesProvidedConfig() async throws {
|
||||
@Test
|
||||
func `Conversation uses provided configuration`() {
|
||||
// Clear any existing default
|
||||
TachikomaConfiguration.default = nil
|
||||
|
||||
@ -445,8 +438,8 @@ struct TachikomaConfigurationTests {
|
||||
TachikomaConfiguration.default = nil
|
||||
}
|
||||
|
||||
@Test("Thread safety stress test")
|
||||
func threadSafetyStressTest() async throws {
|
||||
@Test
|
||||
func `Thread safety stress test`() async {
|
||||
// Clear any existing default
|
||||
TachikomaConfiguration.default = nil
|
||||
|
||||
@ -492,10 +485,9 @@ struct TachikomaConfigurationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Instance Isolation Tests")
|
||||
struct InstanceIsolationTests {
|
||||
@Test("Multiple instances are isolated")
|
||||
func multipleInstancesIsolated() async throws {
|
||||
@Test
|
||||
func `Multiple instances are isolated`() {
|
||||
let config1 = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
let config2 = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
|
||||
@ -510,8 +502,8 @@ struct TachikomaConfigurationTests {
|
||||
#expect(config2.getAPIKey(for: .openai) == "key2") // Should be unaffected
|
||||
}
|
||||
|
||||
@Test("Instance isolation with same provider keys")
|
||||
func instanceIsolationSameProviders() async throws {
|
||||
@Test
|
||||
func `Instance isolation with same provider keys`() {
|
||||
let config1 = TachikomaConfiguration(apiKeys: ["openai": "config1-key"])
|
||||
let config2 = TachikomaConfiguration(apiKeys: ["openai": "config2-key"])
|
||||
|
||||
@ -525,10 +517,9 @@ struct TachikomaConfigurationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Configuration Priority Tests")
|
||||
struct ConfigurationPriorityTests {
|
||||
@Test("Configuration priority chain")
|
||||
func configurationPriorityChain() async throws {
|
||||
@Test
|
||||
func `Configuration priority chain`() {
|
||||
// Clear any existing default
|
||||
TachikomaConfiguration.default = nil
|
||||
|
||||
@ -555,10 +546,9 @@ struct TachikomaConfigurationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Provider Factory Override Tests")
|
||||
struct ProviderFactoryOverrideTests {
|
||||
@Test("Custom factory override takes precedence")
|
||||
func customFactoryOverrideTakesPrecedence() async throws {
|
||||
@Test
|
||||
func `Custom factory override takes precedence`() throws {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
config.setAPIKey("mock-key", for: .openai)
|
||||
|
||||
@ -574,8 +564,8 @@ struct TachikomaConfigurationTests {
|
||||
#expect(capturedModel == .openai(.gpt4o))
|
||||
}
|
||||
|
||||
@Test("makeProvider falls back to real provider without override")
|
||||
func makeProviderUsesRealFactory() async throws {
|
||||
@Test
|
||||
func `makeProvider falls back to real provider without override`() throws {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
config.setAPIKey("mock-key", for: .openai)
|
||||
|
||||
|
||||
@ -2,10 +2,10 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Test Helpers Tests", .serialized)
|
||||
@Suite(.serialized)
|
||||
struct TestHelpersTests {
|
||||
@Test("Test helper create configuration")
|
||||
func helperCreateConfiguration() {
|
||||
@Test
|
||||
func `helper create configuration`() {
|
||||
let config = TestHelpers.createTestConfiguration(apiKeys: ["openai": "test-key"])
|
||||
let expected = TestHelpers.standardTestKeys["openai"]
|
||||
|
||||
@ -13,8 +13,8 @@ struct TestHelpersTests {
|
||||
#expect(config.hasConfiguredAPIKey(for: .openai))
|
||||
}
|
||||
|
||||
@Test("Test helper with empty configuration")
|
||||
func helperEmptyConfiguration() async {
|
||||
@Test
|
||||
func `helper with empty configuration`() async {
|
||||
let result = await TestHelpers.withEmptyTestConfiguration { config in
|
||||
config.getAPIKey(for: .openai)
|
||||
}
|
||||
@ -23,8 +23,8 @@ struct TestHelpersTests {
|
||||
#expect(result == nil)
|
||||
}
|
||||
|
||||
@Test("Test helper with standard test configuration")
|
||||
func helperStandardConfiguration() async {
|
||||
@Test
|
||||
func `helper with standard test configuration`() async {
|
||||
let result = await TestHelpers.withStandardTestConfiguration { config in
|
||||
config.getAPIKey(for: .openai)
|
||||
}
|
||||
@ -32,8 +32,8 @@ struct TestHelpersTests {
|
||||
#expect(result == "test-key")
|
||||
}
|
||||
|
||||
@Test("Test helper with selective configuration")
|
||||
func helperSelectiveConfiguration() async {
|
||||
@Test
|
||||
func `helper with selective configuration`() async {
|
||||
let result = await TestHelpers.withSelectiveTestConfiguration(present: ["openai"]) { config in
|
||||
(config.getAPIKey(for: .openai), config.getAPIKey(for: .anthropic))
|
||||
}
|
||||
|
||||
@ -2,8 +2,8 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Test("Debug Grok streaming issue")
|
||||
func grokStreamingDebug() async throws {
|
||||
@Test
|
||||
func `Debug Grok streaming issue`() async throws {
|
||||
guard ProcessInfo.processInfo.environment["RUN_GROK_DEBUG_TESTS"] == "1" else {
|
||||
print("Skipping Grok debug stream: RUN_GROK_DEBUG_TESTS not set")
|
||||
return
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Embeddings API")
|
||||
struct EmbeddingsAPITests {
|
||||
@Test("EmbeddingModel enum cases")
|
||||
func embeddingModelCases() {
|
||||
@Test
|
||||
func `EmbeddingModel enum cases`() {
|
||||
// OpenAI models
|
||||
let ada = EmbeddingModel.openai(.ada002)
|
||||
let small3 = EmbeddingModel.openai(.small3)
|
||||
@ -43,58 +42,58 @@ struct EmbeddingsAPITests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI embedding model raw values")
|
||||
func openAIEmbeddingModelRawValues() {
|
||||
@Test
|
||||
func `OpenAI embedding model raw values`() {
|
||||
#expect(EmbeddingModel.OpenAIEmbedding.ada002.rawValue == "text-embedding-ada-002")
|
||||
#expect(EmbeddingModel.OpenAIEmbedding.small3.rawValue == "text-embedding-3-small")
|
||||
#expect(EmbeddingModel.OpenAIEmbedding.large3.rawValue == "text-embedding-3-large")
|
||||
}
|
||||
|
||||
@Test("Cohere embedding model raw values")
|
||||
func cohereEmbeddingModelRawValues() {
|
||||
@Test
|
||||
func `Cohere embedding model raw values`() {
|
||||
#expect(EmbeddingModel.CohereEmbedding.english3.rawValue == "embed-english-v3.0")
|
||||
#expect(EmbeddingModel.CohereEmbedding.multilingual3.rawValue == "embed-multilingual-v3.0")
|
||||
#expect(EmbeddingModel.CohereEmbedding.englishLight3.rawValue == "embed-english-light-v3.0")
|
||||
#expect(EmbeddingModel.CohereEmbedding.multilingualLight3.rawValue == "embed-multilingual-light-v3.0")
|
||||
}
|
||||
|
||||
@Test("Voyage embedding model raw values")
|
||||
func voyageEmbeddingModelRawValues() {
|
||||
@Test
|
||||
func `Voyage embedding model raw values`() {
|
||||
#expect(EmbeddingModel.VoyageEmbedding.voyage2.rawValue == "voyage-2")
|
||||
#expect(EmbeddingModel.VoyageEmbedding.voyage2Code.rawValue == "voyage-code-2")
|
||||
#expect(EmbeddingModel.VoyageEmbedding.voyage2Large.rawValue == "voyage-large-2")
|
||||
}
|
||||
|
||||
@Test("EmbeddingInput text variant")
|
||||
func embeddingInputText() {
|
||||
@Test
|
||||
func `EmbeddingInput text variant`() {
|
||||
let input = EmbeddingInput.text("Hello, world!")
|
||||
#expect(input.asTexts == ["Hello, world!"])
|
||||
}
|
||||
|
||||
@Test("EmbeddingInput texts variant")
|
||||
func embeddingInputTexts() {
|
||||
@Test
|
||||
func `EmbeddingInput texts variant`() {
|
||||
let texts = ["First text", "Second text", "Third text"]
|
||||
let input = EmbeddingInput.texts(texts)
|
||||
#expect(input.asTexts == texts)
|
||||
}
|
||||
|
||||
@Test("EmbeddingInput tokens variant")
|
||||
func embeddingInputTokens() {
|
||||
@Test
|
||||
func `EmbeddingInput tokens variant`() {
|
||||
let tokens = [1234, 5678, 9012]
|
||||
let input = EmbeddingInput.tokens(tokens)
|
||||
#expect(input.asTexts.isEmpty) // Tokens don't convert to texts
|
||||
}
|
||||
|
||||
@Test("EmbeddingSettings default values")
|
||||
func embeddingSettingsDefaults() {
|
||||
@Test
|
||||
func `EmbeddingSettings default values`() {
|
||||
let settings = EmbeddingSettings.default
|
||||
#expect(settings.dimensions == nil)
|
||||
#expect(settings.normalizeEmbeddings == true)
|
||||
#expect(settings.truncate == nil)
|
||||
}
|
||||
|
||||
@Test("EmbeddingSettings custom values")
|
||||
func embeddingSettingsCustom() {
|
||||
@Test
|
||||
func `EmbeddingSettings custom values`() {
|
||||
let settings = EmbeddingSettings(
|
||||
dimensions: 512,
|
||||
normalizeEmbeddings: false,
|
||||
@ -106,15 +105,15 @@ struct EmbeddingsAPITests {
|
||||
#expect(settings.truncate == .end)
|
||||
}
|
||||
|
||||
@Test("EmbeddingSettings truncation strategies")
|
||||
func embeddingSettingsTruncationStrategies() {
|
||||
@Test
|
||||
func `EmbeddingSettings truncation strategies`() {
|
||||
#expect(EmbeddingSettings.TruncationStrategy.start.rawValue == "start")
|
||||
#expect(EmbeddingSettings.TruncationStrategy.end.rawValue == "end")
|
||||
#expect(EmbeddingSettings.TruncationStrategy.none.rawValue == "none")
|
||||
}
|
||||
|
||||
@Test("EmbeddingResult properties")
|
||||
func embeddingResultProperties() {
|
||||
@Test
|
||||
func `EmbeddingResult properties`() {
|
||||
let embeddings = [
|
||||
[0.1, 0.2, 0.3],
|
||||
[0.4, 0.5, 0.6],
|
||||
@ -133,8 +132,8 @@ struct EmbeddingsAPITests {
|
||||
#expect(result.metadata?.normalizedL2 == true)
|
||||
}
|
||||
|
||||
@Test("EmbeddingResult convenience properties")
|
||||
func embeddingResultConvenienceProperties() {
|
||||
@Test
|
||||
func `EmbeddingResult convenience properties`() {
|
||||
let embeddings = [[0.1, 0.2, 0.3, 0.4]]
|
||||
|
||||
let result = EmbeddingResult(
|
||||
@ -149,8 +148,8 @@ struct EmbeddingsAPITests {
|
||||
#expect(result.dimensions == 4)
|
||||
}
|
||||
|
||||
@Test("EmbeddingResult with empty embeddings")
|
||||
func embeddingResultEmpty() {
|
||||
@Test
|
||||
func `EmbeddingResult with empty embeddings`() {
|
||||
let result = EmbeddingResult(
|
||||
embeddings: [],
|
||||
model: "test-model",
|
||||
@ -160,8 +159,8 @@ struct EmbeddingsAPITests {
|
||||
#expect(result.dimensions == nil)
|
||||
}
|
||||
|
||||
@Test("EmbeddingMetadata properties")
|
||||
func embeddingMetadata() {
|
||||
@Test
|
||||
func `EmbeddingMetadata properties`() {
|
||||
let metadata1 = EmbeddingMetadata(truncated: true, normalizedL2: false)
|
||||
#expect(metadata1.truncated == true)
|
||||
#expect(metadata1.normalizedL2 == false)
|
||||
@ -171,8 +170,8 @@ struct EmbeddingsAPITests {
|
||||
#expect(metadata2.normalizedL2 == false)
|
||||
}
|
||||
|
||||
@Test("EmbeddingSettings Codable")
|
||||
func embeddingSettingsCodable() throws {
|
||||
@Test
|
||||
func `EmbeddingSettings Codable`() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
@ -190,8 +189,8 @@ struct EmbeddingsAPITests {
|
||||
#expect(decoded.truncate == original.truncate)
|
||||
}
|
||||
|
||||
@Test("EmbeddingMetadata Codable")
|
||||
func embeddingMetadataCodable() throws {
|
||||
@Test
|
||||
func `EmbeddingMetadata Codable`() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
@ -204,8 +203,8 @@ struct EmbeddingsAPITests {
|
||||
#expect(decoded.normalizedL2 == original.normalizedL2)
|
||||
}
|
||||
|
||||
@Test("EmbeddingModel to LanguageModel conversion")
|
||||
func embeddingModelToLanguageModel() {
|
||||
@Test
|
||||
func `EmbeddingModel to LanguageModel conversion`() {
|
||||
let openaiModel = EmbeddingModel.openai(.small3)
|
||||
let languageModel = openaiModel.toLanguageModel()
|
||||
|
||||
@ -226,8 +225,8 @@ struct EmbeddingsAPITests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("EmbeddingRequest structure")
|
||||
func embeddingRequest() {
|
||||
@Test
|
||||
func `EmbeddingRequest structure`() {
|
||||
let request = EmbeddingRequest(
|
||||
input: .text("Test input"),
|
||||
settings: EmbeddingSettings(dimensions: 128),
|
||||
@ -237,8 +236,8 @@ struct EmbeddingsAPITests {
|
||||
#expect(request.settings.dimensions == 128)
|
||||
}
|
||||
|
||||
@Test("EmbeddingProviderFactory produces expected providers")
|
||||
func embeddingProviderFactoryProducesExpectedProviders() throws {
|
||||
@Test
|
||||
func `EmbeddingProviderFactory produces expected providers`() throws {
|
||||
let configuration = TachikomaConfiguration()
|
||||
|
||||
let openAIProvider = try EmbeddingProviderFactory.createProvider(
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Enhanced Tool System")
|
||||
struct EnhancedToolSystemTests {
|
||||
@Test("AgentToolCall supports namespace and recipient")
|
||||
func toolCallNamespaceRecipient() {
|
||||
@Test
|
||||
func `AgentToolCall supports namespace and recipient`() {
|
||||
let toolCall = AgentToolCall(
|
||||
id: "call-123",
|
||||
name: "search",
|
||||
@ -20,8 +19,8 @@ struct EnhancedToolSystemTests {
|
||||
#expect(toolCall.arguments["query"]?.stringValue == "test")
|
||||
}
|
||||
|
||||
@Test("AgentToolCall works without namespace and recipient")
|
||||
func toolCallWithoutNamespaceRecipient() {
|
||||
@Test
|
||||
func `AgentToolCall works without namespace and recipient`() {
|
||||
let toolCall = AgentToolCall(
|
||||
name: "calculate",
|
||||
arguments: ["expression": AnyAgentToolValue(string: "2+2")],
|
||||
@ -32,8 +31,8 @@ struct EnhancedToolSystemTests {
|
||||
#expect(toolCall.name == "calculate")
|
||||
}
|
||||
|
||||
@Test("AgentTool supports namespace and recipient")
|
||||
func agentToolNamespaceRecipient() {
|
||||
@Test
|
||||
func `AgentTool supports namespace and recipient`() {
|
||||
let tool = AgentTool(
|
||||
name: "readFile",
|
||||
description: "Read file contents",
|
||||
@ -59,8 +58,8 @@ struct EnhancedToolSystemTests {
|
||||
#expect(tool.description == "Read file contents")
|
||||
}
|
||||
|
||||
@Test("AgentTool works without namespace and recipient")
|
||||
func agentToolWithoutNamespaceRecipient() {
|
||||
@Test
|
||||
func `AgentTool works without namespace and recipient`() {
|
||||
let tool = AgentTool(
|
||||
name: "echo",
|
||||
description: "Echo input",
|
||||
@ -72,8 +71,8 @@ struct EnhancedToolSystemTests {
|
||||
#expect(tool.name == "echo")
|
||||
}
|
||||
|
||||
@Test("Tool organization by namespace")
|
||||
func toolOrganizationByNamespace() {
|
||||
@Test
|
||||
func `Tool organization by namespace`() {
|
||||
let tools = [
|
||||
AgentTool(
|
||||
name: "readFile",
|
||||
@ -112,8 +111,8 @@ struct EnhancedToolSystemTests {
|
||||
#expect(toolsByNamespace["web"]?.count == 1)
|
||||
}
|
||||
|
||||
@Test("Tool routing by recipient")
|
||||
func toolRoutingByRecipient() {
|
||||
@Test
|
||||
func `Tool routing by recipient`() {
|
||||
let tools = [
|
||||
AgentTool(
|
||||
name: "query",
|
||||
@ -141,8 +140,8 @@ struct EnhancedToolSystemTests {
|
||||
#expect(replicaTool?.name == "query")
|
||||
}
|
||||
|
||||
@Test("AgentToolCall Codable with namespace and recipient")
|
||||
func toolCallCodableWithNamespaceRecipient() throws {
|
||||
@Test
|
||||
func `AgentToolCall Codable with namespace and recipient`() throws {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .sortedKeys
|
||||
let decoder = JSONDecoder()
|
||||
@ -165,8 +164,8 @@ struct EnhancedToolSystemTests {
|
||||
#expect(decoded.arguments["query"] == original.arguments["query"])
|
||||
}
|
||||
|
||||
@Test("AgentToolCall Codable without namespace and recipient")
|
||||
func toolCallCodableWithoutNamespaceRecipient() throws {
|
||||
@Test
|
||||
func `AgentToolCall Codable without namespace and recipient`() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
@ -183,11 +182,13 @@ struct EnhancedToolSystemTests {
|
||||
#expect(decoded.name == "calculate")
|
||||
}
|
||||
|
||||
@Test("Tool execution with namespace context")
|
||||
func toolExecutionWithNamespaceContext() async throws {
|
||||
@Test
|
||||
func `Tool execution with namespace context`() async throws {
|
||||
actor NamespaceCapture {
|
||||
var namespace: String?
|
||||
func set(_ value: String) { self.namespace = value }
|
||||
func set(_ value: String) {
|
||||
self.namespace = value
|
||||
}
|
||||
}
|
||||
|
||||
let capture = NamespaceCapture()
|
||||
@ -210,8 +211,8 @@ struct EnhancedToolSystemTests {
|
||||
#expect(await capture.namespace == "test-namespace")
|
||||
}
|
||||
|
||||
@Test("Multiple tools with same name but different namespaces")
|
||||
func multipleToolsSameNameDifferentNamespaces() {
|
||||
@Test
|
||||
func `Multiple tools with same name but different namespaces`() {
|
||||
let webSearch = AgentTool(
|
||||
name: "search",
|
||||
description: "Web search",
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Multi-Channel Response System")
|
||||
struct MultiChannelResponseTests {
|
||||
@Test("ResponseChannel enum has all expected cases")
|
||||
func responseChannelCases() {
|
||||
@Test
|
||||
func `ResponseChannel enum has all expected cases`() {
|
||||
let allCases = ResponseChannel.allCases
|
||||
#expect(allCases.count == 4)
|
||||
#expect(allCases.contains(.thinking))
|
||||
@ -13,16 +12,16 @@ struct MultiChannelResponseTests {
|
||||
#expect(allCases.contains(.final))
|
||||
}
|
||||
|
||||
@Test("ResponseChannel raw values are correct")
|
||||
func responseChannelRawValues() {
|
||||
@Test
|
||||
func `ResponseChannel raw values are correct`() {
|
||||
#expect(ResponseChannel.thinking.rawValue == "thinking")
|
||||
#expect(ResponseChannel.analysis.rawValue == "analysis")
|
||||
#expect(ResponseChannel.commentary.rawValue == "commentary")
|
||||
#expect(ResponseChannel.final.rawValue == "final")
|
||||
}
|
||||
|
||||
@Test("ModelMessage supports channel property")
|
||||
func modelMessageChannel() {
|
||||
@Test
|
||||
func `ModelMessage supports channel property`() {
|
||||
let message = ModelMessage(
|
||||
role: .assistant,
|
||||
content: [.text("This is my reasoning")],
|
||||
@ -34,8 +33,8 @@ struct MultiChannelResponseTests {
|
||||
#expect(message.content.first == .text("This is my reasoning"))
|
||||
}
|
||||
|
||||
@Test("ModelMessage supports metadata property")
|
||||
func modelMessageMetadata() {
|
||||
@Test
|
||||
func `ModelMessage supports metadata property`() {
|
||||
let metadata = MessageMetadata(
|
||||
conversationId: "conv-123",
|
||||
turnId: "turn-456",
|
||||
@ -53,8 +52,8 @@ struct MultiChannelResponseTests {
|
||||
#expect(message.metadata?.customData?["key"] == "value")
|
||||
}
|
||||
|
||||
@Test("TextStreamDelta supports channel events")
|
||||
func textStreamDeltaChannelEvents() {
|
||||
@Test
|
||||
func `TextStreamDelta supports channel events`() {
|
||||
// Channel information is now passed via the channel property, not event types
|
||||
let reasoningDelta = TextStreamDelta(
|
||||
type: .reasoning,
|
||||
@ -84,8 +83,8 @@ struct MultiChannelResponseTests {
|
||||
#expect(textDelta.content == "Reasoning about the problem")
|
||||
}
|
||||
|
||||
@Test("MessageMetadata equality")
|
||||
func messageMetadataEquality() {
|
||||
@Test
|
||||
func `MessageMetadata equality`() {
|
||||
let metadata1 = MessageMetadata(
|
||||
conversationId: "123",
|
||||
turnId: "456",
|
||||
@ -108,8 +107,8 @@ struct MultiChannelResponseTests {
|
||||
#expect(metadata1 != metadata3)
|
||||
}
|
||||
|
||||
@Test("Legacy messages work without channel")
|
||||
func legacyMessageCompatibility() {
|
||||
@Test
|
||||
func `Legacy messages work without channel`() {
|
||||
// Old API still works
|
||||
let message = ModelMessage.user("Hello")
|
||||
|
||||
@ -119,8 +118,8 @@ struct MultiChannelResponseTests {
|
||||
#expect(message.content.first == .text("Hello"))
|
||||
}
|
||||
|
||||
@Test("Convenience initializers preserve nil channel")
|
||||
func convenienceInitializers() {
|
||||
@Test
|
||||
func `Convenience initializers preserve nil channel`() {
|
||||
let systemMessage = ModelMessage.system("You are helpful")
|
||||
let userMessage = ModelMessage.user("Hello")
|
||||
let assistantMessage = ModelMessage.assistant("Hi there")
|
||||
@ -130,8 +129,8 @@ struct MultiChannelResponseTests {
|
||||
#expect(assistantMessage.channel == nil)
|
||||
}
|
||||
|
||||
@Test("Channel-aware message creation")
|
||||
func channelAwareMessageCreation() {
|
||||
@Test
|
||||
func `Channel-aware message creation`() {
|
||||
let thinkingMessage = ModelMessage(
|
||||
role: .assistant,
|
||||
content: [.text("Let me think about this...")],
|
||||
@ -155,8 +154,8 @@ struct MultiChannelResponseTests {
|
||||
#expect(finalMessage.channel == .final)
|
||||
}
|
||||
|
||||
@Test("Codable conformance for ResponseChannel")
|
||||
func responseChannelCodable() throws {
|
||||
@Test
|
||||
func `Codable conformance for ResponseChannel`() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
@ -171,8 +170,8 @@ struct MultiChannelResponseTests {
|
||||
#expect(jsonString == "\"thinking\"")
|
||||
}
|
||||
|
||||
@Test("Codable conformance for MessageMetadata")
|
||||
func messageMetadataCodable() throws {
|
||||
@Test
|
||||
func `Codable conformance for MessageMetadata`() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Reasoning Effort Levels")
|
||||
struct ReasoningEffortTests {
|
||||
@Test("ReasoningEffort enum has all expected cases")
|
||||
func reasoningEffortCases() {
|
||||
@Test
|
||||
func `ReasoningEffort enum has all expected cases`() {
|
||||
let allCases = ReasoningEffort.allCases
|
||||
#expect(allCases.count == 3)
|
||||
#expect(allCases.contains(.low))
|
||||
@ -12,15 +11,15 @@ struct ReasoningEffortTests {
|
||||
#expect(allCases.contains(.high))
|
||||
}
|
||||
|
||||
@Test("ReasoningEffort raw values are correct")
|
||||
func reasoningEffortRawValues() {
|
||||
@Test
|
||||
func `ReasoningEffort raw values are correct`() {
|
||||
#expect(ReasoningEffort.low.rawValue == "low")
|
||||
#expect(ReasoningEffort.medium.rawValue == "medium")
|
||||
#expect(ReasoningEffort.high.rawValue == "high")
|
||||
}
|
||||
|
||||
@Test("GenerationSettings supports reasoning effort")
|
||||
func generationSettingsReasoningEffort() {
|
||||
@Test
|
||||
func `GenerationSettings supports reasoning effort`() {
|
||||
let settings = GenerationSettings(
|
||||
maxTokens: 1000,
|
||||
temperature: 0.7,
|
||||
@ -32,14 +31,14 @@ struct ReasoningEffortTests {
|
||||
#expect(settings.temperature == 0.7)
|
||||
}
|
||||
|
||||
@Test("GenerationSettings default has nil reasoning effort")
|
||||
func generationSettingsDefaultReasoningEffort() {
|
||||
@Test
|
||||
func `GenerationSettings default has nil reasoning effort`() {
|
||||
let settings = GenerationSettings.default
|
||||
#expect(settings.reasoningEffort == nil)
|
||||
}
|
||||
|
||||
@Test("RetryHandler adapts based on reasoning effort")
|
||||
func retryHandlerReasoningEffortAdaptation() {
|
||||
@Test
|
||||
func `RetryHandler adapts based on reasoning effort`() {
|
||||
// High effort should use aggressive policy
|
||||
let highSettings = GenerationSettings(reasoningEffort: .high)
|
||||
let highHandler = RetryHandler.from(settings: highSettings)
|
||||
@ -65,8 +64,8 @@ struct ReasoningEffortTests {
|
||||
#expect(Bool(true))
|
||||
}
|
||||
|
||||
@Test("Codable conformance for ReasoningEffort")
|
||||
func reasoningEffortCodable() throws {
|
||||
@Test
|
||||
func `Codable conformance for ReasoningEffort`() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
@ -81,8 +80,8 @@ struct ReasoningEffortTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("GenerationSettings Codable with reasoning effort")
|
||||
func generationSettingsCodableWithReasoningEffort() throws {
|
||||
@Test
|
||||
func `GenerationSettings Codable with reasoning effort`() throws {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .sortedKeys
|
||||
let decoder = JSONDecoder()
|
||||
@ -103,8 +102,8 @@ struct ReasoningEffortTests {
|
||||
#expect(decoded.reasoningEffort == original.reasoningEffort)
|
||||
}
|
||||
|
||||
@Test("GenerationSettings Codable without reasoning effort")
|
||||
func generationSettingsCodableWithoutReasoningEffort() throws {
|
||||
@Test
|
||||
func `GenerationSettings Codable without reasoning effort`() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
@ -122,8 +121,8 @@ struct ReasoningEffortTests {
|
||||
#expect(decoded.temperature == 0.7)
|
||||
}
|
||||
|
||||
@Test("All reasoning effort levels properly ordered")
|
||||
func reasoningEffortOrdering() {
|
||||
@Test
|
||||
func `All reasoning effort levels properly ordered`() {
|
||||
// Ensure we have the expected reasoning levels
|
||||
let levels: [ReasoningEffort] = [.low, .medium, .high]
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
// Helper class for thread-safe mutable value in closures
|
||||
/// Helper class for thread-safe mutable value in closures
|
||||
final class Box<T>: @unchecked Sendable {
|
||||
var value: T
|
||||
init(value: T) {
|
||||
@ -9,10 +9,9 @@ final class Box<T>: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Response Caching")
|
||||
struct ResponseCacheTests {
|
||||
@Test("ResponseCache initialization")
|
||||
func responseCacheInitialization() async {
|
||||
@Test
|
||||
func `ResponseCache initialization`() async {
|
||||
let config = CacheConfiguration(maxEntries: 50, defaultTTL: 1800)
|
||||
let cache = ResponseCache(configuration: config)
|
||||
// Note: statistics() is not a public method, commenting out for now
|
||||
@ -32,8 +31,8 @@ struct ResponseCacheTests {
|
||||
#expect(cached == nil)
|
||||
}
|
||||
|
||||
@Test("ResponseCache store and retrieve")
|
||||
func responseCacheStoreRetrieve() async {
|
||||
@Test
|
||||
func `ResponseCache store and retrieve`() async {
|
||||
let cache = ResponseCache()
|
||||
|
||||
let request = ProviderRequest(
|
||||
@ -60,8 +59,8 @@ struct ResponseCacheTests {
|
||||
#expect(cached?.finishReason == .stop)
|
||||
}
|
||||
|
||||
@Test("ResponseCache cache miss")
|
||||
func responseCacheMiss() async {
|
||||
@Test
|
||||
func `ResponseCache cache miss`() async {
|
||||
let cache = ResponseCache()
|
||||
|
||||
let request = ProviderRequest(
|
||||
@ -75,8 +74,8 @@ struct ResponseCacheTests {
|
||||
#expect(cached == nil)
|
||||
}
|
||||
|
||||
@Test("ResponseCache TTL expiration")
|
||||
func responseCacheTTLExpiration() async throws {
|
||||
@Test
|
||||
func `ResponseCache TTL expiration`() async throws {
|
||||
let config = CacheConfiguration(defaultTTL: 0.1) // 100ms TTL
|
||||
let cache = ResponseCache(configuration: config)
|
||||
|
||||
@ -102,8 +101,8 @@ struct ResponseCacheTests {
|
||||
#expect(cached2 == nil)
|
||||
}
|
||||
|
||||
@Test("ResponseCache LRU eviction")
|
||||
func responseCacheLRUEviction() async {
|
||||
@Test
|
||||
func `ResponseCache LRU eviction`() async {
|
||||
let config = CacheConfiguration(maxEntries: 2) // Small cache
|
||||
let cache = ResponseCache(configuration: config)
|
||||
|
||||
@ -151,8 +150,8 @@ struct ResponseCacheTests {
|
||||
#expect(cached3?.text == "Response 3")
|
||||
}
|
||||
|
||||
@Test("ResponseCache clear")
|
||||
func responseCacheClear() async {
|
||||
@Test
|
||||
func `ResponseCache clear`() async {
|
||||
let cache = ResponseCache()
|
||||
|
||||
// Store multiple entries
|
||||
@ -180,8 +179,8 @@ struct ResponseCacheTests {
|
||||
// #expect(stats.validEntries == 0)
|
||||
}
|
||||
|
||||
@Test("ResponseCache statistics")
|
||||
func responseCacheStatistics() async {
|
||||
@Test
|
||||
func `ResponseCache statistics`() async {
|
||||
let config = CacheConfiguration(maxEntries: 100, defaultTTL: 3600)
|
||||
let cache = ResponseCache(configuration: config)
|
||||
|
||||
@ -210,8 +209,8 @@ struct ResponseCacheTests {
|
||||
// #expect(stats.newestEntry != nil)
|
||||
}
|
||||
|
||||
@Test("CacheKey generation deterministic")
|
||||
func cacheKeyDeterministic() {
|
||||
@Test
|
||||
func `CacheKey generation deterministic`() {
|
||||
let messages = [
|
||||
ModelMessage.user("Hello"),
|
||||
ModelMessage.assistant("Hi there"),
|
||||
@ -237,8 +236,8 @@ struct ResponseCacheTests {
|
||||
#expect(key1.model == key2.model)
|
||||
}
|
||||
|
||||
@Test("CacheKey differs for different requests")
|
||||
func cacheKeyDifferent() {
|
||||
@Test
|
||||
func `CacheKey differs for different requests`() {
|
||||
let request1 = ProviderRequest(
|
||||
messages: [ModelMessage.user("Hello")],
|
||||
tools: nil,
|
||||
@ -268,8 +267,8 @@ struct ResponseCacheTests {
|
||||
#expect(key1.hash != key3.hash)
|
||||
}
|
||||
|
||||
@Test("CacheKey includes tools in hash")
|
||||
func cacheKeyIncludesTools() {
|
||||
@Test
|
||||
func `CacheKey includes tools in hash`() {
|
||||
let tool1 = AgentTool(
|
||||
name: "tool1",
|
||||
description: "First tool",
|
||||
@ -312,8 +311,8 @@ struct ResponseCacheTests {
|
||||
#expect(key2.hash != key3.hash)
|
||||
}
|
||||
|
||||
@Test("CachedProvider wraps provider correctly")
|
||||
func cachedProviderWrapper() async throws {
|
||||
@Test
|
||||
func `CachedProvider wraps provider correctly`() async {
|
||||
let cache = ResponseCache()
|
||||
|
||||
// Create a mock provider
|
||||
@ -330,8 +329,8 @@ struct ResponseCacheTests {
|
||||
#expect(cachedProvider.apiKey == mockProvider.apiKey)
|
||||
}
|
||||
|
||||
@Test("CachedProvider caches generateText")
|
||||
func cachedProviderGenerateText() async throws {
|
||||
@Test
|
||||
func `CachedProvider caches generateText`() async throws {
|
||||
let cache = ResponseCache()
|
||||
|
||||
// Use a simple counter that can be modified in the closure
|
||||
@ -363,8 +362,8 @@ struct ResponseCacheTests {
|
||||
#expect(callCount.value == 1) // Provider not called again
|
||||
}
|
||||
|
||||
@Test("CachedProvider doesn't cache streaming")
|
||||
func cachedProviderStreamText() async throws {
|
||||
@Test
|
||||
func `CachedProvider doesn't cache streaming`() async throws {
|
||||
let cache = ResponseCache()
|
||||
|
||||
let callCount = Box(value: 0)
|
||||
@ -401,10 +400,21 @@ private struct ResponseCacheMockProvider: ModelProvider {
|
||||
var onGenerateText: (@Sendable (ProviderRequest) -> Void)?
|
||||
var onStreamText: (@Sendable (ProviderRequest) -> Void)?
|
||||
|
||||
var modelId: String { "mock-model" }
|
||||
var baseURL: String? { nil }
|
||||
var apiKey: String? { nil }
|
||||
var capabilities: ModelCapabilities { ModelCapabilities() }
|
||||
var modelId: String {
|
||||
"mock-model"
|
||||
}
|
||||
|
||||
var baseURL: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var apiKey: String? {
|
||||
nil
|
||||
}
|
||||
|
||||
var capabilities: ModelCapabilities {
|
||||
ModelCapabilities()
|
||||
}
|
||||
|
||||
init(
|
||||
model: LanguageModel,
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Retry Handler")
|
||||
struct RetryHandlerTests {
|
||||
// Helper actor for thread-safe counting
|
||||
/// Helper actor for thread-safe counting
|
||||
actor CallCounter {
|
||||
private(set) var count = 0
|
||||
|
||||
@ -16,8 +15,8 @@ struct RetryHandlerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("RetryPolicy default values")
|
||||
func retryPolicyDefaults() {
|
||||
@Test
|
||||
func `RetryPolicy default values`() {
|
||||
let policy = RetryPolicy()
|
||||
#expect(policy.maxAttempts == 3)
|
||||
#expect(policy.baseDelay == 1.0)
|
||||
@ -26,8 +25,8 @@ struct RetryHandlerTests {
|
||||
#expect(policy.jitterRange == 0.9...1.1)
|
||||
}
|
||||
|
||||
@Test("RetryPolicy aggressive configuration")
|
||||
func retryPolicyAggressive() {
|
||||
@Test
|
||||
func `RetryPolicy aggressive configuration`() {
|
||||
let policy = RetryPolicy.aggressive
|
||||
#expect(policy.maxAttempts == 5)
|
||||
#expect(policy.baseDelay == 0.5)
|
||||
@ -35,8 +34,8 @@ struct RetryHandlerTests {
|
||||
#expect(policy.exponentialBase == 1.5)
|
||||
}
|
||||
|
||||
@Test("RetryPolicy conservative configuration")
|
||||
func retryPolicyConservative() {
|
||||
@Test
|
||||
func `RetryPolicy conservative configuration`() {
|
||||
let policy = RetryPolicy.conservative
|
||||
#expect(policy.maxAttempts == 2)
|
||||
#expect(policy.baseDelay == 2.0)
|
||||
@ -44,8 +43,8 @@ struct RetryHandlerTests {
|
||||
#expect(policy.exponentialBase == 2.0)
|
||||
}
|
||||
|
||||
@Test("RetryPolicy delay calculation")
|
||||
func retryPolicyDelayCalculation() {
|
||||
@Test
|
||||
func `RetryPolicy delay calculation`() {
|
||||
let policy = RetryPolicy(
|
||||
baseDelay: 1.0,
|
||||
maxDelay: 10.0,
|
||||
@ -74,8 +73,8 @@ struct RetryHandlerTests {
|
||||
#expect(delay4 == 10.0) // 1.0 * 2^4 = 16.0, clamped to 10.0
|
||||
}
|
||||
|
||||
@Test("RetryPolicy delay with jitter")
|
||||
func retryPolicyDelayWithJitter() {
|
||||
@Test
|
||||
func `RetryPolicy delay with jitter`() {
|
||||
let policy = RetryPolicy(
|
||||
baseDelay: 1.0,
|
||||
exponentialBase: 2.0,
|
||||
@ -88,8 +87,8 @@ struct RetryHandlerTests {
|
||||
#expect(delay <= 1.5) // 1.0 * 1.5
|
||||
}
|
||||
|
||||
@Test("RetryPolicy default shouldRetry for rate limits")
|
||||
func retryPolicyDefaultShouldRetryRateLimits() {
|
||||
@Test
|
||||
func `RetryPolicy default shouldRetry for rate limits`() {
|
||||
let shouldRetry = RetryPolicy.defaultShouldRetry
|
||||
|
||||
// Should retry rate limits
|
||||
@ -100,8 +99,8 @@ struct RetryHandlerTests {
|
||||
#expect(shouldRetry(rateLimitNoRetryAfter) == true)
|
||||
}
|
||||
|
||||
@Test("RetryPolicy default shouldRetry for network errors")
|
||||
func retryPolicyDefaultShouldRetryNetworkErrors() {
|
||||
@Test
|
||||
func `RetryPolicy default shouldRetry for network errors`() {
|
||||
let shouldRetry = RetryPolicy.defaultShouldRetry
|
||||
|
||||
// Should retry network errors
|
||||
@ -109,8 +108,8 @@ struct RetryHandlerTests {
|
||||
#expect(shouldRetry(networkError) == true)
|
||||
}
|
||||
|
||||
@Test("RetryPolicy default shouldRetry for API errors")
|
||||
func retryPolicyDefaultShouldRetryAPIErrors() {
|
||||
@Test
|
||||
func `RetryPolicy default shouldRetry for API errors`() {
|
||||
let shouldRetry = RetryPolicy.defaultShouldRetry
|
||||
|
||||
// Should retry specific API errors
|
||||
@ -125,8 +124,8 @@ struct RetryHandlerTests {
|
||||
#expect(shouldRetry(TachikomaError.apiError("Model not found")) == false)
|
||||
}
|
||||
|
||||
@Test("RetryPolicy default shouldRetry for non-retryable errors")
|
||||
func retryPolicyDefaultShouldRetryNonRetryableErrors() {
|
||||
@Test
|
||||
func `RetryPolicy default shouldRetry for non-retryable errors`() {
|
||||
let shouldRetry = RetryPolicy.defaultShouldRetry
|
||||
|
||||
// Should not retry authentication failures
|
||||
@ -139,8 +138,8 @@ struct RetryHandlerTests {
|
||||
#expect(shouldRetry(TachikomaError.unsupportedOperation("Not implemented")) == false)
|
||||
}
|
||||
|
||||
@Test("RetryHandler execute with success")
|
||||
func retryHandlerExecuteSuccess() async throws {
|
||||
@Test
|
||||
func `RetryHandler execute with success`() async throws {
|
||||
let handler = RetryHandler()
|
||||
let callCounter = CallCounter()
|
||||
|
||||
@ -153,8 +152,8 @@ struct RetryHandlerTests {
|
||||
#expect(await callCounter.count == 1) // Should succeed on first try
|
||||
}
|
||||
|
||||
@Test("RetryHandler execute with retries")
|
||||
func retryHandlerExecuteWithRetries() async throws {
|
||||
@Test
|
||||
func `RetryHandler execute with retries`() async throws {
|
||||
let policy = RetryPolicy(
|
||||
maxAttempts: 3,
|
||||
baseDelay: 0.01,
|
||||
@ -190,8 +189,8 @@ struct RetryHandlerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("RetryHandler execute exhausts retries")
|
||||
func retryHandlerExecuteExhaustsRetries() async throws {
|
||||
@Test
|
||||
func `RetryHandler execute exhausts retries`() async throws {
|
||||
let policy = RetryPolicy(
|
||||
maxAttempts: 2,
|
||||
baseDelay: 0.01,
|
||||
@ -210,8 +209,8 @@ struct RetryHandlerTests {
|
||||
#expect(await callCounter.count == 2) // Should try maxAttempts times
|
||||
}
|
||||
|
||||
@Test("RetryHandler execute with non-retryable error")
|
||||
func retryHandlerExecuteNonRetryableError() async throws {
|
||||
@Test
|
||||
func `RetryHandler execute with non-retryable error`() async throws {
|
||||
let handler = RetryHandler()
|
||||
let callCounter = CallCounter()
|
||||
|
||||
@ -225,8 +224,8 @@ struct RetryHandlerTests {
|
||||
#expect(await callCounter.count == 1) // Should not retry
|
||||
}
|
||||
|
||||
@Test("RetryHandler executeStream success")
|
||||
func retryHandlerExecuteStreamSuccess() async throws {
|
||||
@Test
|
||||
func `RetryHandler executeStream success`() async throws {
|
||||
let handler = RetryHandler()
|
||||
let callCounter = CallCounter()
|
||||
|
||||
@ -248,8 +247,8 @@ struct RetryHandlerTests {
|
||||
#expect(await callCounter.count == 1)
|
||||
}
|
||||
|
||||
@Test("RetryHandler from GenerationSettings")
|
||||
func retryHandlerFromGenerationSettings() {
|
||||
@Test
|
||||
func `RetryHandler from GenerationSettings`() {
|
||||
// High effort uses aggressive policy
|
||||
let highSettings = GenerationSettings(reasoningEffort: .high)
|
||||
let highHandler = RetryHandler.from(settings: highSettings)
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("TachikomaCLI Placeholder Tests")
|
||||
struct PlaceholderTests {
|
||||
@Test("Placeholder test")
|
||||
func placeholderTest() {
|
||||
@Test
|
||||
func `Placeholder test`() {
|
||||
// This is a placeholder test to satisfy Package.swift test target requirements
|
||||
#expect(Bool(true))
|
||||
}
|
||||
|
||||
@ -3,10 +3,9 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
@testable import TachikomaAgent
|
||||
|
||||
@Suite("Integration Tests")
|
||||
struct IntegrationTests {
|
||||
@Test("End-to-end UI message flow with streaming")
|
||||
func uIMessageFlowWithStreaming() async throws {
|
||||
@Test
|
||||
func `End-to-end UI message flow with streaming`() async {
|
||||
// Create UI messages
|
||||
let uiMessages = [
|
||||
UIMessage(role: .system, content: "You are a helpful assistant"),
|
||||
@ -45,8 +44,8 @@ struct IntegrationTests {
|
||||
#expect(assistantMessage.content == "The answer is 4")
|
||||
}
|
||||
|
||||
@Test("Tool execution with simplified builder and error handling")
|
||||
func toolExecutionWithErrorHandling() async throws {
|
||||
@Test
|
||||
func `Tool execution with simplified builder and error handling`() async throws {
|
||||
struct MathInput: Codable, Sendable {
|
||||
let operation: String
|
||||
let a: Double
|
||||
@ -115,8 +114,8 @@ struct IntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Async operations with timeout and cancellation")
|
||||
func asyncOperationsWithTimeoutAndCancellation() async throws {
|
||||
@Test
|
||||
func `Async operations with timeout and cancellation`() async throws {
|
||||
let token = CancellationToken()
|
||||
|
||||
let task = Task {
|
||||
@ -143,8 +142,8 @@ struct IntegrationTests {
|
||||
#expect(await token.cancelled)
|
||||
}
|
||||
|
||||
@Test("Provider with feature parity and caching")
|
||||
func providerWithFeatureParityAndCaching() async throws {
|
||||
@Test
|
||||
func `Provider with feature parity and caching`() async throws {
|
||||
// Create a mock provider with limited capabilities
|
||||
struct LimitedProvider: ModelProvider {
|
||||
let modelId = "limited-model"
|
||||
@ -219,8 +218,8 @@ struct IntegrationTests {
|
||||
#expect(stats.hits >= 1)
|
||||
}
|
||||
|
||||
@Test("Complete tool workflow with context and error recovery")
|
||||
func completeToolWorkflow() async throws {
|
||||
@Test
|
||||
func `Complete tool workflow with context and error recovery`() async throws {
|
||||
// Create a contextual tool
|
||||
let searchTool = AgentTool.createWithContext(
|
||||
name: "contextual_search",
|
||||
|
||||
@ -2,10 +2,9 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("LMStudio Provider Tests")
|
||||
struct LMStudioProviderTests {
|
||||
@Test("Provider initialization")
|
||||
func providerInitialization() async throws {
|
||||
@Test
|
||||
func `Provider initialization`() {
|
||||
let provider = LMStudioProvider(
|
||||
baseURL: "http://localhost:1234/v1",
|
||||
modelId: "gpt-oss-120b",
|
||||
@ -25,8 +24,8 @@ struct LMStudioProviderTests {
|
||||
#expect(capabilities.supportsStreaming == true)
|
||||
}
|
||||
|
||||
@Test("Model enum integration")
|
||||
func modelEnumIntegration() async throws {
|
||||
@Test
|
||||
func `Model enum integration`() {
|
||||
let model1 = LanguageModel.lmstudio(.gptOSS120B)
|
||||
let model2 = LanguageModel.lmstudio(.gptOSS20B)
|
||||
let model3 = LanguageModel.lmstudio(.current)
|
||||
@ -39,8 +38,8 @@ struct LMStudioProviderTests {
|
||||
#expect(model1.contextLength == 128_000)
|
||||
}
|
||||
|
||||
@Test("Convenience properties")
|
||||
func convenienceProperties() async throws {
|
||||
@Test
|
||||
func `Convenience properties`() {
|
||||
let model1 = LanguageModel.gptOSS120B // Ollama version
|
||||
let model2 = LanguageModel.gptOSS120B_LMStudio // LMStudio version
|
||||
|
||||
@ -51,8 +50,8 @@ struct LMStudioProviderTests {
|
||||
#expect(model2.modelId == "gpt-oss-120b")
|
||||
}
|
||||
|
||||
@Test("Provider factory creation")
|
||||
func providerFactoryCreation() async throws {
|
||||
@Test
|
||||
func `Provider factory creation`() throws {
|
||||
let config = TachikomaConfiguration()
|
||||
|
||||
// Test LMStudio provider creation
|
||||
@ -66,8 +65,8 @@ struct LMStudioProviderTests {
|
||||
#expect(provider is LMStudioProvider)
|
||||
}
|
||||
|
||||
@Test("Response channel parsing")
|
||||
func responseChannelParsing() async throws {
|
||||
@Test
|
||||
func `Response channel parsing`() {
|
||||
let parser = LocalModelResponseParser.self
|
||||
|
||||
// Test multi-channel response
|
||||
@ -106,8 +105,8 @@ struct LMStudioProviderTests {
|
||||
#expect(channels3[.final] == "Here's the answer without a final tag.")
|
||||
}
|
||||
|
||||
@Test("Auto-detection (mock)")
|
||||
func autoDetection() async throws {
|
||||
@Test
|
||||
func `Auto-detection (mock)`() async throws {
|
||||
// This test would normally try to connect to LMStudio
|
||||
// In CI/mock mode, it should handle the failure gracefully
|
||||
|
||||
@ -125,8 +124,8 @@ struct LMStudioProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Request mapping")
|
||||
func requestMapping() async throws {
|
||||
@Test
|
||||
func `Request mapping`() {
|
||||
_ = LMStudioProvider() // Just verify it can be created
|
||||
|
||||
let request = ProviderRequest(
|
||||
|
||||
@ -2,9 +2,8 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Provider Feature Parity Tests")
|
||||
struct ProviderFeatureParityTests {
|
||||
// Mock provider for testing
|
||||
/// Mock provider for testing
|
||||
struct MockProvider: ModelProvider {
|
||||
let modelId = "mock-model"
|
||||
let baseURL: String? = nil
|
||||
@ -27,8 +26,8 @@ struct ProviderFeatureParityTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Provider configuration defaults")
|
||||
func providerConfigurationDefaults() throws {
|
||||
@Test
|
||||
func `Provider configuration defaults`() {
|
||||
let config = ProviderConfiguration()
|
||||
|
||||
#expect(config.maxTokens == 4096)
|
||||
@ -40,8 +39,8 @@ struct ProviderFeatureParityTests {
|
||||
#expect(config.requiresAlternatingRoles == false)
|
||||
}
|
||||
|
||||
@Test("Provider configuration presets")
|
||||
func providerConfigurationPresets() throws {
|
||||
@Test
|
||||
func `Provider configuration presets`() {
|
||||
let openAI = ProviderConfiguration.openAI
|
||||
#expect(openAI.supportsSystemRole == true)
|
||||
#expect(openAI.requiresAlternatingRoles == false)
|
||||
@ -59,8 +58,8 @@ struct ProviderFeatureParityTests {
|
||||
#expect(ollama.maxToolCalls == 0) // No tool support by default
|
||||
}
|
||||
|
||||
@Test("Provider feature detection")
|
||||
func providerFeatureDetection() throws {
|
||||
@Test
|
||||
func `Provider feature detection`() {
|
||||
let provider = MockProvider(
|
||||
capabilities: ModelCapabilities(
|
||||
supportsVision: true,
|
||||
@ -82,8 +81,8 @@ struct ProviderFeatureParityTests {
|
||||
#expect(adapter.isFeatureSupported(ProviderFeature.systemMessages))
|
||||
}
|
||||
|
||||
@Test("System message transformation")
|
||||
func systemMessageTransformation() async throws {
|
||||
@Test
|
||||
func `System message transformation`() throws {
|
||||
let provider = MockProvider(
|
||||
capabilities: ModelCapabilities(
|
||||
supportsVision: false,
|
||||
@ -116,8 +115,8 @@ struct ProviderFeatureParityTests {
|
||||
#expect(validated[1].role == .user)
|
||||
}
|
||||
|
||||
@Test("Alternating roles enforcement")
|
||||
func alternatingRolesEnforcement() async throws {
|
||||
@Test
|
||||
func `Alternating roles enforcement`() throws {
|
||||
let provider = MockProvider(
|
||||
capabilities: ModelCapabilities(
|
||||
supportsVision: false,
|
||||
@ -146,8 +145,8 @@ struct ProviderFeatureParityTests {
|
||||
#expect(validated.count < messages.count)
|
||||
}
|
||||
|
||||
@Test("Vision input validation")
|
||||
func visionInputValidation() async throws {
|
||||
@Test
|
||||
func `Vision input validation`() throws {
|
||||
let provider = MockProvider(
|
||||
capabilities: ModelCapabilities(
|
||||
supportsVision: false, // Doesn't support vision
|
||||
@ -187,8 +186,8 @@ struct ProviderFeatureParityTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Tool limit enforcement")
|
||||
func toolLimitEnforcement() async throws {
|
||||
@Test
|
||||
func `Tool limit enforcement`() {
|
||||
let provider = MockProvider(
|
||||
capabilities: ModelCapabilities(
|
||||
supportsVision: false,
|
||||
@ -222,8 +221,8 @@ struct ProviderFeatureParityTests {
|
||||
#expect(config.maxToolCalls == 2)
|
||||
}
|
||||
|
||||
@Test("Provider feature flags")
|
||||
func providerFeatureFlags() throws {
|
||||
@Test
|
||||
func `Provider feature flags`() {
|
||||
for feature in ProviderFeature.allCases {
|
||||
// Verify all features have string representations
|
||||
#expect(!feature.rawValue.isEmpty)
|
||||
@ -235,8 +234,8 @@ struct ProviderFeatureParityTests {
|
||||
#expect(ProviderFeature.allCases.contains(.visionInputs))
|
||||
}
|
||||
|
||||
@Test("Provider adapter wrapping")
|
||||
func providerAdapterWrapping() throws {
|
||||
@Test
|
||||
func `Provider adapter wrapping`() {
|
||||
let provider = MockProvider(
|
||||
capabilities: ModelCapabilities(
|
||||
supportsVision: true,
|
||||
|
||||
@ -2,10 +2,9 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Anthropic interleaved defaults")
|
||||
struct AnthropicInterleavedDefaultsTests {
|
||||
@Test("Merged beta header includes required interleaved flags")
|
||||
func mergedBetaHeaderIncludesRequiredFlags() {
|
||||
@Test
|
||||
func `Merged beta header includes required interleaved flags`() {
|
||||
let header = AnthropicProvider.mergedBetaHeader(existing: nil)
|
||||
#expect(header.contains("interleaved-thinking-2025-05-14"))
|
||||
#expect(header.contains("fine-grained-tool-streaming-2025-05-14"))
|
||||
@ -23,8 +22,8 @@ struct AnthropicInterleavedDefaultsTests {
|
||||
#expect(parts.contains("fine-grained-tool-streaming-2025-05-14"))
|
||||
}
|
||||
|
||||
@Test("Provider request includes beta header and thinking payload")
|
||||
func providerRequestIncludesBetaHeaderAndThinkingPayload() throws {
|
||||
@Test
|
||||
func `Provider request includes beta header and thinking payload`() throws {
|
||||
let config = TachikomaConfiguration(apiKeys: ["anthropic": "test-key"])
|
||||
let provider = try AnthropicProvider(model: .opus45, configuration: config)
|
||||
|
||||
@ -58,8 +57,8 @@ struct AnthropicInterleavedDefaultsTests {
|
||||
#expect(thinking["budget_tokens"] as? Int == 12000)
|
||||
}
|
||||
|
||||
@Test("Provider respects custom baseURL")
|
||||
func providerRespectsCustomBaseURL() throws {
|
||||
@Test
|
||||
func `Provider respects custom baseURL`() throws {
|
||||
let config = TachikomaConfiguration(
|
||||
apiKeys: ["anthropic": "test-key"],
|
||||
baseURLs: ["anthropic": "https://entropic.example/v1"],
|
||||
@ -71,8 +70,8 @@ struct AnthropicInterleavedDefaultsTests {
|
||||
#expect(urlRequest.url?.absoluteString == "https://entropic.example/v1/messages")
|
||||
}
|
||||
|
||||
@Test("Stream delta decodes thinking_delta payload")
|
||||
func streamDeltaDecodesThinkingDeltaPayload() throws {
|
||||
@Test
|
||||
func `Stream delta decodes thinking_delta payload`() throws {
|
||||
let data = try #require("{\"type\":\"thinking_delta\",\"thinking\":\"ok\"}".data(using: .utf8))
|
||||
let delta = try JSONDecoder().decode(AnthropicStreamDelta.self, from: data)
|
||||
#expect(delta.type == "thinking_delta")
|
||||
@ -80,16 +79,16 @@ struct AnthropicInterleavedDefaultsTests {
|
||||
#expect(delta.text == nil)
|
||||
}
|
||||
|
||||
@Test("Stream delta decodes signature_delta payload")
|
||||
func streamDeltaDecodesSignatureDeltaPayload() throws {
|
||||
@Test
|
||||
func `Stream delta decodes signature_delta payload`() throws {
|
||||
let data = try #require("{\"type\":\"signature_delta\",\"signature\":\"sig\"}".data(using: .utf8))
|
||||
let delta = try JSONDecoder().decode(AnthropicStreamDelta.self, from: data)
|
||||
#expect(delta.type == "signature_delta")
|
||||
#expect(delta.signature == "sig")
|
||||
}
|
||||
|
||||
@Test("Signed thinking blocks are preserved for assistant messages")
|
||||
func signedThinkingBlocksArePreservedForAssistantMessages() throws {
|
||||
@Test
|
||||
func `Signed thinking blocks are preserved for assistant messages`() throws {
|
||||
let config = TachikomaConfiguration(apiKeys: ["anthropic": "test-key"])
|
||||
let provider = try AnthropicProvider(model: .opus45, configuration: config)
|
||||
|
||||
@ -125,8 +124,8 @@ struct AnthropicInterleavedDefaultsTests {
|
||||
#expect(assistant.first?["signature"] as? String == "sig")
|
||||
}
|
||||
|
||||
@Test("Redacted thinking blocks preserve signature without text")
|
||||
func redactedThinkingBlocksPreserveSignatureWithoutText() throws {
|
||||
@Test
|
||||
func `Redacted thinking blocks preserve signature without text`() throws {
|
||||
let config = TachikomaConfiguration(apiKeys: ["anthropic": "test-key"])
|
||||
let provider = try AnthropicProvider(model: .opus45, configuration: config)
|
||||
|
||||
@ -161,8 +160,8 @@ struct AnthropicInterleavedDefaultsTests {
|
||||
#expect(assistant.first?["signature"] as? String == "sig-redacted")
|
||||
}
|
||||
|
||||
@Test("Thinking stays enabled even without signed history")
|
||||
func thinkingStaysEnabledEvenWithoutSignedHistory() throws {
|
||||
@Test
|
||||
func `Thinking stays enabled even without signed history`() throws {
|
||||
let config = TachikomaConfiguration(apiKeys: ["anthropic": "test-key"])
|
||||
let provider = try AnthropicProvider(model: .opus45, configuration: config)
|
||||
|
||||
|
||||
@ -32,8 +32,13 @@ private final class AzureTestURLProtocol: URLProtocol {
|
||||
}
|
||||
""".utf8Data()
|
||||
|
||||
override class func canInit(with _: URLRequest) -> Bool { true }
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
|
||||
override class func canInit(with _: URLRequest) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||
request
|
||||
}
|
||||
|
||||
override func startLoading() {
|
||||
Task { [request] in await AzureTestURLProtocol.storeMedia(request) }
|
||||
@ -65,7 +70,7 @@ private final class AzureTestURLProtocol: URLProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Azure OpenAI Provider", .serialized)
|
||||
@Suite(.serialized)
|
||||
struct AzureOpenAIProviderTests {
|
||||
private func makeSession() -> URLSession {
|
||||
let configuration = URLSessionConfiguration.ephemeral
|
||||
@ -73,8 +78,8 @@ struct AzureOpenAIProviderTests {
|
||||
return URLSession(configuration: configuration)
|
||||
}
|
||||
|
||||
@Test("Builds Azure chat URL with api-version and api-key header")
|
||||
func buildsAzureURLAndHeaders() async throws {
|
||||
@Test
|
||||
func `Builds Azure chat URL with api-version and api-key header`() async throws {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
config.setAPIKey("test-key", for: .azureOpenAI)
|
||||
await AzureTestURLProtocol.reset()
|
||||
@ -106,8 +111,8 @@ struct AzureOpenAIProviderTests {
|
||||
#expect(sentRequest?.value(forHTTPHeaderField: "api-key") == "test-key")
|
||||
}
|
||||
|
||||
@Test("Prefers bearer token auth and explicit endpoint")
|
||||
func prefersBearerToken() async throws {
|
||||
@Test
|
||||
func `Prefers bearer token auth and explicit endpoint`() async throws {
|
||||
setenv("AZURE_OPENAI_BEARER_TOKEN", "bearer-123", 1)
|
||||
setenv("AZURE_OPENAI_ENDPOINT", "https://custom.azure.example.com", 1)
|
||||
defer {
|
||||
|
||||
@ -3,7 +3,7 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Provider Integration Tests", .enabled(if: ProcessInfo.processInfo.environment["INTEGRATION_TESTS"] != nil))
|
||||
@Suite(.enabled(if: ProcessInfo.processInfo.environment["INTEGRATION_TESTS"] != nil))
|
||||
struct ProviderIntegrationTests {
|
||||
// MARK: - Test Configuration
|
||||
|
||||
@ -21,22 +21,34 @@ struct ProviderIntegrationTests {
|
||||
return !value.isEmpty
|
||||
}
|
||||
|
||||
private static var hasOpenAIKey: Bool { hasEnv("OPENAI_API_KEY") }
|
||||
private static var hasAnthropicKey: Bool { hasEnv("ANTHROPIC_API_KEY") }
|
||||
private static var hasOpenAIKey: Bool {
|
||||
hasEnv("OPENAI_API_KEY")
|
||||
}
|
||||
|
||||
private static var hasAnthropicKey: Bool {
|
||||
hasEnv("ANTHROPIC_API_KEY")
|
||||
}
|
||||
|
||||
private static var hasGoogleKey: Bool {
|
||||
hasEnv("GEMINI_API_KEY") || hasEnv("GOOGLE_API_KEY")
|
||||
}
|
||||
|
||||
private static var hasMistralKey: Bool { hasEnv("MISTRAL_API_KEY") }
|
||||
private static var hasGroqKey: Bool { hasEnv("GROQ_API_KEY") }
|
||||
private static var hasMistralKey: Bool {
|
||||
hasEnv("MISTRAL_API_KEY")
|
||||
}
|
||||
|
||||
private static var hasGroqKey: Bool {
|
||||
hasEnv("GROQ_API_KEY")
|
||||
}
|
||||
|
||||
private static var hasGrokKey: Bool {
|
||||
hasEnv("X_AI_API_KEY") || hasEnv("XAI_API_KEY") || hasEnv("GROK_API_KEY")
|
||||
}
|
||||
|
||||
// MARK: - OpenAI Integration Tests
|
||||
|
||||
@Test("OpenAI Provider - Real API Call", .enabled(if: Self.hasOpenAIKey))
|
||||
func openAIIntegration() async throws {
|
||||
@Test(.enabled(if: Self.hasOpenAIKey))
|
||||
func `OpenAI Provider - Real API Call`() async throws {
|
||||
let model = Model.openai(.gpt4oMini)
|
||||
let config = TachikomaConfiguration()
|
||||
do {
|
||||
@ -58,8 +70,8 @@ struct ProviderIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI Provider - Tool Calling", .enabled(if: Self.hasOpenAIKey))
|
||||
func openAIToolCalling() async throws {
|
||||
@Test(.enabled(if: Self.hasOpenAIKey))
|
||||
func `OpenAI Provider - Tool Calling`() async throws {
|
||||
let model = Model.openai(.gpt4oMini)
|
||||
let config = TachikomaConfiguration()
|
||||
|
||||
@ -103,8 +115,8 @@ struct ProviderIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI Provider - Streaming", .enabled(if: Self.hasOpenAIKey))
|
||||
func openAIStreaming() async throws {
|
||||
@Test(.enabled(if: Self.hasOpenAIKey))
|
||||
func `OpenAI Provider - Streaming`() async throws {
|
||||
let model = Model.openai(.gpt4oMini)
|
||||
let config = TachikomaConfiguration()
|
||||
|
||||
@ -150,8 +162,8 @@ struct ProviderIntegrationTests {
|
||||
|
||||
// MARK: - Anthropic Integration Tests
|
||||
|
||||
@Test("Anthropic Provider - Real API Call", .enabled(if: Self.hasAnthropicKey))
|
||||
func anthropicIntegration() async throws {
|
||||
@Test(.enabled(if: Self.hasAnthropicKey))
|
||||
func `Anthropic Provider - Real API Call`() async throws {
|
||||
let model = Model.anthropic(.sonnet4)
|
||||
do {
|
||||
let response = try await generate(
|
||||
@ -169,8 +181,8 @@ struct ProviderIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Anthropic Provider - Tool Calling", .enabled(if: Self.hasAnthropicKey))
|
||||
func anthropicToolCalling() async throws {
|
||||
@Test(.enabled(if: Self.hasAnthropicKey))
|
||||
func `Anthropic Provider - Tool Calling`() async throws {
|
||||
let model = Model.anthropic(.sonnet4)
|
||||
let config = TachikomaConfiguration()
|
||||
|
||||
@ -213,8 +225,8 @@ struct ProviderIntegrationTests {
|
||||
|
||||
// MARK: - Ollama Integration Tests
|
||||
|
||||
@Test("Ollama Provider - Real API Call")
|
||||
func ollamaIntegration() async throws {
|
||||
@Test
|
||||
func `Ollama Provider - Real API Call`() async throws {
|
||||
// Check if Ollama is running
|
||||
let ollamaRunning = await isOllamaRunning()
|
||||
guard ollamaRunning else {
|
||||
@ -247,8 +259,8 @@ struct ProviderIntegrationTests {
|
||||
|
||||
// MARK: - Grok Integration Tests
|
||||
|
||||
@Test("Grok Provider - Real API Call", .enabled(if: Self.hasGrokKey))
|
||||
func grokIntegration() async throws {
|
||||
@Test(.enabled(if: Self.hasGrokKey))
|
||||
func `Grok Provider - Real API Call`() async throws {
|
||||
let model = Model.grok(.grok3)
|
||||
do {
|
||||
let response = try await generate(
|
||||
@ -267,8 +279,8 @@ struct ProviderIntegrationTests {
|
||||
|
||||
// MARK: - Google Integration Tests
|
||||
|
||||
@Test("Google Provider - Real API Call", .enabled(if: Self.hasGoogleKey))
|
||||
func googleIntegration() async throws {
|
||||
@Test(.enabled(if: Self.hasGoogleKey))
|
||||
func `Google Provider - Real API Call`() async throws {
|
||||
let model = Model.google(.gemini25Flash)
|
||||
do {
|
||||
let response = try await generate(
|
||||
@ -287,8 +299,8 @@ struct ProviderIntegrationTests {
|
||||
|
||||
// MARK: - Mistral Integration Tests
|
||||
|
||||
@Test("Mistral Provider - Real API Call", .enabled(if: Self.hasMistralKey))
|
||||
func mistralIntegration() async throws {
|
||||
@Test(.enabled(if: Self.hasMistralKey))
|
||||
func `Mistral Provider - Real API Call`() async throws {
|
||||
let model = Model.mistral(.small)
|
||||
let response = try await generate(TestConfig.shortMessage, using: model, maxTokens: 50, temperature: 0.0)
|
||||
|
||||
@ -298,8 +310,8 @@ struct ProviderIntegrationTests {
|
||||
|
||||
// MARK: - Groq Integration Tests
|
||||
|
||||
@Test("Groq Provider - Real API Call", .enabled(if: Self.hasGroqKey))
|
||||
func groqIntegration() async throws {
|
||||
@Test(.enabled(if: Self.hasGroqKey))
|
||||
func `Groq Provider - Real API Call`() async throws {
|
||||
let model = Model.groq(.llama38b)
|
||||
let response = try await generate(TestConfig.shortMessage, using: model, maxTokens: 50, temperature: 0.0)
|
||||
|
||||
@ -309,8 +321,8 @@ struct ProviderIntegrationTests {
|
||||
|
||||
// MARK: - Multi-Modal Integration Tests
|
||||
|
||||
@Test("Multi-Modal Provider - Vision Support", .enabled(if: Self.hasOpenAIKey))
|
||||
func multiModalVision() async throws {
|
||||
@Test(.enabled(if: Self.hasOpenAIKey))
|
||||
func `Multi-Modal Provider - Vision Support`() async throws {
|
||||
let model = Model.openai(.gpt4o)
|
||||
let config = TachikomaConfiguration()
|
||||
let provider = try ProviderFactory.createProvider(for: model, configuration: config)
|
||||
|
||||
@ -2,12 +2,12 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Provider System Tests", .serialized)
|
||||
@Suite(.serialized)
|
||||
struct ProviderSystemTests {
|
||||
// MARK: - Provider Factory Tests
|
||||
|
||||
@Test("Provider Factory - OpenAI Provider Creation")
|
||||
func providerFactoryOpenAI() async throws {
|
||||
@Test
|
||||
func `Provider Factory - OpenAI Provider Creation`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
|
||||
let model = Model.openai(.gpt4o)
|
||||
let provider = try ProviderFactory.createProvider(for: model, configuration: config)
|
||||
@ -19,8 +19,8 @@ struct ProviderSystemTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Provider Factory - Anthropic Provider Creation")
|
||||
func providerFactoryAnthropic() async throws {
|
||||
@Test
|
||||
func `Provider Factory - Anthropic Provider Creation`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["anthropic": "test-key"]) { config in
|
||||
let model = Model.anthropic(.opus4)
|
||||
let provider = try ProviderFactory.createProvider(for: model, configuration: config)
|
||||
@ -32,8 +32,8 @@ struct ProviderSystemTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Provider Factory - Grok Provider Creation")
|
||||
func providerFactoryGrok() async throws {
|
||||
@Test
|
||||
func `Provider Factory - Grok Provider Creation`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["grok": "test-key"]) { config in
|
||||
let model = Model.grok(.grok4FastReasoning)
|
||||
let provider = try ProviderFactory.createProvider(for: model, configuration: config)
|
||||
@ -44,8 +44,8 @@ struct ProviderSystemTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Provider Factory - Grok catalog coverage")
|
||||
func providerFactoryGrokCatalog() async throws {
|
||||
@Test
|
||||
func `Provider Factory - Grok catalog coverage`() async throws {
|
||||
try await TestHelpers.withTestConfiguration(apiKeys: ["grok": "test-key"]) { config in
|
||||
for grokModel in Model.Grok.allCases {
|
||||
let model = Model.grok(grokModel)
|
||||
@ -55,8 +55,8 @@ struct ProviderSystemTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Provider Factory - Ollama Provider Creation")
|
||||
func providerFactoryOllama() async throws {
|
||||
@Test
|
||||
func `Provider Factory - Ollama Provider Creation`() throws {
|
||||
// No API key needed for Ollama
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
let model = Model.ollama(.llama33)
|
||||
@ -67,8 +67,8 @@ struct ProviderSystemTests {
|
||||
#expect(provider.capabilities.supportsStreaming == true)
|
||||
}
|
||||
|
||||
@Test("Provider Factory - Missing API Key Error")
|
||||
func providerFactoryMissingAPIKey() async throws {
|
||||
@Test
|
||||
func `Provider Factory - Missing API Key Error`() async {
|
||||
await TestHelpers.withEmptyTestConfiguration { config in
|
||||
// Test the actual provider constructors directly since ProviderFactory
|
||||
// uses MockProvider in test mode to avoid hitting real APIs
|
||||
@ -100,8 +100,8 @@ struct ProviderSystemTests {
|
||||
|
||||
// MARK: - Model Capabilities Tests
|
||||
|
||||
@Test("Model Capabilities - Vision Support")
|
||||
func modelCapabilitiesVision() {
|
||||
@Test
|
||||
func `Model Capabilities - Vision Support`() {
|
||||
#expect(Model.openai(.gpt4o).supportsVision == true)
|
||||
#expect(Model.openai(.gpt4oMini).supportsVision == true)
|
||||
#expect(Model.openai(.gpt41).supportsVision == false)
|
||||
@ -119,8 +119,8 @@ struct ProviderSystemTests {
|
||||
#expect(Model.ollama(.custom("qwen2.5vl:latest")).supportsVision == true)
|
||||
}
|
||||
|
||||
@Test("Model Capabilities - Tool Support")
|
||||
func modelCapabilitiesTools() {
|
||||
@Test
|
||||
func `Model Capabilities - Tool Support`() {
|
||||
#expect(Model.openai(.gpt4o).supportsTools == true)
|
||||
#expect(Model.openai(.gpt41).supportsTools == true)
|
||||
|
||||
@ -134,8 +134,8 @@ struct ProviderSystemTests {
|
||||
#expect(Model.ollama(.custom("qwen2.5vl:latest")).supportsTools == false)
|
||||
}
|
||||
|
||||
@Test("Model Capabilities - Streaming Support")
|
||||
func modelCapabilitiesStreaming() {
|
||||
@Test
|
||||
func `Model Capabilities - Streaming Support`() {
|
||||
#expect(Model.openai(.gpt4o).supportsStreaming == true)
|
||||
#expect(Model.anthropic(.opus4).supportsStreaming == true)
|
||||
#expect(Model.grok(.grok4).supportsStreaming == true)
|
||||
@ -144,8 +144,8 @@ struct ProviderSystemTests {
|
||||
|
||||
// MARK: - Generation Request Tests
|
||||
|
||||
@Test("Generation Request Basic Creation")
|
||||
func generationRequestBasic() {
|
||||
@Test
|
||||
func `Generation Request Basic Creation`() {
|
||||
let request = ProviderRequest(
|
||||
messages: [ModelMessage(role: .user, content: [.text("Hello world")])],
|
||||
tools: nil,
|
||||
@ -160,8 +160,8 @@ struct ProviderSystemTests {
|
||||
#expect(request.outputFormat == nil)
|
||||
}
|
||||
|
||||
@Test("Generation Request With Images")
|
||||
func generationRequestWithImages() {
|
||||
@Test
|
||||
func `Generation Request With Images`() {
|
||||
let imageContent = ModelMessage.ContentPart.ImageContent(data: "test-base64-data")
|
||||
let request = ProviderRequest(
|
||||
messages: [
|
||||
@ -186,8 +186,8 @@ struct ProviderSystemTests {
|
||||
|
||||
// MARK: - Stream Token Tests
|
||||
|
||||
@Test("Stream Token Types")
|
||||
func streamTokenTypes() {
|
||||
@Test
|
||||
func `Stream Token Types`() {
|
||||
let textToken = TextStreamDelta(type: .textDelta, content: "hello")
|
||||
#expect(textToken.content == "hello")
|
||||
#expect(textToken.type == .textDelta)
|
||||
@ -205,8 +205,8 @@ struct ProviderSystemTests {
|
||||
|
||||
// MARK: - Usage Statistics Tests
|
||||
|
||||
@Test("Usage Statistics")
|
||||
func usageStatistics() {
|
||||
@Test
|
||||
func `Usage Statistics`() {
|
||||
let usage = Usage(inputTokens: 100, outputTokens: 50)
|
||||
|
||||
#expect(usage.inputTokens == 100)
|
||||
@ -216,8 +216,8 @@ struct ProviderSystemTests {
|
||||
|
||||
// MARK: - Finish Reason Tests
|
||||
|
||||
@Test("Finish Reason Cases")
|
||||
func finishReasonCases() {
|
||||
@Test
|
||||
func `Finish Reason Cases`() {
|
||||
#expect(FinishReason.stop.rawValue == "stop")
|
||||
#expect(FinishReason.length.rawValue == "length")
|
||||
#expect(FinishReason.toolCalls.rawValue == "tool_calls")
|
||||
|
||||
@ -6,14 +6,14 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
#if os(Linux)
|
||||
@Suite("OpenAI Responses API Tests", .disabled("URLProtocol mocking unavailable on Linux"))
|
||||
@Suite(.disabled("URLProtocol mocking unavailable on Linux"))
|
||||
struct OpenAIResponsesProviderTests {}
|
||||
#else
|
||||
|
||||
@Suite("OpenAI Responses API Tests", .serialized)
|
||||
@Suite(.serialized)
|
||||
struct OpenAIResponsesProviderTests {
|
||||
@Test("GPT-5+ uses Responses API provider")
|
||||
func gPT5UsesResponsesProvider() throws {
|
||||
@Test
|
||||
func `GPT-5+ uses Responses API provider`() throws {
|
||||
// Test that GPT-5 models use the OpenAIResponsesProvider
|
||||
let config = self.openAIConfig()
|
||||
|
||||
@ -38,8 +38,8 @@ struct OpenAIResponsesProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("GPT-5.1 text.verbosity parameter is set correctly")
|
||||
func gPT51TextVerbosityParameter() throws {
|
||||
@Test
|
||||
func `GPT-5.1 text.verbosity parameter is set correctly`() throws {
|
||||
// Test that the text.verbosity parameter is properly configured for GPT-5.1
|
||||
let config = self.openAIConfig()
|
||||
|
||||
@ -69,8 +69,8 @@ struct OpenAIResponsesProviderTests {
|
||||
#expect(provider.capabilities.supportsVision == true)
|
||||
}
|
||||
|
||||
@Test("Reasoning models use Responses API")
|
||||
func reasoningModelsUseResponsesAPI() throws {
|
||||
@Test
|
||||
func `Reasoning models use Responses API`() throws {
|
||||
// Test that reasoning-oriented models also use the OpenAIResponsesProvider
|
||||
let config = self.openAIConfig()
|
||||
|
||||
@ -96,8 +96,8 @@ struct OpenAIResponsesProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Legacy models use standard OpenAI provider")
|
||||
func legacyModelsUseStandardProvider() throws {
|
||||
@Test
|
||||
func `Legacy models use standard OpenAI provider`() throws {
|
||||
// Test that non-GPT-5/reasoning models use the standard OpenAIProvider
|
||||
let config = self.openAIConfig()
|
||||
|
||||
@ -116,8 +116,8 @@ struct OpenAIResponsesProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("TextConfig encodes verbosity correctly")
|
||||
func textConfigEncoding() throws {
|
||||
@Test
|
||||
func `TextConfig encodes verbosity correctly`() throws {
|
||||
// Test that TextConfig properly encodes the verbosity parameter
|
||||
let textConfig = TextConfig(verbosity: .high)
|
||||
|
||||
@ -128,8 +128,8 @@ struct OpenAIResponsesProviderTests {
|
||||
#expect(json?["verbosity"] as? String == "high")
|
||||
}
|
||||
|
||||
@Test("OpenAIResponsesRequest includes text config for GPT-5")
|
||||
func responsesRequestTextConfig() throws {
|
||||
@Test
|
||||
func `OpenAIResponsesRequest includes text config for GPT-5`() throws {
|
||||
// Test that the request properly includes text config
|
||||
let textConfig = TextConfig(verbosity: .medium)
|
||||
let request = OpenAIResponsesRequest(
|
||||
@ -165,8 +165,8 @@ struct OpenAIResponsesProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("GPT-5 tool call outputs are parsed")
|
||||
func gpt5ToolCallParsing() throws {
|
||||
@Test
|
||||
func `GPT-5 tool call outputs are parsed`() throws {
|
||||
let toolCall = OpenAIResponsesResponse.ResponsesToolCall(
|
||||
id: "call_1",
|
||||
type: "function",
|
||||
@ -208,8 +208,8 @@ struct OpenAIResponsesProviderTests {
|
||||
#expect(providerResponse.finishReason == .toolCalls)
|
||||
}
|
||||
|
||||
@Test("Responses provider hits /v1/responses and encodes body")
|
||||
func openAIResponsesRequestEncoding() async throws {
|
||||
@Test
|
||||
func `Responses provider hits /v1/responses and encodes body`() async throws {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
config.setAPIKey("live-openai", for: .openai)
|
||||
|
||||
@ -239,8 +239,8 @@ struct OpenAIResponsesProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Responses payload uses data URL string for images")
|
||||
func openAIResponsesImageDataURL() async throws {
|
||||
@Test
|
||||
func `Responses payload uses data URL string for images`() async throws {
|
||||
let config = self.openAIConfig()
|
||||
|
||||
try await self.withMockedSession { request in
|
||||
@ -271,8 +271,8 @@ struct OpenAIResponsesProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Responses image_url accepts legacy object and normalizes to string")
|
||||
func openAIResponsesLegacyImageObject() async throws {
|
||||
@Test
|
||||
func `Responses image_url accepts legacy object and normalizes to string`() throws {
|
||||
// Craft a legacy-style payload (image_url object) and ensure decoder tolerates it.
|
||||
let legacyJSON: [String: Any] = [
|
||||
"type": "input_image",
|
||||
@ -290,8 +290,8 @@ struct OpenAIResponsesProviderTests {
|
||||
#expect(encodedJSON?["image_url"] as? String == "data:image/png;base64,LEGACY")
|
||||
}
|
||||
|
||||
@Test("Responses provider emits tool schemas with parameters")
|
||||
func openAIResponsesIncludesToolSchemas() async throws {
|
||||
@Test
|
||||
func `Responses provider emits tool schemas with parameters`() async throws {
|
||||
let config = self.openAIConfig()
|
||||
let tool = AgentTool(
|
||||
name: "app",
|
||||
@ -349,8 +349,8 @@ struct OpenAIResponsesProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Function call history encodes into Responses input")
|
||||
func openAIResponsesEncodesFunctionCalls() async throws {
|
||||
@Test
|
||||
func `Function call history encodes into Responses input`() async throws {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
config.setAPIKey("live-openai", for: .openai)
|
||||
|
||||
@ -399,8 +399,8 @@ struct OpenAIResponsesProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Responses tool output is emitted even when result is empty")
|
||||
func openAIResponsesEmitsToolOutputForEmptyResult() async throws {
|
||||
@Test
|
||||
func `Responses tool output is emitted even when result is empty`() async throws {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
config.setAPIKey("live-openai", for: .openai)
|
||||
|
||||
@ -441,8 +441,8 @@ struct OpenAIResponsesProviderTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Responses provider streams accumulated deltas")
|
||||
func openAIResponsesStreaming() async throws {
|
||||
@Test
|
||||
func `Responses provider streams accumulated deltas`() async throws {
|
||||
let config = TachikomaConfiguration(loadFromEnvironment: false)
|
||||
config.setAPIKey("live-openai", for: .openai)
|
||||
|
||||
@ -638,7 +638,7 @@ private final class ResponsesTestURLProtocol: URLProtocol {
|
||||
override func stopLoading() {}
|
||||
}
|
||||
|
||||
// Helper to skip tests when API keys aren't available
|
||||
/// Helper to skip tests when API keys aren't available
|
||||
struct TestSkipped: Error {
|
||||
let message: String
|
||||
|
||||
|
||||
@ -12,16 +12,16 @@ import Glibc
|
||||
#endif
|
||||
|
||||
#if os(Linux)
|
||||
@Suite("Provider Network E2E Tests", .disabled("URLProtocol mocking unavailable on Linux"))
|
||||
@Suite(.disabled("URLProtocol mocking unavailable on Linux"))
|
||||
struct ProviderEndToEndTests {}
|
||||
#else
|
||||
|
||||
@Suite("Provider Network E2E Tests", .serialized, .enabled(if: !_isLiveSuite))
|
||||
@Suite(.serialized, .enabled(if: !_isLiveSuite))
|
||||
struct ProviderEndToEndTests {
|
||||
// MARK: - OpenAI Responses (GPT-5)
|
||||
|
||||
@Test("OpenAI Responses provider returns text")
|
||||
func openAIResponsesProvider() async throws {
|
||||
@Test
|
||||
func `OpenAI Responses provider returns text`() async throws {
|
||||
try await NetworkMocking.withMockedNetwork { request in
|
||||
self.expectPath(
|
||||
request,
|
||||
@ -45,8 +45,8 @@ struct ProviderEndToEndTests {
|
||||
|
||||
// MARK: - OpenAI Chat Provider
|
||||
|
||||
@Test("OpenAI chat provider hits /chat/completions")
|
||||
func openAIChatProvider() async throws {
|
||||
@Test
|
||||
func `OpenAI chat provider hits /chat/completions`() async throws {
|
||||
try await NetworkMocking.withMockedNetwork { request in
|
||||
self.expectPath(request, endsWith: "/chat/completions")
|
||||
return NetworkMocking.jsonResponse(
|
||||
@ -65,8 +65,8 @@ struct ProviderEndToEndTests {
|
||||
|
||||
// MARK: - Anthropic
|
||||
|
||||
@Test("Anthropic provider decodes Claude responses")
|
||||
func anthropicProvider() async throws {
|
||||
@Test
|
||||
func `Anthropic provider decodes Claude responses`() async throws {
|
||||
try await NetworkMocking.withMockedNetwork { request in
|
||||
self.expectPath(request, endsWith: "/messages")
|
||||
return NetworkMocking.jsonResponse(for: request, data: Self.anthropicPayload(text: "Claude says hello"))
|
||||
@ -82,8 +82,8 @@ struct ProviderEndToEndTests {
|
||||
|
||||
// MARK: - Google Gemini
|
||||
|
||||
@Test("Google provider processes streamed SSE content")
|
||||
func googleProvider() async throws {
|
||||
@Test
|
||||
func `Google provider processes streamed SSE content`() async throws {
|
||||
try await NetworkMocking.withMockedNetwork { request in
|
||||
#expect(request.url?.path.contains(":streamGenerateContent") == true)
|
||||
return NetworkMocking.streamResponse(for: request, data: Self.googleStreamPayload(text: "Gemini streaming"))
|
||||
@ -99,23 +99,23 @@ struct ProviderEndToEndTests {
|
||||
|
||||
// MARK: - OpenAI-compatible providers
|
||||
|
||||
@Test("Mistral provider uses OpenAI-compatible flow")
|
||||
func mistralProvider() async throws {
|
||||
@Test
|
||||
func `Mistral provider uses OpenAI-compatible flow`() async throws {
|
||||
try await self.assertOpenAICompatibleProvider(.mistral(.small), provider: .mistral)
|
||||
}
|
||||
|
||||
@Test("Groq provider uses OpenAI-compatible flow")
|
||||
func groqProvider() async throws {
|
||||
@Test
|
||||
func `Groq provider uses OpenAI-compatible flow`() async throws {
|
||||
try await self.assertOpenAICompatibleProvider(.groq(.llama38b), provider: .groq)
|
||||
}
|
||||
|
||||
@Test("Grok provider uses OpenAI-compatible flow")
|
||||
func grokProvider() async throws {
|
||||
@Test
|
||||
func `Grok provider uses OpenAI-compatible flow`() async throws {
|
||||
try await self.assertOpenAICompatibleProvider(.grok(.grok4FastReasoning), provider: .grok)
|
||||
}
|
||||
|
||||
@Test("All Grok catalog models share the same OpenAI-compatible flow")
|
||||
func grokCatalogUsesSameFlow() async throws {
|
||||
@Test
|
||||
func `All Grok catalog models share the same OpenAI-compatible flow`() async throws {
|
||||
for grokModel in Model.Grok.allCases {
|
||||
try await self.assertOpenAICompatibleProvider(.grok(grokModel), provider: .grok)
|
||||
}
|
||||
@ -123,8 +123,8 @@ struct ProviderEndToEndTests {
|
||||
|
||||
// MARK: - Ollama
|
||||
|
||||
@Test("Ollama provider handles local responses")
|
||||
func ollamaProvider() async throws {
|
||||
@Test
|
||||
func `Ollama provider handles local responses`() async throws {
|
||||
try await NetworkMocking.withMockedNetwork { request in
|
||||
self.expectPath(request, endsWith: "/api/chat")
|
||||
return NetworkMocking.jsonResponse(for: request, data: Self.ollamaPayload(text: "Ollama local reply"))
|
||||
@ -138,8 +138,8 @@ struct ProviderEndToEndTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Ollama provider encodes vision images as messages[].images")
|
||||
func ollamaProviderEncodesImages() async throws {
|
||||
@Test
|
||||
func `Ollama provider encodes vision images as messages[].images`() async throws {
|
||||
let imageBase64 = Data("test-image".utf8).base64EncodedString()
|
||||
let image = ModelMessage.ContentPart.ImageContent(data: imageBase64, mimeType: "image/png")
|
||||
|
||||
@ -180,8 +180,8 @@ struct ProviderEndToEndTests {
|
||||
|
||||
// MARK: - LMStudio
|
||||
|
||||
@Test("LMStudio provider maps OpenAI-style responses")
|
||||
func lmstudioProvider() async throws {
|
||||
@Test
|
||||
func `LMStudio provider maps OpenAI-style responses`() async throws {
|
||||
try await NetworkMocking.withMockedNetwork { request in
|
||||
let path = request.url?.path ?? ""
|
||||
#expect(path.contains("chat/completions"))
|
||||
@ -199,8 +199,8 @@ struct ProviderEndToEndTests {
|
||||
|
||||
// MARK: - Aggregators & Compatible Providers
|
||||
|
||||
@Test("OpenRouter provider uses OpenAI-compatible flow")
|
||||
func openRouterProvider() async throws {
|
||||
@Test
|
||||
func `OpenRouter provider uses OpenAI-compatible flow`() async throws {
|
||||
try await NetworkMocking.withMockedNetwork { request in
|
||||
self.expectPath(request, endsWith: "/chat/completions")
|
||||
#expect(request.value(forHTTPHeaderField: "HTTP-Referer") == "https://peekaboo.app")
|
||||
@ -216,8 +216,8 @@ struct ProviderEndToEndTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Together provider uses OpenAI-compatible flow")
|
||||
func togetherProvider() async throws {
|
||||
@Test
|
||||
func `Together provider uses OpenAI-compatible flow`() async throws {
|
||||
try await NetworkMocking.withMockedNetwork { request in
|
||||
let path = request.url?.path ?? ""
|
||||
#expect(path.hasSuffix("/chat/completions"))
|
||||
@ -232,8 +232,8 @@ struct ProviderEndToEndTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Replicate provider uses OpenAI-compatible flow")
|
||||
func replicateProvider() async throws {
|
||||
@Test
|
||||
func `Replicate provider uses OpenAI-compatible flow`() async throws {
|
||||
try await NetworkMocking.withMockedNetwork { request in
|
||||
let path = request.url?.path ?? ""
|
||||
#expect(path.hasSuffix("/chat/completions"))
|
||||
@ -251,8 +251,8 @@ struct ProviderEndToEndTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("OpenAI-compatible provider hits custom base URL")
|
||||
func openAICompatibleProvider() async throws {
|
||||
@Test
|
||||
func `OpenAI-compatible provider hits custom base URL`() async throws {
|
||||
try await NetworkMocking.withMockedNetwork { request in
|
||||
#expect(request.url?.absoluteString == "https://compatible.test/chat/completions")
|
||||
return NetworkMocking.jsonResponse(
|
||||
@ -273,8 +273,8 @@ struct ProviderEndToEndTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Anthropic-compatible provider decodes responses")
|
||||
func anthropicCompatibleProvider() async throws {
|
||||
@Test
|
||||
func `Anthropic-compatible provider decodes responses`() async throws {
|
||||
try await NetworkMocking.withMockedNetwork { request in
|
||||
self.expectPath(request, endsWith: "/messages")
|
||||
return NetworkMocking.jsonResponse(for: request, data: Self.anthropicPayload(text: "Compat Claude"))
|
||||
|
||||
@ -3,10 +3,9 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
@testable import TachikomaAgent
|
||||
|
||||
@Suite("Simplified Tools Tests")
|
||||
struct SimplifiedToolsTests {
|
||||
@Test("Create simple tool with schema builder")
|
||||
func toolSchemaBuilder() throws {
|
||||
@Test
|
||||
func `Create simple tool with schema builder`() {
|
||||
let schema = ToolSchemaBuilder()
|
||||
.string("name", description: "User's name", required: true)
|
||||
.integer("age", description: "User's age", required: true)
|
||||
@ -20,8 +19,8 @@ struct SimplifiedToolsTests {
|
||||
#expect(schema.required.contains("age"))
|
||||
}
|
||||
|
||||
@Test("SimplifiedToolBuilder with structured input")
|
||||
func simplifiedToolBuilderWithCodable() async throws {
|
||||
@Test
|
||||
func `SimplifiedToolBuilder with structured input`() async throws {
|
||||
struct CalculatorInput: Codable, Sendable {
|
||||
let expression: String
|
||||
}
|
||||
@ -51,8 +50,8 @@ struct SimplifiedToolsTests {
|
||||
#expect(result.doubleValue == 42.0 || result.objectValue?["result"]?.doubleValue == 42.0)
|
||||
}
|
||||
|
||||
@Test("SimplifiedToolBuilder with context")
|
||||
func testToolWithContext() async throws {
|
||||
@Test
|
||||
func `SimplifiedToolBuilder with context`() async throws {
|
||||
struct SearchInput: Codable, Sendable {
|
||||
let query: String
|
||||
}
|
||||
@ -92,8 +91,8 @@ struct SimplifiedToolsTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Simple tool without structured types")
|
||||
func simpleToolWithoutCodable() async throws {
|
||||
@Test
|
||||
func `Simple tool without structured types`() async throws {
|
||||
let tool = SimplifiedToolBuilder.simpleTool(
|
||||
"echo",
|
||||
description: "Echo the input",
|
||||
@ -122,8 +121,8 @@ struct SimplifiedToolsTests {
|
||||
#expect(result.objectValue?["echoed"]?.stringValue == "HELLO")
|
||||
}
|
||||
|
||||
@Test("AgentTool.create with schema builder")
|
||||
func agentToolCreate() async throws {
|
||||
@Test
|
||||
func `AgentTool.create with schema builder`() async throws {
|
||||
let tool = AgentTool.create(
|
||||
name: "weather",
|
||||
description: "Get weather information",
|
||||
@ -159,8 +158,8 @@ struct SimplifiedToolsTests {
|
||||
#expect(result.objectValue?["units"]?.stringValue == "fahrenheit")
|
||||
}
|
||||
|
||||
@Test("Tool schema builder with all parameter types")
|
||||
func schemaBuilderAllTypes() throws {
|
||||
@Test
|
||||
func `Tool schema builder with all parameter types`() {
|
||||
let schema = ToolSchemaBuilder()
|
||||
.string("text", description: "Text field")
|
||||
.number("price", description: "Price field")
|
||||
@ -182,8 +181,8 @@ struct SimplifiedToolsTests {
|
||||
#expect(properties["metadata"]?.type == .object)
|
||||
}
|
||||
|
||||
@Test("Tool parameter property with enum values")
|
||||
func toolParameterWithEnum() throws {
|
||||
@Test
|
||||
func `Tool parameter property with enum values`() {
|
||||
let schema = ToolSchemaBuilder()
|
||||
.string("status", description: "Status", required: true, enum: ["active", "inactive", "pending"])
|
||||
.build()
|
||||
|
||||
@ -2,10 +2,9 @@ import Testing
|
||||
@testable import Tachikoma
|
||||
@testable import TachikomaAgent
|
||||
|
||||
@Suite("Dynamic Tools System Tests")
|
||||
struct DynamicToolsTests {
|
||||
@Test("DynamicTool creates valid AgentTool")
|
||||
func dynamicToolToAgentTool() async throws {
|
||||
@Test
|
||||
func `DynamicTool creates valid AgentTool`() async throws {
|
||||
let schema = DynamicSchema(
|
||||
type: .object,
|
||||
properties: [
|
||||
@ -36,8 +35,8 @@ struct DynamicToolsTests {
|
||||
#expect(result.stringValue?.contains("Searched for:") == true)
|
||||
}
|
||||
|
||||
@Test("DynamicSchema converts to AgentToolParameters")
|
||||
func schemaConversion() throws {
|
||||
@Test
|
||||
func `DynamicSchema converts to AgentToolParameters`() {
|
||||
let schema = DynamicSchema(
|
||||
type: .object,
|
||||
properties: [
|
||||
@ -57,8 +56,8 @@ struct DynamicToolsTests {
|
||||
#expect(parameters.properties["active"]?.type == .boolean)
|
||||
}
|
||||
|
||||
@Test("SchemaProperty handles nested structures")
|
||||
func nestedSchemaProperty() throws {
|
||||
@Test
|
||||
func `SchemaProperty handles nested structures`() {
|
||||
let addressSchema = DynamicSchema.SchemaProperty(
|
||||
type: .object,
|
||||
description: "Address",
|
||||
@ -80,8 +79,8 @@ struct DynamicToolsTests {
|
||||
#expect(parameters.properties["address"]?.type == .object)
|
||||
}
|
||||
|
||||
@Test("DynamicToolRegistry manages tools")
|
||||
func dynamicToolRegistry() async throws {
|
||||
@Test
|
||||
func `DynamicToolRegistry manages tools`() async throws {
|
||||
let registry = DynamicToolRegistry()
|
||||
|
||||
// Create a mock provider with a tool
|
||||
@ -119,8 +118,8 @@ struct DynamicToolsTests {
|
||||
#expect(remainingTools.isEmpty)
|
||||
}
|
||||
|
||||
@Test("DynamicToolProvider discovers tools")
|
||||
func dynamicToolProvider() async throws {
|
||||
@Test
|
||||
func `DynamicToolProvider discovers tools`() async throws {
|
||||
let searchTool = DynamicTool(
|
||||
name: "search_web",
|
||||
description: "Search the web",
|
||||
@ -213,8 +212,8 @@ struct DynamicToolsTests {
|
||||
// SchemaBuilder doesn't exist in the expected form
|
||||
}*/
|
||||
|
||||
@Test("Box type for recursive schemas")
|
||||
func boxType() throws {
|
||||
@Test
|
||||
func `Box type for recursive schemas`() {
|
||||
// Create a recursive structure (like a tree node)
|
||||
let nodeSchema = DynamicSchema.SchemaProperty(
|
||||
type: .object,
|
||||
@ -233,8 +232,8 @@ struct DynamicToolsTests {
|
||||
#expect(nodeSchema.type == .object)
|
||||
}
|
||||
|
||||
@Test("Non-object schema conversion")
|
||||
func nonObjectSchemaConversion() throws {
|
||||
@Test
|
||||
func `Non-object schema conversion`() {
|
||||
// Test conversion of non-object schema
|
||||
// Since non-object schemas aren't wrapped, we should test object schemas
|
||||
let objectSchema = DynamicSchema(
|
||||
@ -249,8 +248,8 @@ struct DynamicToolsTests {
|
||||
#expect(parameters.required == ["value"])
|
||||
}
|
||||
|
||||
@Test("Clear registry")
|
||||
func clearRegistry() async throws {
|
||||
@Test
|
||||
func `Clear registry`() async throws {
|
||||
let registry = DynamicToolRegistry()
|
||||
|
||||
// Add multiple tools
|
||||
|
||||
@ -2,12 +2,11 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Tool System Tests")
|
||||
struct ToolSystemTests {
|
||||
// MARK: - AgentTool Tests
|
||||
|
||||
@Test("AgentTool Creation")
|
||||
func agentToolCreation() throws {
|
||||
@Test
|
||||
func `AgentTool Creation`() {
|
||||
// Create a simple tool using createTool helper
|
||||
let addTool = createTool(
|
||||
name: "add",
|
||||
@ -37,8 +36,8 @@ struct ToolSystemTests {
|
||||
#expect(addTool.parameters.required == ["a", "b"])
|
||||
}
|
||||
|
||||
@Test("Tool Execution")
|
||||
func toolExecution() async throws {
|
||||
@Test
|
||||
func `Tool Execution`() async throws {
|
||||
// Create calculator tool
|
||||
let calculatorTool = createTool(
|
||||
name: "calculate",
|
||||
@ -72,8 +71,8 @@ struct ToolSystemTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Built-in Tools")
|
||||
func builtInTools() async throws {
|
||||
@Test
|
||||
func `Built-in Tools`() async throws {
|
||||
// Test weatherTool
|
||||
#expect(weatherTool.name == "get_weather")
|
||||
|
||||
@ -95,8 +94,8 @@ struct ToolSystemTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Tool Parameter Types")
|
||||
func toolParameterTypes() {
|
||||
@Test
|
||||
func `Tool Parameter Types`() {
|
||||
// Test all parameter types
|
||||
let params = [
|
||||
AgentToolParameterProperty(
|
||||
@ -137,8 +136,8 @@ struct ToolSystemTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Tool Arguments")
|
||||
func toolArguments() throws {
|
||||
@Test
|
||||
func `Tool Arguments`() throws {
|
||||
let args = AgentToolArguments([
|
||||
"string": AnyAgentToolValue(string: "hello"),
|
||||
"int": AnyAgentToolValue(int: 42),
|
||||
@ -168,8 +167,8 @@ struct ToolSystemTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Tool Error Handling")
|
||||
func toolErrorHandling() throws {
|
||||
@Test
|
||||
func `Tool Error Handling`() throws {
|
||||
let args = AgentToolArguments([:])
|
||||
|
||||
// Test missing required argument
|
||||
|
||||
@ -2,10 +2,9 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("UI Integration Tests")
|
||||
struct UIIntegrationTests {
|
||||
@Test("Convert UIMessage to ModelMessage")
|
||||
func uIMessageToModelMessage() throws {
|
||||
@Test
|
||||
func `Convert UIMessage to ModelMessage`() throws {
|
||||
// Create a UIMessage with various content
|
||||
let uiMessage = try UIMessage(
|
||||
role: .user,
|
||||
@ -51,8 +50,8 @@ struct UIIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Convert ModelMessage to UIMessage")
|
||||
func modelMessageToUIMessage() throws {
|
||||
@Test
|
||||
func `Convert ModelMessage to UIMessage`() throws {
|
||||
// Create a ModelMessage with various content
|
||||
let modelMessage = try ModelMessage(
|
||||
role: .assistant,
|
||||
@ -80,8 +79,8 @@ struct UIIntegrationTests {
|
||||
#expect(uiMessages[0].toolCalls?.count == 1)
|
||||
}
|
||||
|
||||
@Test("StreamTextResult to UI Message Stream")
|
||||
func streamToUIMessageStream() async throws {
|
||||
@Test
|
||||
func `StreamTextResult to UI Message Stream`() async {
|
||||
// Create a mock stream
|
||||
let textStream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
Task {
|
||||
@ -137,8 +136,8 @@ struct UIIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Collect text from stream")
|
||||
func collectTextFromStream() async throws {
|
||||
@Test
|
||||
func `Collect text from stream`() async throws {
|
||||
// Create a mock stream
|
||||
let textStream = AsyncThrowingStream<TextStreamDelta, Error> { continuation in
|
||||
Task {
|
||||
@ -168,8 +167,8 @@ struct UIIntegrationTests {
|
||||
#expect(collectedText == "The quick brown fox")
|
||||
}
|
||||
|
||||
@Test("UIStreamResponse collect message")
|
||||
func uIStreamResponseCollectMessage() async throws {
|
||||
@Test
|
||||
func `UIStreamResponse collect message`() async {
|
||||
let stream = AsyncStream<UIMessageChunk> { continuation in
|
||||
continuation.yield(.text("Hello"))
|
||||
continuation.yield(.text(" world"))
|
||||
@ -195,8 +194,8 @@ struct UIIntegrationTests {
|
||||
#expect(message.toolCalls?[0].name == "test")
|
||||
}
|
||||
|
||||
@Test("UIAttachment with data URL")
|
||||
func uIAttachmentDataURL() throws {
|
||||
@Test
|
||||
func `UIAttachment with data URL`() {
|
||||
let imageData = Data("test image".utf8)
|
||||
let attachment = UIAttachment(
|
||||
type: .image,
|
||||
@ -211,8 +210,8 @@ struct UIIntegrationTests {
|
||||
#expect(attachment.name == "test.png")
|
||||
}
|
||||
|
||||
@Test("Bidirectional conversion preserves content")
|
||||
func bidirectionalConversion() throws {
|
||||
@Test
|
||||
func `Bidirectional conversion preserves content`() {
|
||||
let original = UIMessage(
|
||||
role: .user,
|
||||
content: "Test message",
|
||||
|
||||
@ -2,10 +2,9 @@ import Foundation
|
||||
import Testing
|
||||
@testable import Tachikoma
|
||||
|
||||
@Suite("Unified Errors Tests")
|
||||
struct UnifiedErrorsTests {
|
||||
@Test("Create unified error with recovery suggestion")
|
||||
func unifiedErrorWithRecovery() throws {
|
||||
@Test
|
||||
func `Create unified error with recovery suggestion`() {
|
||||
let error = TachikomaUnifiedError(
|
||||
code: .authenticationFailed,
|
||||
message: "Invalid API key provided",
|
||||
@ -22,8 +21,8 @@ struct UnifiedErrorsTests {
|
||||
#expect(description?.contains("Invalid API key provided") == true)
|
||||
}
|
||||
|
||||
@Test("Convert legacy TachikomaError to unified error")
|
||||
func legacyErrorConversion() throws {
|
||||
@Test
|
||||
func `Convert legacy TachikomaError to unified error`() {
|
||||
let legacyError = TachikomaError.modelNotFound("gpt-5")
|
||||
let unifiedError = legacyError.toUnifiedError()
|
||||
|
||||
@ -32,8 +31,8 @@ struct UnifiedErrorsTests {
|
||||
#expect(unifiedError.recovery?.actions.contains(.selectDifferentModel) == true)
|
||||
}
|
||||
|
||||
@Test("Convert ModelError to unified error")
|
||||
func modelErrorConversion() throws {
|
||||
@Test
|
||||
func `Convert ModelError to unified error`() {
|
||||
let modelError = ModelError.rateLimited(retryAfter: 60)
|
||||
let unifiedError = modelError.toUnifiedError()
|
||||
|
||||
@ -45,8 +44,8 @@ struct UnifiedErrorsTests {
|
||||
} == true)
|
||||
}
|
||||
|
||||
@Test("Convert AgentToolError to unified error")
|
||||
func agentToolErrorConversion() throws {
|
||||
@Test
|
||||
func `Convert AgentToolError to unified error`() {
|
||||
let toolError = AgentToolError.missingParameter("expression")
|
||||
let unifiedError = toolError.toUnifiedError()
|
||||
|
||||
@ -54,8 +53,8 @@ struct UnifiedErrorsTests {
|
||||
#expect(unifiedError.message.contains("expression") == true)
|
||||
}
|
||||
|
||||
@Test("Error details with metadata")
|
||||
func errorDetails() throws {
|
||||
@Test
|
||||
func `Error details with metadata`() {
|
||||
let details = ErrorDetails(
|
||||
reason: "Token limit exceeded",
|
||||
statusCode: 429,
|
||||
@ -73,8 +72,8 @@ struct UnifiedErrorsTests {
|
||||
#expect(details.metadata["tokens_used"] == "5000")
|
||||
}
|
||||
|
||||
@Test("Recovery suggestion with actions")
|
||||
func testRecoverySuggestion() throws {
|
||||
@Test
|
||||
func `Recovery suggestion with actions`() {
|
||||
let recovery = RecoverySuggestion(
|
||||
suggestion: "Reduce request size and try again",
|
||||
actions: [
|
||||
@ -88,8 +87,8 @@ struct UnifiedErrorsTests {
|
||||
#expect(recovery.helpURL == "https://docs.example.com/errors")
|
||||
}
|
||||
|
||||
@Test("Error code categories")
|
||||
func errorCodeCategories() throws {
|
||||
@Test
|
||||
func `Error code categories`() {
|
||||
#expect(ErrorCode.invalidRequest.category == .validation)
|
||||
#expect(ErrorCode.authenticationFailed.category == .authentication)
|
||||
#expect(ErrorCode.rateLimited.category == .rateLimit)
|
||||
@ -99,10 +98,12 @@ struct UnifiedErrorsTests {
|
||||
#expect(ErrorCode.parsingError.category == .parsing)
|
||||
}
|
||||
|
||||
@Test("Generic error conversion")
|
||||
func genericErrorConversion() throws {
|
||||
@Test
|
||||
func `Generic error conversion`() {
|
||||
struct CustomError: Error, LocalizedError {
|
||||
var errorDescription: String? { "Custom error occurred" }
|
||||
var errorDescription: String? {
|
||||
"Custom error occurred"
|
||||
}
|
||||
}
|
||||
|
||||
let customError = CustomError()
|
||||
@ -113,8 +114,8 @@ struct UnifiedErrorsTests {
|
||||
#expect(unifiedError.underlyingError != nil)
|
||||
}
|
||||
|
||||
@Test("API call error conversion")
|
||||
func aPICallErrorConversion() throws {
|
||||
@Test
|
||||
func `API call error conversion`() {
|
||||
let apiError = APICallError(
|
||||
statusCode: 500,
|
||||
responseBody: "Internal server error",
|
||||
@ -135,8 +136,8 @@ struct UnifiedErrorsTests {
|
||||
#expect(unifiedError.details?.requestId == "req-456")
|
||||
}
|
||||
|
||||
@Test("Retry error conversion")
|
||||
func retryErrorConversion() throws {
|
||||
@Test
|
||||
func `Retry error conversion`() {
|
||||
let retryError = RetryError(
|
||||
reason: "All attempts failed",
|
||||
lastError: TachikomaError.networkError(NSError(domain: "test", code: -1)),
|
||||
@ -152,8 +153,8 @@ struct UnifiedErrorsTests {
|
||||
#expect(unifiedError.underlyingError != nil)
|
||||
}
|
||||
|
||||
@Test("Error with nil recovery")
|
||||
func errorWithoutRecovery() throws {
|
||||
@Test
|
||||
func `Error with nil recovery`() {
|
||||
let error = TachikomaUnifiedError(
|
||||
code: .invalidParameter,
|
||||
message: "Invalid parameter value",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user