* fix(ui): 接受订阅额度窗口的 source_id 字段 修复 #574 新增 source_id 后前端严格投影拒绝成功响应的问题。补齐可选 DTO 字段、白名单及字符串投影,保持其他未知字段校验,不修改后端额度匹配或限流行为。 新增前后端额度字段契约回归测试,避免后端新增字段却遗漏前端白名单和 DTO。 * chore(ui): 移除额度字段修复中的跨语言契约测试 * fix(subscription): 按 Active-Limit 判定被动额度通用组的归属 上游的 X-Codex-Primary/Secondary-* 报告的是本次请求实际计费到的额度, 而不是固定的普通账号额度。请求 Spark 等专属额度时这一组装的是该额度的 副本,按 codex 收下会用它覆盖同周期的普通窗口,把用尽的 7d 刷成满额。 改为由 X-Codex-Active-Limit 决定该组归属:premium/codex 归普通额度, 指向专属额度时改绑到该来源,上游已单独报告该来源则让位避免歧义。 缺少该头且响应里已有别的来源时按副本丢弃。 * fix(subscription): 按窗口去重被动额度副本而非丢弃整组 Active-Limit 指向专属额度时,通用组只有与独立命名空间来源和周期都相同 的窗口才是重复报告。原先只要该来源存在任意窗口就丢掉整组,会漏掉另一 个周期的刷新。改为解析后按来源加周期逐窗口去重。 * fix(subscription): 限定被动额度去重只比较独立命名空间 通用组内部的 primary 与 secondary 是两份数据而非互为副本,周期相同时 原实现让它们互相判定为重复并双双丢弃。去重范围收窄为通用窗口与独立 命名空间窗口之间。
68 lines
2.8 KiB
Go
68 lines
2.8 KiB
Go
package subscription
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"gpt-load/internal/storage/models"
|
|
"gpt-load/internal/subscription/providers/codex"
|
|
providerobservation "gpt-load/internal/subscription/providers/observation"
|
|
)
|
|
|
|
func TestFlushPassiveQuotaSourcesPreservesCardWindowsAndHealth(t *testing.T) {
|
|
manager, db, registry, _, row := newCredentialManagerFixture(t,
|
|
credentialJSON("access", "refresh", time.Now().Add(time.Hour)))
|
|
raw, err := codex.NormalizeQuota([]byte(codexSourceQuotaPayload), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
initial := newFlushableCredentialObservation(t, manager, row.ID, models.CredentialObservationFresh, string(raw))
|
|
ref, ok := registry.CredentialRef(row.ID)
|
|
if !ok {
|
|
t.Fatal("credential ref is unavailable")
|
|
}
|
|
observedAt := time.UnixMilli(2000)
|
|
patches := codex.NormalizePassiveQuotaWindows(map[string]string{
|
|
"X-Codex-Active-Limit": "premium",
|
|
"X-Codex-Primary-Used-Percent": "0",
|
|
"X-Codex-Primary-Window-Minutes": "300",
|
|
"X-Codex-Secondary-Used-Percent": "100",
|
|
"X-Codex-Secondary-Window-Minutes": "10080",
|
|
"X-Codex-Secondary-Reset-At": "1800000000",
|
|
"X-Codex-Bengalfox-Primary-Used-Percent": "30",
|
|
"X-Codex-Bengalfox-Primary-Window-Minutes": "300",
|
|
}, observedAt)
|
|
manager.RecordPassiveQuotaObservation(row.ID, ref.IdentityGeneration, observedAt.UnixMilli(), patches)
|
|
pending, err := manager.FlushPassiveQuotaObservations(t.Context())
|
|
if err != nil || pending || len(manager.DirtyPassiveQuotaObservations(1)) != 0 {
|
|
t.Fatalf("flush pending=%v error=%v", pending, err)
|
|
}
|
|
var stored models.CredentialObservation
|
|
if err := db.Take(&stored, "credential_id = ?", row.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored.ObservedAtMS == nil || *stored.ObservedAtMS != 2000 ||
|
|
stored.ObservationVersion != initial.ObservationVersion || stored.State != initial.State {
|
|
t.Fatalf("unexpected observation metadata: %#v", stored)
|
|
}
|
|
var snapshot providerobservation.Snapshot
|
|
if err := json.Unmarshal(stored.SnapshotJSON, &snapshot); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(snapshot.QuotaWindows) != 2 {
|
|
t.Fatalf("passive update changed the card's window set: %s", stored.SnapshotJSON)
|
|
}
|
|
weekly, spark := snapshot.QuotaWindows[0], snapshot.QuotaWindows[1]
|
|
if weekly.ID != "primary" || weekly.SourceID != "codex" ||
|
|
weekly.WindowSeconds == nil || *weekly.WindowSeconds != 604800 ||
|
|
weekly.Used == nil || *weekly.Used != 100 || weekly.State != "exhausted" ||
|
|
spark.SourceID != "codex_bengalfox" || spark.Used == nil || *spark.Used != 30 {
|
|
t.Fatalf("quota sources were not refreshed independently: %s", stored.SnapshotJSON)
|
|
}
|
|
views := registry.Snapshot()
|
|
if len(views) != 1 || views[0].ObservedQuotaRemaining() == nil || *views[0].ObservedQuotaRemaining() != 0 {
|
|
t.Fatalf("Spark or the untracked 5h window polluted account health: %#v", views)
|
|
}
|
|
}
|