account_usage_service_fable_test.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. package service
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "time"
  6. "github.com/stretchr/testify/require"
  7. )
  8. func TestClaudeUsageResponse_FableWindowDecoding(t *testing.T) {
  9. t.Run("seven_day_overage_included", func(t *testing.T) {
  10. raw := `{
  11. "five_hour": {"utilization": 12.0, "resets_at": "2026-07-03T10:00:00Z"},
  12. "seven_day": {"utilization": 34.0, "resets_at": "2026-07-08T00:00:00Z"},
  13. "seven_day_overage_included": {"utilization": 56.0, "resets_at": "2026-07-08T03:00:00Z"}
  14. }`
  15. var resp ClaudeUsageResponse
  16. require.NoError(t, json.Unmarshal([]byte(raw), &resp))
  17. require.Equal(t, 56.0, resp.SevenDayOverageIncluded.Utilization)
  18. require.Equal(t, "2026-07-08T03:00:00Z", resp.SevenDayOverageIncluded.ResetsAt)
  19. })
  20. t.Run("absent", func(t *testing.T) {
  21. raw := `{"five_hour": {"utilization": 12.0, "resets_at": "2026-07-03T10:00:00Z"}}`
  22. var resp ClaudeUsageResponse
  23. require.NoError(t, json.Unmarshal([]byte(raw), &resp))
  24. require.Zero(t, resp.SevenDayOverageIncluded.Utilization)
  25. require.Empty(t, resp.SevenDayOverageIncluded.ResetsAt)
  26. })
  27. }
  28. func TestBuildUsageInfo_SevenDayFable(t *testing.T) {
  29. svc := &AccountUsageService{}
  30. now := time.Now()
  31. resetAt := now.Add(72 * time.Hour).UTC().Truncate(time.Second)
  32. var resp ClaudeUsageResponse
  33. resp.FiveHour.Utilization = 10
  34. resp.SevenDayOverageIncluded = ClaudeUsageWindow{
  35. Utilization: 88,
  36. ResetsAt: resetAt.Format(time.RFC3339),
  37. }
  38. info := svc.buildUsageInfo(&resp, &now)
  39. require.NotNil(t, info.SevenDayFable)
  40. require.Equal(t, 88.0, info.SevenDayFable.Utilization)
  41. require.NotNil(t, info.SevenDayFable.ResetsAt)
  42. require.True(t, info.SevenDayFable.ResetsAt.Equal(resetAt))
  43. require.Greater(t, info.SevenDayFable.RemainingSeconds, 0)
  44. // 无 Fable 数据时不应创建窗口
  45. var empty ClaudeUsageResponse
  46. empty.FiveHour.Utilization = 10
  47. info = svc.buildUsageInfo(&empty, &now)
  48. require.Nil(t, info.SevenDayFable)
  49. }
  50. func TestBuildPassiveUsageWindow(t *testing.T) {
  51. future := time.Now().Add(48 * time.Hour).Unix()
  52. t.Run("utilization and reset", func(t *testing.T) {
  53. window := buildPassiveUsageWindow(map[string]any{
  54. "passive_usage_7d_oi_utilization": 0.87,
  55. "passive_usage_7d_oi_reset": float64(future),
  56. }, "passive_usage_7d_oi_utilization", "passive_usage_7d_oi_reset")
  57. require.NotNil(t, window)
  58. require.InDelta(t, 87.0, window.Utilization, 1e-9)
  59. require.NotNil(t, window.ResetsAt)
  60. require.Equal(t, future, window.ResetsAt.Unix())
  61. require.Greater(t, window.RemainingSeconds, 0)
  62. })
  63. t.Run("no data returns nil", func(t *testing.T) {
  64. require.Nil(t, buildPassiveUsageWindow(nil, "u", "r"))
  65. require.Nil(t, buildPassiveUsageWindow(map[string]any{}, "u", "r"))
  66. })
  67. t.Run("expired reset clamps remaining to zero", func(t *testing.T) {
  68. past := time.Now().Add(-time.Hour).Unix()
  69. window := buildPassiveUsageWindow(map[string]any{
  70. "u": 0.5,
  71. "r": float64(past),
  72. }, "u", "r")
  73. require.NotNil(t, window)
  74. require.Equal(t, 0, window.RemainingSeconds)
  75. })
  76. t.Run("utilization only", func(t *testing.T) {
  77. window := buildPassiveUsageWindow(map[string]any{"u": 0.25}, "u", "r")
  78. require.NotNil(t, window)
  79. require.InDelta(t, 25.0, window.Utilization, 1e-9)
  80. require.Nil(t, window.ResetsAt)
  81. })
  82. }
  83. func TestSyncActiveToPassive_WritesFableExtras(t *testing.T) {
  84. repo := &accountUsageCodexProbeRepo{updateExtraCh: make(chan map[string]any, 1)}
  85. svc := &AccountUsageService{accountRepo: repo}
  86. resetAt := time.Now().Add(72 * time.Hour).Truncate(time.Second)
  87. usage := &UsageInfo{
  88. SevenDayFable: &UsageProgress{
  89. Utilization: 87,
  90. ResetsAt: &resetAt,
  91. },
  92. }
  93. svc.syncActiveToPassive(t.Context(), 1, usage)
  94. select {
  95. case updates := <-repo.updateExtraCh:
  96. require.InDelta(t, 0.87, updates["passive_usage_7d_oi_utilization"], 1e-9)
  97. require.Equal(t, resetAt.Unix(), updates["passive_usage_7d_oi_reset"])
  98. require.Contains(t, updates, "passive_usage_sampled_at")
  99. default:
  100. t.Fatal("expected UpdateExtra to be called with fable extras")
  101. }
  102. }