cricinfo-cli-go 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/AGENTS.md +63 -0
  2. package/CONTRIBUTORS.md +75 -0
  3. package/LICENSE +21 -0
  4. package/Makefile +131 -0
  5. package/README.md +130 -0
  6. package/bin/cricinfo.js +44 -0
  7. package/cmd/cricinfo/main.go +15 -0
  8. package/go.mod +10 -0
  9. package/go.sum +10 -0
  10. package/internal/app/app.go +11 -0
  11. package/internal/app/app_test.go +122 -0
  12. package/internal/buildinfo/buildinfo.go +16 -0
  13. package/internal/cli/analysis.go +262 -0
  14. package/internal/cli/analysis_test.go +175 -0
  15. package/internal/cli/competitions.go +154 -0
  16. package/internal/cli/competitions_test.go +165 -0
  17. package/internal/cli/leagues.go +297 -0
  18. package/internal/cli/leagues_test.go +194 -0
  19. package/internal/cli/matches.go +403 -0
  20. package/internal/cli/matches_test.go +413 -0
  21. package/internal/cli/players.go +263 -0
  22. package/internal/cli/players_test.go +384 -0
  23. package/internal/cli/root.go +141 -0
  24. package/internal/cli/search.go +119 -0
  25. package/internal/cli/teams.go +214 -0
  26. package/internal/cli/teams_test.go +192 -0
  27. package/internal/cricinfo/analysis.go +1401 -0
  28. package/internal/cricinfo/analysis_phase15_test.go +267 -0
  29. package/internal/cricinfo/client.go +471 -0
  30. package/internal/cricinfo/client_test.go +280 -0
  31. package/internal/cricinfo/cmd/fixture-refresh/main.go +145 -0
  32. package/internal/cricinfo/competitions.go +405 -0
  33. package/internal/cricinfo/competitions_phase13_test.go +234 -0
  34. package/internal/cricinfo/coverage_ledger.go +122 -0
  35. package/internal/cricinfo/coverage_ledger_test.go +253 -0
  36. package/internal/cricinfo/decode.go +115 -0
  37. package/internal/cricinfo/decode_test.go +100 -0
  38. package/internal/cricinfo/entity_index.go +618 -0
  39. package/internal/cricinfo/entity_index_test.go +175 -0
  40. package/internal/cricinfo/fixture_matrix.go +243 -0
  41. package/internal/cricinfo/fixture_matrix_test.go +49 -0
  42. package/internal/cricinfo/fixtures_test.go +264 -0
  43. package/internal/cricinfo/historical_hydration.go +1641 -0
  44. package/internal/cricinfo/historical_phase14_test.go +542 -0
  45. package/internal/cricinfo/leagues.go +1210 -0
  46. package/internal/cricinfo/leagues_phase12_test.go +324 -0
  47. package/internal/cricinfo/live_leagues_test.go +169 -0
  48. package/internal/cricinfo/live_matches_test.go +203 -0
  49. package/internal/cricinfo/live_matrix_test.go +118 -0
  50. package/internal/cricinfo/live_players_test.go +122 -0
  51. package/internal/cricinfo/live_search_test.go +86 -0
  52. package/internal/cricinfo/live_smoke_test.go +213 -0
  53. package/internal/cricinfo/live_teams_test.go +104 -0
  54. package/internal/cricinfo/matches.go +1508 -0
  55. package/internal/cricinfo/matches_phase7_test.go +207 -0
  56. package/internal/cricinfo/matches_phase9_test.go +253 -0
  57. package/internal/cricinfo/normalize_entities.go +1727 -0
  58. package/internal/cricinfo/normalize_leagues.go +346 -0
  59. package/internal/cricinfo/players.go +1332 -0
  60. package/internal/cricinfo/players_phase10_test.go +174 -0
  61. package/internal/cricinfo/players_phase11_test.go +373 -0
  62. package/internal/cricinfo/render_contract.go +1088 -0
  63. package/internal/cricinfo/render_phase4_test.go +633 -0
  64. package/internal/cricinfo/renderer.go +1689 -0
  65. package/internal/cricinfo/resolver.go +813 -0
  66. package/internal/cricinfo/resolver_test.go +244 -0
  67. package/internal/cricinfo/teams.go +603 -0
  68. package/internal/cricinfo/teams_phase8_test.go +231 -0
  69. package/internal/cricinfo/testdata/fixtures/README.md +43 -0
  70. package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/broadcasts.json +11 -0
  71. package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/officials.json +150 -0
  72. package/internal/cricinfo/testdata/fixtures/details-plays/detail-110.json +157 -0
  73. package/internal/cricinfo/testdata/fixtures/details-plays/detail-52545007.json +145 -0
  74. package/internal/cricinfo/testdata/fixtures/details-plays/detail-52559021.json +143 -0
  75. package/internal/cricinfo/testdata/fixtures/details-plays/plays.json +15 -0
  76. package/internal/cricinfo/testdata/fixtures/endpoint-matrix.tsv +19 -0
  77. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow-1.json +12 -0
  78. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow.json +42 -0
  79. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/innings-1-2.json +38 -0
  80. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnership-1.json +31 -0
  81. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnerships.json +42 -0
  82. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-offdays.json +20 -0
  83. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-ondays.json +21 -0
  84. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar.json +14 -0
  85. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-2025.json +13 -0
  86. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-group-1.json +13 -0
  87. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-groups.json +11 -0
  88. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-type-1.json +13 -0
  89. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-types.json +11 -0
  90. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/seasons.json +30 -0
  91. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-item-1.json +72 -0
  92. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-root.json +3 -0
  93. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings.json +15 -0
  94. package/internal/cricinfo/testdata/fixtures/matches-competitions/competition.json +460 -0
  95. package/internal/cricinfo/testdata/fixtures/matches-competitions/event-1529474.json +86 -0
  96. package/internal/cricinfo/testdata/fixtures/matches-competitions/matchcards-1527966.json +368 -0
  97. package/internal/cricinfo/testdata/fixtures/matches-competitions/situation-1529474.json +10 -0
  98. package/internal/cricinfo/testdata/fixtures/players/athlete-1361257-statistics.json +126 -0
  99. package/internal/cricinfo/testdata/fixtures/players/athlete-1361257.json +113 -0
  100. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-1-statistics-0.json +208 -0
  101. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-2-statistics-0.json +252 -0
  102. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores.json +74 -0
  103. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-statistics-0.json +1008 -0
  104. package/internal/cricinfo/testdata/fixtures/root-discovery/events.json +72 -0
  105. package/internal/cricinfo/testdata/fixtures/root-discovery/root.json +28 -0
  106. package/internal/cricinfo/testdata/fixtures/team-competitor/competitor-789643.json +40 -0
  107. package/internal/cricinfo/testdata/fixtures/team-competitor/leaders-789643.json +353 -0
  108. package/internal/cricinfo/testdata/fixtures/team-competitor/records-789643.json +91 -0
  109. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772-object.json +231 -0
  110. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772.json +235 -0
  111. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-789643.json +322 -0
  112. package/internal/cricinfo/testdata/fixtures/team-competitor/scores-789643.json +19 -0
  113. package/internal/cricinfo/testdata/fixtures/team-competitor/statistics-789643.json +629 -0
  114. package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643-athletes.json +7 -0
  115. package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643.json +67 -0
  116. package/internal/cricinfo/testdata/golden/match-empty.golden +1 -0
  117. package/internal/cricinfo/testdata/golden/match-list.golden +2 -0
  118. package/internal/cricinfo/testdata/golden/match-partial.golden +3 -0
  119. package/internal/cricinfo/types.go +54 -0
  120. package/package.json +51 -0
  121. package/scripts/postinstall.js +153 -0
@@ -0,0 +1,542 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "net/http"
7
+ "net/http/httptest"
8
+ "path/filepath"
9
+ "reflect"
10
+ "strings"
11
+ "sync"
12
+ "testing"
13
+ "time"
14
+ )
15
+
16
+ func TestPhase14ScopedTraversalBySeasonGroupAndDateRange(t *testing.T) {
17
+ t.Parallel()
18
+
19
+ harness := newPhase14HistoricalHarness(t, 0)
20
+ ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
21
+ defer cancel()
22
+
23
+ seasonSession, err := harness.service.BeginScope(ctx, HistoricalScopeOptions{
24
+ LeagueQuery: "Mirwais Nika",
25
+ SeasonQuery: "2025",
26
+ })
27
+ if err != nil {
28
+ t.Fatalf("BeginScope season error: %v", err)
29
+ }
30
+
31
+ seasonMatches := seasonSession.ScopedMatches()
32
+ if len(seasonMatches) != 2 {
33
+ t.Fatalf("expected 2 season-scoped matches, got %d", len(seasonMatches))
34
+ }
35
+
36
+ groupSession, err := harness.service.BeginScope(ctx, HistoricalScopeOptions{
37
+ LeagueQuery: "Mirwais Nika",
38
+ SeasonQuery: "2025",
39
+ TypeQuery: "1",
40
+ GroupQuery: "1",
41
+ })
42
+ if err != nil {
43
+ t.Fatalf("BeginScope group error: %v", err)
44
+ }
45
+
46
+ groupMatches := groupSession.ScopedMatches()
47
+ if len(groupMatches) != 1 {
48
+ t.Fatalf("expected 1 group-scoped match, got %d", len(groupMatches))
49
+ }
50
+ if groupMatches[0].ID != "1529474" {
51
+ t.Fatalf("expected group-scoped match 1529474, got %+v", groupMatches[0])
52
+ }
53
+
54
+ dateSession, err := harness.service.BeginScope(ctx, HistoricalScopeOptions{
55
+ LeagueQuery: "Mirwais Nika",
56
+ DateFrom: "2025-05-01",
57
+ DateTo: "2025-05-30",
58
+ })
59
+ if err != nil {
60
+ t.Fatalf("BeginScope date-range error: %v", err)
61
+ }
62
+
63
+ dateMatches := dateSession.ScopedMatches()
64
+ if len(dateMatches) != 1 {
65
+ t.Fatalf("expected 1 date-range match, got %d", len(dateMatches))
66
+ }
67
+ if dateMatches[0].ID != "1529475" {
68
+ t.Fatalf("expected date-range match 1529475, got %+v", dateMatches[0])
69
+ }
70
+ }
71
+
72
+ func TestPhase14RunScopedHydrationReuseAvoidsDuplicateFetches(t *testing.T) {
73
+ t.Parallel()
74
+
75
+ harness := newPhase14HistoricalHarness(t, 0)
76
+ ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
77
+ defer cancel()
78
+
79
+ session, err := harness.service.BeginScope(ctx, HistoricalScopeOptions{
80
+ LeagueQuery: "Mirwais Nika",
81
+ SeasonQuery: "2025",
82
+ MatchLimit: 1,
83
+ })
84
+ if err != nil {
85
+ t.Fatalf("BeginScope error: %v", err)
86
+ }
87
+
88
+ matches := session.ScopedMatches()
89
+ if len(matches) == 0 {
90
+ t.Fatalf("expected at least one scoped match")
91
+ }
92
+ matchID := matches[0].ID
93
+
94
+ if _, _, err := session.HydrateMatchSummaries(ctx); err != nil {
95
+ t.Fatalf("HydrateMatchSummaries error: %v", err)
96
+ }
97
+ if _, _, err := session.HydrateInnings(ctx, matchID); err != nil {
98
+ t.Fatalf("HydrateInnings error: %v", err)
99
+ }
100
+ if _, _, err := session.HydratePlayerMatchSummaries(ctx, matchID); err != nil {
101
+ t.Fatalf("HydratePlayerMatchSummaries error: %v", err)
102
+ }
103
+ if _, _, err := session.HydrateDeliverySummaries(ctx, matchID); err != nil {
104
+ t.Fatalf("HydrateDeliverySummaries error: %v", err)
105
+ }
106
+
107
+ metricsAfterFirst := session.Metrics()
108
+ countsAfterFirst := harness.requestCounts()
109
+
110
+ if _, _, err := session.HydrateMatchSummaries(ctx); err != nil {
111
+ t.Fatalf("HydrateMatchSummaries second pass error: %v", err)
112
+ }
113
+ if _, _, err := session.HydrateInnings(ctx, matchID); err != nil {
114
+ t.Fatalf("HydrateInnings second pass error: %v", err)
115
+ }
116
+ if _, _, err := session.HydratePlayerMatchSummaries(ctx, matchID); err != nil {
117
+ t.Fatalf("HydratePlayerMatchSummaries second pass error: %v", err)
118
+ }
119
+ if _, _, err := session.HydrateDeliverySummaries(ctx, matchID); err != nil {
120
+ t.Fatalf("HydrateDeliverySummaries second pass error: %v", err)
121
+ }
122
+
123
+ metricsAfterSecond := session.Metrics()
124
+ countsAfterSecond := harness.requestCounts()
125
+
126
+ if metricsAfterSecond.ResolveCacheMisses != metricsAfterFirst.ResolveCacheMisses {
127
+ t.Fatalf("expected no new resolve cache misses in second pass, first=%+v second=%+v", metricsAfterFirst, metricsAfterSecond)
128
+ }
129
+ if metricsAfterSecond.DomainCacheHits <= metricsAfterFirst.DomainCacheHits {
130
+ t.Fatalf("expected domain cache hits to increase in second pass, first=%+v second=%+v", metricsAfterFirst, metricsAfterSecond)
131
+ }
132
+ if !reflect.DeepEqual(countsAfterFirst, countsAfterSecond) {
133
+ t.Fatalf("expected no new HTTP requests in second pass\nfirst=%v\nsecond=%v", countsAfterFirst, countsAfterSecond)
134
+ }
135
+ }
136
+
137
+ func TestPhase14PerformanceLimitedMultiMatchSample(t *testing.T) {
138
+ t.Parallel()
139
+
140
+ harness := newPhase14HistoricalHarness(t, 8*time.Millisecond)
141
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
142
+ defer cancel()
143
+
144
+ session, err := harness.service.BeginScope(ctx, HistoricalScopeOptions{
145
+ LeagueQuery: "Mirwais Nika",
146
+ SeasonQuery: "2025",
147
+ MatchLimit: 2,
148
+ })
149
+ if err != nil {
150
+ t.Fatalf("BeginScope error: %v", err)
151
+ }
152
+
153
+ matches := session.ScopedMatches()
154
+ if len(matches) < 2 {
155
+ t.Fatalf("expected at least two matches for limited performance sample")
156
+ }
157
+
158
+ firstStart := time.Now()
159
+ if _, _, err := session.HydrateMatchSummaries(ctx); err != nil {
160
+ t.Fatalf("HydrateMatchSummaries error: %v", err)
161
+ }
162
+ for _, match := range matches {
163
+ if _, _, err := session.HydrateInnings(ctx, match.ID); err != nil {
164
+ t.Fatalf("HydrateInnings(%s) error: %v", match.ID, err)
165
+ }
166
+ if _, _, err := session.HydratePlayerMatchSummaries(ctx, match.ID); err != nil {
167
+ t.Fatalf("HydratePlayerMatchSummaries(%s) error: %v", match.ID, err)
168
+ }
169
+ if _, _, err := session.HydrateDeliverySummaries(ctx, match.ID); err != nil {
170
+ t.Fatalf("HydrateDeliverySummaries(%s) error: %v", match.ID, err)
171
+ }
172
+ }
173
+ firstDuration := time.Since(firstStart)
174
+ firstMetrics := session.Metrics()
175
+
176
+ secondStart := time.Now()
177
+ if _, _, err := session.HydrateMatchSummaries(ctx); err != nil {
178
+ t.Fatalf("HydrateMatchSummaries second pass error: %v", err)
179
+ }
180
+ for _, match := range matches {
181
+ if _, _, err := session.HydrateInnings(ctx, match.ID); err != nil {
182
+ t.Fatalf("HydrateInnings second pass (%s) error: %v", match.ID, err)
183
+ }
184
+ if _, _, err := session.HydratePlayerMatchSummaries(ctx, match.ID); err != nil {
185
+ t.Fatalf("HydratePlayerMatchSummaries second pass (%s) error: %v", match.ID, err)
186
+ }
187
+ if _, _, err := session.HydrateDeliverySummaries(ctx, match.ID); err != nil {
188
+ t.Fatalf("HydrateDeliverySummaries second pass (%s) error: %v", match.ID, err)
189
+ }
190
+ }
191
+ secondDuration := time.Since(secondStart)
192
+ secondMetrics := session.Metrics()
193
+
194
+ if firstDuration > 5*time.Second {
195
+ t.Fatalf("expected limited multi-match sample to finish quickly, duration=%s", firstDuration)
196
+ }
197
+ if secondDuration >= firstDuration {
198
+ t.Fatalf("expected reused in-process pass to be faster, first=%s second=%s", firstDuration, secondDuration)
199
+ }
200
+ if secondMetrics.ResolveCacheMisses != firstMetrics.ResolveCacheMisses {
201
+ t.Fatalf("expected no additional resolve misses in second pass, first=%+v second=%+v", firstMetrics, secondMetrics)
202
+ }
203
+ }
204
+
205
+ func TestLivePhase14ScopedHydrationReuse(t *testing.T) {
206
+ t.Parallel()
207
+ requireLiveMatrix(t)
208
+
209
+ service, err := NewHistoricalHydrationService(HistoricalHydrationServiceConfig{})
210
+ if err != nil {
211
+ t.Fatalf("NewHistoricalHydrationService error: %v", err)
212
+ }
213
+ defer func() {
214
+ _ = service.Close()
215
+ }()
216
+
217
+ ctx, cancel := context.WithTimeout(context.Background(), 75*time.Second)
218
+ defer cancel()
219
+
220
+ session, err := service.BeginScope(ctx, HistoricalScopeOptions{
221
+ LeagueQuery: "19138",
222
+ MatchLimit: 2,
223
+ })
224
+ if err != nil {
225
+ if isLive503(err) {
226
+ t.Skipf("skipping scoped traversal after transient 503: %v", err)
227
+ }
228
+ t.Fatalf("BeginScope live error: %v", err)
229
+ }
230
+
231
+ matches := session.ScopedMatches()
232
+ if len(matches) == 0 {
233
+ t.Skip("skipping live scoped hydration: no matches found for selected scope")
234
+ }
235
+ matchID := matches[0].ID
236
+
237
+ if _, warnings, err := session.HydrateMatchSummaries(ctx); err != nil {
238
+ if isLive503(err) {
239
+ t.Skipf("skipping live match summary hydration after transient 503: %v", err)
240
+ }
241
+ t.Fatalf("HydrateMatchSummaries live error: %v", err)
242
+ } else if hasLive503Warning(warnings) {
243
+ t.Skipf("skipping live run after transient 503 warnings: %v", warnings)
244
+ }
245
+
246
+ if _, warnings, err := session.HydrateInnings(ctx, matchID); err != nil {
247
+ if isLive503(err) {
248
+ t.Skipf("skipping live innings hydration after transient 503: %v", err)
249
+ }
250
+ t.Fatalf("HydrateInnings live error: %v", err)
251
+ } else if hasLive503Warning(warnings) {
252
+ t.Skipf("skipping live run after transient 503 innings warnings: %v", warnings)
253
+ }
254
+
255
+ if _, warnings, err := session.HydratePlayerMatchSummaries(ctx, matchID); err != nil {
256
+ if isLive503(err) {
257
+ t.Skipf("skipping live player hydration after transient 503: %v", err)
258
+ }
259
+ t.Fatalf("HydratePlayerMatchSummaries live error: %v", err)
260
+ } else if hasLive503Warning(warnings) {
261
+ t.Skipf("skipping live run after transient 503 player warnings: %v", warnings)
262
+ }
263
+
264
+ if _, warnings, err := session.HydrateDeliverySummaries(ctx, matchID); err != nil {
265
+ if isLive503(err) {
266
+ t.Skipf("skipping live delivery hydration after transient 503: %v", err)
267
+ }
268
+ t.Fatalf("HydrateDeliverySummaries live error: %v", err)
269
+ } else if hasLive503Warning(warnings) {
270
+ t.Skipf("skipping live run after transient 503 delivery warnings: %v", warnings)
271
+ }
272
+
273
+ firstMetrics := session.Metrics()
274
+
275
+ if _, _, err := session.HydrateMatchSummaries(ctx); err != nil {
276
+ t.Fatalf("HydrateMatchSummaries live second pass error: %v", err)
277
+ }
278
+ if _, _, err := session.HydrateInnings(ctx, matchID); err != nil {
279
+ t.Fatalf("HydrateInnings live second pass error: %v", err)
280
+ }
281
+ if _, _, err := session.HydratePlayerMatchSummaries(ctx, matchID); err != nil {
282
+ t.Fatalf("HydratePlayerMatchSummaries live second pass error: %v", err)
283
+ }
284
+ if _, _, err := session.HydrateDeliverySummaries(ctx, matchID); err != nil {
285
+ t.Fatalf("HydrateDeliverySummaries live second pass error: %v", err)
286
+ }
287
+
288
+ secondMetrics := session.Metrics()
289
+ if secondMetrics.ResolveCacheMisses != firstMetrics.ResolveCacheMisses {
290
+ t.Fatalf("expected no new resolve misses in second live pass, first=%+v second=%+v", firstMetrics, secondMetrics)
291
+ }
292
+ if secondMetrics.DomainCacheHits <= firstMetrics.DomainCacheHits {
293
+ t.Fatalf("expected domain cache hits to increase in second live pass, first=%+v second=%+v", firstMetrics, secondMetrics)
294
+ }
295
+ }
296
+
297
+ type phase14HistoricalHarness struct {
298
+ service *HistoricalHydrationService
299
+
300
+ mu sync.Mutex
301
+ counts map[string]int
302
+ }
303
+
304
+ func (h *phase14HistoricalHarness) requestCounts() map[string]int {
305
+ h.mu.Lock()
306
+ defer h.mu.Unlock()
307
+
308
+ out := make(map[string]int, len(h.counts))
309
+ for path, count := range h.counts {
310
+ out[path] = count
311
+ }
312
+ return out
313
+ }
314
+
315
+ func newPhase14HistoricalHarness(t *testing.T, latency time.Duration) *phase14HistoricalHarness {
316
+ t.Helper()
317
+
318
+ h := &phase14HistoricalHarness{counts: map[string]int{}}
319
+
320
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
321
+ h.mu.Lock()
322
+ h.counts[r.URL.Path]++
323
+ h.mu.Unlock()
324
+
325
+ if latency > 0 {
326
+ time.Sleep(latency)
327
+ }
328
+
329
+ base := "http://" + r.Host + "/v2/sports/cricket"
330
+ path := r.URL.Path
331
+
332
+ switch path {
333
+ case "/v2/sports/cricket/events":
334
+ _, _ = w.Write([]byte(`{"count":0,"pageIndex":1,"pageSize":25,"pageCount":0,"items":[]}`))
335
+ case "/v2/sports/cricket/leagues":
336
+ _, _ = w.Write([]byte(`{"count":1,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"` + base + `/leagues/19138"}]}`))
337
+ case "/v2/sports/cricket/leagues/19138":
338
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/leagues/19138","id":"19138","name":"Mirwais Nika Provincial 3-Day","slug":"19138","events":{"$ref":"` + base + `/leagues/19138/events"},"seasons":{"$ref":"` + base + `/leagues/19138/seasons"}}`))
339
+ case "/v2/sports/cricket/leagues/19138/events":
340
+ _, _ = w.Write([]byte(`{"count":3,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"` + base + `/leagues/19138/events/1529474"},{"$ref":"` + base + `/leagues/19138/events/1529475"},{"$ref":"` + base + `/leagues/19138/events/1529476"}]}`))
341
+ case "/v2/sports/cricket/leagues/19138/seasons":
342
+ _, _ = w.Write([]byte(`{"count":2,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"` + base + `/leagues/19138/seasons/2025"},{"$ref":"` + base + `/leagues/19138/seasons/2024"}]}`))
343
+ case "/v2/sports/cricket/leagues/19138/seasons/2025":
344
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/leagues/19138/seasons/2025","id":"2025","year":2025,"types":{"$ref":"` + base + `/leagues/19138/seasons/2025/types"}}`))
345
+ case "/v2/sports/cricket/leagues/19138/seasons/2024":
346
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/leagues/19138/seasons/2024","id":"2024","year":2024,"types":{"$ref":"` + base + `/leagues/19138/seasons/2024/types"}}`))
347
+ case "/v2/sports/cricket/leagues/19138/seasons/2025/types":
348
+ _, _ = w.Write([]byte(`{"count":1,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"` + base + `/leagues/19138/seasons/2025/types/1"}]}`))
349
+ case "/v2/sports/cricket/leagues/19138/seasons/2024/types":
350
+ _, _ = w.Write([]byte(`{"count":1,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"` + base + `/leagues/19138/seasons/2024/types/1"}]}`))
351
+ case "/v2/sports/cricket/leagues/19138/seasons/2025/types/1":
352
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/leagues/19138/seasons/2025/types/1","id":"1","name":"Regular Season","abbreviation":"RS","startDate":"2025-01-01","endDate":"2025-12-31","groups":{"$ref":"` + base + `/leagues/19138/seasons/2025/types/1/groups"}}`))
353
+ case "/v2/sports/cricket/leagues/19138/seasons/2024/types/1":
354
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/leagues/19138/seasons/2024/types/1","id":"1","name":"Regular Season","abbreviation":"RS","startDate":"2024-01-01","endDate":"2024-12-31","groups":{"$ref":"` + base + `/leagues/19138/seasons/2024/types/1/groups"}}`))
355
+ case "/v2/sports/cricket/leagues/19138/seasons/2025/types/1/groups":
356
+ _, _ = w.Write([]byte(`{"count":1,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"` + base + `/leagues/19138/seasons/2025/types/1/groups/1"}]}`))
357
+ case "/v2/sports/cricket/leagues/19138/seasons/2024/types/1/groups":
358
+ _, _ = w.Write([]byte(`{"count":1,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"` + base + `/leagues/19138/seasons/2024/types/1/groups/1"}]}`))
359
+ case "/v2/sports/cricket/leagues/19138/seasons/2025/types/1/groups/1":
360
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/leagues/19138/seasons/2025/types/1/groups/1","id":"1","name":"Group A","abbreviation":"A","standings":{"$ref":"` + base + `/leagues/19138/seasons/2025/types/1/groups/1/standings"}}`))
361
+ case "/v2/sports/cricket/leagues/19138/seasons/2024/types/1/groups/1":
362
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/leagues/19138/seasons/2024/types/1/groups/1","id":"1","name":"Group A","abbreviation":"A","standings":{"$ref":"` + base + `/leagues/19138/seasons/2024/types/1/groups/1/standings"}}`))
363
+ case "/v2/sports/cricket/leagues/19138/seasons/2025/types/1/groups/1/standings":
364
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/leagues/19138/seasons/2025/types/1/groups/1/standings","items":[{"$ref":"` + base + `/leagues/19138/seasons/2025/types/1/groups/1/standings/1"}]}`))
365
+ case "/v2/sports/cricket/leagues/19138/seasons/2024/types/1/groups/1/standings":
366
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/leagues/19138/seasons/2024/types/1/groups/1/standings","items":[{"$ref":"` + base + `/leagues/19138/seasons/2024/types/1/groups/1/standings/1"}]}`))
367
+ case "/v2/sports/cricket/leagues/19138/seasons/2025/types/1/groups/1/standings/1":
368
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/leagues/19138/seasons/2025/types/1/groups/1/standings/1","id":"1","standings":[{"team":{"$ref":"` + base + `/teams/789643","id":"789643","displayName":"Group Team 1","shortDisplayName":"GT1"}},{"team":{"$ref":"` + base + `/teams/789644","id":"789644","displayName":"Group Team 2","shortDisplayName":"GT2"}}]}`))
369
+ case "/v2/sports/cricket/leagues/19138/seasons/2024/types/1/groups/1/standings/1":
370
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/leagues/19138/seasons/2024/types/1/groups/1/standings/1","id":"1","standings":[]}`))
371
+ case "/v2/sports/cricket/leagues/19138/events/1529474":
372
+ _, _ = w.Write([]byte(phase14EventPayload(base, "1529474", "2025-04-10T10:00:00Z", "789643", "789644", "Season Match A")))
373
+ case "/v2/sports/cricket/leagues/19138/events/1529475":
374
+ _, _ = w.Write([]byte(phase14EventPayload(base, "1529475", "2025-05-12T10:00:00Z", "789650", "789651", "Season Match B")))
375
+ case "/v2/sports/cricket/leagues/19138/events/1529476":
376
+ _, _ = w.Write([]byte(phase14EventPayload(base, "1529476", "2024-05-12T10:00:00Z", "789660", "789661", "Older Match")))
377
+ default:
378
+ if strings.HasPrefix(path, "/v2/sports/cricket/leagues/19138/events/") {
379
+ if handled := writePhase14MatchSubresource(w, base, path); handled {
380
+ return
381
+ }
382
+ }
383
+ if strings.HasPrefix(path, "/v2/sports/cricket/teams/") {
384
+ teamID := strings.TrimPrefix(path, "/v2/sports/cricket/teams/")
385
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/teams/` + teamID + `","id":"` + teamID + `","displayName":"Team ` + teamID + `","shortDisplayName":"T` + teamID + `","abbreviation":"T` + teamID + `"}`))
386
+ return
387
+ }
388
+ http.NotFound(w, r)
389
+ }
390
+ }))
391
+ t.Cleanup(server.Close)
392
+
393
+ index, err := OpenEntityIndex(filepath.Join(t.TempDir(), "resolver-index.json"))
394
+ if err != nil {
395
+ t.Fatalf("OpenEntityIndex error: %v", err)
396
+ }
397
+ if err := index.Upsert(IndexedEntity{
398
+ Kind: EntityLeague,
399
+ ID: "19138",
400
+ Ref: "/leagues/19138",
401
+ Name: "Mirwais Nika Provincial 3-Day",
402
+ ShortName: "19138",
403
+ Aliases: []string{"Mirwais Nika", "19138"},
404
+ UpdatedAt: time.Now().UTC(),
405
+ }); err != nil {
406
+ t.Fatalf("index upsert error: %v", err)
407
+ }
408
+ index.SetLastEventsSeedAt(time.Now().UTC())
409
+
410
+ client, err := NewClient(Config{BaseURL: server.URL + "/v2/sports/cricket"})
411
+ if err != nil {
412
+ t.Fatalf("NewClient error: %v", err)
413
+ }
414
+
415
+ resolver, err := NewResolver(ResolverConfig{
416
+ Client: client,
417
+ Index: index,
418
+ EventSeedTTL: 24 * time.Hour,
419
+ Now: func() time.Time { return time.Now().UTC() },
420
+ })
421
+ if err != nil {
422
+ t.Fatalf("NewResolver error: %v", err)
423
+ }
424
+ t.Cleanup(func() {
425
+ _ = resolver.Close()
426
+ })
427
+
428
+ service, err := NewHistoricalHydrationService(HistoricalHydrationServiceConfig{
429
+ Client: client,
430
+ Resolver: resolver,
431
+ })
432
+ if err != nil {
433
+ t.Fatalf("NewHistoricalHydrationService error: %v", err)
434
+ }
435
+ t.Cleanup(func() {
436
+ _ = service.Close()
437
+ })
438
+
439
+ h.service = service
440
+ return h
441
+ }
442
+
443
+ func phase14EventPayload(base, id, date, teamA, teamB, label string) string {
444
+ competitionRef := base + "/leagues/19138/events/" + id + "/competitions/" + id
445
+ teamABase := competitionRef + "/competitors/" + teamA
446
+ teamBBase := competitionRef + "/competitors/" + teamB
447
+
448
+ return fmt.Sprintf(`{"$ref":"%s/leagues/19138/events/%s","id":"%s","description":"%s","shortDescription":"%s","date":"%s","competitions":[{"$ref":"%s","id":"%s","description":"%s","shortDescription":"%s","date":"%s","status":{"$ref":"%s/status"},"details":{"$ref":"%s/details"},"competitors":[{"$ref":"%s","id":"%s","team":{"$ref":"%s/teams/%s","id":"%s","displayName":"Team %s","shortDisplayName":"T%s"},"score":{"$ref":"%s/scores"},"roster":{"$ref":"%s/roster"},"linescores":{"$ref":"%s/linescores"}},{"$ref":"%s","id":"%s","team":{"$ref":"%s/teams/%s","id":"%s","displayName":"Team %s","shortDisplayName":"T%s"},"score":{"$ref":"%s/scores"},"roster":{"$ref":"%s/roster"},"linescores":{"$ref":"%s/linescores"}}]}]}`,
449
+ base, id, id, label, label, date,
450
+ competitionRef, id, label, label, date,
451
+ competitionRef, competitionRef,
452
+ teamABase, teamA, base, teamA, teamA, teamA, teamA, teamABase, teamABase, teamABase,
453
+ teamBBase, teamB, base, teamB, teamB, teamB, teamB, teamBBase, teamBBase, teamBBase,
454
+ )
455
+ }
456
+
457
+ func writePhase14MatchSubresource(w http.ResponseWriter, base, path string) bool {
458
+ trimmed := strings.TrimPrefix(path, "/v2/sports/cricket/")
459
+ segments := strings.Split(trimmed, "/")
460
+ if len(segments) < 7 {
461
+ return false
462
+ }
463
+
464
+ leagueID := segments[1]
465
+ eventID := segments[3]
466
+ competitionID := segments[5]
467
+ if leagueID != "19138" || eventID != competitionID {
468
+ return false
469
+ }
470
+
471
+ baseCompetition := base + "/leagues/19138/events/" + eventID + "/competitions/" + competitionID
472
+ subroute := strings.Join(segments[6:], "/")
473
+
474
+ switch subroute {
475
+ case "status":
476
+ _, _ = w.Write([]byte(`{"$ref":"` + baseCompetition + `/status","summary":"In Progress","longSummary":"In Progress","type":{"state":"in","detail":"live","shortDetail":"live"}}`))
477
+ return true
478
+ case "details":
479
+ _, _ = w.Write([]byte(`{"count":1,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"` + baseCompetition + `/details/110"}]}`))
480
+ return true
481
+ case "details/110":
482
+ _, _ = w.Write([]byte(`{"$ref":"` + baseCompetition + `/details/110","id":"110","period":1,"periodText":"1","over":{"number":1,"ball":1},"scoreValue":1,"shortText":"single","text":"single to midwicket","homeScore":"1/0","awayScore":"0/0","batsman":{"athlete":{"$ref":"` + base + `/athletes/1361257"}},"bowler":{"athlete":{"$ref":"` + base + `/athletes/1436502"}},"dismissal":{"dismissal":false}}`))
483
+ return true
484
+ }
485
+
486
+ if len(segments) < 9 || segments[6] != "competitors" {
487
+ return false
488
+ }
489
+
490
+ competitorID := segments[7]
491
+ competitorBase := baseCompetition + "/competitors/" + competitorID
492
+ playerID := phase14PlayerIDForTeam(competitorID)
493
+ subroute = strings.Join(segments[8:], "/")
494
+
495
+ switch subroute {
496
+ case "scores":
497
+ _, _ = w.Write([]byte(`{"$ref":"` + competitorBase + `/scores","displayValue":"120/3","value":"120/3"}`))
498
+ return true
499
+ case "roster":
500
+ _, _ = w.Write([]byte(`{"$ref":"` + competitorBase + `/roster","entries":[{"playerId":"` + playerID + `","athlete":{"$ref":"` + base + `/athletes/` + playerID + `","displayName":"Player ` + playerID + `"},"statistics":{"$ref":"` + competitorBase + `/roster/` + playerID + `/statistics/0"},"linescores":{"$ref":"` + competitorBase + `/roster/` + playerID + `/linescores"}}]}`))
501
+ return true
502
+ case "roster/" + playerID + "/statistics/0":
503
+ _, _ = w.Write([]byte(`{"$ref":"` + competitorBase + `/roster/` + playerID + `/statistics/0","athlete":{"$ref":"` + base + `/athletes/` + playerID + `"},"competition":{"$ref":"` + baseCompetition + `"},"splits":{"id":"0","name":"Total","abbreviation":"Total","categories":[{"name":"general","displayName":"General","stats":[{"name":"runs","displayValue":"33","value":33},{"name":"ballsFaced","displayValue":"60","value":60},{"name":"dots","displayValue":"18","value":18},{"name":"economyRate","displayValue":"4.50","value":4.5},{"name":"dismissalName","displayValue":"caught","value":"caught"},{"name":"dismissalCard","displayValue":"c","value":"c"}]}]}}`))
504
+ return true
505
+ case "roster/" + playerID + "/linescores":
506
+ _, _ = w.Write([]byte(`{"count":1,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"` + competitorBase + `/roster/` + playerID + `/linescores/1/1","period":1,"value":1,"isBatting":true,"statistics":{"$ref":"` + competitorBase + `/roster/` + playerID + `/linescores/1/1/statistics/0"}}]}`))
507
+ return true
508
+ case "linescores":
509
+ _, _ = w.Write([]byte(`{"count":1,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"` + competitorBase + `/linescores/1/1","period":1,"value":1,"runs":120,"wickets":3,"score":"120/3","statistics":{"$ref":"` + competitorBase + `/linescores/1/1/statistics/0"},"partnerships":{"$ref":"` + competitorBase + `/linescores/1/1/partnerships"},"fow":{"$ref":"` + competitorBase + `/linescores/1/1/fow"}}]}`))
510
+ return true
511
+ case "linescores/1/1/statistics/0":
512
+ _, _ = w.Write([]byte(`{"$ref":"` + competitorBase + `/linescores/1/1/statistics/0","splits":{"overs":[{"number":1,"runs":6,"wicket":[{"number":1,"fow":"10/1","over":"1.4","fowType":"caught","runs":10,"ballsFaced":8,"strikeRate":125,"dismissalCard":"c","shortText":"c fielder","details":{"$ref":"` + baseCompetition + `/details/110","shortText":"single","text":"single to midwicket"}}]}]}}`))
513
+ return true
514
+ case "linescores/1/1/partnerships":
515
+ _, _ = w.Write([]byte(`{"count":0,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[]}`))
516
+ return true
517
+ case "linescores/1/1/fow":
518
+ _, _ = w.Write([]byte(`{"count":0,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[]}`))
519
+ return true
520
+ default:
521
+ return false
522
+ }
523
+ }
524
+
525
+ func phase14PlayerIDForTeam(teamID string) string {
526
+ switch teamID {
527
+ case "789643":
528
+ return "1361257"
529
+ case "789644":
530
+ return "1361258"
531
+ case "789650":
532
+ return "1361260"
533
+ case "789651":
534
+ return "1361261"
535
+ case "789660":
536
+ return "1361262"
537
+ case "789661":
538
+ return "1361263"
539
+ default:
540
+ return "1361200"
541
+ }
542
+ }