ops_alert_evaluator_service_test.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "testing"
  6. "time"
  7. "github.com/stretchr/testify/require"
  8. )
  9. var _ OpsRepository = (*stubOpsRepo)(nil)
  10. type stubOpsRepo struct {
  11. OpsRepository
  12. overview *OpsDashboardOverview
  13. err error
  14. }
  15. func (s *stubOpsRepo) GetDashboardOverview(ctx context.Context, filter *OpsDashboardFilter) (*OpsDashboardOverview, error) {
  16. if s.err != nil {
  17. return nil, s.err
  18. }
  19. if s.overview != nil {
  20. return s.overview, nil
  21. }
  22. return &OpsDashboardOverview{}, nil
  23. }
  24. func TestComputeGroupAvailableRatio(t *testing.T) {
  25. t.Parallel()
  26. t.Run("正常情况: 10个账号, 8个可用 = 80%", func(t *testing.T) {
  27. t.Parallel()
  28. got := computeGroupAvailableRatio(&GroupAvailability{
  29. TotalAccounts: 10,
  30. AvailableCount: 8,
  31. })
  32. require.InDelta(t, 80.0, got, 0.0001)
  33. })
  34. t.Run("边界情况: TotalAccounts = 0 应返回 0", func(t *testing.T) {
  35. t.Parallel()
  36. got := computeGroupAvailableRatio(&GroupAvailability{
  37. TotalAccounts: 0,
  38. AvailableCount: 8,
  39. })
  40. require.Equal(t, 0.0, got)
  41. })
  42. t.Run("边界情况: AvailableCount = 0 应返回 0%", func(t *testing.T) {
  43. t.Parallel()
  44. got := computeGroupAvailableRatio(&GroupAvailability{
  45. TotalAccounts: 10,
  46. AvailableCount: 0,
  47. })
  48. require.Equal(t, 0.0, got)
  49. })
  50. }
  51. func TestCountAccountsByCondition(t *testing.T) {
  52. t.Parallel()
  53. t.Run("测试限流账号统计: acc.IsRateLimited", func(t *testing.T) {
  54. t.Parallel()
  55. accounts := map[int64]*AccountAvailability{
  56. 1: {IsRateLimited: true},
  57. 2: {IsRateLimited: false},
  58. 3: {IsRateLimited: true},
  59. }
  60. got := countAccountsByCondition(accounts, func(acc *AccountAvailability) bool {
  61. return acc.IsRateLimited
  62. })
  63. require.Equal(t, int64(2), got)
  64. })
  65. t.Run("测试错误账号统计(排除临时不可调度): acc.HasError && acc.TempUnschedulableUntil == nil", func(t *testing.T) {
  66. t.Parallel()
  67. until := time.Now().UTC().Add(5 * time.Minute)
  68. accounts := map[int64]*AccountAvailability{
  69. 1: {HasError: true},
  70. 2: {HasError: true, TempUnschedulableUntil: &until},
  71. 3: {HasError: false},
  72. }
  73. got := countAccountsByCondition(accounts, func(acc *AccountAvailability) bool {
  74. return acc.HasError && acc.TempUnschedulableUntil == nil
  75. })
  76. require.Equal(t, int64(1), got)
  77. })
  78. t.Run("边界情况: 空 map 应返回 0", func(t *testing.T) {
  79. t.Parallel()
  80. got := countAccountsByCondition(map[int64]*AccountAvailability{}, func(acc *AccountAvailability) bool {
  81. return acc.IsRateLimited
  82. })
  83. require.Equal(t, int64(0), got)
  84. })
  85. }
  86. // TestComputeRuleMetric_AccountTempUnscheduledCount verifies the new
  87. // account_temp_unscheduled_count metric counts accounts currently in the
  88. // temp-unscheduled window and ignores those whose window has expired or
  89. // were never temp-unscheduled.
  90. func TestComputeRuleMetric_AccountTempUnscheduledCount(t *testing.T) {
  91. t.Parallel()
  92. now := time.Now().UTC()
  93. futureUntil := now.Add(5 * time.Minute)
  94. pastUntil := now.Add(-1 * time.Minute)
  95. availability := &OpsAccountAvailability{
  96. Accounts: map[int64]*AccountAvailability{
  97. // currently temp-unscheduled (window active)
  98. 1: {TempUnschedulableUntil: &futureUntil},
  99. 2: {TempUnschedulableUntil: &futureUntil},
  100. // temp-unsched window already expired → should NOT count
  101. 3: {TempUnschedulableUntil: &pastUntil},
  102. // never temp-unscheduled
  103. 4: {HasError: true},
  104. 5: {IsRateLimited: true},
  105. },
  106. }
  107. opsService := &OpsService{
  108. getAccountAvailability: func(_ context.Context, _ string, _ *int64) (*OpsAccountAvailability, error) {
  109. return availability, nil
  110. },
  111. }
  112. svc := &OpsAlertEvaluatorService{
  113. opsService: opsService,
  114. opsRepo: &stubOpsRepo{},
  115. }
  116. rule := &OpsAlertRule{MetricType: "account_temp_unscheduled_count"}
  117. val, ok := svc.computeRuleMetric(context.Background(), rule, nil,
  118. now.Add(-5*time.Minute), now, "", nil)
  119. require.True(t, ok)
  120. require.InDelta(t, 2.0, val, 0.0001, "only 2 accounts have an active temp-unsched window")
  121. }
  122. func TestComputeRuleMetricNewIndicators(t *testing.T) {
  123. t.Parallel()
  124. groupID := int64(101)
  125. platform := "openai"
  126. availability := &OpsAccountAvailability{
  127. Group: &GroupAvailability{
  128. GroupID: groupID,
  129. TotalAccounts: 10,
  130. AvailableCount: 8,
  131. },
  132. Accounts: map[int64]*AccountAvailability{
  133. 1: {IsRateLimited: true},
  134. 2: {IsRateLimited: true},
  135. 3: {HasError: true},
  136. 4: {HasError: true, TempUnschedulableUntil: timePtr(time.Now().UTC().Add(2 * time.Minute))},
  137. 5: {HasError: false, IsRateLimited: false},
  138. },
  139. }
  140. opsService := &OpsService{
  141. getAccountAvailability: func(_ context.Context, _ string, _ *int64) (*OpsAccountAvailability, error) {
  142. return availability, nil
  143. },
  144. }
  145. svc := &OpsAlertEvaluatorService{
  146. opsService: opsService,
  147. opsRepo: &stubOpsRepo{overview: &OpsDashboardOverview{}},
  148. }
  149. start := time.Now().UTC().Add(-5 * time.Minute)
  150. end := time.Now().UTC()
  151. ctx := context.Background()
  152. tests := []struct {
  153. name string
  154. metricType string
  155. groupID *int64
  156. wantValue float64
  157. wantOK bool
  158. }{
  159. {
  160. name: "group_available_accounts",
  161. metricType: "group_available_accounts",
  162. groupID: &groupID,
  163. wantValue: 8,
  164. wantOK: true,
  165. },
  166. {
  167. name: "group_available_ratio",
  168. metricType: "group_available_ratio",
  169. groupID: &groupID,
  170. wantValue: 80.0,
  171. wantOK: true,
  172. },
  173. {
  174. name: "account_rate_limited_count",
  175. metricType: "account_rate_limited_count",
  176. groupID: nil,
  177. wantValue: 2,
  178. wantOK: true,
  179. },
  180. {
  181. name: "account_error_count",
  182. metricType: "account_error_count",
  183. groupID: nil,
  184. wantValue: 1,
  185. wantOK: true,
  186. },
  187. {
  188. name: "group_available_accounts without group_id returns false",
  189. metricType: "group_available_accounts",
  190. groupID: nil,
  191. wantValue: 0,
  192. wantOK: false,
  193. },
  194. {
  195. name: "group_available_ratio without group_id returns false",
  196. metricType: "group_available_ratio",
  197. groupID: nil,
  198. wantValue: 0,
  199. wantOK: false,
  200. },
  201. }
  202. for _, tt := range tests {
  203. tt := tt
  204. t.Run(tt.name, func(t *testing.T) {
  205. t.Parallel()
  206. rule := &OpsAlertRule{
  207. MetricType: tt.metricType,
  208. }
  209. gotValue, gotOK := svc.computeRuleMetric(ctx, rule, nil, start, end, platform, tt.groupID)
  210. require.Equal(t, tt.wantOK, gotOK)
  211. if !tt.wantOK {
  212. return
  213. }
  214. require.InDelta(t, tt.wantValue, gotValue, 0.0001)
  215. })
  216. }
  217. }