packages feed

bloodhound-1.0.0.0: tests/Test/NeuralClearCacheSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}

module Test.NeuralClearCacheSpec (spec) where

import Database.Bloodhound.OpenSearch3.Client qualified as OS3.Client
import Database.Bloodhound.OpenSearch3.Requests qualified as OS3Requests
import TestsUtils.Common
import TestsUtils.Import
import Prelude

-- | Pure endpoint-shape tests for the Neural Search plugin Clear Cache API
-- (@POST /_plugins/_neural/clear_cache\/{index}@) for OpenSearch 3. These
-- mirror 'Test.NeuralWarmupSpec's shape tests but assert the
-- @\/_plugins\/_neural\/clear_cache@ path, the POST method, and the
-- empty-object body. JSON decoding of the shared 'ShardsResult' response
-- is re-verified here on the clear-cache response shape
-- (@{\"_shards\":{...}}@). The wire shape for the neural clear-cache API
-- is live-verified against OS 3.7.0 in bead bloodhound-at5. The
-- neural-sparse clear-cache route is OS 3.x only: OS 1.3.x does not ship
-- the neural-search plugin (bloodhound-ie4j) and OS 2.19.5 does not
-- register the route or the sparse_vector field type (bloodhound-sh7o),
-- so neither OS1 nor OS2 ships a builder and no cross-backend parity
-- block is meaningful.
spec :: Spec
spec = describe "Neural Clear Cache API" $ do
  describe "clearNeuralCache endpoint shape (OpenSearch 3)" $ do
    it "POSTs to /_plugins/_neural/clear_cache/{index} when given an IndexName" $ do
      let req = OS3Requests.clearNeuralCache [[qqIndexName|my-index|]]
      bhRequestMethod req `shouldBe` "POST"
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["_plugins", "_neural", "clear_cache", "my-index"]
      getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []

    it "attaches an empty JSON object as the body" $ do
      let req = OS3Requests.clearNeuralCache [[qqIndexName|my-index|]]
      bhRequestBody req `shouldBe` Just (encode (object []))

    it "uses a distinct index in the path when given a different IndexName" $ do
      let req = OS3Requests.clearNeuralCache [[qqIndexName|other-index|]]
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["_plugins", "_neural", "clear_cache", "other-index"]

    it "keeps the index name as a single opaque path segment" $ do
      let req = OS3Requests.clearNeuralCache [[qqIndexName|my-index_42|]]
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["_plugins", "_neural", "clear_cache", "my-index_42"]

  -- ---------------------------------------------------------------- --
  -- Multi-index (comma-separated) path rendering: bloodhound-6jy.    --
  -- The Neural Clear Cache API documents comma-separated lists and   --
  -- index patterns in the {index} path segment; the [IndexName]       --
  -- argument is rendered as a single comma-joined segment, matching   --
  -- the OpenSearch multi-target syntax.                              --
  -- ---------------------------------------------------------------- --
  describe "clearNeuralCache multi-index path rendering (OpenSearch 3)" $ do
    it "renders [i1,i2,i3] as a single comma-joined path segment" $ do
      let req =
            OS3Requests.clearNeuralCache
              [[qqIndexName|index1|], [qqIndexName|index2|], [qqIndexName|index3|]]
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["_plugins", "_neural", "clear_cache", "index1,index2,index3"]

    it "preserves index name order in the comma-joined segment" $ do
      let req =
            OS3Requests.clearNeuralCache
              [[qqIndexName|zebra|], [qqIndexName|alpha|], [qqIndexName|mike|]]
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["_plugins", "_neural", "clear_cache", "zebra,alpha,mike"]

    it "renders an empty segment for an empty index list (server will error)" $ do
      let req = OS3Requests.clearNeuralCache []
      getRawEndpoint (bhRequestEndpoint req)
        `shouldBe` ["_plugins", "_neural", "clear_cache", ""]

  describe "clearNeuralCache is distinct from warmupNeuralIndex" $ do
    -- Guards against an accidental copy-paste regression where
    -- clearNeuralCache silently re-uses the warmup endpoint. The two APIs
    -- share an identical wire shape (POST + empty body + ShardsResult
    -- response), differing only in the @warmup@ vs @clear_cache@ path
    -- segment, so the path is the single distinguishing feature.
    let idx = [qqIndexName|shared-index|]

    it "OS3 clear_cache path differs from warmup path" $ do
      let clearPath = getRawEndpoint (bhRequestEndpoint (OS3Requests.clearNeuralCache [idx]))
          warmupPath = getRawEndpoint (bhRequestEndpoint (OS3Requests.warmupNeuralIndex [idx]))
      clearPath `shouldNotBe` warmupPath

  describe "ShardsResult response decoding" $ do
    -- Pure unit tests (no OS round-trip): lock the 'FromJSON ShardsResult'
    -- decoder against the wire shape returned by
    -- @POST /_plugins/_neural/clear_cache\/{index}@. Live OS 3.7.0
    -- confirmed the response is the @_shards@ envelope
    -- (@{"_shards":{"total":2,"successful":1,"failed":0}}@), matching
    -- the warmup response shape (bead bloodhound-at5).
    it "decodes {\"_shards\":{\"total\":2,\"successful\":1,\"skipped\":0,\"failed\":0}} into ShardsResult" $ do
      let payload = "{\"_shards\":{\"total\":2,\"successful\":1,\"skipped\":0,\"failed\":0}}"
          Just decoded = decode payload :: Maybe ShardsResult
      decoded `shouldBe` ShardsResult (ShardResult 2 1 0 0)

    it "rejects a payload missing the _shards envelope" $ do
      let payload = "{\"acknowledged\":true}"
          decoded = decode payload :: Maybe ShardsResult
      decoded `shouldBe` Nothing

  -- ---------------------------------------------------------------- --
  -- Live round-trip: gated per-OS with os{n}OnlyIT (the              --
  -- 'Test.KnnClearCacheSpec' pattern).                                 --
  --                                                                    --
  -- Wire shape live-verified against OS 3.7.0 (port 9205) in bead     --
  -- bloodhound-at5: the response is                                    --
  -- @{"_shards":{"total":2,"successful":1,"failed":0}}@ — the         --
  -- @_shards@ envelope, decoded into 'ShardsResult'. The endpoint     --
  -- requires an index with @index.sparse: true@ and a @sparse_vector@  --
  -- field mapping.                                                     --
  --                                                                    --
  -- The neural-sparse clear-cache route is OS 3.x only: OS 1.3.x does  --
  -- not ship the neural-search plugin (bloodhound-ie4j), and OS 2.19.5 --
  -- does not register the clear-cache route or the @sparse_vector@     --
  -- field type (bloodhound-sh7o), so the live round-trip can only be   --
  -- exercised on OS 3.                                                 --
  -- ---------------------------------------------------------------- --
  describe "clearNeuralCache live round-trip" $ do
    os3It <- runIO os3OnlyIT

    let assertShards :: ShardsResult -> IO ()
        assertShards (ShardsResult sr) = do
          shardTotal sr `shouldSatisfy` (>= 1)
          shardsFailed sr `shouldBe` 0

    os3It "OS3: returns a ShardsResult with at least one shard and no failures" $
      withTestEnv $ do
        _ <- tryEsError resetOsNeuralSparseIndex
        _ <- OS3.Client.warmupNeuralIndex osNeuralSparseTestIndex
        result <- OS3.Client.clearNeuralCache osNeuralSparseTestIndex
        liftIO $ assertShards result