mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-09 09:34:46 +08:00
fix(codex): 修复额度窗口过期展示并补齐高覆盖测试
- 后端新增绝对重置时间字段计算(codex_5h_reset_at/codex_7d_reset_at) - 前端统一窗口解析逻辑:绝对时间优先,updated_at+seconds 回退,过期自动归零 - 新增后端与前端单元测试,覆盖关键边界与异常场景
This commit is contained in:
206
frontend/src/utils/__tests__/codexUsage.spec.ts
Normal file
206
frontend/src/utils/__tests__/codexUsage.spec.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveCodexUsageWindow } from '@/utils/codexUsage'
|
||||
|
||||
describe('resolveCodexUsageWindow', () => {
|
||||
it('快照为空时返回空窗口', () => {
|
||||
const result = resolveCodexUsageWindow(null, '5h', new Date('2026-02-20T08:00:00Z'))
|
||||
expect(result).toEqual({ usedPercent: null, resetAt: null })
|
||||
})
|
||||
|
||||
it('优先使用后端提供的绝对重置时间', () => {
|
||||
const now = new Date('2026-02-20T08:00:00Z')
|
||||
const result = resolveCodexUsageWindow(
|
||||
{
|
||||
codex_5h_used_percent: 55,
|
||||
codex_5h_reset_at: '2026-02-20T10:00:00Z',
|
||||
codex_5h_reset_after_seconds: 1
|
||||
},
|
||||
'5h',
|
||||
now
|
||||
)
|
||||
|
||||
expect(result.usedPercent).toBe(55)
|
||||
expect(result.resetAt).toBe('2026-02-20T10:00:00.000Z')
|
||||
})
|
||||
|
||||
it('窗口已过期时自动归零', () => {
|
||||
const now = new Date('2026-02-20T08:00:00Z')
|
||||
const result = resolveCodexUsageWindow(
|
||||
{
|
||||
codex_7d_used_percent: 100,
|
||||
codex_7d_reset_at: '2026-02-20T07:00:00Z'
|
||||
},
|
||||
'7d',
|
||||
now
|
||||
)
|
||||
|
||||
expect(result.usedPercent).toBe(0)
|
||||
expect(result.resetAt).toBe('2026-02-20T07:00:00.000Z')
|
||||
})
|
||||
|
||||
it('无绝对时间时使用 updated_at + seconds 回退计算', () => {
|
||||
const now = new Date('2026-02-20T07:00:00Z')
|
||||
const result = resolveCodexUsageWindow(
|
||||
{
|
||||
codex_5h_used_percent: 20,
|
||||
codex_5h_reset_after_seconds: 3600,
|
||||
codex_usage_updated_at: '2026-02-20T06:30:00Z'
|
||||
},
|
||||
'5h',
|
||||
now
|
||||
)
|
||||
|
||||
expect(result.usedPercent).toBe(20)
|
||||
expect(result.resetAt).toBe('2026-02-20T07:30:00.000Z')
|
||||
})
|
||||
|
||||
it('支持 legacy primary/secondary 字段映射', () => {
|
||||
const now = new Date('2026-02-20T07:05:00Z')
|
||||
const result5h = resolveCodexUsageWindow(
|
||||
{
|
||||
codex_primary_window_minutes: 10080,
|
||||
codex_primary_used_percent: 70,
|
||||
codex_primary_reset_after_seconds: 86400,
|
||||
codex_secondary_window_minutes: 300,
|
||||
codex_secondary_used_percent: 15,
|
||||
codex_secondary_reset_after_seconds: 1200,
|
||||
codex_usage_updated_at: '2026-02-20T07:00:00Z'
|
||||
},
|
||||
'5h',
|
||||
now
|
||||
)
|
||||
const result7d = resolveCodexUsageWindow(
|
||||
{
|
||||
codex_primary_window_minutes: 10080,
|
||||
codex_primary_used_percent: 70,
|
||||
codex_primary_reset_after_seconds: 86400,
|
||||
codex_secondary_window_minutes: 300,
|
||||
codex_secondary_used_percent: 15,
|
||||
codex_secondary_reset_after_seconds: 1200,
|
||||
codex_usage_updated_at: '2026-02-20T07:00:00Z'
|
||||
},
|
||||
'7d',
|
||||
now
|
||||
)
|
||||
|
||||
expect(result5h.usedPercent).toBe(15)
|
||||
expect(result5h.resetAt).toBe('2026-02-20T07:20:00.000Z')
|
||||
expect(result7d.usedPercent).toBe(70)
|
||||
expect(result7d.resetAt).toBe('2026-02-21T07:00:00.000Z')
|
||||
})
|
||||
|
||||
it('legacy 5h 在 primary<=360 时优先 primary 并支持字符串数字', () => {
|
||||
const result = resolveCodexUsageWindow(
|
||||
{
|
||||
codex_primary_window_minutes: '300',
|
||||
codex_primary_used_percent: '21',
|
||||
codex_primary_reset_after_seconds: '1800',
|
||||
codex_secondary_window_minutes: '10080',
|
||||
codex_secondary_used_percent: '99',
|
||||
codex_secondary_reset_after_seconds: '99999',
|
||||
codex_usage_updated_at: '2026-02-20T08:00:00Z'
|
||||
},
|
||||
'5h',
|
||||
new Date('2026-02-20T08:10:00Z')
|
||||
)
|
||||
|
||||
expect(result.usedPercent).toBe(21)
|
||||
expect(result.resetAt).toBe('2026-02-20T08:30:00.000Z')
|
||||
})
|
||||
|
||||
it('legacy 5h 在无窗口信息时回退 secondary', () => {
|
||||
const result = resolveCodexUsageWindow(
|
||||
{
|
||||
codex_secondary_used_percent: 19,
|
||||
codex_secondary_reset_after_seconds: 120,
|
||||
codex_usage_updated_at: '2026-02-20T08:00:00Z'
|
||||
},
|
||||
'5h',
|
||||
new Date('2026-02-20T08:00:01Z')
|
||||
)
|
||||
|
||||
expect(result.usedPercent).toBe(19)
|
||||
expect(result.resetAt).toBe('2026-02-20T08:02:00.000Z')
|
||||
})
|
||||
|
||||
it('legacy 场景下 secondary 为 7d 时能正确识别', () => {
|
||||
const now = new Date('2026-02-20T07:30:00Z')
|
||||
const result = resolveCodexUsageWindow(
|
||||
{
|
||||
codex_primary_window_minutes: 300,
|
||||
codex_primary_used_percent: 5,
|
||||
codex_primary_reset_after_seconds: 600,
|
||||
codex_secondary_window_minutes: 10080,
|
||||
codex_secondary_used_percent: 66,
|
||||
codex_secondary_reset_after_seconds: 7200,
|
||||
codex_usage_updated_at: '2026-02-20T07:00:00Z'
|
||||
},
|
||||
'7d',
|
||||
now
|
||||
)
|
||||
|
||||
expect(result.usedPercent).toBe(66)
|
||||
expect(result.resetAt).toBe('2026-02-20T09:00:00.000Z')
|
||||
})
|
||||
|
||||
it('绝对时间非法时回退到 updated_at + seconds', () => {
|
||||
const now = new Date('2026-02-20T07:40:00Z')
|
||||
const result = resolveCodexUsageWindow(
|
||||
{
|
||||
codex_5h_used_percent: 33,
|
||||
codex_5h_reset_at: 'not-a-date',
|
||||
codex_5h_reset_after_seconds: 900,
|
||||
codex_usage_updated_at: '2026-02-20T07:30:00Z'
|
||||
},
|
||||
'5h',
|
||||
now
|
||||
)
|
||||
|
||||
expect(result.usedPercent).toBe(33)
|
||||
expect(result.resetAt).toBe('2026-02-20T07:45:00.000Z')
|
||||
})
|
||||
|
||||
it('updated_at 非法且无绝对时间时 resetAt 返回 null', () => {
|
||||
const result = resolveCodexUsageWindow(
|
||||
{
|
||||
codex_5h_used_percent: 10,
|
||||
codex_5h_reset_after_seconds: 123,
|
||||
codex_usage_updated_at: 'invalid-time'
|
||||
},
|
||||
'5h',
|
||||
new Date('2026-02-20T08:00:00Z')
|
||||
)
|
||||
|
||||
expect(result.usedPercent).toBe(10)
|
||||
expect(result.resetAt).toBeNull()
|
||||
})
|
||||
|
||||
it('reset_after_seconds 为负数时按 0 秒处理', () => {
|
||||
const result = resolveCodexUsageWindow(
|
||||
{
|
||||
codex_5h_used_percent: 80,
|
||||
codex_5h_reset_after_seconds: -30,
|
||||
codex_usage_updated_at: '2026-02-20T08:00:00Z'
|
||||
},
|
||||
'5h',
|
||||
new Date('2026-02-20T07:59:00Z')
|
||||
)
|
||||
|
||||
expect(result.usedPercent).toBe(80)
|
||||
expect(result.resetAt).toBe('2026-02-20T08:00:00.000Z')
|
||||
})
|
||||
|
||||
it('百分比缺失时仍可计算 resetAt 供倒计时展示', () => {
|
||||
const result = resolveCodexUsageWindow(
|
||||
{
|
||||
codex_7d_reset_after_seconds: 60,
|
||||
codex_usage_updated_at: '2026-02-20T08:00:00Z'
|
||||
},
|
||||
'7d',
|
||||
new Date('2026-02-20T08:00:01Z')
|
||||
)
|
||||
|
||||
expect(result.usedPercent).toBeNull()
|
||||
expect(result.resetAt).toBe('2026-02-20T08:01:00.000Z')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user