gcli2api / internal /server /handler_credit_floor_test.go
a3216's picture
chore: 同步到上游 1.12.0-panel + 凭证同步/独立启动器/保活
6d60378 verified
Raw History Blame Contribute Delete
5.62 kB
// handler_credit_floor_test.go 积分保底的可观测性与端到端拦截:
// /status 透出 credit_floor;触底账号打收费模型时网关无号可选(503)。
package server
import (
"encoding/json"
"net/http/httptest"
"strings"
"testing"
"github.com/linguo2625469/workbuddy2api-panel/internal/auth"
"github.com/linguo2625469/workbuddy2api-panel/internal/upstream"
)
// TestStatusCreditFloor /status 透出 pool 层的 credit_floor 生效值:
// 运维查账时一并看到保底线(结合 accounts[].credits 与 model_costs 即可判定
// 某号为何对某模型不出票)。关闭(0)时也显式透出——缺失会让人误以为没记录。
func TestStatusCreditFloor(t *testing.T) {
p := testPoolWith(&auth.Auth{UID: "u1", AccessToken: "at", ExpiresAt: 9999999999})
p.SetCreditFloor(100)
h := NewHandler(Config{Pool: p, Upstream: upstream.New()})
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/status", nil))
if rec.Code != 200 {
t.Fatalf("code=%d body=%s", rec.Code, rec.Body)
}
var body map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("status not json: %v", err)
}
if body["credit_floor"] != float64(100) {
t.Errorf("credit_floor=%v want 100", body["credit_floor"])
}
}
// TestStatusCreditFloorZeroOff 未配置(默认 0)同样透出 0(显式写出,零值不省略)。
func TestStatusCreditFloorZeroOff(t *testing.T) {
p := testPoolWith(&auth.Auth{UID: "u1", AccessToken: "at", ExpiresAt: 9999999999})
h := NewHandler(Config{Pool: p, Upstream: upstream.New()})
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/status", nil))
var body map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("status not json: %v", err)
}
if body["credit_floor"] != float64(0) {
t.Errorf("credit_floor=%v want 0 (off)", body["credit_floor"])
}
}
// TestCreditFloorBlocksPaidRequest 端到端:触底 + 实测收费模型 → 选号无候选,
// 网关回 503(硬语义:宁 503 不打穿;免费模型照常可用)。
//
// 诚实性约束:本用例的 upstream 是打不通的假上游,floor 开与关最终都回 503——
// 若只断言 503,就无法区分「floor 拦的」与「上游不可达」,断言会被虚假原因满足。
// 真正证明拦截落在选号层的是**对照断言**:对照组(不开 floor)必须能选到 poor,
// 实验组(开 floor)必须在同一条件下选不到号。
func TestCreditFloorBlocksPaidRequest(t *testing.T) {
const body = `{"model":"paid-model","messages":[{"role":"user","content":"hi"}]}`
// 对照组:不设 floor,其余逐字相同(同一触底号、同一 tier 2 观测)。
ctlPool := testPoolWith(&auth.Auth{UID: "poor", AccessToken: "at", ExpiresAt: 9999999999})
ctlPool.SetCredits("poor", 30, 0)
ctlPool.NoteModelCost("poor", "paid-model", 2.9, 1000)
if a := ctlPool.PickExcludingForRealm(nil, "paid-model", ""); a == nil {
t.Fatal("对照组前置失效:未设 floor 时应选到 poor(否则本用例不构成对照)")
}
// 实验组:开 floor 100。
p := testPoolWith(&auth.Auth{UID: "poor", AccessToken: "at", ExpiresAt: 9999999999})
p.SetCreditFloor(100)
p.SetCredits("poor", 30, 0)
p.NoteModelCost("poor", "paid-model", 2.9, 1000)
if a := p.PickExcludingForRealm(nil, "paid-model", ""); a != nil {
t.Fatalf("floor 应在 pool 层拦住触底号,got %v(拦截必须发生在选号层,而非靠上游报错)", a.UID)
}
h := NewHandler(Config{Pool: p, Upstream: upstream.New()})
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("POST", "/v1/chat/completions", strings.NewReader(body)))
if rec.Code != 503 {
t.Fatalf("code=%d want 503(全池触底 + 收费模型 → 不放行), body=%s", rec.Code, rec.Body)
}
if !strings.Contains(rec.Body.String(), "no_healthy_account") {
t.Errorf("body=%s want no_healthy_account", rec.Body)
}
}
// TestCreditFloorAllowsFreeRequest 端到端:同一触底号打免费模型照常透出(非 503)。
// 这是保底的立身之本——保完了就得还能用。
func TestCreditFloorAllowsFreeRequest(t *testing.T) {
p := testPoolWith(&auth.Auth{UID: "poor", AccessToken: "at", ExpiresAt: 9999999999})
p.SetCreditFloor(100)
p.SetCredits("poor", 30, 0)
p.NoteModelCost("poor", "free-model", 0, 1000)
// 选号层面必须出票(上游转发失败与否不在本用例关心范围,见 TestCreditFloor* 单测)。
if a := p.PickExcludingForRealm(nil, "free-model", ""); a == nil || a.UID != "poor" {
t.Fatalf("触底号打免费模型应选到号,got %v", a)
}
// pool 层与 handler 层口径一致(避免 handler 绕过 pool 的 floor 判定)。
if p.PickByUIDForModel("poor", "free-model") == nil {
t.Error("粘性路径对免费模型不应被 floor 拦")
}
}
// TestCreditFloorPersistsAcquireRelease 保底拦截不影响在途租约语义:
// 触底 + 收费 → Acquire 仍可占名额(floor 只在选号层拦),释放幂等。
func TestCreditFloorPersistsAcquireRelease(t *testing.T) {
p := testPoolWith(&auth.Auth{UID: "poor", AccessToken: "at", ExpiresAt: 9999999999})
p.SetCreditFloor(100)
p.SetCredits("poor", 30, 0)
p.NoteModelCost("poor", "paid-model", 2.9, 1000)
p.SetMaxInFlight(2)
if !p.Acquire("poor") {
t.Fatal("Acquire 应成功(floor 不介入租约语义)")
}
p.Release("poor")
p.Release("poor") // 幂等释放不扣成负数
}