packages feed

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

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

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

import Control.Concurrent (threadDelay)
import Data.IORef (IORef, modifyIORef, newIORef, readIORef)
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 String 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 "Error"
                    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 "Error")
                retryMock = Retry mock 2 1000 -- 2 retries
            result <- invoke retryMock ("input" :: String)
            cnt <- readIORef counter
            assertEqual "All retries exhausted" (Left "Error" :: Either String 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 "Operation timed out" :: Either String String) result
        ]
    ]

data MockRunnable a b = MockRunnable {runMock :: a -> IO (Either String b)}

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