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,203 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "strings"
7
+ "testing"
8
+ "time"
9
+ )
10
+
11
+ func TestLiveMatchesTraversalFromEvents(t *testing.T) {
12
+ t.Parallel()
13
+ requireLiveMatrix(t)
14
+
15
+ service, err := NewMatchService(MatchServiceConfig{})
16
+ if err != nil {
17
+ t.Fatalf("NewMatchService error: %v", err)
18
+ }
19
+ defer func() {
20
+ _ = service.Close()
21
+ }()
22
+
23
+ ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
24
+ defer cancel()
25
+
26
+ result, err := service.List(ctx, MatchListOptions{Limit: 5})
27
+ if err != nil {
28
+ t.Fatalf("MatchService.List error: %v", err)
29
+ }
30
+
31
+ if result.Status == ResultStatusError {
32
+ if result.Error != nil && result.Error.StatusCode == 503 {
33
+ t.Skipf("skipping live traversal after persistent 503: %s", result.Message)
34
+ }
35
+ t.Fatalf("unexpected error result: %+v", result)
36
+ }
37
+ if !strings.Contains(result.RequestedRef, "/events") {
38
+ t.Fatalf("expected list traversal to begin from /events, requestedRef=%q", result.RequestedRef)
39
+ }
40
+
41
+ if len(result.Items) == 0 {
42
+ if hasLive503Warning(result.Warnings) {
43
+ t.Skipf("skipping after transient 503 warnings: %v", result.Warnings)
44
+ }
45
+ t.Fatalf("expected at least one match from /events traversal")
46
+ }
47
+
48
+ first, ok := result.Items[0].(Match)
49
+ if !ok {
50
+ t.Fatalf("expected first result item to be Match, got %T", result.Items[0])
51
+ }
52
+ if first.LeagueID == "" || first.EventID == "" || first.CompetitionID == "" {
53
+ t.Fatalf("expected match IDs for drill-down use, got %+v", first)
54
+ }
55
+ if len(first.Teams) == 0 {
56
+ t.Fatalf("expected match teams in live traversal output")
57
+ }
58
+ if strings.TrimSpace(first.Date) == "" {
59
+ t.Fatalf("expected match date in live traversal output")
60
+ }
61
+ }
62
+
63
+ func TestLiveMatchDrillDownRoutes(t *testing.T) {
64
+ t.Parallel()
65
+ requireLiveMatrix(t)
66
+
67
+ client, err := NewClient(Config{
68
+ Timeout: 12 * time.Second,
69
+ MaxRetries: 3,
70
+ })
71
+ if err != nil {
72
+ t.Fatalf("NewClient error: %v", err)
73
+ }
74
+
75
+ routes := []struct {
76
+ name string
77
+ ref string
78
+ }{
79
+ {name: "details", ref: "/leagues/19138/events/1529474/competitions/1529474/details"},
80
+ {name: "plays", ref: "/leagues/19138/events/1529474/competitions/1529474/plays"},
81
+ {name: "matchcards", ref: "/leagues/11132/events/1527966/competitions/1527966/matchcards"},
82
+ {name: "situation", ref: "/leagues/19138/events/1529474/competitions/1529474/situation"},
83
+ }
84
+
85
+ for _, tc := range routes {
86
+ tc := tc
87
+ t.Run(tc.name, func(t *testing.T) {
88
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
89
+ defer cancel()
90
+
91
+ resolved, err := client.ResolveRefChain(ctx, tc.ref)
92
+ if err != nil {
93
+ if isLive503(err) {
94
+ t.Skipf("skipping %s after transient 503: %v", tc.name, err)
95
+ }
96
+ t.Fatalf("ResolveRefChain(%q) error: %v", tc.ref, err)
97
+ }
98
+
99
+ switch tc.name {
100
+ case "details", "plays":
101
+ page, err := DecodePage[Ref](resolved.Body)
102
+ if err != nil {
103
+ t.Fatalf("DecodePage %s error: %v", tc.name, err)
104
+ }
105
+ if len(page.Items) == 0 {
106
+ t.Fatalf("expected at least one %s item ref", tc.name)
107
+ }
108
+ case "matchcards":
109
+ var payload map[string]any
110
+ if err := json.Unmarshal(resolved.Body, &payload); err != nil {
111
+ t.Fatalf("unmarshal matchcards payload: %v", err)
112
+ }
113
+ items := mapSliceField(payload, "items")
114
+ if len(items) == 0 {
115
+ t.Fatalf("expected matchcards items")
116
+ }
117
+ headlines := map[string]bool{}
118
+ for _, item := range items {
119
+ headline := strings.ToLower(strings.TrimSpace(stringField(item, "headline")))
120
+ if headline != "" {
121
+ headlines[headline] = true
122
+ }
123
+ }
124
+ if !headlines["batting"] || !headlines["bowling"] {
125
+ t.Fatalf("expected matchcards to include batting and bowling sections, got %v", headlines)
126
+ }
127
+ case "situation":
128
+ var payload map[string]any
129
+ if err := json.Unmarshal(resolved.Body, &payload); err != nil {
130
+ t.Fatalf("unmarshal situation payload: %v", err)
131
+ }
132
+ if len(payload) == 0 {
133
+ t.Fatalf("expected non-empty situation payload")
134
+ }
135
+ if _, ok := payload["$ref"]; !ok {
136
+ t.Fatalf("expected situation payload to include $ref")
137
+ }
138
+ }
139
+ })
140
+ }
141
+ }
142
+
143
+ func TestLiveMatchInningsDepthRoutes(t *testing.T) {
144
+ t.Parallel()
145
+ requireLiveMatrix(t)
146
+
147
+ client, err := NewClient(Config{
148
+ Timeout: 12 * time.Second,
149
+ MaxRetries: 3,
150
+ })
151
+ if err != nil {
152
+ t.Fatalf("NewClient error: %v", err)
153
+ }
154
+
155
+ routes := []struct {
156
+ name string
157
+ ref string
158
+ keys []string
159
+ }{
160
+ {
161
+ name: "linescores",
162
+ ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/linescores",
163
+ keys: []string{"items", "count"},
164
+ },
165
+ {
166
+ name: "period-statistics",
167
+ ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/linescores/1/1/statistics/0",
168
+ keys: []string{"splits", "team"},
169
+ },
170
+ {
171
+ name: "partnerships",
172
+ ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/linescores/1/1/partnerships",
173
+ keys: []string{"items", "count"},
174
+ },
175
+ {
176
+ name: "fow",
177
+ ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/linescores/1/1/fow",
178
+ keys: []string{"items", "count"},
179
+ },
180
+ }
181
+
182
+ for _, tc := range routes {
183
+ tc := tc
184
+ t.Run(tc.name, func(t *testing.T) {
185
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
186
+ defer cancel()
187
+
188
+ resolved, err := client.ResolveRefChain(ctx, tc.ref)
189
+ if err != nil {
190
+ if isLive503(err) {
191
+ t.Skipf("skipping %s after transient 503: %v", tc.name, err)
192
+ }
193
+ t.Fatalf("ResolveRefChain(%q) error: %v", tc.ref, err)
194
+ }
195
+
196
+ var payload map[string]any
197
+ if err := json.Unmarshal(resolved.Body, &payload); err != nil {
198
+ t.Fatalf("unmarshal %s payload: %v", tc.name, err)
199
+ }
200
+ requireAnyKey(t, payload, tc.keys...)
201
+ })
202
+ }
203
+ }
@@ -0,0 +1,118 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "os"
9
+ "testing"
10
+ "time"
11
+ )
12
+
13
+ const (
14
+ liveMatrixEnv = "CRICINFO_LIVE_MATRIX"
15
+ liveMatrixFamiliesEnv = "CRICINFO_LIVE_FAMILIES"
16
+ )
17
+
18
+ func TestLiveFixtureMatrixByFamily(t *testing.T) {
19
+ t.Parallel()
20
+ requireLiveMatrix(t)
21
+
22
+ selected, err := ParseFixtureFamilies(os.Getenv(liveMatrixFamiliesEnv))
23
+ if err != nil {
24
+ t.Fatalf("ParseFixtureFamilies error: %v", err)
25
+ }
26
+
27
+ matrix := FixtureMatrix()
28
+ matrix = FilterFixtureMatrixByFamily(matrix, selected)
29
+ matrix = LiveProbeMatrix(matrix)
30
+
31
+ if len(matrix) == 0 {
32
+ t.Fatalf("no live probes selected from fixture matrix")
33
+ }
34
+
35
+ requiredFamilies := map[FixtureFamily]struct{}{}
36
+ for _, spec := range matrix {
37
+ requiredFamilies[spec.Family] = struct{}{}
38
+ }
39
+ for family := range selected {
40
+ if _, ok := requiredFamilies[family]; !ok {
41
+ t.Fatalf("selected family %q has no live probe in matrix", family)
42
+ }
43
+ }
44
+
45
+ client, err := NewClient(Config{
46
+ Timeout: 12 * time.Second,
47
+ MaxRetries: 3,
48
+ })
49
+ if err != nil {
50
+ t.Fatalf("NewClient error: %v", err)
51
+ }
52
+
53
+ for _, spec := range matrix {
54
+ spec := spec
55
+ t.Run(fmt.Sprintf("%s/%s", spec.Family, spec.Name), func(t *testing.T) {
56
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
57
+ defer cancel()
58
+
59
+ resolved, err := client.ResolveRefChain(ctx, spec.Ref)
60
+ if err != nil {
61
+ if isLive503(err) {
62
+ t.Skipf("skipping after persistent 503 for %s: %v", spec.Ref, err)
63
+ }
64
+ t.Fatalf("ResolveRefChain(%q) error: %v", spec.Ref, err)
65
+ }
66
+
67
+ var payload map[string]any
68
+ if err := json.Unmarshal(resolved.Body, &payload); err != nil {
69
+ t.Fatalf("unmarshal %q: %v", spec.Ref, err)
70
+ }
71
+ if len(payload) == 0 {
72
+ t.Fatalf("empty payload from %q", spec.Ref)
73
+ }
74
+
75
+ validateLiveFamilyPayload(t, spec.Family, payload)
76
+ })
77
+ }
78
+ }
79
+
80
+ func requireLiveMatrix(t *testing.T) {
81
+ t.Helper()
82
+ if os.Getenv(liveMatrixEnv) != "1" {
83
+ t.Skip("set CRICINFO_LIVE_MATRIX=1 to run live fixture matrix tests")
84
+ }
85
+ }
86
+
87
+ func isLive503(err error) bool {
88
+ var statusErr *HTTPStatusError
89
+ if errors.As(err, &statusErr) {
90
+ return statusErr.StatusCode == 503
91
+ }
92
+ return false
93
+ }
94
+
95
+ func validateLiveFamilyPayload(t *testing.T, family FixtureFamily, payload map[string]any) {
96
+ t.Helper()
97
+
98
+ switch family {
99
+ case FixtureFamilyRootDiscovery:
100
+ requireAnyKey(t, payload, "events", "leagues", "items")
101
+ case FixtureFamilyMatchesCompetition:
102
+ requireAnyKey(t, payload, "competitors", "status", "date")
103
+ case FixtureFamilyDetailsPlays:
104
+ requireAnyKey(t, payload, "items", "count")
105
+ case FixtureFamilyTeamCompetitor:
106
+ requireAnyKey(t, payload, "id", "team", "record")
107
+ case FixtureFamilyInningsDepth:
108
+ requireAnyKey(t, payload, "period", "runs", "wickets")
109
+ case FixtureFamilyPlayers:
110
+ requireAnyKey(t, payload, "id", "displayName", "fullName")
111
+ case FixtureFamilyLeagueSeason:
112
+ requireAnyKey(t, payload, "children", "items", "entries")
113
+ case FixtureFamilyAuxCompetitionMeta:
114
+ requireAnyKey(t, payload, "items", "count", "entries")
115
+ default:
116
+ t.Fatalf("unsupported family %q", family)
117
+ }
118
+ }
@@ -0,0 +1,122 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "testing"
7
+ "time"
8
+ )
9
+
10
+ func TestLivePlayerRoutes(t *testing.T) {
11
+ t.Parallel()
12
+ requireLiveMatrix(t)
13
+
14
+ client, err := NewClient(Config{
15
+ Timeout: 12 * time.Second,
16
+ MaxRetries: 3,
17
+ })
18
+ if err != nil {
19
+ t.Fatalf("NewClient error: %v", err)
20
+ }
21
+
22
+ routes := []struct {
23
+ name string
24
+ ref string
25
+ keys []string
26
+ }{
27
+ {
28
+ name: "athlete-profile",
29
+ ref: "/athletes/1361257",
30
+ keys: []string{"id", "displayName", "styles", "team"},
31
+ },
32
+ {
33
+ name: "athlete-news",
34
+ ref: "/athletes/253802/news",
35
+ keys: []string{"items", "count"},
36
+ },
37
+ {
38
+ name: "athlete-statistics",
39
+ ref: "/athletes/1361257/statistics",
40
+ keys: []string{"splits", "athlete"},
41
+ },
42
+ }
43
+
44
+ for _, tc := range routes {
45
+ tc := tc
46
+ t.Run(tc.name, func(t *testing.T) {
47
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
48
+ defer cancel()
49
+
50
+ resolved, err := client.ResolveRefChain(ctx, tc.ref)
51
+ if err != nil {
52
+ if isLive503(err) {
53
+ t.Skipf("skipping %s after transient 503: %v", tc.name, err)
54
+ }
55
+ t.Fatalf("ResolveRefChain(%q) error: %v", tc.ref, err)
56
+ }
57
+
58
+ var payload map[string]any
59
+ if err := json.Unmarshal(resolved.Body, &payload); err != nil {
60
+ t.Fatalf("unmarshal %s payload: %v", tc.name, err)
61
+ }
62
+ requireAnyKey(t, payload, tc.keys...)
63
+ })
64
+ }
65
+ }
66
+
67
+ func TestLivePlayerMatchContextRoutes(t *testing.T) {
68
+ t.Parallel()
69
+ requireLiveMatrix(t)
70
+
71
+ client, err := NewClient(Config{
72
+ Timeout: 12 * time.Second,
73
+ MaxRetries: 3,
74
+ })
75
+ if err != nil {
76
+ t.Fatalf("NewClient error: %v", err)
77
+ }
78
+
79
+ routes := []struct {
80
+ name string
81
+ ref string
82
+ keys []string
83
+ }{
84
+ {
85
+ name: "roster-player-statistics",
86
+ ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/roster/1361257/statistics/0",
87
+ keys: []string{"splits", "athlete", "competition"},
88
+ },
89
+ {
90
+ name: "roster-player-linescores",
91
+ ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/roster/1361257/linescores",
92
+ keys: []string{"items", "count"},
93
+ },
94
+ {
95
+ name: "roster-player-linescore-statistics",
96
+ ref: "/leagues/19138/events/1529474/competitions/1529474/competitors/789643/roster/1361257/linescores/1/1/statistics/0",
97
+ keys: []string{"splits", "athlete", "competition"},
98
+ },
99
+ }
100
+
101
+ for _, tc := range routes {
102
+ tc := tc
103
+ t.Run(tc.name, func(t *testing.T) {
104
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
105
+ defer cancel()
106
+
107
+ resolved, err := client.ResolveRefChain(ctx, tc.ref)
108
+ if err != nil {
109
+ if isLive503(err) {
110
+ t.Skipf("skipping %s after transient 503: %v", tc.name, err)
111
+ }
112
+ t.Fatalf("ResolveRefChain(%q) error: %v", tc.ref, err)
113
+ }
114
+
115
+ var payload map[string]any
116
+ if err := json.Unmarshal(resolved.Body, &payload); err != nil {
117
+ t.Fatalf("unmarshal %s payload: %v", tc.name, err)
118
+ }
119
+ requireAnyKey(t, payload, tc.keys...)
120
+ })
121
+ }
122
+ }
@@ -0,0 +1,86 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "context"
5
+ "strings"
6
+ "testing"
7
+ "time"
8
+ )
9
+
10
+ func TestLiveSearchDiscoveryAcrossEntityFamilies(t *testing.T) {
11
+ t.Parallel()
12
+ requireLiveMatrix(t)
13
+
14
+ resolver, err := NewResolver(ResolverConfig{
15
+ EventSeedTTL: time.Minute,
16
+ MaxEventSeed: 24,
17
+ IndexPath: t.TempDir() + "/live-search-index.json",
18
+ })
19
+ if err != nil {
20
+ t.Fatalf("NewResolver error: %v", err)
21
+ }
22
+ defer func() {
23
+ _ = resolver.Close()
24
+ }()
25
+
26
+ tests := []struct {
27
+ name string
28
+ kind EntityKind
29
+ query string
30
+ opts ResolveOptions
31
+ id string
32
+ }{
33
+ {name: "player", kind: EntityPlayer, query: "1361257", opts: ResolveOptions{Limit: 5}, id: "1361257"},
34
+ {name: "team", kind: EntityTeam, query: "789643", opts: ResolveOptions{Limit: 5}, id: "789643"},
35
+ {name: "league", kind: EntityLeague, query: "19138", opts: ResolveOptions{Limit: 5}, id: "19138"},
36
+ {name: "match", kind: EntityMatch, query: "1529474", opts: ResolveOptions{Limit: 5, LeagueID: "19138"}, id: "1529474"},
37
+ }
38
+
39
+ for _, tc := range tests {
40
+ tc := tc
41
+ t.Run(tc.name, func(t *testing.T) {
42
+ ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
43
+ defer cancel()
44
+
45
+ result, err := resolver.Search(ctx, tc.kind, tc.query, tc.opts)
46
+ if err != nil {
47
+ if isLiveSearchTransient503(err.Error()) {
48
+ t.Skipf("skipping %s search after transient 503: %v", tc.name, err)
49
+ }
50
+ t.Fatalf("Search error for %s: %v", tc.name, err)
51
+ }
52
+
53
+ if len(result.Entities) == 0 {
54
+ if hasLive503Warning(result.Warnings) {
55
+ t.Skipf("skipping %s search after transient 503 warnings: %v", tc.name, result.Warnings)
56
+ }
57
+ t.Fatalf("expected at least one %s result for query %q", tc.name, tc.query)
58
+ }
59
+
60
+ found := false
61
+ for _, entity := range result.Entities {
62
+ if entity.ID == tc.id {
63
+ found = true
64
+ break
65
+ }
66
+ }
67
+ if !found {
68
+ t.Fatalf("expected %s result set to contain id %s, got %+v", tc.name, tc.id, result.Entities)
69
+ }
70
+ })
71
+ }
72
+ }
73
+
74
+ func hasLive503Warning(warnings []string) bool {
75
+ for _, warning := range warnings {
76
+ if isLiveSearchTransient503(warning) {
77
+ return true
78
+ }
79
+ }
80
+ return false
81
+ }
82
+
83
+ func isLiveSearchTransient503(raw string) bool {
84
+ raw = strings.ToLower(strings.TrimSpace(raw))
85
+ return strings.Contains(raw, "status 503") || strings.Contains(raw, "503 service unavailable")
86
+ }