This commit is contained in:
2026-08-30 13:12:15 +03:00
parent f213b139da
commit 995c5fba0e
15 changed files with 969 additions and 375 deletions
@@ -0,0 +1,249 @@
package pw.binom.llmproxy
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import net.mamoe.yamlkt.Yaml
class ConfigLogicTest {
@Test
fun mergeDeepMergesNestedObjectsAndReplacesScalars() {
val base = Json.parseToJsonElement("""{"a":{"x":1,"y":2},"b":1}""").jsonObject
val patch = Json.parseToJsonElement("""{"a":{"y":3,"z":4},"c":5}""").jsonObject
val merged = merge(base, patch)
assertEquals("""{"a":{"x":1,"y":3,"z":4},"b":1,"c":5}""", merged.toString())
}
@Test
fun mergeReplacesArraysByKey() {
val base = Json.parseToJsonElement("""{"provider":{"ignore":["a"]}}""").jsonObject
val patch = Json.parseToJsonElement("""{"provider":{"ignore":["b","c"]}}""").jsonObject
assertEquals("""{"provider":{"ignore":["b","c"]}}""", merge(base, patch).toString())
}
@Test
fun parseConfigReadsAllBlocksAndConvertsPatchToJsonObject() {
val yaml = """
providers:
- id: p1
url: "https://x.ru/api/v1"
key: "k"
patch:
provider:
allow_fallbacks: false
upstreams:
- id: u1
provider: p1
model: real-1
max_concurrency: 2
patch:
provider:
ignore: [deepseek]
- id: bad
provider: missing
model: x
models:
- name: m1
upstreams: [u1, bad]
patch:
reasoning:
enabled: false
""".trimIndent()
val cfg = parseConfig(Yaml.decodeYamlFromString(yaml))
assertEquals(1, cfg.providers.size)
assertEquals("https://x.ru/api/v1", cfg.providers[0].url)
assertEquals(2, cfg.upstreams.size)
assertEquals(2, cfg.upstreams[0].max_concurrency)
assertEquals(listOf("u1", "bad"), cfg.models[0].upstreams)
assertEquals("""{"provider":{"allow_fallbacks":false}}""", cfg.providers[0].patch.toString())
assertEquals("""{"provider":{"ignore":["deepseek"]}}""", cfg.upstreams[0].patch.toString())
assertEquals("""{"reasoning":{"enabled":false}}""", cfg.models[0].patch.toString())
}
@Test
fun parseConfigOmitsPatchWhenAbsent() {
val yaml = """
providers:
- id: p1
url: "https://x.ru/api/v1"
upstreams:
- id: u1
provider: p1
model: real-1
models:
- name: m1
upstreams: [u1]
""".trimIndent()
val cfg = parseConfig(Yaml.decodeYamlFromString(yaml))
assertEquals(null, cfg.providers[0].patch)
assertEquals(null, cfg.models[0].patch)
assertEquals(null, cfg.upstreams[0].max_concurrency)
}
@Test
fun buildBodySubstitutesModelAndAppliesLayersInOrder() {
val provider = ProviderConf(
"p1", "https://x", "",
Json.parseToJsonElement("""{"provider":{"allow_fallbacks":false}}""").jsonObject,
)
val up = UpstreamConf(
"u1", "p1", "real-1", null,
Json.parseToJsonElement("""{"provider":{"ignore":["deepseek"]}}""").jsonObject,
)
val model = ModelConf(
"m1", listOf("u1"),
Json.parseToJsonElement("""{"reasoning":{"enabled":false}}""").jsonObject,
)
val body = Json.parseToJsonElement(
"""{"model":"m1","messages":[],"temperature":0.7}""",
).jsonObject
val out = buildBody(body, provider, up, model)
assertEquals(
"""{"model":"real-1","messages":[],"temperature":0.7,"provider":{"allow_fallbacks":false,"ignore":["deepseek"]},"reasoning":{"enabled":false}}""",
out.toString(),
)
}
@Test
fun buildBodyWithNoPatchesReturnsClientBodyWithSubstitutedModel() {
val provider = ProviderConf("p1", "https://x", "")
val up = UpstreamConf("u1", "p1", "real-1", null, null)
val model = ModelConf("m1", listOf("u1"), null)
val body = Json.parseToJsonElement("""{"model":"m1","a":1}""").jsonObject
assertEquals("""{"model":"real-1","a":1}""", buildBody(body, provider, up, model).toString())
}
@Test
fun resolveEnvSubstitutesMissingVarWithEmptyAndPassesLiteralThrough() {
assertEquals("", resolveEnv("\${LLM_PROXY_TEST_MISSING_VAR}"))
assertEquals("plain", resolveEnv("plain"))
assertEquals("pre--post", resolveEnv("pre-\${LLM_PROXY_TEST_MISSING_VAR}-post"))
}
@Test
fun tryClaimRespectsMaxConcurrencyAndReleaseFreesSlot() {
val active = mapOf("u1" to UpstreamCounter(0))
val up = UpstreamConf("u1", "p", "m", 1, null)
assertTrue(tryClaim(up, active))
assertFalse(tryClaim(up, active))
release(up, active)
assertTrue(tryClaim(up, active))
}
@Test
fun tryClaimUnlimitedWhenMaxConcurrencyIsNull() {
val active = mapOf("u2" to UpstreamCounter(0))
val up = UpstreamConf("u2", "p", "m", null, null)
assertTrue(tryClaim(up, active))
assertTrue(tryClaim(up, active))
}
@Test
fun pickFreeUpstreamReturnsFirstFreeInDeclarationOrder() {
val active = mapOf("u1" to UpstreamCounter(0), "u2" to UpstreamCounter(0))
val pool = listOf(
UpstreamConf("u1", "p", "m", 1, null),
UpstreamConf("u2", "p", "m", 2, null),
)
val up = pickFreeUpstream(pool, active, emptySet())
assertEquals("u1", up?.id)
// слот реально занят
assertEquals(1, active.getValue("u1").current)
}
@Test
fun pickFreeUpstreamSkipsExcluded() {
val active = mapOf("u1" to UpstreamCounter(0), "u2" to UpstreamCounter(0))
val pool = listOf(
UpstreamConf("u1", "p", "m", 1, null),
UpstreamConf("u2", "p", "m", 2, null),
)
val up = pickFreeUpstream(pool, active, setOf("u1"))
assertEquals("u2", up?.id)
}
@Test
fun pickFreeUpstreamReturnsNullWhenAllBusy() {
val active = mapOf("u1" to UpstreamCounter(1, 1)) // уже на лимите 1
val pool = listOf(UpstreamConf("u1", "p", "m", 1, null))
assertEquals(null, pickFreeUpstream(pool, active, emptySet()))
}
@Test
fun pickFreeUpstreamImplementsFailoverOrder() {
// dead исключён (упал ранее) — выбирается следующий живой u1
val active = mapOf("dead" to UpstreamCounter(0), "u1" to UpstreamCounter(0))
val pool = listOf(
UpstreamConf("dead", "p", "m", 1, null),
UpstreamConf("u1", "p", "m", 1, null),
)
val up = pickFreeUpstream(pool, active, setOf("dead"))
assertEquals("u1", up?.id)
}
@Test
fun pickFreeUpstreamExhaustsConcurrencyThenReturnsNull() {
val active = mapOf("u1" to UpstreamCounter(0), "u2" to UpstreamCounter(0))
val pool = listOf(
UpstreamConf("u1", "p", "m", 1, null),
UpstreamConf("u2", "p", "m", 1, null),
)
assertEquals("u1", pickFreeUpstream(pool, active, emptySet())?.id)
assertEquals("u2", pickFreeUpstream(pool, active, emptySet())?.id)
assertEquals(null, pickFreeUpstream(pool, active, emptySet())?.id)
}
@Test
fun rebuildFromChunksPreservesAllUpstreamFields() {
val sse = """
data: {"id":"c1","object":"chat.completion.chunk","created":123,"model":"m","provider":"Baidu","system_fingerprint":"sf1","choices":[{"index":0,"delta":{"role":"assistant","content":"He"},"finish_reason":null}]}
data: {"id":"c1","created":123,"model":"m","provider":"Baidu","choices":[{"index":0,"delta":{"content":"llo"},"finish_reason":null}]}
data: {"id":"c1","created":123,"model":"m","provider":"Baidu","choices":[{"index":1,"delta":{"content":"B"},"finish_reason":null}]}
data: {"id":"c1","created":123,"model":"m","provider":"Baidu","choices":[{"index":0,"delta":{},"finish_reason":"stop"},{"index":1,"delta":{},"finish_reason":"stop"}]}
data: {"id":"c1","created":123,"model":"m","provider":"Baidu","usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3}}
data: [DONE]
""".trimIndent()
val out = Json.parseToJsonElement(rebuildFromChunks(sse)).jsonObject
assertEquals("c1", out["id"]?.jsonPrimitive?.content)
assertEquals("m", out["model"]?.jsonPrimitive?.content)
assertEquals("Baidu", out["provider"]?.jsonPrimitive?.content)
assertEquals("sf1", out["system_fingerprint"]?.jsonPrimitive?.content)
assertEquals("chat.completion", out["object"]?.jsonPrimitive?.content)
assertEquals(123, out["created"]?.jsonPrimitive?.content?.toLong())
assertEquals(3, out["usage"]?.jsonObject?.get("total_tokens")?.jsonPrimitive?.content?.toInt())
val choices = out["choices"]?.jsonArray ?: error("no choices")
assertEquals(2, choices.size)
assertEquals("Hello", choices[0].jsonObject["message"]?.jsonObject?.get("content")?.jsonPrimitive?.content)
assertEquals("B", choices[1].jsonObject["message"]?.jsonObject?.get("content")?.jsonPrimitive?.content)
assertEquals("stop", choices[0].jsonObject["finish_reason"]?.jsonPrimitive?.content)
}
@Test
fun rebuildFromChunksReturnsUpstreamErrorVerbatim() {
val sse = """
data: {"id":"c1","error":{"message":"Unsupported field 'foo'","type":"invalid_request"},"choices":[]}
data: [DONE]
""".trimIndent()
val out = Json.parseToJsonElement(rebuildFromChunks(sse)).jsonObject
val err = out["error"]?.jsonObject ?: error("error block missing")
assertEquals("Unsupported field 'foo'", err["message"]?.jsonPrimitive?.content)
}
}