@@ -181,12 +181,14 @@ func (s *ShardRoutingSuite) SetupSuite() {
181181 require .NoError (t , err )
182182 warmResp .Body .Close ()
183183
184- // --- Discover shard mappings for the new index ---
184+ // --- Discover shard mappings and verify end-to-end shard-exact routing ---
185185 // DiscoverNodes fetches /_cat/shards and populates the router's shard
186- // catalog. Poll until:
186+ // catalog. Poll until all three conditions hold :
187187 // 1. The shard map for the test index has all shards populated
188- // 2. No connections have lcNeedsCatUpdate set (which would cause
188+ // 2. No connections have needsCatUpdate set (which would cause
189189 // shardExactCandidates to filter them out)
190+ // 3. An actual probe search gets ShardExactMatch=true through the
191+ // observer, proving the full pipeline has converged
190192 routerPolicy , ok := s .transport .router .(Policy )
191193 require .True (t , ok , "router does not implement Policy" )
192194 s .cache = findRouterCache (routerPolicy )
@@ -218,9 +220,26 @@ func (s *ShardRoutingSuite) SetupSuite() {
218220 }
219221 }
220222 }
221- return true
223+
224+ // End-to-end probe: issue a routed search and verify ShardExactMatch.
225+ // Structural checks above can pass while shardExactCandidates still
226+ // returns nil (e.g. connection Names haven't converged into sorted
227+ // snapshots). The probe tests the full pipeline in a single assertion.
228+ s .obs .reset ()
229+ probeURL := apiURL ([]string {"/" , s .index , "_search" }, map [string ]string {"routing" : "probe-canary" })
230+ probeReq , _ := http .NewRequestWithContext (s .ctx , http .MethodPost ,
231+ probeURL ,
232+ bytes .NewReader ([]byte (`{"query":{"match_all":{}},"size":0}` )))
233+ probeReq .Header .Set ("Content-Type" , "application/json" )
234+ probeResp , probeErr := s .transport .Perform (probeReq )
235+ if probeErr != nil {
236+ return false
237+ }
238+ probeResp .Body .Close ()
239+ probeEvent := s .obs .lastEvent ()
240+ return probeEvent != nil && probeEvent .ShardExactMatch
222241 }, 30 * time .Second , 500 * time .Millisecond ,
223- "shard catalog for index %q not ready after repeated DiscoverNodes" , s .index )
242+ "shard-exact routing for index %q not working after repeated DiscoverNodes" , s .index )
224243 s .obs .reset ()
225244}
226245
@@ -310,13 +329,26 @@ func (s *ShardRoutingSuite) TestDocIDAsDefaultRouting() {
310329func (s * ShardRoutingSuite ) TestFullPipelineRouting () {
311330 t := s .T ()
312331
313- // Refresh the shard catalog and clear any needsCatUpdate flags that
314- // previous test methods (e.g., TestDocIDAsDefaultRouting) may have set
315- // via transport errors.
332+ // Refresh the shard catalog and verify end-to-end shard-exact routing
333+ // works before running assertions. Previous test methods (e.g.,
334+ // TestDocIDAsDefaultRouting) may have set needsCatUpdate flags via
335+ // transport errors, and the resulting DiscoverNodes can replace the
336+ // shard map with incomplete data if shards are still settling. Poll
337+ // until an actual probe search gets ShardExactMatch=true through the
338+ // observer — this tests the full pipeline (shard map populated,
339+ // routing_num_shards fetched, connection names converged, no
340+ // connections filtered by needsCatUpdate) in a single assertion.
316341 require .Eventually (t , func () bool {
317342 if err := s .transport .DiscoverNodes (s .ctx ); err != nil {
318343 return false
319344 }
345+ // Quick structural check: shard map must have all shards populated.
346+ slot := s .cache .getOrCreate (s .index )
347+ sm := slot .shardMap .Load ()
348+ if sm == nil || sm .RoutingNumShards == 0 || len (sm .Shards ) < s .numShards {
349+ return false
350+ }
351+ // Check no connections have needsCatUpdate set.
320352 s .transport .mu .RLock ()
321353 pool := s .transport .mu .connectionPool
322354 s .transport .mu .RUnlock ()
@@ -327,8 +359,22 @@ func (s *ShardRoutingSuite) TestFullPipelineRouting() {
327359 }
328360 }
329361 }
330- return true
331- }, 30 * time .Second , 500 * time .Millisecond , "connections still have needsCatUpdate after DiscoverNodes" )
362+ // End-to-end probe: issue a routed search and verify ShardExactMatch.
363+ s .obs .reset ()
364+ probeURL := apiURL ([]string {"/" , s .index , "_search" }, map [string ]string {"routing" : "probe-canary" })
365+ probeReq , _ := http .NewRequestWithContext (s .ctx , http .MethodPost ,
366+ probeURL ,
367+ bytes .NewReader ([]byte (`{"query":{"match_all":{}},"size":0}` )))
368+ probeReq .Header .Set ("Content-Type" , "application/json" )
369+ probeResp , probeErr := s .transport .Perform (probeReq )
370+ if probeErr != nil {
371+ return false
372+ }
373+ probeResp .Body .Close ()
374+ probeEvent := s .obs .lastEvent ()
375+ return probeEvent != nil && probeEvent .ShardExactMatch
376+ }, 30 * time .Second , 500 * time .Millisecond ,
377+ "shard-exact routing for index %q not working after repeated DiscoverNodes" , s .index )
332378 s .obs .reset ()
333379
334380 type groundTruth struct {
0 commit comments