From 6cd99935c829a50a4612d4b788401c9e3b033bb7 Mon Sep 17 00:00:00 2001 From: Camy <209573702+camy-x@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:10:22 +0800 Subject: [PATCH] fix(auth): preserve refreshed quota observations --- internal/runtime/executor/devin_executor.go | 4 +- .../runtime/executor/devin_executor_test.go | 14 ++++++ sdk/cliproxy/auth/conductor_lifecycle.go | 6 ++- sdk/cliproxy/auth/conductor_update_test.go | 49 +++++++++++++++++++ sdk/cliproxy/auth/metadata_merge.go | 1 + 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/internal/runtime/executor/devin_executor.go b/internal/runtime/executor/devin_executor.go index 7aa6015b9ab..f5d637883d4 100644 --- a/internal/runtime/executor/devin_executor.go +++ b/internal/runtime/executor/devin_executor.go @@ -200,9 +200,7 @@ func (e *DevinExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (* } // Quota observation signals for management UI and conductor - if updated.Quota.Signals == nil { - updated.Quota.Signals = make(map[string]string) - } + updated.Quota.Signals = make(map[string]string) if status.Plan != "" { updated.Quota.Signals["plan"] = status.Plan } diff --git a/internal/runtime/executor/devin_executor_test.go b/internal/runtime/executor/devin_executor_test.go index fb9305f37e9..17a0aeadcd3 100644 --- a/internal/runtime/executor/devin_executor_test.go +++ b/internal/runtime/executor/devin_executor_test.go @@ -638,6 +638,15 @@ func TestDevinExecutor_Refresh(t *testing.T) { "api_key": "devin-session-token$test", "base_url": server.URL, }, + Quota: cliproxyauth.QuotaState{ + ObservedAt: time.Unix(10, 0), + Signals: map[string]string{ + "plan": "Free", + "plan_start": "2025-01-01T00:00:00Z", + "plan_end": "2025-02-01T00:00:00Z", + "obsolete_signal": "stale", + }, + }, } updated, err := exec.Refresh(context.Background(), auth) @@ -666,6 +675,11 @@ func TestDevinExecutor_Refresh(t *testing.T) { if updated.Quota.Signals["weekly_quota_remaining_percent"] != "45%" { t.Errorf("expected quota signal 45%%, got %q", updated.Quota.Signals["weekly_quota_remaining_percent"]) } + for _, staleKey := range []string{"plan_start", "plan_end", "obsolete_signal"} { + if _, exists := updated.Quota.Signals[staleKey]; exists { + t.Errorf("expected stale quota signal %q to be removed, got %#v", staleKey, updated.Quota.Signals) + } + } if updated.Quota.ObservedAt.IsZero() { t.Error("expected non-zero Quota.ObservedAt") } diff --git a/sdk/cliproxy/auth/conductor_lifecycle.go b/sdk/cliproxy/auth/conductor_lifecycle.go index abbc150f6c3..96fc16cad8c 100644 --- a/sdk/cliproxy/auth/conductor_lifecycle.go +++ b/sdk/cliproxy/auth/conductor_lifecycle.go @@ -235,7 +235,11 @@ func (m *Manager) updateInternal(ctx context.Context, base, auth *Auth, mode upd if existing.Quota.Exceeded && existing.Quota.Reason == "credential_quota" && existing.Quota.NextRecoverAt.After(time.Now()) { auth.Unavailable = existing.Unavailable auth.NextRetryAfter = existing.NextRetryAfter - auth.Quota = existing.Quota + if mode == updateModeRefresh { + applyCooldownFields(&auth.Quota, existing.Quota) + } else { + auth.Quota = existing.Quota + } if auth.Status == StatusActive { auth.Status = existing.Status } diff --git a/sdk/cliproxy/auth/conductor_update_test.go b/sdk/cliproxy/auth/conductor_update_test.go index e87b70b67c8..49da88e1539 100644 --- a/sdk/cliproxy/auth/conductor_update_test.go +++ b/sdk/cliproxy/auth/conductor_update_test.go @@ -251,3 +251,52 @@ func TestManager_Update_ActiveInheritsModelStates(t *testing.T) { t.Fatalf("expected BackoffLevel to be %d, got %d", backoffLevel, state.Quota.BackoffLevel) } } + +func TestManager_UpdateRefreshedAuthPreservesQuotaObservationDuringCredentialCooldown(t *testing.T) { + manager := NewManager(nil, nil, nil) + ctx := context.Background() + baseObservedAt := time.Unix(10, 0) + refreshedObservedAt := time.Unix(20, 0) + recoverAt := time.Now().Add(time.Hour) + + base, errRegister := manager.Register(ctx, &Auth{ + ID: "auth-devin-quota-refresh", + Provider: "devin", + Status: StatusActive, + Quota: QuotaState{ + ObservedAt: baseObservedAt, + Signals: map[string]string{"plan": "free"}, + }, + }) + if errRegister != nil { + t.Fatalf("Register() error = %v", errRegister) + } + + concurrent := base.Clone() + concurrent.Status = StatusError + concurrent.Unavailable = true + concurrent.Quota.Exceeded = true + concurrent.Quota.Reason = "credential_quota" + concurrent.Quota.NextRecoverAt = recoverAt + concurrent.Quota.BackoffLevel = 2 + if _, errUpdate := manager.Update(ctx, concurrent); errUpdate != nil { + t.Fatalf("Update() concurrent cooldown error = %v", errUpdate) + } + + refreshed := base.Clone() + refreshed.Quota.ObservedAt = refreshedObservedAt + refreshed.Quota.Signals = map[string]string{ + "plan": "pro", + "daily_quota_remaining_percent": "75%", + } + merged, errRefresh := manager.UpdateRefreshedAuth(ctx, base, refreshed) + if errRefresh != nil { + t.Fatalf("UpdateRefreshedAuth() error = %v", errRefresh) + } + if !merged.Quota.ObservedAt.Equal(refreshedObservedAt) || merged.Quota.Signals["plan"] != "pro" || merged.Quota.Signals["daily_quota_remaining_percent"] != "75%" { + t.Fatalf("refreshed quota observation was not preserved: %#v", merged.Quota) + } + if !merged.Quota.Exceeded || merged.Quota.Reason != "credential_quota" || !merged.Quota.NextRecoverAt.Equal(recoverAt) || merged.Quota.BackoffLevel != 2 { + t.Fatalf("concurrent cooldown was not preserved: %#v", merged.Quota) + } +} diff --git a/sdk/cliproxy/auth/metadata_merge.go b/sdk/cliproxy/auth/metadata_merge.go index d8bf63c07c1..b9efd1a2322 100644 --- a/sdk/cliproxy/auth/metadata_merge.go +++ b/sdk/cliproxy/auth/metadata_merge.go @@ -63,6 +63,7 @@ func MergeRefreshedAuth(base, current, updated *Auth) *Auth { if base != nil && current.RegistrationEpoch != base.RegistrationEpoch { return merged } + merged.Quota = mergeQuotaObservation(current.Quota, updated.Quota) // 1. Refresh Lifecycle Timestamps if !updated.LastRefreshedAt.IsZero() {