baikai-0.6.0.0: test/ErrorSpec.hs
module ErrorSpec (tests) where
import Baikai.Error
( BaikaiError (..),
ErrorCategory (..),
classifyHttpStatus,
classifyHttpStatusWithBody,
decodeError,
httpError,
invalidRequest,
isRetryable,
parseHttpDate,
parseRetryAfterSeconds,
processError,
rateLimited,
retryAfterSecondsAt,
)
import Data.Time (UTCTime)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))
tests :: TestTree
tests =
testGroup
"Baikai.Error"
[ classifyTests,
bodyClassifyTests,
httpHelperTests,
retryTests,
constructorTests
]
httpHelperTests :: TestTree
httpHelperTests =
testGroup
"httpError / parseRetryAfterSeconds"
[ testCase "429 + retry-after -> RateLimited with hint" $ do
let e = httpError 429 (Just 12) "slow down"
category e @?= RateLimited
httpStatus e @?= Just 429
retryAfterSeconds e @?= Just 12,
-- Re-homed from the provider suites' servant fixtures: the
-- assertion is about 'httpError', which is where it belongs.
testCase "429 without Retry-After -> RateLimited, no hint" $ do
let e = httpError 429 Nothing "slow down"
category e @?= RateLimited
retryAfterSeconds e @?= Nothing,
testCase "400 + overflow body -> ContextOverflow" $
category (httpError 400 Nothing "maximum context length exceeded")
@?= ContextOverflow,
testCase "integer Retry-After parses as seconds" $
parseRetryAfterSeconds "12" @?= Just 12,
-- The integer-only contract is now deliberate rather than a
-- limitation: converting a date needs a reference instant, which
-- 'retryAfterSecondsAt' takes and this function cannot.
testCase "parseRetryAfterSeconds is integer-only" $
parseRetryAfterSeconds "Wed, 21 Oct 2026 07:28:00 GMT" @?= Nothing,
testCase "HTTP-date Retry-After yields seconds from the reference instant" $
retryAfterSecondsAt referenceInstant "Wed, 21 Oct 2026 07:28:00 GMT" @?= Just 30,
-- The server is saying "now", not "some time last week".
testCase "HTTP-date Retry-After in the past yields zero" $
retryAfterSecondsAt referenceInstant "Wed, 21 Oct 2026 07:00:00 GMT" @?= Just 0,
testCase "integer Retry-After ignores the reference instant" $
retryAfterSecondsAt referenceInstant "12" @?= Just 12,
testCase "malformed Retry-After yields Nothing" $
retryAfterSecondsAt referenceInstant "soonish" @?= Nothing,
testCase "parseHttpDate accepts IMF-fixdate, RFC 850 and asctime" $ do
let expected = Just (read "1994-11-06 08:49:37 UTC" :: UTCTime)
parseHttpDate "Sun, 06 Nov 1994 08:49:37 GMT" @?= expected
parseHttpDate "Sunday, 06-Nov-94 08:49:37 GMT" @?= expected
parseHttpDate "Sun Nov 6 08:49:37 1994" @?= expected,
testCase "parseHttpDate rejects text that is not a date" $
parseHttpDate "tomorrow" @?= Nothing
]
-- | Thirty seconds before the @Retry-After@ date the cases above use, so
-- the expected answer is a number a reader can check by eye.
referenceInstant :: UTCTime
referenceInstant = read "2026-10-21 07:27:30 UTC"
bodyClassifyTests :: TestTree
bodyClassifyTests =
testGroup
"classifyHttpStatusWithBody"
[ testCase "400 + overflow body -> ContextOverflow" $
classifyHttpStatusWithBody 400 Nothing "This model's maximum context length is 8192 tokens"
@?= ContextOverflow,
testCase "400 + prompt-too-long body -> ContextOverflow" $
classifyHttpStatusWithBody 400 Nothing "prompt is too long: 9000 tokens > 8000"
@?= ContextOverflow,
testCase "400 + ordinary body -> InvalidRequest" $
classifyHttpStatusWithBody 400 Nothing "missing required field 'model'"
@?= InvalidRequest,
testCase "429 + overflow-looking body still RateLimited (status wins)" $
classifyHttpStatusWithBody 429 Nothing "context length whatever"
@?= RateLimited,
testCase "500 defers to status -> TransientError" $
classifyHttpStatusWithBody 500 Nothing "context length" @?= TransientError,
-- 413 is the size-limit status, so the body's wording changes
-- nothing: the caller's remedy is to shrink the input either way.
testCase "413 + ordinary body -> ContextOverflow" $
classifyHttpStatusWithBody 413 Nothing "payload too large" @?= ContextOverflow,
testCase "413 + request_too_large body -> ContextOverflow" $
classifyHttpStatusWithBody 413 Nothing "request_too_large" @?= ContextOverflow
]
classifyTests :: TestTree
classifyTests =
testGroup
"classifyHttpStatus"
[ testCase "401 -> AuthError" $ classifyHttpStatus 401 Nothing @?= AuthError,
testCase "403 -> AuthError" $ classifyHttpStatus 403 Nothing @?= AuthError,
testCase "429 -> RateLimited" $ classifyHttpStatus 429 Nothing @?= RateLimited,
testCase "408 -> TransientError" $ classifyHttpStatus 408 Nothing @?= TransientError,
testCase "400 -> InvalidRequest" $ classifyHttpStatus 400 Nothing @?= InvalidRequest,
testCase "404 -> InvalidRequest" $ classifyHttpStatus 404 Nothing @?= InvalidRequest,
testCase "422 -> InvalidRequest" $ classifyHttpStatus 422 Nothing @?= InvalidRequest,
testCase "500 -> TransientError" $ classifyHttpStatus 500 Nothing @?= TransientError,
testCase "502 -> TransientError" $ classifyHttpStatus 502 Nothing @?= TransientError,
testCase "503 -> TransientError" $ classifyHttpStatus 503 Nothing @?= TransientError,
testCase "413 -> ContextOverflow" $ classifyHttpStatus 413 Nothing @?= ContextOverflow,
testCase "418 -> OtherError" $ classifyHttpStatus 418 Nothing @?= OtherError
]
retryTests :: TestTree
retryTests =
testGroup
"isRetryable / retryAfterSeconds"
[ testCase "rate limited is retryable" $
isRetryable (rateLimited (Just 30) "slow down") @?= True,
testCase "rate limited carries retry-after" $
retryAfterSeconds (rateLimited (Just 30) "slow down") @?= Just 30,
testCase "rate limited sets httpStatus 429" $
httpStatus (rateLimited Nothing "slow down") @?= Just 429,
testCase "invalid request is not retryable" $
isRetryable (invalidRequest "bad shape") @?= False,
testCase "decode failure is not retryable" $
isRetryable (decodeError "garbled") @?= False
]
constructorTests :: TestTree
constructorTests =
testGroup
"smart constructors"
[ testCase "processError carries exit code" $
exitCode (processError 2 "boom") @?= Just 2,
testCase "processError category is ProcessFailure" $
category (processError 2 "boom") @?= ProcessFailure,
testCase "invalidRequest category" $
category (invalidRequest "x") @?= InvalidRequest,
testCase "decodeError category" $
category (decodeError "x") @?= DecodeFailure
]