From 371c53b282727c38446fe6e9499f8964130362de Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 16:28:06 +0000 Subject: [PATCH] test: expand talk config contract fixtures --- .../app/voice/TalkModeConfigContractTest.kt | 24 +++++++ .../TalkConfigContractTests.swift | 20 ++++++ .../protocol/talk-config.contract.test.ts | 20 ++++++ test-fixtures/talk-config-contract.json | 65 +++++++++++++++++-- 4 files changed, 124 insertions(+), 5 deletions(-) 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 index d2df33793e2..ca9be8b1280 100644 --- 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 @@ -14,6 +14,7 @@ import org.junit.Test @Serializable private data class TalkConfigContractFixture( @SerialName("selectionCases") val selectionCases: List, + @SerialName("timeoutCases") val timeoutCases: List, ) { @Serializable data class SelectionCase( @@ -29,6 +30,15 @@ private data class TalkConfigContractFixture( val provider: String, val normalizedPayload: Boolean, val voiceId: String? = null, + val apiKey: String? = null, + ) + + @Serializable + data class TimeoutCase( + val id: String, + val fallback: Long, + val expectedTimeoutMs: Long, + val talk: JsonObject, ) } @@ -52,10 +62,24 @@ class TalkModeConfigContractTest { expected.voiceId, (selection?.config?.get("voiceId") as? JsonPrimitive)?.content, ) + assertEquals( + fixture.id, + expected.apiKey, + (selection?.config?.get("apiKey") as? JsonPrimitive)?.content, + ) assertEquals(fixture.id, true, fixture.payloadValid) } } + @Test + fun timeoutFixtures() { + for (fixture in loadFixtures().timeoutCases) { + val timeout = TalkModeGatewayConfigParser.resolvedSilenceTimeoutMs(fixture.talk) + assertEquals(fixture.id, fixture.expectedTimeoutMs, timeout) + assertEquals(fixture.id, TalkDefaults.defaultSilenceTimeoutMs, fixture.fallback) + } + } + private fun loadFixtures(): TalkConfigContractFixture { val fixturePath = findFixtureFile() return json.decodeFromString(File(fixturePath).readText()) diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigContractTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigContractTests.swift index 3ca2031e03e..1903d917860 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigContractTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkConfigContractTests.swift @@ -4,6 +4,7 @@ import Testing private struct TalkConfigContractFixture: Decodable { let selectionCases: [SelectionCase] + let timeoutCases: [TimeoutCase] struct SelectionCase: Decodable { let id: String @@ -17,6 +18,14 @@ private struct TalkConfigContractFixture: 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] } } @@ -51,10 +60,21 @@ struct TalkConfigContractTests { #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)") + } + } } diff --git a/src/gateway/protocol/talk-config.contract.test.ts b/src/gateway/protocol/talk-config.contract.test.ts index 7d77dbc37ca..d6bc1a74440 100644 --- a/src/gateway/protocol/talk-config.contract.test.ts +++ b/src/gateway/protocol/talk-config.contract.test.ts @@ -1,11 +1,13 @@ import fs from "node:fs"; import { describe, expect, it } from "vitest"; +import { buildTalkConfigResponse } from "../../config/talk.js"; import { validateTalkConfigResult } from "./index.js"; type ExpectedSelection = { provider: string; normalizedPayload: boolean; voiceId?: string; + apiKey?: string; }; type SelectionContractCase = { @@ -16,8 +18,16 @@ type SelectionContractCase = { talk: Record; }; +type TimeoutContractCase = { + id: string; + fallback: number; + expectedTimeoutMs: number; + talk: Record; +}; + type TalkConfigContractFixture = { selectionCases: SelectionContractCase[]; + timeoutCases: TimeoutContractCase[]; }; const fixturePath = new URL("../../../test-fixtures/talk-config-contract.json", import.meta.url); @@ -42,9 +52,11 @@ describe("talk.config contract fixtures", () => { provider?: string; config?: { voiceId?: string; + apiKey?: string; }; }; voiceId?: string; + apiKey?: string; }; expect(talk.resolved?.provider ?? fixture.defaultProvider).toBe( fixture.expectedSelection.provider, @@ -52,6 +64,14 @@ describe("talk.config contract fixtures", () => { expect(talk.resolved?.config?.voiceId ?? talk.voiceId).toBe( fixture.expectedSelection.voiceId, ); + expect(talk.resolved?.config?.apiKey ?? talk.apiKey).toBe(fixture.expectedSelection.apiKey); + }); + } + + for (const fixture of fixtures.timeoutCases) { + it(`timeout:${fixture.id}`, () => { + const payload = buildTalkConfigResponse(fixture.talk); + expect(payload?.silenceTimeoutMs ?? fixture.fallback).toBe(fixture.expectedTimeoutMs); }); } }); diff --git a/test-fixtures/talk-config-contract.json b/test-fixtures/talk-config-contract.json index ed3a7a46055..c94952acaf9 100644 --- a/test-fixtures/talk-config-contract.json +++ b/test-fixtures/talk-config-contract.json @@ -7,22 +7,26 @@ "expectedSelection": { "provider": "elevenlabs", "normalizedPayload": true, - "voiceId": "voice-resolved" + "voiceId": "voice-resolved", + "apiKey": "resolved-key" }, "talk": { "resolved": { "provider": "elevenlabs", "config": { - "voiceId": "voice-resolved" + "voiceId": "voice-resolved", + "apiKey": "resolved-key" } }, "provider": "elevenlabs", "providers": { "elevenlabs": { - "voiceId": "voice-normalized" + "voiceId": "voice-normalized", + "apiKey": "normalized-key" } }, - "voiceId": "voice-legacy" + "voiceId": "voice-legacy", + "apiKey": "legacy-key" } }, { @@ -77,12 +81,63 @@ "expectedSelection": { "provider": "elevenlabs", "normalizedPayload": false, - "voiceId": "voice-legacy" + "voiceId": "voice-legacy", + "apiKey": "legacy-key" }, "talk": { "voiceId": "voice-legacy", "apiKey": "xxxxx" } } + ], + "timeoutCases": [ + { + "id": "integer_timeout_kept", + "fallback": 700, + "expectedTimeoutMs": 1500, + "talk": { + "silenceTimeoutMs": 1500 + } + }, + { + "id": "integer_like_double_timeout_kept", + "fallback": 700, + "expectedTimeoutMs": 1500, + "talk": { + "silenceTimeoutMs": 1500.0 + } + }, + { + "id": "zero_timeout_falls_back", + "fallback": 700, + "expectedTimeoutMs": 700, + "talk": { + "silenceTimeoutMs": 0 + } + }, + { + "id": "boolean_timeout_falls_back", + "fallback": 700, + "expectedTimeoutMs": 700, + "talk": { + "silenceTimeoutMs": true + } + }, + { + "id": "string_timeout_falls_back", + "fallback": 700, + "expectedTimeoutMs": 700, + "talk": { + "silenceTimeoutMs": "1500" + } + }, + { + "id": "fractional_timeout_falls_back", + "fallback": 700, + "expectedTimeoutMs": 700, + "talk": { + "silenceTimeoutMs": 1500.5 + } + } ] }