File size: 6,551 Bytes
6d60378 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | package pool
import (
"testing"
"time"
"github.com/linguo2625469/workbuddy2api-panel/internal/auth"
)
// ---------------------------------------------------------------------------
// ModelBlocked:区分「号都在但都对这个模型关闭」与「池子真没号」(issue #102a)
// ---------------------------------------------------------------------------
// TestModelBlockedAllAccountsCooled 全部账号都对该模型冷却 → Blocked,
// 且带出账号数、最早解封时间与上游原因。
func TestModelBlockedAllAccountsCooled(t *testing.T) {
p := New("")
p.Add(&auth.Auth{UID: "u1"})
p.Add(&auth.Auth{UID: "u2"})
base := time.Now()
// 两个号对同一模型都有冷却,解封时间不同 → 应取最早的。
p.mu.Lock()
p.byUID["u1"].modelCooldowns = map[string]modelCooldown{
"glm-5.3": {Until: base.Add(2 * time.Hour), Reason: "11102 model [glm-5.3] service info not found"},
}
p.byUID["u2"].modelCooldowns = map[string]modelCooldown{
"glm-5.3": {Until: base.Add(1 * time.Hour), Reason: "11102 model [glm-5.3] service info not found"},
}
p.mu.Unlock()
st := p.ModelBlocked("glm-5.3")
if !st.Blocked {
t.Fatalf("全部账号冷却时 Blocked 应为 true,得到 %+v", st)
}
if st.Count != 2 {
t.Errorf("Count=%d want 2", st.Count)
}
if st.Reason == "" {
t.Errorf("Reason 应带出上游原因,得到空")
}
// 最早解封应接近 1h 后(u2),不是 2h(u1)。
if d := time.Until(st.Until); d > 70*time.Minute || d < 50*time.Minute {
t.Errorf("Until=%v 应取最早解封(约 1h 后),得到 %v", st.Until, d)
}
}
// TestModelBlockedOneAccountStillServes 只要还有账号能服务该模型,就不算模型级阻塞
// ——否则会把「在途占满/积分保底」导致的选号失败误报成模型问题。
func TestModelBlockedOneAccountStillServes(t *testing.T) {
p := New("")
p.Add(&auth.Auth{UID: "u1"})
p.Add(&auth.Auth{UID: "u2"})
p.mu.Lock()
p.byUID["u1"].modelCooldowns = map[string]modelCooldown{
"glm-5.3": {Until: time.Now().Add(time.Hour), Reason: "11102 ..."},
}
p.mu.Unlock()
if st := p.ModelBlocked("glm-5.3"); st.Blocked {
t.Fatalf("u2 仍可服务该模型,不应判定为模型级阻塞:%+v", st)
}
// 但 u2 对该模型无冷却这一点必须是真的(防止测试自身构造错误)。
p.mu.RLock()
cooled := p.byUID["u2"].modelCooled(time.Now(), "glm-5.3")
p.mu.RUnlock()
if cooled {
t.Fatalf("测试构造有误:u2 不该对该模型冷却")
}
}
// TestModelBlockedDisabledDoesNotCount 禁用号不参与判定:它不可用与模型无关,
// 既不构成"还能服务"的证明,也不该被算进被挡账号数。
func TestModelBlockedDisabledDoesNotCount(t *testing.T) {
p := New("")
p.Add(&auth.Auth{UID: "u1"})
p.Add(&auth.Auth{UID: "u2"})
p.mu.Lock()
p.byUID["u1"].modelCooldowns = map[string]modelCooldown{
"glm-5.3": {Until: time.Now().Add(time.Hour), Reason: "11102 ..."},
}
p.byUID["u2"].disabled = true // 唯一还能服务的号被禁用了
p.mu.Unlock()
st := p.ModelBlocked("glm-5.3")
if !st.Blocked {
t.Fatalf("除禁用号外全部冷却 → 应判定为模型级阻塞,得到 %+v", st)
}
if st.Count != 1 {
t.Errorf("Count=%d want 1(禁用号不计入)", st.Count)
}
}
// TestModelBlockedAuditOnlyNotBlocking AuditOnly 条目只审计不拦截,不能算阻塞
// ——口径必须与选号的 modelCooled 一致,否则会把"其实能调"的模型报成不可用。
func TestModelBlockedAuditOnlyNotBlocking(t *testing.T) {
p := New("")
p.Add(&auth.Auth{UID: "u1"})
p.mu.Lock()
p.byUID["u1"].modelCooldowns = map[string]modelCooldown{
"glm-5.3": {Until: time.Now().Add(time.Hour), Reason: "audit", AuditOnly: true},
}
p.mu.Unlock()
if st := p.ModelBlocked("glm-5.3"); st.Blocked {
t.Fatalf("AuditOnly 条目不得判定为阻塞:%+v", st)
}
}
// TestModelBlockedExpiredNotBlocking 已过期的冷却不算阻塞。
func TestModelBlockedExpiredNotBlocking(t *testing.T) {
p := New("")
p.Add(&auth.Auth{UID: "u1"})
p.mu.Lock()
p.byUID["u1"].modelCooldowns = map[string]modelCooldown{
"glm-5.3": {Until: time.Now().Add(-time.Minute), Reason: "11102 ..."},
}
p.mu.Unlock()
if st := p.ModelBlocked("glm-5.3"); st.Blocked {
t.Fatalf("已过期冷却不得判定为阻塞:%+v", st)
}
}
// TestModelBlockedEmptyInputs 空模型名与空池都返回"未阻塞":前者无从判断,
// 后者属于"真的没号",绝不能冒充模型问题。
func TestModelBlockedEmptyInputs(t *testing.T) {
p := New("")
// 空池
if st := p.ModelBlocked("glm-5.3"); st.Blocked {
t.Fatalf("空池不应判定为模型阻塞:%+v", st)
}
// 空模型名
p.Add(&auth.Auth{UID: "u1"})
if st := p.ModelBlocked(""); st.Blocked {
t.Fatalf("空模型名不应判定为模型阻塞:%+v", st)
}
}
// TestModelBlockedThenClear 回归:BlockModelBackoff 写入后应判定为阻塞,
// BlockModelClear 清除后应恢复——覆盖真实的写入/清除通道,而不是只手搓 map。
func TestModelBlockedThenClear(t *testing.T) {
p := New("")
p.Add(&auth.Auth{UID: "u1"})
p.BlockModelBackoff("u1", "glm-5.3", "11102 model [glm-5.3] service info not found")
st := p.ModelBlocked("glm-5.3")
if !st.Blocked || st.Count != 1 {
t.Fatalf("写入负缓存后应阻塞:%+v", st)
}
if st.Reason == "" {
t.Errorf("应带出 reason")
}
p.BlockModelClear("u1", "glm-5.3")
if st := p.ModelBlocked("glm-5.3"); st.Blocked {
t.Fatalf("清除后不应再阻塞:%+v", st)
}
}
// TestModelBlockedPausedDoesNotMaskBlock 暂停号不构成「能服务」的证明:
// 除暂停号外全部模型冷却时仍应判定阻塞(合并 #113 时补的口径——
// 否则一个暂停的健康号会掩盖「其余号全被模型级冷却挡住」)。
func TestModelBlockedPausedDoesNotMaskBlock(t *testing.T) {
p := New("")
p.Add(&auth.Auth{UID: "u1"})
p.Add(&auth.Auth{UID: "u2"})
p.mu.Lock()
p.byUID["u1"].modelCooldowns = map[string]modelCooldown{
"glm-5.3": {Until: time.Now().Add(time.Hour), Reason: "11102 ..."},
}
p.byUID["u2"].paused = true // 唯一「健康」的号在暂停让位
p.mu.Unlock()
st := p.ModelBlocked("glm-5.3")
if !st.Blocked {
t.Fatalf("暂停号不得掩盖模型级阻塞:%+v", st)
}
if st.Count != 1 {
t.Errorf("Count=%d want 1(暂停号不计入)", st.Count)
}
}
|