packages feed

baikai-0.6.0.0: test/CatalogSpec.hs

-- | Regression test that locks down the contract between the JSON
-- catalog files under @baikai\/data\/models\/@ and the auto-generated
-- @baikai\/src\/Baikai\/Models\/Generated.hs@ module.
--
-- The test invokes the @baikai-gen-models@ executable (made available
-- on @PATH@ by the @build-tool-depends@ entry in @baikai.cabal@) with
-- @--out@ pointing at a fresh temp file, then asserts the regenerated
-- output is byte-identical to the committed module. If the test fails
-- the remediation is always:
--
-- @
-- cabal run baikai-gen-models
-- git add baikai\/src\/Baikai\/Models\/Generated.hs
-- @
--
-- A failure means either someone edited @Baikai.Models.Generated@ by
-- hand (the file says \"do not edit\" at the top) or a @data\/models@
-- JSON file changed without a paired regeneration.
module CatalogSpec (tests) where

import Baikai.Api (Api (AnthropicMessages))
import Baikai.Compat
  ( AnthropicMessagesCompat,
    AnthropicThinkingStyle (..),
    supportsSamplingParameters,
    thinkingStyle,
  )
import Baikai.Model
  ( Compat (CompatAnthropicMessages),
    Model,
    api,
    compat,
    modelId,
  )
import Baikai.Models.Generated (allModels)
import Data.ByteString qualified as BS
import Data.List (sort)
import Data.Text (Text)
import System.IO.Temp (withSystemTempDirectory)
import System.Process (callProcess)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertEqual, assertFailure, testCase, (@?=))

tests :: TestTree
tests =
  testGroup
    "Baikai.Models.Generated"
    [ testCase "regenerating from data/models produces no diff" $
        withSystemTempDirectory "baikai-catalog-spec" $ \tmpDir -> do
          let regenPath = tmpDir <> "/Generated.hs"
              committedPath = "src/Baikai/Models/Generated.hs"
          callProcess "baikai-gen-models" ["--out", regenPath]
          committed <- BS.readFile committedPath
          regenerated <- BS.readFile regenPath
          assertEqual
            "Generated.hs is out of sync with data/models/*.json.\n\
            \Run `cabal run baikai-gen-models` and commit the result."
            committed
            regenerated,
      -- Which extended-thinking wire shape a generation accepts, and
      -- whether it accepts sampling parameters, cannot be recovered
      -- from the model id or the base URL. Every Anthropic catalog
      -- entry must therefore carry an explicit compat record stating
      -- both, and this table is where the shipped values are pinned:
      -- a catalog refresh that changes one has to change this row too.
      testCase "every Anthropic catalog entry carries an explicit thinking style and sampling flag" $ do
        assertEqual
          "the pinned table must cover exactly the catalog's Anthropic ids"
          (sort (map fst expectedAnthropicFacts))
          (sort (map modelId anthropicCatalogModels))
        mapM_ assertFacts anthropicCatalogModels
    ]

-- | Every Anthropic model in the generated catalog.
anthropicCatalogModels :: [Model]
anthropicCatalogModels = [m | m <- allModels, api m == AnthropicMessages]

-- | The shipped thinking style and sampling support of each Anthropic
-- catalog id, written out by hand from
-- @baikai\/data\/models\/anthropic.json@.
expectedAnthropicFacts :: [(Text, (AnthropicThinkingStyle, Bool))]
expectedAnthropicFacts =
  [ ("claude-fable-5", (AnthropicThinkingAdaptive, False)),
    ("claude-haiku-4-5", (AnthropicThinkingBudget, True)),
    ("claude-opus-4-5", (AnthropicThinkingBudget, True)),
    ("claude-opus-4-6", (AnthropicThinkingAdaptive, True)),
    ("claude-opus-4-7", (AnthropicThinkingAdaptive, False)),
    ("claude-opus-4-8", (AnthropicThinkingAdaptive, False)),
    ("claude-opus-5", (AnthropicThinkingAdaptive, False)),
    ("claude-sonnet-4-5", (AnthropicThinkingBudget, True)),
    ("claude-sonnet-4-6", (AnthropicThinkingAdaptive, True)),
    ("claude-sonnet-5", (AnthropicThinkingAdaptive, False))
  ]

assertFacts :: Model -> IO ()
assertFacts m = case compat m of
  CompatAnthropicMessages c -> case lookup (modelId m) expectedAnthropicFacts of
    Just expected -> facts c @?= expected
    Nothing ->
      assertFailure
        ("no pinned facts for Anthropic catalog model " <> show (modelId m))
  other ->
    assertFailure
      ( "Anthropic catalog model "
          <> show (modelId m)
          <> " must carry an explicit CompatAnthropicMessages record, not "
          <> show other
      )
  where
    facts :: AnthropicMessagesCompat -> (AnthropicThinkingStyle, Bool)
    facts c = (thinkingStyle c, supportsSamplingParameters c)