import Foundation import OpenClawKit import Testing private struct TalkConfigContractFixture: Decodable { let selectionCases: [SelectionCase] let timeoutCases: [TimeoutCase] struct SelectionCase: Decodable { let id: String let defaultProvider: String let payloadValid: Bool let expectedSelection: ExpectedSelection? let talk: [String: AnyCodable] } struct ExpectedSelection: Decodable { let provider: String let normalizedPayload: Bool let voiceId: String? let apiKey: String? } struct TimeoutCase: Decodable { let id: String let fallback: Int let expectedTimeoutMs: Int let talk: [String: AnyCodable] } } private enum TalkConfigContractFixtureLoader { static func load() throws -> TalkConfigContractFixture { let fixtureURL = try self.findFixtureURL(startingAt: URL(fileURLWithPath: #filePath)) let data = try Data(contentsOf: fixtureURL) return try JSONDecoder().decode(TalkConfigContractFixture.self, from: data) } private static func findFixtureURL(startingAt fileURL: URL) throws -> URL { var directory = fileURL.deletingLastPathComponent() while directory.path != "/" { let candidate = directory.appendingPathComponent("test-fixtures/talk-config-contract.json") if FileManager.default.fileExists(atPath: candidate.path) { return candidate } directory.deleteLastPathComponent() } throw NSError(domain: "TalkConfigContractFixtureLoader", code: 1) } } struct TalkConfigContractTests { @Test func selectionFixtures() throws { for fixture in try TalkConfigContractFixtureLoader.load().selectionCases { let selection = TalkConfigParsing.selectProviderConfig( fixture.talk, defaultProvider: fixture.defaultProvider) if let expected = fixture.expectedSelection { #expect(selection != nil) #expect(selection?.provider == expected.provider) #expect(selection?.normalizedPayload == expected.normalizedPayload) #expect(selection?.config["voiceId"]?.stringValue == expected.voiceId) #expect(selection?.config["apiKey"]?.stringValue == expected.apiKey) } else { #expect(selection == nil) } #expect(fixture.payloadValid == (selection != nil)) } } @Test func timeoutFixtures() throws { for fixture in try TalkConfigContractFixtureLoader.load().timeoutCases { #expect( TalkConfigParsing.resolvedSilenceTimeoutMs( fixture.talk, fallback: fixture.fallback) == fixture.expectedTimeoutMs, "\(fixture.id)") } } }