openai_gateway_service_codex_snapshot_test.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. package service
  2. import (
  3. "testing"
  4. "time"
  5. )
  6. func TestCodexSnapshotBaseTime(t *testing.T) {
  7. fallback := time.Date(2026, 2, 20, 9, 0, 0, 0, time.UTC)
  8. t.Run("nil snapshot uses fallback", func(t *testing.T) {
  9. got := codexSnapshotBaseTime(nil, fallback)
  10. if !got.Equal(fallback) {
  11. t.Fatalf("got %v, want fallback %v", got, fallback)
  12. }
  13. })
  14. t.Run("empty updatedAt uses fallback", func(t *testing.T) {
  15. got := codexSnapshotBaseTime(&OpenAICodexUsageSnapshot{}, fallback)
  16. if !got.Equal(fallback) {
  17. t.Fatalf("got %v, want fallback %v", got, fallback)
  18. }
  19. })
  20. t.Run("valid updatedAt wins", func(t *testing.T) {
  21. got := codexSnapshotBaseTime(&OpenAICodexUsageSnapshot{UpdatedAt: "2026-02-16T10:00:00Z"}, fallback)
  22. want := time.Date(2026, 2, 16, 10, 0, 0, 0, time.UTC)
  23. if !got.Equal(want) {
  24. t.Fatalf("got %v, want %v", got, want)
  25. }
  26. })
  27. t.Run("invalid updatedAt uses fallback", func(t *testing.T) {
  28. got := codexSnapshotBaseTime(&OpenAICodexUsageSnapshot{UpdatedAt: "invalid"}, fallback)
  29. if !got.Equal(fallback) {
  30. t.Fatalf("got %v, want fallback %v", got, fallback)
  31. }
  32. })
  33. }
  34. func TestCodexResetAtRFC3339(t *testing.T) {
  35. base := time.Date(2026, 2, 16, 10, 0, 0, 0, time.UTC)
  36. t.Run("nil reset returns nil", func(t *testing.T) {
  37. if got := codexResetAtRFC3339(base, nil); got != nil {
  38. t.Fatalf("expected nil, got %v", *got)
  39. }
  40. })
  41. t.Run("positive seconds", func(t *testing.T) {
  42. sec := 90
  43. got := codexResetAtRFC3339(base, &sec)
  44. if got == nil {
  45. t.Fatal("expected non-nil")
  46. }
  47. if *got != "2026-02-16T10:01:30Z" {
  48. t.Fatalf("got %s, want %s", *got, "2026-02-16T10:01:30Z")
  49. }
  50. })
  51. t.Run("negative seconds clamp to base", func(t *testing.T) {
  52. sec := -3
  53. got := codexResetAtRFC3339(base, &sec)
  54. if got == nil {
  55. t.Fatal("expected non-nil")
  56. }
  57. if *got != "2026-02-16T10:00:00Z" {
  58. t.Fatalf("got %s, want %s", *got, "2026-02-16T10:00:00Z")
  59. }
  60. })
  61. }
  62. func TestBuildCodexUsageExtraUpdates_UsesSnapshotUpdatedAt(t *testing.T) {
  63. primaryUsed := 88.0
  64. primaryReset := 86400
  65. primaryWindow := 10080
  66. secondaryUsed := 12.0
  67. secondaryReset := 3600
  68. secondaryWindow := 300
  69. snapshot := &OpenAICodexUsageSnapshot{
  70. PrimaryUsedPercent: &primaryUsed,
  71. PrimaryResetAfterSeconds: &primaryReset,
  72. PrimaryWindowMinutes: &primaryWindow,
  73. SecondaryUsedPercent: &secondaryUsed,
  74. SecondaryResetAfterSeconds: &secondaryReset,
  75. SecondaryWindowMinutes: &secondaryWindow,
  76. UpdatedAt: "2026-02-16T10:00:00Z",
  77. }
  78. updates := buildCodexUsageExtraUpdates(snapshot, time.Date(2026, 2, 20, 8, 0, 0, 0, time.UTC))
  79. if updates == nil {
  80. t.Fatal("expected non-nil updates")
  81. }
  82. if got := updates["codex_usage_updated_at"]; got != "2026-02-16T10:00:00Z" {
  83. t.Fatalf("codex_usage_updated_at = %v, want %s", got, "2026-02-16T10:00:00Z")
  84. }
  85. if got := updates["codex_5h_reset_at"]; got != "2026-02-16T11:00:00Z" {
  86. t.Fatalf("codex_5h_reset_at = %v, want %s", got, "2026-02-16T11:00:00Z")
  87. }
  88. if got := updates["codex_7d_reset_at"]; got != "2026-02-17T10:00:00Z" {
  89. t.Fatalf("codex_7d_reset_at = %v, want %s", got, "2026-02-17T10:00:00Z")
  90. }
  91. }
  92. // TestBuildCodexUsageExtraUpdates_FreshAccountUsedPercentNotInverted_Issue2994 locks in the
  93. // canonical "used %" semantics for the 5h window. A fresh account reports a tiny
  94. // secondary-used-percent (~1%); the stored codex_5h_used_percent must equal that value
  95. // directly and must NOT be inverted to ~99%. Regression guard for issue #2994 / the reverted
  96. // commit b65dde63 (PR #2918), which applied `100 - used` and made fresh accounts look
  97. // exhausted, tripping auto-pause and excluding them from scheduling.
  98. func TestBuildCodexUsageExtraUpdates_FreshAccountUsedPercentNotInverted_Issue2994(t *testing.T) {
  99. secondaryUsed := 1.0 // 5h window: barely used
  100. secondaryWindow := 300
  101. primaryUsed := 2.0 // 7d window: barely used
  102. primaryWindow := 10080
  103. snapshot := &OpenAICodexUsageSnapshot{
  104. PrimaryUsedPercent: &primaryUsed,
  105. PrimaryWindowMinutes: &primaryWindow,
  106. SecondaryUsedPercent: &secondaryUsed,
  107. SecondaryWindowMinutes: &secondaryWindow,
  108. UpdatedAt: "2026-02-16T10:00:00Z",
  109. }
  110. updates := buildCodexUsageExtraUpdates(snapshot, time.Date(2026, 2, 16, 10, 0, 0, 0, time.UTC))
  111. if updates == nil {
  112. t.Fatal("expected non-nil updates")
  113. }
  114. if got := updates["codex_5h_used_percent"]; got != 1.0 {
  115. t.Fatalf("codex_5h_used_percent = %v, want 1.0 (direct used%%, NOT inverted to 99)", got)
  116. }
  117. if got := updates["codex_7d_used_percent"]; got != 2.0 {
  118. t.Fatalf("codex_7d_used_percent = %v, want 2.0 (direct used%%, NOT inverted to 98)", got)
  119. }
  120. }
  121. func TestBuildCodexUsageExtraUpdates_FallbackToNowWhenUpdatedAtInvalid(t *testing.T) {
  122. primaryUsed := 15.0
  123. primaryReset := 30
  124. primaryWindow := 300
  125. fallbackNow := time.Date(2026, 2, 20, 8, 30, 0, 0, time.UTC)
  126. snapshot := &OpenAICodexUsageSnapshot{
  127. PrimaryUsedPercent: &primaryUsed,
  128. PrimaryResetAfterSeconds: &primaryReset,
  129. PrimaryWindowMinutes: &primaryWindow,
  130. UpdatedAt: "invalid-time",
  131. }
  132. updates := buildCodexUsageExtraUpdates(snapshot, fallbackNow)
  133. if updates == nil {
  134. t.Fatal("expected non-nil updates")
  135. }
  136. if got := updates["codex_usage_updated_at"]; got != "2026-02-20T08:30:00Z" {
  137. t.Fatalf("codex_usage_updated_at = %v, want %s", got, "2026-02-20T08:30:00Z")
  138. }
  139. if got := updates["codex_5h_reset_at"]; got != "2026-02-20T08:30:30Z" {
  140. t.Fatalf("codex_5h_reset_at = %v, want %s", got, "2026-02-20T08:30:30Z")
  141. }
  142. }
  143. func TestBuildCodexUsageExtraUpdates_ClampNegativeResetSeconds(t *testing.T) {
  144. primaryUsed := 90.0
  145. primaryReset := 7200
  146. primaryWindow := 10080
  147. secondaryUsed := 100.0
  148. secondaryReset := -15
  149. secondaryWindow := 300
  150. snapshot := &OpenAICodexUsageSnapshot{
  151. PrimaryUsedPercent: &primaryUsed,
  152. PrimaryResetAfterSeconds: &primaryReset,
  153. PrimaryWindowMinutes: &primaryWindow,
  154. SecondaryUsedPercent: &secondaryUsed,
  155. SecondaryResetAfterSeconds: &secondaryReset,
  156. SecondaryWindowMinutes: &secondaryWindow,
  157. UpdatedAt: "2026-02-16T10:00:00Z",
  158. }
  159. updates := buildCodexUsageExtraUpdates(snapshot, time.Time{})
  160. if updates == nil {
  161. t.Fatal("expected non-nil updates")
  162. }
  163. if got := updates["codex_5h_reset_after_seconds"]; got != -15 {
  164. t.Fatalf("codex_5h_reset_after_seconds = %v, want %d", got, -15)
  165. }
  166. if got := updates["codex_5h_reset_at"]; got != "2026-02-16T10:00:00Z" {
  167. t.Fatalf("codex_5h_reset_at = %v, want %s", got, "2026-02-16T10:00:00Z")
  168. }
  169. }
  170. func TestBuildCodexUsageExtraUpdates_NilSnapshot(t *testing.T) {
  171. if got := buildCodexUsageExtraUpdates(nil, time.Now()); got != nil {
  172. t.Fatalf("expected nil updates, got %v", got)
  173. }
  174. }
  175. func TestBuildCodexUsageExtraUpdates_WithoutNormalizedWindowFields(t *testing.T) {
  176. primaryUsed := 42.0
  177. fallbackNow := time.Date(2026, 2, 20, 9, 15, 0, 0, time.UTC)
  178. snapshot := &OpenAICodexUsageSnapshot{
  179. PrimaryUsedPercent: &primaryUsed,
  180. UpdatedAt: "",
  181. }
  182. updates := buildCodexUsageExtraUpdates(snapshot, fallbackNow)
  183. if updates == nil {
  184. t.Fatal("expected non-nil updates")
  185. }
  186. if got := updates["codex_usage_updated_at"]; got != "2026-02-20T09:15:00Z" {
  187. t.Fatalf("codex_usage_updated_at = %v, want %s", got, "2026-02-20T09:15:00Z")
  188. }
  189. if _, ok := updates["codex_5h_reset_at"]; ok {
  190. t.Fatalf("did not expect codex_5h_reset_at in updates: %v", updates["codex_5h_reset_at"])
  191. }
  192. if _, ok := updates["codex_7d_reset_at"]; ok {
  193. t.Fatalf("did not expect codex_7d_reset_at in updates: %v", updates["codex_7d_reset_at"])
  194. }
  195. }