From dc5645d45997ad51e1cc69c60f365e7f9c5cedc3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 16:11:03 +0000 Subject: [PATCH] test: add talk config contract fixtures --- .../app/voice/TalkModeConfigContractTest.kt | 76 ++++++++++++++++ .../TalkConfigContractTests.swift | 60 +++++++++++++ .../protocol/talk-config.contract.test.ts | 57 ++++++++++++ test-fixtures/talk-config-contract.json | 88 +++++++++++++++++++ 4 files changed, 281 insertions(+) create mode 100644 apps/android/app/src/test/java/ai/openclaw/app/voice/TalkModeConfigContractTest.kt create mode 100644 apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigContractTests.swift create mode 100644 src/gateway/protocol/talk-config.contract.test.ts create mode 100644 test-fixtures/talk-config-contract.json diff --git a/apps/android/app/src/test/java/ai/openclaw/app/voice/TalkModeConfigContractTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/voice/TalkModeConfigContractTest.kt new file mode 100644 index 00000000000..16336d65706 --- /dev/null +++ b/apps/android/app/src/test/java/ai/openclaw/app/voice/TalkModeConfigContractTest.kt @@ -0,0 +1,76 @@ +package ai.openclaw.app.voice + +import java.io.File +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test + +@Serializable +private data class TalkConfigContractFixture( + @SerialName("selectionCases") val selectionCases: List, +) { + @Serializable + data class SelectionCase( + val id: String, + val defaultProvider: String, + val payloadValid: Boolean, + val expectedSelection: ExpectedSelection? = null, + val talk: JsonObject, + ) + + @Serializable + data class ExpectedSelection( + val provider: String, + val normalizedPayload: Boolean, + val voiceId: String? = null, + ) +} + +class TalkModeConfigContractTest { + private val json = Json { ignoreUnknownKeys = true } + + @Test + fun selectionFixtures() { + for (fixture in loadFixtures().selectionCases) { + val selection = TalkModeManager.selectTalkProviderConfig(fixture.talk) + val expected = fixture.expectedSelection + if (expected == null) { + assertNull(fixture.id, selection) + continue + } + assertNotNull(fixture.id, selection) + assertEquals(fixture.id, expected.provider, selection?.provider) + assertEquals(fixture.id, expected.normalizedPayload, selection?.normalizedPayload) + assertEquals( + fixture.id, + expected.voiceId, + (selection?.config?.get("voiceId") as? JsonPrimitive)?.content, + ) + assertEquals(fixture.id, true, fixture.payloadValid) + } + } + + private fun loadFixtures(): TalkConfigContractFixture { + val fixturePath = findFixtureFile() + return json.decodeFromString(File(fixturePath).readText()) + } + + private fun findFixtureFile(): String { + val startDir = System.getProperty("user.dir") ?: error("user.dir unavailable") + var current = File(startDir).absoluteFile + while (true) { + val candidate = File(current, "test-fixtures/talk-config-contract.json") + if (candidate.exists()) { + return candidate.absolutePath + } + current = current.parentFile ?: break + } + error("talk-config-contract.json not found from $startDir") + } +} diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigContractTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigContractTests.swift new file mode 100644 index 00000000000..3ca2031e03e --- /dev/null +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigContractTests.swift @@ -0,0 +1,60 @@ +import Foundation +import OpenClawKit +import Testing + +private struct TalkConfigContractFixture: Decodable { + let selectionCases: [SelectionCase] + + 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? + } +} + +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) + } else { + #expect(selection == nil) + } + #expect(fixture.payloadValid == (selection != nil)) + } + } +} diff --git a/src/gateway/protocol/talk-config.contract.test.ts b/src/gateway/protocol/talk-config.contract.test.ts new file mode 100644 index 00000000000..59cac413a90 --- /dev/null +++ b/src/gateway/protocol/talk-config.contract.test.ts @@ -0,0 +1,57 @@ +import fs from "node:fs"; +import { describe, expect, it } from "vitest"; +import { validateTalkConfigResult } from "./index.js"; + +type ExpectedSelection = { + provider: string; + normalizedPayload: boolean; + voiceId?: string; +}; + +type SelectionContractCase = { + id: string; + defaultProvider: string; + payloadValid: boolean; + expectedSelection: ExpectedSelection | null; + talk: Record; +}; + +type TalkConfigContractFixture = { + selectionCases: SelectionContractCase[]; +}; + +const fixturePath = new URL("../../../test-fixtures/talk-config-contract.json", import.meta.url); +const fixtures = JSON.parse(fs.readFileSync(fixturePath, "utf-8")) as TalkConfigContractFixture; + +describe("talk.config contract fixtures", () => { + for (const fixture of fixtures.selectionCases) { + it(fixture.id, () => { + const payload = { config: { talk: fixture.talk } }; + if (fixture.payloadValid) { + expect(validateTalkConfigResult(payload)).toBe(true); + } else { + expect((payload.config.talk as { resolved?: unknown }).resolved).toBeUndefined(); + } + + if (!fixture.expectedSelection) { + return; + } + + const talk = payload.config.talk as { + resolved?: { + provider?: string; + config?: { + voiceId?: string; + }; + }; + voiceId?: string; + }; + expect(talk.resolved?.provider ?? fixture.defaultProvider).toBe( + fixture.expectedSelection.provider, + ); + expect(talk.resolved?.config?.voiceId ?? talk.voiceId).toBe( + fixture.expectedSelection.voiceId, + ); + }); + } +}); diff --git a/test-fixtures/talk-config-contract.json b/test-fixtures/talk-config-contract.json new file mode 100644 index 00000000000..8877c189385 --- /dev/null +++ b/test-fixtures/talk-config-contract.json @@ -0,0 +1,88 @@ +{ + "selectionCases": [ + { + "id": "canonical_resolved_wins", + "defaultProvider": "elevenlabs", + "payloadValid": true, + "expectedSelection": { + "provider": "elevenlabs", + "normalizedPayload": true, + "voiceId": "voice-resolved" + }, + "talk": { + "resolved": { + "provider": "elevenlabs", + "config": { + "voiceId": "voice-resolved" + } + }, + "provider": "elevenlabs", + "providers": { + "elevenlabs": { + "voiceId": "voice-normalized" + } + }, + "voiceId": "voice-legacy" + } + }, + { + "id": "normalized_missing_resolved", + "defaultProvider": "elevenlabs", + "payloadValid": false, + "expectedSelection": null, + "talk": { + "provider": "elevenlabs", + "providers": { + "elevenlabs": { + "voiceId": "voice-normalized" + } + }, + "voiceId": "voice-legacy" + } + }, + { + "id": "provider_mismatch_missing_resolved", + "defaultProvider": "elevenlabs", + "payloadValid": false, + "expectedSelection": null, + "talk": { + "provider": "acme", + "providers": { + "elevenlabs": { + "voiceId": "voice-normalized" + } + } + } + }, + { + "id": "ambiguous_providers_missing_resolved", + "defaultProvider": "elevenlabs", + "payloadValid": false, + "expectedSelection": null, + "talk": { + "providers": { + "acme": { + "voiceId": "voice-acme" + }, + "elevenlabs": { + "voiceId": "voice-normalized" + } + } + } + }, + { + "id": "legacy_payload_fallback", + "defaultProvider": "elevenlabs", + "payloadValid": true, + "expectedSelection": { + "provider": "elevenlabs", + "normalizedPayload": false, + "voiceId": "voice-legacy" + }, + "talk": { + "voiceId": "voice-legacy", + "apiKey": "legacy-key" + } + } + ] +}