From a2f83ff0328526252987777268c1c69343d578df Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Sun, 11 Jan 2026 20:55:44 +0800 Subject: [PATCH] =?UTF-8?q?test(ops):=20=E6=B7=BB=E5=8A=A0=E5=91=8A?= =?UTF-8?q?=E8=AD=A6=E8=AF=84=E4=BC=B0=E6=9C=8D=E5=8A=A1=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ops_alert_evaluator_service_test.go | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 backend/internal/service/ops_alert_evaluator_service_test.go diff --git a/backend/internal/service/ops_alert_evaluator_service_test.go b/backend/internal/service/ops_alert_evaluator_service_test.go new file mode 100644 index 00000000..068ab6bb --- /dev/null +++ b/backend/internal/service/ops_alert_evaluator_service_test.go @@ -0,0 +1,210 @@ +//go:build unit + +package service + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type stubOpsRepo struct { + OpsRepository + overview *OpsDashboardOverview + err error +} + +func (s *stubOpsRepo) GetDashboardOverview(ctx context.Context, filter *OpsDashboardFilter) (*OpsDashboardOverview, error) { + if s.err != nil { + return nil, s.err + } + if s.overview != nil { + return s.overview, nil + } + return &OpsDashboardOverview{}, nil +} + +func TestComputeGroupAvailableRatio(t *testing.T) { + t.Parallel() + + t.Run("正常情况: 10个账号, 8个可用 = 80%", func(t *testing.T) { + t.Parallel() + + got := computeGroupAvailableRatio(&GroupAvailability{ + TotalAccounts: 10, + AvailableCount: 8, + }) + require.InDelta(t, 80.0, got, 0.0001) + }) + + t.Run("边界情况: TotalAccounts = 0 应返回 0", func(t *testing.T) { + t.Parallel() + + got := computeGroupAvailableRatio(&GroupAvailability{ + TotalAccounts: 0, + AvailableCount: 8, + }) + require.Equal(t, 0.0, got) + }) + + t.Run("边界情况: AvailableCount = 0 应返回 0%", func(t *testing.T) { + t.Parallel() + + got := computeGroupAvailableRatio(&GroupAvailability{ + TotalAccounts: 10, + AvailableCount: 0, + }) + require.Equal(t, 0.0, got) + }) +} + +func TestCountAccountsByCondition(t *testing.T) { + t.Parallel() + + t.Run("测试限流账号统计: acc.IsRateLimited", func(t *testing.T) { + t.Parallel() + + accounts := map[int64]*AccountAvailability{ + 1: {IsRateLimited: true}, + 2: {IsRateLimited: false}, + 3: {IsRateLimited: true}, + } + + got := countAccountsByCondition(accounts, func(acc *AccountAvailability) bool { + return acc.IsRateLimited + }) + require.Equal(t, int64(2), got) + }) + + t.Run("测试错误账号统计(排除临时不可调度): acc.HasError && acc.TempUnschedulableUntil == nil", func(t *testing.T) { + t.Parallel() + + until := time.Now().UTC().Add(5 * time.Minute) + accounts := map[int64]*AccountAvailability{ + 1: {HasError: true}, + 2: {HasError: true, TempUnschedulableUntil: &until}, + 3: {HasError: false}, + } + + got := countAccountsByCondition(accounts, func(acc *AccountAvailability) bool { + return acc.HasError && acc.TempUnschedulableUntil == nil + }) + require.Equal(t, int64(1), got) + }) + + t.Run("边界情况: 空 map 应返回 0", func(t *testing.T) { + t.Parallel() + + got := countAccountsByCondition(map[int64]*AccountAvailability{}, func(acc *AccountAvailability) bool { + return acc.IsRateLimited + }) + require.Equal(t, int64(0), got) + }) +} + +func TestComputeRuleMetricNewIndicators(t *testing.T) { + t.Parallel() + + groupID := int64(101) + platform := "openai" + + availability := &OpsAccountAvailability{ + Group: &GroupAvailability{ + GroupID: groupID, + TotalAccounts: 10, + AvailableCount: 8, + }, + Accounts: map[int64]*AccountAvailability{ + 1: {IsRateLimited: true}, + 2: {IsRateLimited: true}, + 3: {HasError: true}, + 4: {HasError: true, TempUnschedulableUntil: timePtr(time.Now().UTC().Add(2 * time.Minute))}, + 5: {HasError: false, IsRateLimited: false}, + }, + } + + opsService := &OpsService{ + getAccountAvailability: func(_ context.Context, _ string, _ *int64) (*OpsAccountAvailability, error) { + return availability, nil + }, + } + + svc := &OpsAlertEvaluatorService{ + opsService: opsService, + opsRepo: &stubOpsRepo{overview: &OpsDashboardOverview{}}, + } + + start := time.Now().UTC().Add(-5 * time.Minute) + end := time.Now().UTC() + ctx := context.Background() + + tests := []struct { + name string + metricType string + groupID *int64 + wantValue float64 + wantOK bool + }{ + { + name: "group_available_accounts", + metricType: "group_available_accounts", + groupID: &groupID, + wantValue: 8, + wantOK: true, + }, + { + name: "group_available_ratio", + metricType: "group_available_ratio", + groupID: &groupID, + wantValue: 80.0, + wantOK: true, + }, + { + name: "account_rate_limited_count", + metricType: "account_rate_limited_count", + groupID: nil, + wantValue: 2, + wantOK: true, + }, + { + name: "account_error_count", + metricType: "account_error_count", + groupID: nil, + wantValue: 1, + wantOK: true, + }, + { + name: "group_available_accounts without group_id returns false", + metricType: "group_available_accounts", + groupID: nil, + wantValue: 0, + wantOK: false, + }, + { + name: "group_available_ratio without group_id returns false", + metricType: "group_available_ratio", + groupID: nil, + wantValue: 0, + wantOK: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + rule := &OpsAlertRule{ + MetricType: tt.metricType, + } + gotValue, gotOK := svc.computeRuleMetric(ctx, rule, nil, start, end, platform, tt.groupID) + require.Equal(t, tt.wantOK, gotOK) + if !tt.wantOK { + return + } + require.InDelta(t, tt.wantValue, gotValue, 0.0001) + }) + } +}