packages feed

langchain-hs-0.0.3.0: test/Test/Langchain/Runnable/Utils.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

module Test.Langchain.Runnable.Utils (tests) where

import Control.Concurrent (threadDelay)
import Data.IORef (IORef, modifyIORef, newIORef, readIORef)
import Langchain.Error (LangchainError, llmError)
import Langchain.Runnable.Core
import Langchain.Runnable.Utils
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertEqual, testCase)

data InvocationCounter a b = InvocationCounter (IORef Int) (a -> IO (Either LangchainError b))

instance Runnable (InvocationCounter a b) where
  type RunnableInput (InvocationCounter a b) = a
  type RunnableOutput (InvocationCounter a b) = b
  invoke (InvocationCounter counter f) input = do
    modifyIORef counter (+ 1)
    f input

tests :: TestTree
tests =
  testGroup
    "Runnable Utils Tests"
    [ testGroup
        "WithConfig Tests"
        [ testCase "WithConfig delegates to underlying runnable" $ do
            let mock = MockRunnable (\s -> return $ Right (s ++ " processed"))
                config = WithConfig mock ()
            result <- invoke config "input"
            assertEqual "Should delegate to mock" (Right "input processed") result
        ]
    , testGroup
        "Cached Tests"
        [ testCase "Cached returns cached result on second call" $ do
            counter <- newIORef 0
            let mock = InvocationCounter counter (\s -> return $ Right (s ++ "!"))
            cachedMock <- cached mock
            result1 <- invoke cachedMock "test"
            _ <- readIORef counter
            result2 <- invoke cachedMock "test"
            count2 <- readIORef counter
            assertEqual "First call result" (Right "test!") result1
            assertEqual "Second call result" (Right "test!") result2
            assertEqual "Only one invocation" 1 count2
        , testCase "Cached handles different inputs separately" $ do
            counter <- newIORef 0
            let mock = InvocationCounter counter (\s -> return $ Right (s ++ "!"))
            cachedMock <- cached mock
            _ <- invoke cachedMock "test1"
            _ <- invoke cachedMock "test2"
            count <- readIORef counter
            assertEqual "Two separate invocations" 2 count
        ]
    , testGroup
        "Retry Tests"
        [ testCase "Retry succeeds after one failure" $ do
            counter <- newIORef 0
            let mock = InvocationCounter counter $ \_ -> do
                  cnt <- readIORef counter
                  if cnt < 1
                    then return $ Left (llmError "Error" Nothing Nothing)
                    else return $ Right ("Success" :: String)
                retryMock = Retry mock 3 5000 -- 1 retry, 5ms delay
            result <- invoke retryMock ("input" :: String)
            cnt <- readIORef counter
            assertEqual "Retry succeeds" (Right "Success") result
            assertEqual "Invoked twice" 1 cnt
        , testCase "Retry exhausts retries and fails" $ do
            counter <- newIORef 0
            let mock = InvocationCounter counter (\_ -> return $ Left (llmError "Error" Nothing Nothing))
                retryMock = Retry mock 2 1000 -- 2 retries
            result <- invoke retryMock ("input" :: String)
            cnt <- readIORef counter
            assertEqual
              "All retries exhausted"
              (Left (llmError "Error" Nothing Nothing) :: Either LangchainError String)
              result
            assertEqual "Three attempts made" 3 cnt
        ]
    , testGroup
        "WithTimeout Tests"
        [ testCase "WithTimeout returns result before timeout" $ do
            let mock = MockRunnable (\_ -> return $ Right "Quick response")
                timeoutMock = WithTimeout mock 100000 -- 100ms timeout
            result <- invoke timeoutMock ("input" :: String)
            assertEqual "Returns result" (Right ("Quick response" :: String)) result
        , testCase "WithTimeout triggers timeout error" $ do
            let mock = MockRunnable $ \_ -> do
                  threadDelay 200000 -- 200ms delay
                  return $ Right "Too slow"
                timeoutMock = WithTimeout mock 100000 -- 100ms timeout
            result <- invoke timeoutMock ("input" :: String)
            assertEqual
              "Timeout error"
              (Left (llmError "Operation timed out" Nothing Nothing) :: Either LangchainError String)
              result
        ]
    ]

newtype MockRunnable a b = MockRunnable {runMock :: a -> IO (Either LangchainError b)}

instance Runnable (MockRunnable a b) where
  type RunnableInput (MockRunnable a b) = a
  type RunnableOutput (MockRunnable a b) = b
  invoke = runMock