From 869952d113d88df8ae59c4e91980405cac0bfe67 Mon Sep 17 00:00:00 2001 From: haruka <1628615876@qq.com> Date: Tue, 17 Mar 2026 10:19:20 +0800 Subject: [PATCH] fix(review): address Copilot PR feedback - Add compile-time interface assertion for sessionWindowMockRepo - Fix flaky fallback test by capturing time.Now() before calling UpdateSessionWindow - Replace stale hardcoded timestamps with dynamic future values - Add millisecond detection and bounds validation for reset header timestamp - Use pause/resume pattern for interval in UsageProgressBar to avoid idle timers on large lists - Fix gofmt comment alignment Co-Authored-By: Claude Sonnet 4.6 --- backend/internal/service/ratelimit_service.go | 16 +++++++++-- .../service/ratelimit_session_window_test.go | 16 ++++++----- .../components/account/UsageProgressBar.vue | 27 +++++++++++++++---- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/backend/internal/service/ratelimit_service.go b/backend/internal/service/ratelimit_service.go index 1f8adf59..ef8a65c9 100644 --- a/backend/internal/service/ratelimit_service.go +++ b/backend/internal/service/ratelimit_service.go @@ -1054,14 +1054,26 @@ func (s *RateLimitService) UpdateSessionWindow(ctx context.Context, account *Acc // 优先使用响应头中的真实重置时间(比预测更准确) if resetStr := headers.Get("anthropic-ratelimit-unified-5h-reset"); resetStr != "" { if ts, err := strconv.ParseInt(resetStr, 10, 64); err == nil { + // 检测可能的毫秒时间戳(秒级约为 1e9,毫秒约为 1e12) + if ts > 1e11 { + slog.Warn("account_session_window_header_millis_detected", "account_id", account.ID, "raw_reset", resetStr) + ts = ts / 1000 + } end := time.Unix(ts, 0) - // 窗口需要初始化,或者真实重置时间与已存储的不同,则更新 - if needInitWindow || account.SessionWindowEnd == nil || !end.Equal(*account.SessionWindowEnd) { + // 校验时间戳是否在合理范围内(不早于 5h 前,不晚于 7 天后) + minAllowed := time.Now().Add(-5 * time.Hour) + maxAllowed := time.Now().Add(7 * 24 * time.Hour) + if end.Before(minAllowed) || end.After(maxAllowed) { + slog.Warn("account_session_window_header_out_of_range", "account_id", account.ID, "raw_reset", resetStr, "parsed_end", end) + } else if needInitWindow || account.SessionWindowEnd == nil || !end.Equal(*account.SessionWindowEnd) { + // 窗口需要初始化,或者真实重置时间与已存储的不同,则更新 start := end.Add(-5 * time.Hour) windowStart = &start windowEnd = &end slog.Info("account_session_window_from_header", "account_id", account.ID, "window_start", start, "window_end", end, "status", status) } + } else { + slog.Warn("account_session_window_header_parse_failed", "account_id", account.ID, "raw_reset", resetStr, "error", err) } } diff --git a/backend/internal/service/ratelimit_session_window_test.go b/backend/internal/service/ratelimit_session_window_test.go index 8ef6abf9..1e990e3a 100644 --- a/backend/internal/service/ratelimit_session_window_test.go +++ b/backend/internal/service/ratelimit_session_window_test.go @@ -19,6 +19,8 @@ type sessionWindowMockRepo struct { clearRateLimitIDs []int64 } +var _ AccountRepository = (*sessionWindowMockRepo)(nil) + type swCall struct { ID int64 Start *time.Time @@ -160,7 +162,7 @@ func newRateLimitServiceForTest(repo AccountRepository) *RateLimitService { func TestUpdateSessionWindow_UsesResetHeader(t *testing.T) { // The reset header provides the real window end as a Unix timestamp. // UpdateSessionWindow should use it instead of the hour-truncated prediction. - resetUnix := int64(1771020000) // 2026-02-14T10:00:00Z + resetUnix := time.Now().Add(3 * time.Hour).Unix() wantEnd := time.Unix(resetUnix, 0) wantStart := wantEnd.Add(-5 * time.Hour) @@ -203,6 +205,11 @@ func TestUpdateSessionWindow_FallbackPredictionWhenNoResetHeader(t *testing.T) { headers.Set("anthropic-ratelimit-unified-5h-status", "allowed_warning") // No anthropic-ratelimit-unified-5h-reset header + // Capture now before the call to avoid hour-boundary races + now := time.Now() + expectedStart := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) + expectedEnd := expectedStart.Add(5 * time.Hour) + svc.UpdateSessionWindow(context.Background(), account, headers) if len(repo.sessionWindowCalls) != 1 { @@ -214,9 +221,6 @@ func TestUpdateSessionWindow_FallbackPredictionWhenNoResetHeader(t *testing.T) { t.Fatal("expected window end to be set (fallback prediction)") } // Fallback: start = current hour truncated, end = start + 5h - now := time.Now() - expectedStart := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) - expectedEnd := expectedStart.Add(5 * time.Hour) if !call.End.Equal(expectedEnd) { t.Errorf("expected fallback end %v, got %v", expectedEnd, *call.End) @@ -229,7 +233,7 @@ func TestUpdateSessionWindow_FallbackPredictionWhenNoResetHeader(t *testing.T) { func TestUpdateSessionWindow_CorrectsStalePrediction(t *testing.T) { // When the stored SessionWindowEnd is wrong (from a previous prediction), // and the reset header provides the real time, it should update the window. - staleEnd := time.Now().Add(2 * time.Hour) // existing prediction: 2h from now + staleEnd := time.Now().Add(2 * time.Hour) // existing prediction: 2h from now realResetUnix := time.Now().Add(4 * time.Hour).Unix() // real reset: 4h from now wantEnd := time.Unix(realResetUnix, 0) @@ -291,7 +295,7 @@ func TestUpdateSessionWindow_NoUpdateWhenHeaderMatchesStored(t *testing.T) { func TestUpdateSessionWindow_ClearsUtilizationOnWindowReset(t *testing.T) { // When needInitWindow=true and window is set, utilization should be cleared. - resetUnix := int64(1771020000) + resetUnix := time.Now().Add(3 * time.Hour).Unix() repo := &sessionWindowMockRepo{} svc := newRateLimitServiceForTest(repo) diff --git a/frontend/src/components/account/UsageProgressBar.vue b/frontend/src/components/account/UsageProgressBar.vue index e04f92e2..506071fa 100644 --- a/frontend/src/components/account/UsageProgressBar.vue +++ b/frontend/src/components/account/UsageProgressBar.vue @@ -56,7 +56,7 @@