From 958f6c4d757f87c8bd0a8bbbd533a68119ff3a4d Mon Sep 17 00:00:00 2001 From: chengqian Date: Fri, 28 Aug 2026 17:40:24 +0800 Subject: [PATCH] =?UTF-8?q?type(relay):=E4=BF=AE=E6=AD=A3=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E4=BB=A3=E7=A0=81=E9=87=8C=E9=9D=A2=E7=9A=84=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E5=90=8D=E4=B8=BA=E7=BA=AF=E8=8B=B1=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/reference/goframe-conventions.md | 2 +- internal/dispatchadapter/catalog_test.go | 38 ++++----- internal/dispatchadapter/maintenance_test.go | 18 ++-- internal/dispatchadapter/state_redis_test.go | 88 ++++++++++---------- internal/logic/common/cron_test.go | 24 +++--- internal/logic/update/replacer_test.go | 14 ++-- internal/logic/update/types_test.go | 14 ++-- internal/logic/update/verifier_test.go | 10 +-- relaykit/dispatch/breaker_test.go | 12 +-- relaykit/dispatch/classify_test.go | 14 ++-- relaykit/dispatch/coordinator_test.go | 68 +++++++-------- relaykit/dispatch/hrw_test.go | 12 +-- relaykit/dispatch/policy_test.go | 30 +++---- relaykit/dispatch/score_test.go | 14 ++-- relaykit/dispatch/session_test.go | 12 +-- 15 files changed, 185 insertions(+), 185 deletions(-) diff --git a/docs/reference/goframe-conventions.md b/docs/reference/goframe-conventions.md index c89df67..e7a7d35 100644 --- a/docs/reference/goframe-conventions.md +++ b/docs/reference/goframe-conventions.md @@ -786,7 +786,7 @@ err := g.DB().Transaction(dbCtx, func(txCtx context.Context, tx gdb.TX) error { **原因**:`g.Redis().Do(ctx, "HMGET", key, "f1", "f2")` 对**不存在的 key/字段**,经 gf redis contrib 驱动转换后返回的是 `[]string{"", ""}`(空字符串),`.Vars()` 后每个元素 **`IsNil()==false`**、`Float64()==0`。`ReadRuntime` 用 `!vals[0].IsNil()` 判「有数据才覆盖乐观默认值 1」,判断永远为真 → 默认值被 0 覆盖。调度打分侧 `healthFactor` 恰好有 `succ<=0 && lat==0 视为满分` 的兜底所以转发不受影响,但快照聚合没有兜底,展示值被拖垮。`readBreaker` 同模式(空串 state 恰好 Int()==0==CLOSED 侥幸正确)。 -**修复**:`internal/dispatchadapter/state_redis.go` `ReadRuntime`/`readBreaker` 改为 `vals[i].String() != ""` 判有无数据;空串一律按无数据处理保留默认值。回归测试 `Test缺失健康键读默认值`。 +**修复**:`internal/dispatchadapter/state_redis.go` `ReadRuntime`/`readBreaker` 改为 `vals[i].String() != ""` 判有无数据;空串一律按无数据处理保留默认值。回归测试 `TestMissingHealthKey_ReadsDefault`。 **正确做法(通用规则)**:判断 gf redis `HMGET`/批量读的字段是否存在,**不要依赖 `IsNil()`**(驱动转换后 nil 会变空字符串);用 `String() != ""` 判断,或改用 Lua 脚本内 `HGET ... or '默认值'` 在 Redis 侧兜底。单 key `HGET` 缺失返回的是真 nil(`IsNil()==true`),两者行为不一致,混用时尤其注意。 diff --git a/internal/dispatchadapter/catalog_test.go b/internal/dispatchadapter/catalog_test.go index 966efd8..78cbecc 100644 --- a/internal/dispatchadapter/catalog_test.go +++ b/internal/dispatchadapter/catalog_test.go @@ -24,7 +24,7 @@ func testCatalog(rows []catalogRow, keys map[int64][]int64, runtime runtimeReade return c } -func TestCatalog_快照构建与范围过滤(t *testing.T) { +func TestCatalog_SnapshotBuildAndScopeFilter(t *testing.T) { rows := []catalogRow{ {ChannelID: 1, ChannelName: "A", ModelName: "gpt-4o", Weight: 10, Tier: "primary", CostRatio: 0.8}, {ChannelID: 2, ChannelName: "B", ModelName: "gpt-4o", Weight: 5, Tier: "secondary", CostRatio: 1.0}, @@ -50,7 +50,7 @@ func TestCatalog_快照构建与范围过滤(t *testing.T) { assert.Empty(t, c.Snapshot(context.Background(), 0, "unknown", nil)) } -func TestCatalog_转发元数据(t *testing.T) { +func TestCatalog_ForwardMeta(t *testing.T) { rows := []catalogRow{ {ChannelID: 2, ChannelName: "B", ChannelType: 3, BaseURL: "https://up.example.com", ModelName: "claude-sonnet", UpstreamModel: "claude-sonnet-4", Weight: 5, Tier: "secondary", @@ -76,7 +76,7 @@ func TestCatalog_转发元数据(t *testing.T) { assert.False(t, ok) } -func TestCatalog_运行时读值合并(t *testing.T) { +func TestCatalog_RuntimeReadoutMerge(t *testing.T) { rows := []catalogRow{ {ChannelID: 1, ModelName: "m", Weight: 10, Tier: "primary", MaxConcurrency: 50}, } @@ -96,7 +96,7 @@ func TestCatalog_运行时读值合并(t *testing.T) { assert.Equal(t, dispatch.BreakerHalfOpen, got[0].Breaker) } -func TestCatalog_严格容量查询(t *testing.T) { +func TestCatalog_StrictCapacityLookup(t *testing.T) { rows := []catalogRow{ {ChannelID: 1, ModelName: "m", Weight: 10, Tier: "primary", StrictCapacity: true, MaxConcurrency: 30}, {ChannelID: 2, ModelName: "m", Weight: 10, Tier: "primary"}, @@ -111,7 +111,7 @@ func TestCatalog_严格容量查询(t *testing.T) { assert.False(t, strict) } -func TestCatalog_加载失败保留上一份快照(t *testing.T) { +func TestCatalog_LoadFailureKeepsPreviousSnapshot(t *testing.T) { pol := dispatch.DefaultRoutingPolicy() calls := 0 c := NewCatalog( @@ -133,10 +133,10 @@ func TestCatalog_加载失败保留上一份快照(t *testing.T) { assert.Len(t, c.Snapshot(ctx, 0, "m", nil), 1, "last-known 快照必须保留") } -// TestCatalog_Redis降级沿用上一份健康读值 Redis 读失败时若让乐观默认值 succ=1 落进 +// TestCatalog_RedisDegradedCarriesForwardHealth Redis 读失败时若让乐观默认值 succ=1 落进 // 新快照,一次 Redis 抖动就会把全渠道的健康记忆清零、healthFactor 集体回满分, // 与 Rebuild 声称的 last-known 语义相悖。 -func TestCatalog_Redis降级沿用上一份健康读值(t *testing.T) { +func TestCatalog_RedisDegradedCarriesForwardHealth(t *testing.T) { rows := []catalogRow{ {ChannelID: 1, ChannelName: "A", ModelName: "gpt-4o", Weight: 10, Tier: "primary", CostRatio: 1}, } @@ -160,9 +160,9 @@ func TestCatalog_Redis降级沿用上一份健康读值(t *testing.T) { assert.Equal(t, 4000.0, got[0].LatEwmaMs, "延迟读值同样沿用") } -// TestCatalog_首次刷新即降级无历史可沿用 新实例启动时 Redis 就不可用:没有 last-known +// TestCatalog_DegradedOnFirstRefreshHasNoHistory 新实例启动时 Redis 就不可用:没有 last-known // 可沿用,保持乐观默认满分(不能让新实例启动即把全渠道判死)。 -func TestCatalog_首次刷新即降级无历史可沿用(t *testing.T) { +func TestCatalog_DegradedOnFirstRefreshHasNoHistory(t *testing.T) { rows := []catalogRow{ {ChannelID: 1, ChannelName: "A", ModelName: "gpt-4o", Weight: 10, Tier: "primary", CostRatio: 1}, } @@ -191,9 +191,9 @@ func TestRampElapsed(t *testing.T) { assert.Equal(t, int64(-1), rampElapsed(now, now-10, 0, 0)) } -// TestCatalog_渠道运行状态聚合 验证 ChannelRuntimeStates 按渠道去重、渠道级熔断取同值、 +// TestCatalog_ChannelRuntimeStateAggregation 验证 ChannelRuntimeStates 按渠道去重、渠道级熔断取同值、 // 模型级熔断按模型计数。 -func TestCatalog_渠道运行状态聚合(t *testing.T) { +func TestCatalog_ChannelRuntimeStateAggregation(t *testing.T) { rows := []catalogRow{ {ChannelID: 1, ChannelName: "A", ModelName: "gpt-4o", Weight: 10, Tier: "primary"}, {ChannelID: 1, ChannelName: "A", ModelName: "claude-sonnet", Weight: 10, Tier: "primary"}, @@ -227,16 +227,16 @@ func TestCatalog_渠道运行状态聚合(t *testing.T) { assert.Equal(t, 0, got[3].BreakerModels) } -// TestCatalog_运行状态_未Rebuild返回空 验证目录未构建时返回空 map(而非 nil), +// TestCatalog_RuntimeState_EmptyBeforeRebuild 验证目录未构建时返回空 map(而非 nil), // 调用方遍历时安全。 -func TestCatalog_运行状态_未Rebuild返回空(t *testing.T) { +func TestCatalog_RuntimeState_EmptyBeforeRebuild(t *testing.T) { c := NewCatalog(func() *dispatch.RoutingPolicy { return dispatch.DefaultRoutingPolicy() }, nil, nil) assert.Empty(t, c.ChannelRuntimeStates()) } -// TestCatalog_最差模型_单模型故障不被均值摊薄 渠道健康分是均值,10 个健康模型 + 1 个 +// TestCatalog_WorstModel_SingleFailureNotDilutedByAverage 渠道健康分是均值,10 个健康模型 + 1 个 // 全挂仍能算出 90 分绿灯;最差项要如实报出那个挂掉的模型。 -func TestCatalog_最差模型_单模型故障不被均值摊薄(t *testing.T) { +func TestCatalog_WorstModel_SingleFailureNotDilutedByAverage(t *testing.T) { rows := []catalogRow{ {ChannelID: 1, ChannelName: "A", ModelName: "healthy-a", Weight: 10, Tier: "primary"}, {ChannelID: 1, ChannelName: "A", ModelName: "healthy-b", Weight: 10, Tier: "primary"}, @@ -256,9 +256,9 @@ func TestCatalog_最差模型_单模型故障不被均值摊薄(t *testing.T) { assert.InDelta(t, 1.0, got[1].WorstModelScore, 1e-6) } -// TestCatalog_最差模型_冷模型不被误报 无真实上报的模型读作满分 1.0,取最小值天然 +// TestCatalog_WorstModel_ColdModelNotReported 无真实上报的模型读作满分 1.0,取最小值天然 // 不会把它选成最差项——这正是拖垮渠道均值的缺陷在 min 聚合下不存在的原因。 -func TestCatalog_最差模型_冷模型不被误报(t *testing.T) { +func TestCatalog_WorstModel_ColdModelNotReported(t *testing.T) { rows := []catalogRow{ {ChannelID: 1, ChannelName: "A", ModelName: "cold", Weight: 10, Tier: "primary"}, {ChannelID: 1, ChannelName: "A", ModelName: "warm", Weight: 10, Tier: "primary"}, @@ -275,9 +275,9 @@ func TestCatalog_最差模型_冷模型不被误报(t *testing.T) { assert.InDelta(t, 64.0, got[1].WorstModelScore, 1e-6) // 0.8² × 100 } -// TestCatalog_最差模型_同分时结果稳定 byModel 是 map,遍历顺序随机;同分不做确定性 +// TestCatalog_WorstModel_StableOnTiedScores byModel 是 map,遍历顺序随机;同分不做确定性 // 打破平局的话,全部模型满分时每次请求返回的模型名都会跳变。 -func TestCatalog_最差模型_同分时结果稳定(t *testing.T) { +func TestCatalog_WorstModel_StableOnTiedScores(t *testing.T) { rows := []catalogRow{ {ChannelID: 1, ChannelName: "A", ModelName: "zeta", Weight: 10, Tier: "primary"}, {ChannelID: 1, ChannelName: "A", ModelName: "alpha", Weight: 10, Tier: "primary"}, diff --git a/internal/dispatchadapter/maintenance_test.go b/internal/dispatchadapter/maintenance_test.go index f734416..bb82106 100644 --- a/internal/dispatchadapter/maintenance_test.go +++ b/internal/dispatchadapter/maintenance_test.go @@ -24,7 +24,7 @@ func staticReader(m map[string]RuntimeReadout) func(string) RuntimeReadout { } } -func TestAggregateChannelHealth_只统计有真实上报的模型(t *testing.T) { +func TestAggregateChannelHealth_CountsOnlyModelsWithRealReports(t *testing.T) { // 20 个模型的渠道死掉 1 个:旧实现把 19 个无数据模型按满分计入均值, // avgSucc≈0.95 → 91 分绿灯,完全掩盖故障。 models := make([]string, 0, 20) @@ -43,7 +43,7 @@ func TestAggregateChannelHealth_只统计有真实上报的模型(t *testing.T) assert.Contains(t, agg.Detail, "b=无数据") } -func TestAggregateChannelHealth_多模型均值(t *testing.T) { +func TestAggregateChannelHealth_MultiModelAverage(t *testing.T) { agg := aggregateChannelHealth([]string{"m1", "m2", "m3"}, staticReader(map[string]RuntimeReadout{ "m1": readoutOf(1.0, 100), "m2": readoutOf(0.6, 300), @@ -55,7 +55,7 @@ func TestAggregateChannelHealth_多模型均值(t *testing.T) { assert.InDelta(t, 200, agg.AvgLat, 1e-9) } -func TestAggregateChannelHealth_全部无数据不产生均值(t *testing.T) { +func TestAggregateChannelHealth_NoAverageWhenAllModelsLackData(t *testing.T) { agg := aggregateChannelHealth([]string{"m1", "m2"}, staticReader(nil)) assert.False(t, agg.Degraded) @@ -63,7 +63,7 @@ func TestAggregateChannelHealth_全部无数据不产生均值(t *testing.T) { assert.Zero(t, agg.AvgSucc, "不得回落成默认满分 1.0 造成 100 分落盘") } -func TestAggregateChannelHealth_读失败整体降级(t *testing.T) { +func TestAggregateChannelHealth_ReadFailureDegradesWhole(t *testing.T) { // Redis 故障时读值既非实测也非"确实无数据",必须整体作废而非按满分聚合 agg := aggregateChannelHealth([]string{"m1", "m2", "m3"}, staticReader(map[string]RuntimeReadout{ "m1": readoutOf(0.9, 100), @@ -75,7 +75,7 @@ func TestAggregateChannelHealth_读失败整体降级(t *testing.T) { assert.Zero(t, agg.Counted, "降级时不得给出任何聚合结论") } -func TestAggregateChannelHealth_首模型读值供自动禁用判定(t *testing.T) { +func TestAggregateChannelHealth_FirstModelReadoutFeedsAutoDisable(t *testing.T) { first := readoutOf(0.5, 100) first.FirstOpenedMs = 12345 agg := aggregateChannelHealth([]string{"m1", "m2"}, staticReader(map[string]RuntimeReadout{ @@ -86,9 +86,9 @@ func TestAggregateChannelHealth_首模型读值供自动禁用判定(t *testing. assert.Equal(t, int64(12345), agg.ChannelRt.FirstOpenedMs, "渠道级熔断读值取首个模型") } -// TestRequestHealthSnapshot_不阻塞且满时丢弃 调用方是管理后台请求线程(渠道测试 / +// TestRequestHealthSnapshot_NonBlockingDropsWhenFull 调用方是管理后台请求线程(渠道测试 / // 重置健康度),队列满时必须丢弃而非阻塞——下一轮定时维护会兜底重算。 -func TestRequestHealthSnapshot_不阻塞且满时丢弃(t *testing.T) { +func TestRequestHealthSnapshot_NonBlockingDropsWhenFull(t *testing.T) { drain := func() { for { select { @@ -119,9 +119,9 @@ func TestRequestHealthSnapshot_不阻塞且满时丢弃(t *testing.T) { assert.Len(t, healthRefresh, cap(healthRefresh), "满队列不应再增长") } -// TestRefreshChannelHealth_未组装时静默返回 管理后台可能在首次 relay 请求前就触发重算, +// TestRefreshChannelHealth_SilentWhenNotWired 管理后台可能在首次 relay 请求前就触发重算, // 此时调度引擎尚未组装(catalog / redisState 为 nil),不得 panic。 -func TestRefreshChannelHealth_未组装时静默返回(t *testing.T) { +func TestRefreshChannelHealth_SilentWhenNotWired(t *testing.T) { require.Nil(t, catalog, "前置条件:本包测试不组装调度引擎") assert.NotPanics(t, func() { refreshChannelHealth(context.Background(), 1) }) } diff --git a/internal/dispatchadapter/state_redis_test.go b/internal/dispatchadapter/state_redis_test.go index a71f44a..eb1bd7f 100644 --- a/internal/dispatchadapter/state_redis_test.go +++ b/internal/dispatchadapter/state_redis_test.go @@ -45,9 +45,9 @@ func newTestState(t *testing.T, mutate func(*dispatch.RoutingPolicy)) *RedisStat return NewRedisState(func() *dispatch.RoutingPolicy { return pol }, nil) } -// Test并发健康EWMA原子性 H1 修复验证:100 并发失败经 Lua 原子读-算-写, +// TestHealthEwma_ConcurrentAtomicity H1 修复验证:100 并发失败经 Lua 原子读-算-写, // EWMA 精确等于 0.93^100,熔断窗口计数精确 =100(读改写竞态会导致丢失更新)。 -func Test并发健康EWMA原子性(t *testing.T) { +func TestHealthEwma_ConcurrentAtomicity(t *testing.T) { ctx := context.Background() s := newTestState(t, func(p *dispatch.RoutingPolicy) { p.Breaker.FailThreshold = 1000 // 阈值调高,只验证计数不触发熔断 @@ -76,7 +76,7 @@ func Test并发健康EWMA原子性(t *testing.T) { assert.Equal(t, 100, v.Int(), "熔断窗口失败计数必须精确 =100") } -func Test健康衰减分档(t *testing.T) { +func TestHealthDecay_Tiers(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -104,10 +104,10 @@ func Test健康衰减分档(t *testing.T) { assert.InEpsilon(t, 800, rt.LatEwmaMs, 1e-9) } -// Test探测失败不衰减健康EWMA 探测类(Probe)失败只喂熔断窗口,健康分不变—— +// TestProbeFailure_DoesNotDecayHealthEwma 探测类(Probe)失败只喂熔断窗口,健康分不变—— // 防止每 5 分钟的自动探测持续失败把无流量渠道健康分指数拖垮(0.93^N → 个位数); // 探测成功照常回升健康并记录延迟。 -func Test探测失败不衰减健康EWMA(t *testing.T) { +func TestProbeFailure_DoesNotDecayHealthEwma(t *testing.T) { ctx := context.Background() s := newTestState(t, func(p *dispatch.RoutingPolicy) { p.Breaker.FailThreshold = 1000 // 阈值调高,只验证窗口计数不触发熔断 @@ -139,10 +139,10 @@ func Test探测失败不衰减健康EWMA(t *testing.T) { assert.InEpsilon(t, 500, rt.LatEwmaMs, 1e-9) } -// Test缺失健康键读默认值 gf 驱动对 HMGET 缺失字段返回空字符串而非 nil(IsNil()==false)。 +// TestMissingHealthKey_ReadsDefault gf 驱动对 HMGET 缺失字段返回空字符串而非 nil(IsNil()==false)。 // ReadRuntime 必须按无数据处理保留乐观默认(succ=1):无流量模型或 TTL(24h)过期键 // 若读成 succ=0,健康快照取平均会把渠道健康度拖到个位数。 -func Test缺失健康键读默认值(t *testing.T) { +func TestMissingHealthKey_ReadsDefault(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -153,7 +153,7 @@ func Test缺失健康键读默认值(t *testing.T) { assert.Equal(t, dispatch.BreakerClosed, rt.ModelBreaker) } -func Test限流不喂熔断窗口(t *testing.T) { +func TestRateLimit_DoesNotFeedBreakerWindow(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -168,7 +168,7 @@ func Test限流不喂熔断窗口(t *testing.T) { assert.Equal(t, dispatch.BreakerClosed, rt.Breaker) } -func Test熔断转移与探测令牌(t *testing.T) { +func TestBreaker_TransitionAndProbeToken(t *testing.T) { ctx := context.Background() s := newTestState(t, func(p *dispatch.RoutingPolicy) { p.Breaker.ModelFailThreshold = 100 // 调高模型级阈值,聚焦验证渠道级 8 次语义 @@ -201,7 +201,7 @@ func Test熔断转移与探测令牌(t *testing.T) { assert.Greater(t, rt.RecoveredMs, int64(0), "恢复时间必须记录") } -func Test熔断探测失败冷却翻倍(t *testing.T) { +func TestBreaker_ProbeFailureDoublesCooldown(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -225,9 +225,9 @@ func Test熔断探测失败冷却翻倍(t *testing.T) { // 模型级隔离(错误作用域分流 + 去重守卫) // --------------------------------------------------------------------------- -// Test模型致命一次直达模型级 404/模型不存在:模型级 fatal 直达 OPEN, +// TestModelFatal_SingleHitOpensModelBreaker 404/模型不存在:模型级 fatal 直达 OPEN, // 渠道级仅窗口计 1 票;模型级已 OPEN 后同模型后续失败不再污染渠道级。 -func Test模型致命一次直达模型级(t *testing.T) { +func TestModelFatal_SingleHitOpensModelBreaker(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -248,9 +248,9 @@ func Test模型致命一次直达模型级(t *testing.T) { assert.Equal(t, 1, v.Int(), "模型 OPEN 后渠道级不再计数") } -// Test模型级窗口阈值与渠道级隔离 单模型慢性故障(瞬时类):模型级阈值 4 先打开, +// TestModelBreaker_WindowThresholdIsolatedFromChannel 单模型慢性故障(瞬时类):模型级阈值 4 先打开, // 渠道级贡献封顶在 4 票(< 8),渠道级永不熔断——单模型故障不误伤其它模型。 -func Test模型级窗口阈值与渠道级隔离(t *testing.T) { +func TestModelBreaker_WindowThresholdIsolatedFromChannel(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) // failThreshold=8, modelFailThreshold=4 @@ -274,9 +274,9 @@ func Test模型级窗口阈值与渠道级隔离(t *testing.T) { assert.Equal(t, 4, v.Int(), "单模型对渠道级贡献封顶在 modelFailThreshold") } -// Test跨模型共识渠道级熔断 两个模型各恶化 4 次(4+4=8)→ 渠道级 OPEN: +// TestChannelBreaker_CrossModelConsensus 两个模型各恶化 4 次(4+4=8)→ 渠道级 OPEN: // 渠道级熔断保留给真正的多模型同时恶化(整渠道故障)。 -func Test跨模型共识渠道级熔断(t *testing.T) { +func TestChannelBreaker_CrossModelConsensus(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -290,9 +290,9 @@ func Test跨模型共识渠道级熔断(t *testing.T) { assert.Equal(t, dispatch.BreakerOpen, rt.ModelBreaker, "两模型各自也达阈值 4") } -// Test渠道致命不喂模型级 402/Key 耗尽等渠道级信号:fatal 只打渠道级,模型级 key 不被触碰 +// TestChannelFatal_DoesNotFeedModelBreaker 402/Key 耗尽等渠道级信号:fatal 只打渠道级,模型级 key 不被触碰 // (渠道恢复后模型无残留熔断)。 -func Test渠道致命不喂模型级(t *testing.T) { +func TestChannelFatal_DoesNotFeedModelBreaker(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -305,9 +305,9 @@ func Test渠道致命不喂模型级(t *testing.T) { assert.Equal(t, 0, v.Int(), "渠道级致命不创建/触碰模型级 key") } -// Test模型级探测成功复位 模型级 OPEN 冷却期满 → HALF_OPEN → 成功 → CLOSED + recovered_ms +// TestModelBreaker_ProbeSuccessResets 模型级 OPEN 冷却期满 → HALF_OPEN → 成功 → CLOSED + recovered_ms // (模型级熔断可靠流量探测恢复,不再死锁到 24h TTL)。 -func Test模型级探测成功复位(t *testing.T) { +func TestModelBreaker_ProbeSuccessResets(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -321,9 +321,9 @@ func Test模型级探测成功复位(t *testing.T) { assert.Greater(t, v.Int64(), int64(0), "模型级恢复时间必须记录") } -// Test模型级探测令牌与探测失败翻倍 模型级令牌按渠道×模型 key 独立限流; +// TestModelBreaker_ProbeTokenAndFailureDoublesCooldown 模型级令牌按渠道×模型 key 独立限流; // 冷却期满后的失败走 HALF_OPEN 探测失败分支(模型级回 OPEN 冷却翻倍,渠道级不计数)。 -func Test模型级探测令牌与探测失败翻倍(t *testing.T) { +func TestModelBreaker_ProbeTokenAndFailureDoublesCooldown(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -346,7 +346,7 @@ func Test模型级探测令牌与探测失败翻倍(t *testing.T) { assert.Equal(t, 1, v.Int(), "渠道级不因探测失败新增计数") } -func Test绑定CAS与反向索引(t *testing.T) { +func TestBinding_CASAndReverseIndex(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -389,7 +389,7 @@ func Test绑定CAS与反向索引(t *testing.T) { assert.False(t, ok) } -func Test绑定续期(t *testing.T) { +func TestBinding_Touch(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -402,7 +402,7 @@ func Test绑定续期(t *testing.T) { s.TouchBinding(ctx, "sk:test:none", time.Hour) } -func Test容量租约(t *testing.T) { +func TestLease_Capacity(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -441,7 +441,7 @@ func Test容量租约(t *testing.T) { assert.Equal(t, 20, rt.Inflight) } -func Test租约过期自愈(t *testing.T) { +func TestLease_ExpirySelfHeals(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -459,7 +459,7 @@ func Test租约过期自愈(t *testing.T) { assert.True(t, v.IsNil(), "已释放的租约不得被续租复活") } -func Test凭证冷却(t *testing.T) { +func TestCredentialCooldown(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -472,9 +472,9 @@ func Test凭证冷却(t *testing.T) { assert.False(t, s.IsCredentialCooled(ctx, 101), "冷却到期后必须恢复可用") } -// Test凭证冷却可手动解除 覆盖「更换 Key / 重置健康度」的解除路径:冷却按 keyID 打标, +// TestCredentialCooldown_ManualClear 覆盖「更换 Key / 重置健康度」的解除路径:冷却按 keyID 打标, // 而换 Key 是原地改同一行(keyID 不变),不解除的话新 Key 会被整段跳过直到 TTL 到期。 -func Test凭证冷却可手动解除(t *testing.T) { +func TestCredentialCooldown_ManualClear(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -492,7 +492,7 @@ func Test凭证冷却可手动解除(t *testing.T) { assert.False(t, s.IsCredentialCooled(ctx, 999)) } -func TestReportOutcome异步消费(t *testing.T) { +func TestReportOutcome_AsyncConsumption(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) s.Start(ctx) @@ -506,11 +506,11 @@ func TestReportOutcome异步消费(t *testing.T) { }, 3*time.Second, 20*time.Millisecond, "后台 worker 必须消费上报事件") } -// TestReadRuntime无数据回落默认值 从未被访问过的渠道×模型(Redis key 不存在)必须回落 +// TestReadRuntime_FallsBackToDefaultWhenNoData 从未被访问过的渠道×模型(Redis key 不存在)必须回落 // 默认满分 1.0。HMGET 缺失字段经 gredis 包装成非 nil 空 gvar(IsNil() 恒为 false、 // Float64() 静默返回 0),旧代码用 IsNil() 判空失效,把全部冷模型读成 0 分, // 拖垮渠道聚合健康分与调度 healthFactor。 -func TestReadRuntime无数据回落默认值(t *testing.T) { +func TestReadRuntime_FallsBackToDefaultWhenNoData(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -527,8 +527,8 @@ func TestReadRuntime无数据回落默认值(t *testing.T) { assert.Equal(t, 100.0, rt.LatEwmaMs) } -// TestReadRuntime异常值防御 succ_ewma 为空串/非法值/精确 0 时视为无数据(保持默认 1.0)。 -func TestReadRuntime异常值防御(t *testing.T) { +// TestReadRuntime_GuardsAgainstOutOfRangeValues succ_ewma 为空串/非法值/精确 0 时视为无数据(保持默认 1.0)。 +func TestReadRuntime_GuardsAgainstOutOfRangeValues(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -553,10 +553,10 @@ func TestReadRuntime异常值防御(t *testing.T) { assert.Equal(t, 0.0, rt.LatEwmaMs) } -// TestReadRuntime区分无数据与读失败 「没读到」与「读失败」必须可区分:前者是真实的 +// TestReadRuntime_DistinguishesNoDataFromReadFailure 「没读到」与「读失败」必须可区分:前者是真实的 // 「该模型没有流量」,后者是 Redis 不可用。二者都会回落成默认满分,若调用方无法区分, // Redis 抖动期间维护快照会把全渠道健康分刷成 100,事故现场被销毁。 -func TestReadRuntime区分无数据与读失败(t *testing.T) { +func TestReadRuntime_DistinguishesNoDataFromReadFailure(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -583,9 +583,9 @@ func TestReadRuntime区分无数据与读失败(t *testing.T) { assert.Equal(t, 1.0, rt.SuccEwma) } -// TestReadRuntime脏值不算有效数据 精确 0 / 非法值视为无数据,HasHealth 保持 false, +// TestReadRuntime_DirtyValueIsNotValidData 精确 0 / 非法值视为无数据,HasHealth 保持 false, // 避免脏数据以「实测值」身份参与渠道聚合(2026-08-27 全渠道健康分坍缩事故的读侧防线)。 -func TestReadRuntime脏值不算有效数据(t *testing.T) { +func TestReadRuntime_DirtyValueIsNotValidData(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -609,7 +609,7 @@ func TestLocalFallbackLimit(t *testing.T) { assert.Equal(t, 10, localFallbackLimit(10, 0), "副本数缺省按 1") } -func TestLocal严格容量租约(t *testing.T) { +func TestLocal_StrictCapacityLease(t *testing.T) { l := newLocalState() require.True(t, l.acquireLease(1, "a", 2, 60_000)) require.True(t, l.acquireLease(1, "b", 2, 60_000)) @@ -622,7 +622,7 @@ func TestLocal严格容量租约(t *testing.T) { require.False(t, l.acquireLease(2, "x", 0, 60_000)) } -func TestLocal探测限流(t *testing.T) { +func TestLocal_ProbeThrottle(t *testing.T) { l := newLocalState() assert.True(t, l.tryProbe(1, "", 10_000)) assert.False(t, l.tryProbe(1, "", 10_000), "同窗口本实例只放行一个") @@ -630,7 +630,7 @@ func TestLocal探测限流(t *testing.T) { assert.True(t, l.tryProbe(1, "gpt-4o", 10_000), "同渠道的模型级令牌与渠道级互不影响") } -func TestLocal凭证冷却过期(t *testing.T) { +func TestLocal_CredentialCooldownExpiry(t *testing.T) { l := newLocalState() l.coolCred(9, 50*time.Millisecond) assert.True(t, l.isCredCooled(9)) @@ -638,7 +638,7 @@ func TestLocal凭证冷却过期(t *testing.T) { assert.False(t, l.isCredCooled(9)) } -func TestLocal凭证冷却解除(t *testing.T) { +func TestLocal_CredentialCooldownClear(t *testing.T) { l := newLocalState() l.coolCred(9, time.Hour) require.True(t, l.isCredCooled(9)) @@ -647,7 +647,7 @@ func TestLocal凭证冷却解除(t *testing.T) { l.clearCred(9) // 幂等 } -func Test429估计器水位收敛(t *testing.T) { +func TestRateLimit429Estimator_OnsetConverges(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) @@ -677,7 +677,7 @@ func TestEffectiveSoftLimit(t *testing.T) { assert.Equal(t, 4, effectiveSoftLimit(0, 2), "下限保护 max(4, ...)") } -func Test手动恢复复位熔断并开启爬坡(t *testing.T) { +func TestManualRecovery_ResetsBreakerAndStartsRamp(t *testing.T) { ctx := context.Background() s := newTestState(t, nil) diff --git a/internal/logic/common/cron_test.go b/internal/logic/common/cron_test.go index ae5ff0e..369d212 100644 --- a/internal/logic/common/cron_test.go +++ b/internal/logic/common/cron_test.go @@ -8,10 +8,10 @@ import ( "github.com/stretchr/testify/require" ) -// TestRunHandlerSafely_Panic转为错误 robfig/cron v3 的 cron.New() 不带 Recover 链, +// TestRunHandlerSafely_PanicBecomesError robfig/cron v3 的 cron.New() 不带 Recover 链, // startJob 是裸的 `go j.Run()`——任务 panic 会崩掉整个进程。此处必须兜住并转成 // 普通 error,才能既保住进程、又让失败落进 sys_cron_jobs 供管理后台查看。 -func TestRunHandlerSafely_Panic转为错误(t *testing.T) { +func TestRunHandlerSafely_PanicBecomesError(t *testing.T) { job := &CronJob{ Name: "panicking_job", Handler: func(_ context.Context) error { @@ -27,7 +27,7 @@ func TestRunHandlerSafely_Panic转为错误(t *testing.T) { assert.Contains(t, err.Error(), "boom", "错误信息应保留 panic 内容供排查") } -func TestRunHandlerSafely_正常返回值透传(t *testing.T) { +func TestRunHandlerSafely_PassesThroughReturnValue(t *testing.T) { ctx := context.Background() okJob := &CronJob{Name: "ok", Handler: func(_ context.Context) error { return nil }} @@ -39,9 +39,9 @@ func TestRunHandlerSafely_正常返回值透传(t *testing.T) { assert.ErrorIs(t, runHandlerSafely(ctx, failJob), assert.AnError, "普通错误必须原样返回") } -// TestReschedule_未启动时只更新记录 StartBackground 之前没有 cron 条目, +// TestReschedule_OnlyUpdatesRecordWhenNotStarted StartBackground 之前没有 cron 条目, // 此时重排只改 job.Schedule,供随后的 StartBackground 按新表达式注册。 -func TestReschedule_未启动时只更新记录(t *testing.T) { +func TestReschedule_OnlyUpdatesRecordWhenNotStarted(t *testing.T) { cs := NewCronScheduler() cs.Register("job_a", "任务A", "*/5 * * * *", func(_ context.Context) error { return nil }) @@ -52,14 +52,14 @@ func TestReschedule_未启动时只更新记录(t *testing.T) { assert.Equal(t, "@every 30m", jobs[0].Schedule) } -func TestReschedule_未注册任务报错(t *testing.T) { +func TestReschedule_UnregisteredTaskErrors(t *testing.T) { cs := NewCronScheduler() assert.Error(t, cs.Reschedule(context.Background(), "nonexistent", "@every 5m")) } -// TestReschedule_非法表达式保持原调度 一次错误配置不能把任务从调度器里彻底摘掉, +// TestReschedule_InvalidExprKeepsExistingSchedule 一次错误配置不能把任务从调度器里彻底摘掉, // 因此实现是「先加新条目、成功后再摘旧条目」。 -func TestReschedule_非法表达式保持原调度(t *testing.T) { +func TestReschedule_InvalidExprKeepsExistingSchedule(t *testing.T) { ctx := context.Background() cs := NewCronScheduler() cs.Register("job_b", "任务B", "@every 5m", func(_ context.Context) error { return nil }) @@ -74,9 +74,9 @@ func TestReschedule_非法表达式保持原调度(t *testing.T) { assert.Len(t, cs.cron.Entries(), 1) } -// TestReschedule_有效表达式替换条目 同时验证 @every 语法确实被 cron.New() 的 +// TestReschedule_ValidExprReplacesEntry 同时验证 @every 语法确实被 cron.New() 的 // 默认解析器接受——探测间隔配置正是靠它支持任意分钟数(*/N 在 N>=60 时会静默失真)。 -func TestReschedule_有效表达式替换条目(t *testing.T) { +func TestReschedule_ValidExprReplacesEntry(t *testing.T) { ctx := context.Background() cs := NewCronScheduler() cs.Register("job_c", "任务C", "@every 5m", func(_ context.Context) error { return nil }) @@ -93,9 +93,9 @@ func TestReschedule_有效表达式替换条目(t *testing.T) { assert.NotEqual(t, oldID, cs.entryIDs["job_c"], "条目 ID 应已更新") } -// TestOnSettingsChanged_回调panic不影响其余回调 回调在订阅 goroutine 内串行执行, +// TestOnSettingsChanged_CallbackPanicDoesNotAffectOthers 回调在订阅 goroutine 内串行执行, // 一个回调 panic 不能带崩订阅循环或阻断后续回调。 -func TestOnSettingsChanged_回调panic不影响其余回调(t *testing.T) { +func TestOnSettingsChanged_CallbackPanicDoesNotAffectOthers(t *testing.T) { settingsHookMu.Lock() saved := settingsHooks settingsHooks = nil diff --git a/internal/logic/update/replacer_test.go b/internal/logic/update/replacer_test.go index 720a67d..a0df544 100644 --- a/internal/logic/update/replacer_test.go +++ b/internal/logic/update/replacer_test.go @@ -44,7 +44,7 @@ func createTarball(t *testing.T, files map[string]string) string { return path } -func TestExtractBinary_正常提取(t *testing.T) { +func TestExtractBinary_Success(t *testing.T) { tarball := createTarball(t, map[string]string{ "team-api-1.0.0-linux-amd64/team-api": "new-binary-content", "team-api-1.0.0-linux-amd64/README.md": "docs", @@ -79,7 +79,7 @@ func TestExtractBinary_正常提取(t *testing.T) { } } -func TestExtractBinary_归档中无二进制(t *testing.T) { +func TestExtractBinary_NoBinaryInArchive(t *testing.T) { tarball := createTarball(t, map[string]string{ "team-api-1.0.0-linux-amd64/README.md": "docs", }) @@ -90,7 +90,7 @@ func TestExtractBinary_归档中无二进制(t *testing.T) { } } -func TestReplaceBinary_正常替换(t *testing.T) { +func TestReplaceBinary_Success(t *testing.T) { dir := t.TempDir() current := filepath.Join(dir, "team-api") newBin := filepath.Join(dir, "team-api.new") @@ -125,7 +125,7 @@ func TestReplaceBinary_正常替换(t *testing.T) { } } -func TestReplaceBinary_残留old文件被清理(t *testing.T) { +func TestReplaceBinary_StaleOldFileCleaned(t *testing.T) { dir := t.TempDir() current := filepath.Join(dir, "team-api") newBin := filepath.Join(dir, "team-api.new") @@ -146,7 +146,7 @@ func TestReplaceBinary_残留old文件被清理(t *testing.T) { } } -func TestReplaceBinary_失败时自救恢复(t *testing.T) { +func TestReplaceBinary_RollbackOnFailure(t *testing.T) { dir := t.TempDir() current := filepath.Join(dir, "team-api") @@ -169,7 +169,7 @@ func TestReplaceBinary_失败时自救恢复(t *testing.T) { } } -func TestCopyFile_内容与权限一致(t *testing.T) { +func TestCopyFile_PreservesContentAndPermissions(t *testing.T) { dir := t.TempDir() src := filepath.Join(dir, "src") dst := filepath.Join(dir, "dst") @@ -193,7 +193,7 @@ func TestCopyFile_内容与权限一致(t *testing.T) { } } -func TestCopyFile_源文件不存在(t *testing.T) { +func TestCopyFile_SourceMissing(t *testing.T) { if err := copyFile(filepath.Join(t.TempDir(), "missing"), filepath.Join(t.TempDir(), "dst")); err == nil { t.Fatal("源文件不存在时应返回错误") } diff --git a/internal/logic/update/types_test.go b/internal/logic/update/types_test.go index d4ca3eb..b1deb91 100644 --- a/internal/logic/update/types_test.go +++ b/internal/logic/update/types_test.go @@ -10,7 +10,7 @@ func newTestManager() *UpdateManager { return &UpdateManager{} } -func TestUpdateManager_默认状态(t *testing.T) { +func TestUpdateManager_DefaultState(t *testing.T) { m := newTestManager() status := m.GetStatus() @@ -28,7 +28,7 @@ func TestUpdateManager_默认状态(t *testing.T) { } } -func TestUpdateManager_进度状态机(t *testing.T) { +func TestUpdateManager_ProgressStateMachine(t *testing.T) { m := newTestManager() m.setProgress(PhaseDownloading, "正在下载...", 50) @@ -47,7 +47,7 @@ func TestUpdateManager_进度状态机(t *testing.T) { } } -func TestUpdateManager_失败收尾(t *testing.T) { +func TestUpdateManager_FailureFinalizes(t *testing.T) { m := newTestManager() m.setProgress(PhaseDownloading, "正在下载...", 50) @@ -71,7 +71,7 @@ func TestUpdateManager_失败收尾(t *testing.T) { } } -func TestUpdateManager_更新锁互斥(t *testing.T) { +func TestUpdateManager_UpdateLockMutualExclusion(t *testing.T) { m := newTestManager() if !m.updating.CompareAndSwap(false, true) { @@ -90,7 +90,7 @@ func TestUpdateManager_更新锁互斥(t *testing.T) { } } -func TestGetStatus_终态进度过期自动失效(t *testing.T) { +func TestGetStatus_TerminalProgressExpiresAutomatically(t *testing.T) { m := newTestManager() // 上次升级留下的过期 complete 快照:不应再下发 @@ -119,7 +119,7 @@ func TestGetStatus_终态进度过期自动失效(t *testing.T) { } } -func TestGetStatus_终态进度窗口内保留(t *testing.T) { +func TestGetStatus_TerminalProgressKeptWithinWindow(t *testing.T) { m := newTestManager() // 刚完成的 complete:管理员仍在轮询确认窗口内,应正常返回 @@ -135,7 +135,7 @@ func TestGetStatus_终态进度窗口内保留(t *testing.T) { } } -func TestGetStatus_非终态进度不过期(t *testing.T) { +func TestGetStatus_NonTerminalProgressDoesNotExpire(t *testing.T) { m := newTestManager() // 进行中的进度(无 FinishedAt)不受过期机制影响 diff --git a/internal/logic/update/verifier_test.go b/internal/logic/update/verifier_test.go index 9d1501f..13608de 100644 --- a/internal/logic/update/verifier_test.go +++ b/internal/logic/update/verifier_test.go @@ -33,7 +33,7 @@ func serveChecksums(t *testing.T, status int, body string) *httptest.Server { return srv } -func TestVerifyChecksum_成功(t *testing.T) { +func TestVerifyChecksum_Success(t *testing.T) { content := "fake-binary-content" filePath := writeTempFile(t, "team-api-1.0.0-linux-amd64.tar.gz", content) @@ -47,7 +47,7 @@ func TestVerifyChecksum_成功(t *testing.T) { } } -func TestVerifyChecksum_哈希不一致须报错(t *testing.T) { +func TestVerifyChecksum_HashMismatchErrors(t *testing.T) { filePath := writeTempFile(t, "asset.tar.gz", "actual-content") wrongHash := strings.Repeat("0", 64) @@ -62,7 +62,7 @@ func TestVerifyChecksum_哈希不一致须报错(t *testing.T) { } } -func TestVerifyChecksum_缺少校验文件地址须拒绝(t *testing.T) { +func TestVerifyChecksum_MissingChecksumURLRejected(t *testing.T) { filePath := writeTempFile(t, "asset.tar.gz", "content") err := VerifyChecksum(context.Background(), filePath, "", "asset.tar.gz") @@ -74,7 +74,7 @@ func TestVerifyChecksum_缺少校验文件地址须拒绝(t *testing.T) { } } -func TestVerifyChecksum_校验文件拉取失败须拒绝(t *testing.T) { +func TestVerifyChecksum_ChecksumFetchFailureRejected(t *testing.T) { filePath := writeTempFile(t, "asset.tar.gz", "content") // 服务返回 500,模拟校验文件不可用 @@ -86,7 +86,7 @@ func TestVerifyChecksum_校验文件拉取失败须拒绝(t *testing.T) { } } -func TestFetchExpectedHash_解析格式(t *testing.T) { +func TestFetchExpectedHash_ParsesFormat(t *testing.T) { content := "aaa file-a.tar.gz\nbbb *file-b.tar.gz\n" srv := serveChecksums(t, http.StatusOK, content) ctx := context.Background() diff --git a/relaykit/dispatch/breaker_test.go b/relaykit/dispatch/breaker_test.go index c106348..44df05b 100644 --- a/relaykit/dispatch/breaker_test.go +++ b/relaykit/dispatch/breaker_test.go @@ -8,7 +8,7 @@ import ( func breakerPol() BreakerPolicy { return DefaultRoutingPolicy().Breaker } -func TestEffectiveBreakerState_惰性转移(t *testing.T) { +func TestEffectiveBreakerState_LazyTransition(t *testing.T) { p := breakerPol() s := BreakerSnapshot{State: BreakerOpen, OpenedAtMs: 0, CooldownMs: int64(p.CooldownSeconds) * 1000} @@ -17,7 +17,7 @@ func TestEffectiveBreakerState_惰性转移(t *testing.T) { assert.Equal(t, BreakerClosed, EffectiveBreakerState(BreakerSnapshot{State: BreakerClosed}, 0)) } -func TestBreakerOnFailure_窗口阈值熔断(t *testing.T) { +func TestBreakerOnFailure_WindowThresholdOpens(t *testing.T) { p := breakerPol() // failThreshold=8 s := BreakerSnapshot{State: BreakerClosed, FailWindowCount: 7} @@ -31,14 +31,14 @@ func TestBreakerOnFailure_窗口阈值熔断(t *testing.T) { assert.Equal(t, int64(30_000), s.CooldownMs) } -func TestBreakerOnFailure_致命一次直达(t *testing.T) { +func TestBreakerOnFailure_FatalOpensImmediately(t *testing.T) { p := breakerPol() s := BreakerSnapshot{State: BreakerClosed, FailWindowCount: 0} s = BreakerOnFailure(s, true, 1000, p) assert.Equal(t, BreakerOpen, s.State, "CHANNEL_FATAL 一次直达熔断") } -func TestBreakerOnFailure_探测失败冷却翻倍(t *testing.T) { +func TestBreakerOnFailure_ProbeFailureDoublesCooldown(t *testing.T) { p := breakerPol() // cooldown 30s, max 300s // OPEN 冷却期满(有效态 HALF_OPEN)时失败 = 探测失败 → 回 OPEN,冷却翻倍 @@ -60,7 +60,7 @@ func TestBreakerOnFailure_探测失败冷却翻倍(t *testing.T) { assert.Equal(t, s2, got) } -func TestBreakerOnSuccess_探测成功复位(t *testing.T) { +func TestBreakerOnSuccess_ProbeSuccessResets(t *testing.T) { // HALF_OPEN(有效态)成功 → CLOSED 并复位 s := BreakerSnapshot{State: BreakerOpen, OpenedAtMs: 0, CooldownMs: 30_000, FailWindowCount: 9} s = BreakerOnSuccess(s, 30_000) @@ -79,7 +79,7 @@ func TestBreakerOnSuccess_探测成功复位(t *testing.T) { assert.Zero(t, s.FailWindowCount, "成功清失败计数") } -func TestBreaker_全转移路径(t *testing.T) { +func TestBreaker_AllTransitionPaths(t *testing.T) { p := breakerPol() // CLOSED → OPEN → HALF_OPEN(探测失败) → OPEN → HALF_OPEN(探测成功) → CLOSED diff --git a/relaykit/dispatch/classify_test.go b/relaykit/dispatch/classify_test.go index 6c5e5a8..e57f2db 100644 --- a/relaykit/dispatch/classify_test.go +++ b/relaykit/dispatch/classify_test.go @@ -11,7 +11,7 @@ import ( "github.com/qianfree/team-api/relaykit/types" ) -func TestClassify_状态码全表(t *testing.T) { +func TestClassify_StatusCodeTable(t *testing.T) { tests := []struct { code int want ErrorClass @@ -41,7 +41,7 @@ func TestClassify_状态码全表(t *testing.T) { } } -func TestClassify_NewAPIError错误码(t *testing.T) { +func TestClassify_NewAPIErrorCodes(t *testing.T) { tests := []struct { name string code types.ErrorCode @@ -66,32 +66,32 @@ func TestClassify_NewAPIError错误码(t *testing.T) { } } -func TestClassify_SkipRetry语义(t *testing.T) { +func TestClassify_SkipRetrySemantics(t *testing.T) { // SkipRetry 标记优先于一切:即使状态码本可重试也归客户端类(不重试) e := types.NewErrorWithStatusCode(errors.New("x"), types.ErrorCodeBadResponse, 502, types.ErrOptionWithSkipRetry()) assert.Equal(t, ErrClassClient, Classify(502, e, DeliveryResponseReceived)) } -func TestClassify_NewAPIError状态码兜底(t *testing.T) { +func TestClassify_NewAPIErrorStatusCodeFallback(t *testing.T) { // 错误码不能定类时用其携带的状态码 e := types.NewErrorWithStatusCode(errors.New("x"), types.ErrorCodeBadResponse, 429) assert.Equal(t, ErrClassRateLimit, Classify(0, e, DeliveryResponseReceived)) } -func TestClassify_上下文错误(t *testing.T) { +func TestClassify_ContextErrors(t *testing.T) { assert.Equal(t, ErrClassTimeout, Classify(0, context.DeadlineExceeded, DeliveryMaybeSent)) assert.Equal(t, ErrClassClient, Classify(0, context.Canceled, DeliveryMaybeSent), "客户端取消不重试不计健康") wrapped := fmt.Errorf("do request: %w", context.DeadlineExceeded) assert.Equal(t, ErrClassTimeout, Classify(0, wrapped, DeliveryNotSent)) } -func TestClassify_纯网络错误(t *testing.T) { +func TestClassify_PureNetworkError(t *testing.T) { err := errors.New("connection refused") assert.Equal(t, ErrClassTransient, Classify(0, err, DeliveryNotSent)) assert.Equal(t, ErrClassTransient, Classify(0, err, DeliveryMaybeSent), "MaybeSent 仍归 TRANSIENT,禁止原地由 FSM 硬规则处理") } -func TestClassify_无错误(t *testing.T) { +func TestClassify_NoError(t *testing.T) { assert.Equal(t, ErrClassNone, Classify(0, nil, DeliveryNotSent)) assert.Equal(t, ErrClassNone, Classify(200, nil, DeliveryResponseReceived)) } diff --git a/relaykit/dispatch/coordinator_test.go b/relaykit/dispatch/coordinator_test.go index ca894ca..6e3d381 100644 --- a/relaykit/dispatch/coordinator_test.go +++ b/relaykit/dispatch/coordinator_test.go @@ -113,7 +113,7 @@ func testProfile() RequestProfile { // 场景测试 // --------------------------------------------------------------------------- -func TestCoordinator_首次选择与绑定命中(t *testing.T) { +func TestCoordinator_FirstSelectionAndBindingHit(t *testing.T) { ctx := context.Background() state := newFakeState() co, _ := newTestCoordinator(state, @@ -143,7 +143,7 @@ func TestCoordinator_首次选择与绑定命中(t *testing.T) { assert.Equal(t, d1.Channel.ID, d2.Channel.ID) } -func TestCoordinator_守卫_健康跌破重绑(t *testing.T) { +func TestCoordinator_Guard_RebindsOnHealthDrop(t *testing.T) { ctx := context.Background() state := newFakeState() @@ -160,7 +160,7 @@ func TestCoordinator_守卫_健康跌破重绑(t *testing.T) { assert.Equal(t, 1, state.setBinds, "重绑写入新绑定") } -func TestCoordinator_守卫_饱和渠道重绑(t *testing.T) { +func TestCoordinator_Guard_RebindsOnSaturatedChannel(t *testing.T) { ctx := context.Background() state := newFakeState() @@ -178,9 +178,9 @@ func TestCoordinator_守卫_饱和渠道重绑(t *testing.T) { assert.NotEqual(t, ReasonBind, d.Reason) } -// TestCoordinator_守卫_glm缺陷回归 溢出到 secondary 建立的绑定必须保持稳定: +// TestCoordinator_Guard_GLMDefectRegression 溢出到 secondary 建立的绑定必须保持稳定: // 守卫判据与 tier/cost 解耦,不得因 tierFactor 低而每请求重绑(规避 keepThreshold 缺陷)。 -func TestCoordinator_守卫_glm缺陷回归(t *testing.T) { +func TestCoordinator_Guard_GLMDefectRegression(t *testing.T) { ctx := context.Background() state := newFakeState() co, _ := newTestCoordinator(state, @@ -201,7 +201,7 @@ func TestCoordinator_守卫_glm缺陷回归(t *testing.T) { assert.Zero(t, state.setBinds, "不得发生任何重绑") } -func TestCoordinator_502原地重试与预算耗尽(t *testing.T) { +func TestCoordinator_502InPlaceRetryUntilBudgetExhausted(t *testing.T) { ctx := context.Background() state := newFakeState() co, _ := newTestCoordinator(state, @@ -245,7 +245,7 @@ func TestCoordinator_502原地重试与预算耗尽(t *testing.T) { } } -func TestCoordinator_401凭证轮换链路(t *testing.T) { +func TestCoordinator_401CredentialRotationPath(t *testing.T) { ctx := context.Background() state := newFakeState() @@ -284,7 +284,7 @@ func TestCoordinator_401凭证轮换链路(t *testing.T) { assert.Equal(t, int64(21), d.KeyID) } -func TestCoordinator_单Key渠道401直接failover(t *testing.T) { +func TestCoordinator_SingleKeyChannel401FailsOverDirectly(t *testing.T) { ctx := context.Background() state := newFakeState() @@ -301,7 +301,7 @@ func TestCoordinator_单Key渠道401直接failover(t *testing.T) { assert.True(t, state.cooled[11]) } -func TestCoordinator_全Key冷却渠道跳过(t *testing.T) { +func TestCoordinator_SkipsChannelWithAllKeysCooled(t *testing.T) { ctx := context.Background() state := newFakeState() state.cooled[11] = true @@ -320,10 +320,10 @@ func TestCoordinator_全Key冷却渠道跳过(t *testing.T) { assert.Equal(t, 1, d.Excluded.Request) } -// TestCoordinator_无可用渠道诊断带冷却KeyID 凭证冷却只存在于 Redis 且只靠 TTL 自愈, +// TestCoordinator_NoChannelDiagIncludesCooledKeyIDs 凭证冷却只存在于 Redis 且只靠 TTL 自愈, // 诊断不给出 keyID 的话运维无从判断该等 TTL 还是该去解除(换 Key 不改 keyID, // 陈旧标记会让新 Key 被整段跳过,且期间连上游请求都不会发)。 -func TestCoordinator_无可用渠道诊断带冷却KeyID(t *testing.T) { +func TestCoordinator_NoChannelDiagIncludesCooledKeyIDs(t *testing.T) { ctx := context.Background() state := newFakeState() state.cooled[11] = true @@ -344,7 +344,7 @@ func TestCoordinator_无可用渠道诊断带冷却KeyID(t *testing.T) { assert.Contains(t, diag.Summary(), "凭证全部冷却×2(key=11,21)") } -func TestCoordinator_租约拒绝不扣预算(t *testing.T) { +func TestCoordinator_LeaseDenialDoesNotConsumeBudget(t *testing.T) { ctx := context.Background() state := newFakeState() state.leaseDeny[1] = true @@ -364,7 +364,7 @@ func TestCoordinator_租约拒绝不扣预算(t *testing.T) { assert.Zero(t, s.attempt.FailoverUsed, "租约失败不扣 failover 预算") } -func TestCoordinator_熔断排除与探测放行(t *testing.T) { +func TestCoordinator_BreakerExclusionAndProbeAdmission(t *testing.T) { ctx := context.Background() open := healthyChannel(1, TierPrimary, 100) @@ -393,7 +393,7 @@ func TestCoordinator_熔断排除与探测放行(t *testing.T) { assert.Equal(t, ReasonProbe, d2.Reason) } -func TestCoordinator_模型级熔断排除(t *testing.T) { +func TestCoordinator_ModelBreakerExclusion(t *testing.T) { ctx := context.Background() state := newFakeState() @@ -407,9 +407,9 @@ func TestCoordinator_模型级熔断排除(t *testing.T) { assert.Equal(t, int64(2), d.Channel.ID) } -// TestCoordinator_模型级半开探测 模型级熔断冷却期满(HALF_OPEN)走模型级探测令牌: +// TestCoordinator_ModelHalfOpenProbe 模型级熔断冷却期满(HALF_OPEN)走模型级探测令牌: // 渠道级熔断 CLOSED 时不再被渠道级令牌误拒(修复模型级熔断无法靠流量恢复的死锁)。 -func TestCoordinator_模型级半开探测(t *testing.T) { +func TestCoordinator_ModelHalfOpenProbe(t *testing.T) { ctx := context.Background() modelHalf := healthyChannel(1, TierPrimary, 100) @@ -436,9 +436,9 @@ func TestCoordinator_模型级半开探测(t *testing.T) { assert.Equal(t, 1, d2.Excluded.Breaker, "模型级探测拒绝计入 Breaker 排除") } -// TestCoordinator_双级别半开组合 渠道级与模型级同时 HALF_OPEN:两枚令牌都取, +// TestCoordinator_DualLevelHalfOpenCombination 渠道级与模型级同时 HALF_OPEN:两枚令牌都取, // 任一被拒即排除;模型级令牌先取(被拒不消耗渠道级令牌)。 -func TestCoordinator_双级别半开组合(t *testing.T) { +func TestCoordinator_DualLevelHalfOpenCombination(t *testing.T) { ctx := context.Background() both := healthyChannel(1, TierPrimary, 100) @@ -467,7 +467,7 @@ func TestCoordinator_双级别半开组合(t *testing.T) { assert.Equal(t, ReasonProbe, d2.Reason) } -func TestCoordinator_tier扩组兜底(t *testing.T) { +func TestCoordinator_TierExpansionFallback(t *testing.T) { ctx := context.Background() state := newFakeState() @@ -488,7 +488,7 @@ func TestCoordinator_tier扩组兜底(t *testing.T) { assert.Equal(t, int64(2), d.Channel.ID) } -func TestCoordinator_溢出reason(t *testing.T) { +func TestCoordinator_OverflowReason(t *testing.T) { ctx := context.Background() state := newFakeState() @@ -504,7 +504,7 @@ func TestCoordinator_溢出reason(t *testing.T) { assert.Equal(t, ReasonOverflow, d.Reason, "primary 在场但饱和 → 溢出") } -func TestCoordinator_ReplayUnsafe_MaybeSent必Abort(t *testing.T) { +func TestCoordinator_ReplayUnsafe_MaybeSentMustAbort(t *testing.T) { ctx := context.Background() state := newFakeState() co, _ := newTestCoordinator(state, healthyChannel(1, TierPrimary, 10)) @@ -519,7 +519,7 @@ func TestCoordinator_ReplayUnsafe_MaybeSent必Abort(t *testing.T) { assert.Nil(t, s.Next(ctx)) } -func TestCoordinator_504不原地重试(t *testing.T) { +func TestCoordinator_504NoInPlaceRetry(t *testing.T) { ctx := context.Background() state := newFakeState() co, _ := newTestCoordinator(state, @@ -538,7 +538,7 @@ func TestCoordinator_504不原地重试(t *testing.T) { assert.NotEqual(t, first, d.Channel.ID) } -func TestCoordinator_429RetryAfter原地等待(t *testing.T) { +func TestCoordinator_429RetryAfterWaitsInPlace(t *testing.T) { ctx := context.Background() state := newFakeState() co, _ := newTestCoordinator(state, healthyChannel(1, TierPrimary, 10)) @@ -551,7 +551,7 @@ func TestCoordinator_429RetryAfter原地等待(t *testing.T) { assert.Equal(t, 1500*time.Millisecond, backoff, "遵从 Retry-After(entropy=0 无 jitter)") } -func TestCoordinator_客户端错误Abort且不上报(t *testing.T) { +func TestCoordinator_ClientErrorAbortsWithoutReporting(t *testing.T) { ctx := context.Background() state := newFakeState() co, _ := newTestCoordinator(state, healthyChannel(1, TierPrimary, 10)) @@ -564,7 +564,7 @@ func TestCoordinator_客户端错误Abort且不上报(t *testing.T) { assert.Empty(t, state.outcomes, "客户端错误不计渠道健康") } -func TestCoordinator_总时限耗尽Abort(t *testing.T) { +func TestCoordinator_TotalDeadlineExhaustedAborts(t *testing.T) { ctx := context.Background() state := newFakeState() co, clock := newTestCoordinator(state, @@ -580,7 +580,7 @@ func TestCoordinator_总时限耗尽Abort(t *testing.T) { assert.Equal(t, DecisionAbort, dec) } -func TestCoordinator_全渠道耗尽返回nil(t *testing.T) { +func TestCoordinator_ReturnsNilWhenAllChannelsExhausted(t *testing.T) { ctx := context.Background() state := newFakeState() co, _ := newTestCoordinator(state, @@ -605,7 +605,7 @@ func TestCoordinator_全渠道耗尽返回nil(t *testing.T) { s.Finish(ctx, false, 0) } -func TestCoordinator_jitter叠加(t *testing.T) { +func TestCoordinator_JitterApplied(t *testing.T) { ctx := context.Background() state := newFakeState() clock := &fakeClock{t: time.UnixMilli(0)} @@ -622,7 +622,7 @@ func TestCoordinator_jitter叠加(t *testing.T) { assert.Equal(t, 150*time.Millisecond, backoff, "100ms + 50% jitter") } -func TestCoordinator_RefreshLease透传(t *testing.T) { +func TestCoordinator_RefreshLeasePassThrough(t *testing.T) { ctx := context.Background() state := newFakeState() co, _ := newTestCoordinator(state, healthyChannel(1, TierPrimary, 10)) @@ -634,7 +634,7 @@ func TestCoordinator_RefreshLease透传(t *testing.T) { assert.Equal(t, 1, state.refreshed[d.Channel.ID]) } -func TestCoordinator_Finish幂等且失败不续绑(t *testing.T) { +func TestCoordinator_FinishIdempotentAndNoTouchOnFailure(t *testing.T) { ctx := context.Background() state := newFakeState() co, _ := newTestCoordinator(state, healthyChannel(1, TierPrimary, 10)) @@ -651,7 +651,7 @@ func TestCoordinator_Finish幂等且失败不续绑(t *testing.T) { assert.Nil(t, s.Next(ctx), "Finish 后不再返回决策") } -func TestCoordinator_策略热更新(t *testing.T) { +func TestCoordinator_PolicyHotReload(t *testing.T) { co, _ := newTestCoordinator(newFakeState(), healthyChannel(1, TierPrimary, 10)) p2 := DefaultRoutingPolicy() p2.Version = 2 @@ -661,9 +661,9 @@ func TestCoordinator_策略热更新(t *testing.T) { assert.Equal(t, 2, co.Policy().Version, "nil 策略不生效") } -// TestCoordinator_防抖_headroom抖动 绑定守卫防抖:被绑渠道余量在守卫线上方抖动时, +// TestCoordinator_Debounce_HeadroomJitter 绑定守卫防抖:被绑渠道余量在守卫线上方抖动时, // 绑定必须保持稳定(重绑次数为 0)——纯 HRW 会横跳,守卫是防抖层。 -func TestCoordinator_防抖_headroom抖动(t *testing.T) { +func TestCoordinator_Debounce_HeadroomJitter(t *testing.T) { ctx := context.Background() state := newFakeState() @@ -699,8 +699,8 @@ func TestCoordinator_防抖_headroom抖动(t *testing.T) { assert.Equal(t, rebinds, state.setBinds, "抖动期间重绑次数必须为 0") } -// TestCoordinator_租户级策略覆盖 profile.Policy 非空时本会话使用租户策略而非全局策略。 -func TestCoordinator_租户级策略覆盖(t *testing.T) { +// TestCoordinator_TenantPolicyOverride profile.Policy 非空时本会话使用租户策略而非全局策略。 +func TestCoordinator_TenantPolicyOverride(t *testing.T) { ctx := context.Background() state := newFakeState() co, _ := newTestCoordinator(state, diff --git a/relaykit/dispatch/hrw_test.go b/relaykit/dispatch/hrw_test.go index 73a9d76..045ae64 100644 --- a/relaykit/dispatch/hrw_test.go +++ b/relaykit/dispatch/hrw_test.go @@ -17,7 +17,7 @@ func scoredSet(weights map[int64]float64) []ScoredChannel { return out } -func TestPickHRW_粘性稳定(t *testing.T) { +func TestPickHRW_StickyStability(t *testing.T) { cands := scoredSet(map[int64]float64{1: 5, 2: 3, 3: 2}) first := PickHRW(cands, "session-abc") require.NotNil(t, first) @@ -27,7 +27,7 @@ func TestPickHRW_粘性稳定(t *testing.T) { } } -func TestPickHRW_候选顺序无关(t *testing.T) { +func TestPickHRW_CandidateOrderIndependent(t *testing.T) { a := []ScoredChannel{ {Channel: Channel{ID: 1}, Weight: 5}, {Channel: Channel{ID: 2}, Weight: 3}, @@ -41,7 +41,7 @@ func TestPickHRW_候选顺序无关(t *testing.T) { } } -func TestPickHRW_权重比例分布(t *testing.T) { +func TestPickHRW_WeightProportionalDistribution(t *testing.T) { // 权重 5:3:2 → 期望占比 50%/30%/20%,10 万采样误差 < 2 个百分点 cands := scoredSet(map[int64]float64{1: 5, 2: 3, 3: 2}) const n = 100_000 @@ -57,7 +57,7 @@ func TestPickHRW_权重比例分布(t *testing.T) { } } -func TestPickHRW_权重非正不参与(t *testing.T) { +func TestPickHRW_NonPositiveWeightExcluded(t *testing.T) { cands := scoredSet(map[int64]float64{1: 0, 2: -1, 3: 2}) for i := range 1000 { p := PickHRW(cands, fmt.Sprintf("s-%d", i)) @@ -68,7 +68,7 @@ func TestPickHRW_权重非正不参与(t *testing.T) { assert.Nil(t, PickHRW(nil, "s")) } -func TestPickHRW_单调性(t *testing.T) { +func TestPickHRW_Monotonicity(t *testing.T) { // 增加一个渠道只迁移有限比例会话:不含新渠道时选中 A 的会话, // 加入新渠道后要么仍选 A,要么选新渠道,绝不迁到其它旧渠道。 oldSet := scoredSet(map[int64]float64{1: 5, 2: 3, 3: 2}) @@ -87,7 +87,7 @@ func TestPickHRW_单调性(t *testing.T) { assert.InDelta(t, 3.0/13.0, float64(moved)/n, 0.02) } -func TestUniformHash_值域(t *testing.T) { +func TestUniformHash_Range(t *testing.T) { for i := range 10_000 { u := uniformHash(fmt.Sprintf("k-%d", i), int64(i)) require.Greater(t, u, 0.0) diff --git a/relaykit/dispatch/policy_test.go b/relaykit/dispatch/policy_test.go index aa05d8a..3ecb198 100644 --- a/relaykit/dispatch/policy_test.go +++ b/relaykit/dispatch/policy_test.go @@ -10,7 +10,7 @@ import ( func retryPol() RetryPolicy { return DefaultRoutingPolicy().Retry } -func TestDecide_硬规则优先(t *testing.T) { +func TestDecide_HardRulesTakePrecedence(t *testing.T) { p := retryPol() // 已向客户端写出响应:任何类别都终止 @@ -33,7 +33,7 @@ func TestDecide_硬规则优先(t *testing.T) { assert.Equal(t, DecisionAbort, d) } -func TestDecide_客户端错误不重试(t *testing.T) { +func TestDecide_ClientErrorNoRetry(t *testing.T) { p := retryPol() d, _ := Decide(ErrClassClient, DeliveryResponseReceived, ReplaySafe, 0, AttemptState{}, p) assert.Equal(t, DecisionAbort, d) @@ -41,7 +41,7 @@ func TestDecide_客户端错误不重试(t *testing.T) { assert.Equal(t, DecisionAbort, d) } -func TestDecide_瞬时错误原地重试与退避(t *testing.T) { +func TestDecide_TransientInPlaceRetryWithBackoff(t *testing.T) { p := retryPol() // inPlaceBudget=2, base=100ms, max=1000ms d, backoff := Decide(ErrClassTransient, DeliveryResponseReceived, ReplayCostly, 0, AttemptState{InPlaceUsed: 0}, p) @@ -61,14 +61,14 @@ func TestDecide_瞬时错误原地重试与退避(t *testing.T) { assert.Equal(t, DecisionAbort, d) } -func TestDecide_MaybeSent禁止原地(t *testing.T) { +func TestDecide_MaybeSentForbidsInPlace(t *testing.T) { p := retryPol() // TRANSIENT + MaybeSent:可能已送达,原地重发有重复风险 → 直接 failover d, _ := Decide(ErrClassTransient, DeliveryMaybeSent, ReplayCostly, 0, AttemptState{}, p) assert.Equal(t, DecisionFailover, d) } -func TestDecide_超时不原地重试(t *testing.T) { +func TestDecide_TimeoutNoInPlaceRetry(t *testing.T) { p := retryPol() // TIMEOUT(含 504)不原地重试 d, _ := Decide(ErrClassTimeout, DeliveryResponseReceived, ReplayCostly, 0, AttemptState{}, p) @@ -82,7 +82,7 @@ func TestDecide_超时不原地重试(t *testing.T) { assert.Equal(t, DecisionAbort, d) } -func TestDecide_限流RetryAfter(t *testing.T) { +func TestDecide_RateLimitRetryAfter(t *testing.T) { p := retryPol() // rateLimitWaitMax=2000ms // Retry-After 短 → 原地等待,退避 = Retry-After @@ -99,7 +99,7 @@ func TestDecide_限流RetryAfter(t *testing.T) { assert.Equal(t, DecisionFailover, d) } -func TestDecide_凭证轮换(t *testing.T) { +func TestDecide_CredentialRotation(t *testing.T) { p := retryPol() // credRotateBudget=1 // 有备用 Key 且预算未耗尽 → 轮换 @@ -120,13 +120,13 @@ func TestDecide_凭证轮换(t *testing.T) { assert.Equal(t, DecisionAbort, d) } -func TestDecide_渠道致命零原地(t *testing.T) { +func TestDecide_ChannelFatalZeroInPlace(t *testing.T) { p := retryPol() d, _ := Decide(ErrClassChannelFatal, DeliveryResponseReceived, ReplayCostly, 0, AttemptState{}, p) assert.Equal(t, DecisionFailover, d) } -func TestDecide_模型致命直接failover(t *testing.T) { +func TestDecide_ModelFatalFailsOverDirectly(t *testing.T) { p := retryPol() // 模型致命与渠道致命同语义:零原地重试、立即 failover d, _ := Decide(ErrClassModelFatal, DeliveryResponseReceived, ReplayCostly, 0, AttemptState{}, p) @@ -137,7 +137,7 @@ func TestDecide_模型致命直接failover(t *testing.T) { assert.Equal(t, DecisionAbort, d) } -func TestDecide_ReplayUnsafe预算收紧(t *testing.T) { +func TestDecide_ReplayUnsafeTightensBudget(t *testing.T) { p := retryPol() // unsafe 预算默认 0/0 // 已收到错误响应(非 NotSent)→ Abort @@ -156,9 +156,9 @@ func TestDecide_ReplayUnsafe预算收紧(t *testing.T) { assert.Equal(t, DecisionAbort, d, "非 NotSent 即使有预算也不允许 failover") } -// TestDecide_属性_任意错误序列不突破预算 属性测试:模拟协调器的预算记账, +// TestDecide_Property_AnyErrorSequenceStaysWithinBudget 属性测试:模拟协调器的预算记账, // 用随机错误序列驱动 FSM,断言任何序列都不会突破预算且必然终止。 -func TestDecide_属性_任意错误序列不突破预算(t *testing.T) { +func TestDecide_Property_AnyErrorSequenceStaysWithinBudget(t *testing.T) { p := retryPol() classes := []ErrorClass{ErrClassClient, ErrClassTransient, ErrClassRateLimit, ErrClassCredential, ErrClassChannelFatal, ErrClassModelFatal, ErrClassTimeout} deliveries := []DeliveryState{DeliveryNotSent, DeliveryMaybeSent, DeliveryResponseReceived} @@ -203,7 +203,7 @@ func TestDecide_属性_任意错误序列不突破预算(t *testing.T) { } } -func TestBackoffMs_封顶(t *testing.T) { +func TestBackoffMs_Capped(t *testing.T) { p := retryPol() // base=100, max=1000 assert.Equal(t, int64(100), backoffMs(0, p)) assert.Equal(t, int64(200), backoffMs(1, p)) @@ -213,11 +213,11 @@ func TestBackoffMs_封顶(t *testing.T) { assert.Equal(t, int64(1000), backoffMs(60, p), "大次数不溢出") } -func TestRoutingPolicy_默认值合法(t *testing.T) { +func TestRoutingPolicy_DefaultsAreValid(t *testing.T) { assert.NoError(t, DefaultRoutingPolicy().Validate()) } -func TestRoutingPolicy_校验拒绝非法配置(t *testing.T) { +func TestRoutingPolicy_ValidateRejectsInvalidConfig(t *testing.T) { mutate := func(f func(*RoutingPolicy)) *RoutingPolicy { p := DefaultRoutingPolicy() f(p) diff --git a/relaykit/dispatch/score_test.go b/relaykit/dispatch/score_test.go index 031f772..4270a2d 100644 --- a/relaykit/dispatch/score_test.go +++ b/relaykit/dispatch/score_test.go @@ -16,7 +16,7 @@ func healthyChannel(id int64, tier Tier, weight float64) Channel { } } -func TestEffectiveWeight_健康满分基准(t *testing.T) { +func TestEffectiveWeight_FullHealthBaseline(t *testing.T) { pol := DefaultRoutingPolicy() w, bd := EffectiveWeight(healthyChannel(1, TierPrimary, 10), pol) assert.InDelta(t, 10.0, w, 1e-9) @@ -27,7 +27,7 @@ func TestEffectiveWeight_健康满分基准(t *testing.T) { assert.Equal(t, 1.0, bd.Ramp) } -func TestEffectiveWeight_层级偏置(t *testing.T) { +func TestEffectiveWeight_TierBias(t *testing.T) { pol := DefaultRoutingPolicy() wp, _ := EffectiveWeight(healthyChannel(1, TierPrimary, 10), pol) ws, _ := EffectiveWeight(healthyChannel(2, TierSecondary, 10), pol) @@ -40,7 +40,7 @@ func TestEffectiveWeight_层级偏置(t *testing.T) { assert.Zero(t, wu) } -func TestEffectiveWeight_健康因子(t *testing.T) { +func TestEffectiveWeight_HealthFactor(t *testing.T) { pol := DefaultRoutingPolicy() // alpha=2 c := healthyChannel(1, TierPrimary, 10) @@ -73,7 +73,7 @@ func TestEffectiveWeight_健康因子(t *testing.T) { assert.Equal(t, 1.0, bd.Health) } -func TestEffectiveWeight_负载余量(t *testing.T) { +func TestEffectiveWeight_HeadroomFactor(t *testing.T) { pol := DefaultRoutingPolicy() // gamma=2 c := healthyChannel(1, TierPrimary, 10) @@ -99,7 +99,7 @@ func TestEffectiveWeight_负载余量(t *testing.T) { assert.Equal(t, 1.0, bd.Headroom) } -func TestEffectiveWeight_成本因子(t *testing.T) { +func TestEffectiveWeight_CostFactor(t *testing.T) { pol := DefaultRoutingPolicy() // beta=0.5, clamp [0.5, 2.0] c := healthyChannel(1, TierPrimary, 10) @@ -120,7 +120,7 @@ func TestEffectiveWeight_成本因子(t *testing.T) { assert.Equal(t, 1.0, bd.Cost) } -func TestEffectiveWeight_爬坡因子(t *testing.T) { +func TestEffectiveWeight_RampFactor(t *testing.T) { pol := DefaultRoutingPolicy() // window=120s, floor=0.05 c := healthyChannel(1, TierPrimary, 10) @@ -142,7 +142,7 @@ func TestEffectiveWeight_爬坡因子(t *testing.T) { assert.Equal(t, 1.0, bd.Ramp, "非爬坡期恒为 1") } -func TestEffectiveWeight_基础权重非正(t *testing.T) { +func TestEffectiveWeight_NonPositiveBaseWeight(t *testing.T) { pol := DefaultRoutingPolicy() c := healthyChannel(1, TierPrimary, 0) w, _ := EffectiveWeight(c, pol) diff --git a/relaykit/dispatch/session_test.go b/relaykit/dispatch/session_test.go index 8af3d3a..2b8d1c3 100644 --- a/relaykit/dispatch/session_test.go +++ b/relaykit/dispatch/session_test.go @@ -15,7 +15,7 @@ func defaultSessionPolicy() SessionPolicy { return DefaultRoutingPolicy().Session } -func TestResolveSessionKey_解析链优先级(t *testing.T) { +func TestResolveSessionKey_ResolutionChainPriority(t *testing.T) { pol := defaultSessionPolicy() tests := []struct { @@ -38,7 +38,7 @@ func TestResolveSessionKey_解析链优先级(t *testing.T) { } } -func TestResolveSessionKey_Anthropic会话段提取(t *testing.T) { +func TestResolveSessionKey_AnthropicSessionSegmentExtraction(t *testing.T) { pol := defaultSessionPolicy() // 含 session UUID:提取段后哈希;两个不同 user 前缀但相同 session → 相同键 @@ -56,7 +56,7 @@ func TestResolveSessionKey_Anthropic会话段提取(t *testing.T) { assert.NotEqual(t, a.Key, c.Key) } -func TestResolveSessionKey_策略开关(t *testing.T) { +func TestResolveSessionKey_PolicyToggles(t *testing.T) { pol := defaultSessionPolicy() pol.ParseAnthropicMetadata = false got := ResolveSessionKey(profileWith(SessionSignals{AnthropicUserID: "user_x_session_11111111-2222-3333-4444-555555555555"}), pol) @@ -68,7 +68,7 @@ func TestResolveSessionKey_策略开关(t *testing.T) { assert.Equal(t, SourceIdentity, got.Source, "关闭 openai 解析后应回退身份级") } -func TestResolveSessionKey_非法信号跳过(t *testing.T) { +func TestResolveSessionKey_SkipsInvalidSignal(t *testing.T) { pol := defaultSessionPolicy() tests := []struct { @@ -87,7 +87,7 @@ func TestResolveSessionKey_非法信号跳过(t *testing.T) { } } -func TestResolveSessionKey_命名空间隔离(t *testing.T) { +func TestResolveSessionKey_NamespaceIsolation(t *testing.T) { pol := defaultSessionPolicy() sig := SessionSignals{HeaderSessionID: "same-session"} @@ -101,7 +101,7 @@ func TestResolveSessionKey_命名空间隔离(t *testing.T) { assert.NotEqual(t, ResolveSessionKey(p1, pol).Key, ResolveSessionKey(p3, pol).Key, "不同模型的绑定应互相独立") } -func TestResolveSessionKey_确定性(t *testing.T) { +func TestResolveSessionKey_Deterministic(t *testing.T) { pol := defaultSessionPolicy() p := profileWith(SessionSignals{HeaderSessionID: "stable"}) first := ResolveSessionKey(p, pol)