packages feed

llm-simple-0.4.0.0: test/LLM/HttpConfigSpec.hs

module LLM.HttpConfigSpec (spec) where

import Control.Concurrent (forkIO, killThread, threadDelay)
import Control.Exception (SomeException, bracket, finally, try)
import Control.Monad (forever, void)
import Data.Aeson (Value, object)
import Data.ByteString qualified as BS
import Data.IORef (atomicModifyIORef', newIORef, readIORef)
import LLM.Core.ProviderUtils (mkLenientConfig)
import Network.HTTP.Client qualified as HC
import Network.HTTP.Req (POST (..), ReqBodyJson (..), http, ignoreResponse, port, req, runReq, (/:))
import Network.Socket qualified as N
import Network.Socket.ByteString qualified as NB
import Test.Hspec (Spec, describe, it, shouldBe)

spec :: Spec
spec = describe "HTTP config" $ do
  it "sends a POST exactly once when the response times out" $ do
    posts <- newIORef (0 :: Int)
    -- Answer each request after 2 s; the client gives up after 100 ms.
    withSlowServer 2_000_000 (atomicModifyIORef' posts (\n -> (n + 1, ()))) $ \p -> do
      mgr <- HC.newManager HC.defaultManagerSettings {HC.managerResponseTimeout = HC.responseTimeoutMicro 100_000}
      r <-
        try $
          runReq (mkLenientConfig mgr) $
            req POST (http "127.0.0.1" /: "v1") (ReqBodyJson (object [] :: Value)) ignoreResponse (port p)
      case r of
        Left (_ :: SomeException) -> pure ()
        Right _ -> fail "expected the request to time out"
      readIORef posts >>= (`shouldBe` 1)

-- | A TCP server on a free local port that counts each request it receives
-- and answers it only after the given delay.
withSlowServer :: Int -> IO () -> (Int -> IO a) -> IO a
withSlowServer delay onRequest action =
  bracket open N.close $ \sock -> do
    p <- fromIntegral <$> N.socketPort sock
    tid <- forkIO $ forever $ do
      (conn, _) <- N.accept sock
      void $ forkIO $ flip finally (N.close conn) $ do
        chunk <- NB.recv conn 4096
        if "POST" `BS.isPrefixOf` chunk
          then do
            onRequest
            threadDelay delay
            NB.sendAll conn "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
          else pure ()
    action p `finally` killThread tid
  where
    open = do
      sock <- N.socket N.AF_INET N.Stream N.defaultProtocol
      N.bind sock (N.SockAddrInet 0 (N.tupleToHostAddress (127, 0, 0, 1)))
      N.listen sock 16
      pure sock