mirror of
https://github.com/yuangyaa/openclaw.git
synced 2026-07-14 11:07:29 +08:00
test: add talk config contract fixtures
This commit is contained in:
@@ -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<SelectionCase>,
|
||||||
|
) {
|
||||||
|
@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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/gateway/protocol/talk-config.contract.test.ts
Normal file
57
src/gateway/protocol/talk-config.contract.test.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
88
test-fixtures/talk-config-contract.json
Normal file
88
test-fixtures/talk-config-contract.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user