packages feed

langchain-hs-core-0.0.6.0: test/Test/Langchain/Resilience.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Langchain.Resilience (tests) where

import Control.Concurrent.STM
import Control.Monad.Except (runExceptT, throwError)
import Control.Monad.IO.Class (liftIO)
import Test.Tasty
import Test.Tasty.HUnit

import Langchain.Error (internalError)
import Langchain.Resilience

tests :: TestTree
tests =
  testGroup
    "Langchain.Resilience"
    [ testGroup
        "CircuitBreaker"
        [ testCase "CircuitBreaker starts in Closed state and passes successful requests" $ do
            cb <- newCircuitBreaker "test-cb" defaultCircuitConfig
            st <- getCircuitState cb
            st @?= CircuitClosed
            res <- runExceptT $ withCircuitBreaker cb (pure ("ok" :: String))
            res @?= Right "ok"
        , testCase "CircuitBreaker transitions to Open after exceeding failure threshold" $ do
            let cfg = CircuitBreakerConfig {failureThreshold = 2, resetTimeoutSec = 0.1}
            cb <- newCircuitBreaker "failing-cb" cfg
            -- First failure
            _ <- runExceptT $ withCircuitBreaker cb (throwError (internalError "fail 1" Nothing Nothing))
            st1 <- getCircuitState cb
            st1 @?= CircuitClosed
            -- Second failure -> should open
            _ <- runExceptT $ withCircuitBreaker cb (throwError (internalError "fail 2" Nothing Nothing))
            st2 <- getCircuitState cb
            case st2 of
              CircuitOpen _ -> pure ()
              _ -> assertFailure "Expected CircuitOpen state"
            -- Third request fast-fails without executing action
            resFastFail <- runExceptT $ withCircuitBreaker cb (pure ("should not execute" :: String))
            case resFastFail of
              Left _ -> pure ()
              Right _ -> assertFailure "Expected circuit breaker fast-fail error"
        ]
    , testGroup
        "Retry"
        [ testCase "withRetry succeeds after failing attempts" $ do
            attemptVar <- newTVarIO (0 :: Int)
            let policy = defaultRetryPolicy {maxRetries = 3, baseDelayMicros = 1000, useJitter = False}
                action = do
                  curr <- liftIO $ atomically $ do
                    c <- readTVar attemptVar
                    writeTVar attemptVar (c + 1)
                    pure c
                  if curr < 2
                    then throwError $ internalError "Temporary failure" Nothing Nothing
                    else pure ("Success on attempt " ++ show (curr + 1))
            res <- runExceptT $ withRetry policy action
            res @?= Right "Success on attempt 3"
        , testCase "RateLimiter consumes tokens and executes action" $ do
            limiter <- newRateLimiter 5.0 5.0
            res <- withRateLimit limiter (pure (42 :: Int))
            res @?= 42
        ]
    ]