packages feed

baikai-0.3.0.0: test/ErrorInfoSpec.hs

-- | End-to-end check that a structured error set on an 'EventError'
-- terminal is threaded through reassembly onto the 'Response' a caller
-- gets back from 'completeRequest'. This is the user-visible payoff of
-- the in-band error-classification work: branch on category/retry
-- without parsing error text.
module ErrorInfoSpec (tests) where

import Baikai
import Baikai.Prelude
import Streamly.Data.Stream qualified as Stream
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))

errApi :: Api
errApi = Custom "baikai-errinfo"

errModel :: Model
errModel =
  emptyModel
    & #modelId
    .~ "err-model"
    & #api
    .~ errApi
    & #provider
    .~ "test"

-- | A stream that emits a single 'EventError' carrying a structured
-- 'RateLimited' error with a retry-after hint.
errStream :: Model -> Context -> Options -> Stream.Stream IO AssistantMessageEvent
errStream _ _ _ =
  let payload =
        (emptyResponse ^. #message)
          & #stopReason
          .~ ErrorReason
          & #errorMessage
          .~ Just "rate limited, slow down"
      term =
        errorTerminal
          Nothing
          ErrorReason
          (AssistantMessage payload)
          (rateLimited (Just 5) "rate limited, slow down")
      start = EventStart StartPayload {partial = AssistantMessage payload, responseId = Nothing}
   in Stream.fromList [start, EventError term]

errResponse :: Model -> Context -> Options -> IO Response
errResponse m _ _ =
  pure (errorResponse m (read "2026-06-05 00:00:00 UTC") 0 (rateLimited (Just 5) "rate limited, slow down"))

registerErr :: IO ()
registerErr =
  registerApiProvider
    ApiProvider
      { apiTag = errApi,
        stream = errStream,
        complete = streamingComplete errStream
      }

tests :: TestTree
tests =
  testGroup
    "Response.errorInfo threading"
    [ testCase "completeRequest surfaces structured errorInfo" $ do
        registerErr
        resp <- completeRequest errModel emptyContext emptyOptions
        case responseError resp of
          Just be -> do
            be ^. #category @?= RateLimited
            be ^. #retryAfterSeconds @?= Just 5
          Nothing -> assertFailure "expected Response.errorInfo to be populated",
      testCase "completeRequestWith reports missing provider in-band" $ do
        reg <- newProviderRegistry
        resp <- completeRequestWith reg errModel emptyContext emptyOptions
        case responseError resp of
          Just be -> be ^. #category @?= ProviderUnavailable
          Nothing -> assertFailure "expected ProviderUnavailable response",
      testCase "liftCompleteToStream preserves error-shaped responses as EventError" $ do
        let stream = liftCompleteToStream errResponse
        events <- Stream.toList (stream errModel emptyContext emptyOptions)
        assertBool "expected exactly one terminal" (length (filter isTerminal events) == 1)
        case last events of
          EventError TerminalPayload {errorInfo = Just be} -> do
            be ^. #category @?= RateLimited
            be ^. #retryAfterSeconds @?= Just 5
          other -> assertFailure ("expected terminal EventError, got: " <> show other),
      testCase "reassembly normalizes ErrorReason terminals without errorInfo" $ do
        let payload =
              (emptyResponse ^. #message)
                & #stopReason
                .~ ErrorReason
                & #errorMessage
                .~ Just "legacy unclassified failure"
            terminal =
              doneTerminal
                Nothing
                ErrorReason
                (AssistantMessage payload)
            events =
              [ EventStart StartPayload {partial = AssistantMessage payload, responseId = Nothing},
                EventDone terminal
              ]
        resp <- Stream.fold (reassembleResponse errModel) (Stream.fromList events)
        case responseError resp of
          Just be -> do
            be ^. #category @?= OtherError
            be ^. #message @?= "legacy unclassified failure"
          Nothing -> assertFailure "expected synthesized errorInfo"
    ]