From 7d15d9d282ef533db1ccc2045198320bc2dd9c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 17 Mar 2026 16:46:54 +0800 Subject: [PATCH] ccm: emit status updates for plan-weight-only changes --- service/ccm/credential_external.go | 4 ++- service/ccm/credential_status_test.go | 35 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/service/ccm/credential_external.go b/service/ccm/credential_external.go index fbd82e60a..3d39a88ca 100644 --- a/service/ccm/credential_external.go +++ b/service/ccm/credential_external.go @@ -479,7 +479,9 @@ func (c *externalCredential) updateStateFromHeaders(headers http.Header) { } c.logger.Debug("usage update for ", c.tag, ": 5h=", c.state.fiveHourUtilization, "%, weekly=", c.state.weeklyUtilization, "%", resetSuffix) } - shouldEmit := hadData && (c.state.fiveHourUtilization != oldFiveHour || c.state.weeklyUtilization != oldWeekly || c.state.remotePlanWeight != oldPlanWeight) + utilizationChanged := c.state.fiveHourUtilization != oldFiveHour || c.state.weeklyUtilization != oldWeekly + planWeightChanged := c.state.remotePlanWeight != oldPlanWeight + shouldEmit := (hadData && utilizationChanged) || planWeightChanged shouldInterrupt := c.checkTransitionLocked() c.stateAccess.Unlock() if shouldInterrupt { diff --git a/service/ccm/credential_status_test.go b/service/ccm/credential_status_test.go index e675f73cc..f92b27e85 100644 --- a/service/ccm/credential_status_test.go +++ b/service/ccm/credential_status_test.go @@ -195,6 +195,41 @@ func TestExternalCredentialConnectStatusStreamMultiFrameKeepsLastUpdated(t *test } } +func TestExternalCredentialPlanWeightOnlyHeaderEmitsStatus(t *testing.T) { + subscriber := observable.NewSubscriber[struct{}](8) + subscription, _ := subscriber.Subscription() + credential := &externalCredential{ + tag: "test", + logger: newTestLogger(), + statusSubscriber: subscriber, + } + credential.stateAccess.Lock() + credential.state.remotePlanWeight = 2 + oldTime := time.Unix(123, 0) + credential.state.lastUpdated = oldTime + credential.stateAccess.Unlock() + + headers := make(http.Header) + headers.Set("X-CCM-Plan-Weight", "3") + credential.updateStateFromHeaders(headers) + + if weight := credential.planWeight(); weight != 3 { + t.Fatalf("expected plan weight 3, got %v", weight) + } + if count := drainStatusEvents(subscription); count != 1 { + t.Fatalf("expected 1 status event, got %d", count) + } + if !credential.lastUpdatedTime().Equal(oldTime) { + t.Fatalf("expected lastUpdated to stay %v, got %v", oldTime, credential.lastUpdatedTime()) + } + + credential.updateStateFromHeaders(headers) + + if count := drainStatusEvents(subscription); count != 0 { + t.Fatalf("expected no status event for unchanged plan weight, got %d", count) + } +} + func TestDefaultCredentialStatusChangesEmitStatus(t *testing.T) { credentialPath := filepath.Join(t.TempDir(), "credentials.json") err := os.WriteFile(credentialPath, []byte("{\"claudeAiOauth\":{\"accessToken\":\"token\",\"refreshToken\":\"\",\"expiresAt\":0,\"subscriptionType\":\"max\"}}\n"), 0o600)