packages feed

langchain-hs-0.0.5.0: test/Test/Langchain/Graph/CompilationSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Langchain.Graph.CompilationSpec (tests) where

import Control.Monad.Except (runExceptT)
import qualified Data.Text as T
import Test.Tasty
import Test.Tasty.HUnit

import Langchain.Core.Error (errorMessage)
import Langchain.Graph.StateGraph

tests :: TestTree
tests =
  testGroup
    "Langchain.Graph.CompilationSpec"
    [ testCase "Empty graph fails compilation" $ do
        let g = emptyStateGraph replaceFieldReducer :: StateGraph T.Text IO
        case compileGraph g of
          Left err -> assertBool "Error indicates empty nodes" ("at least one node" `T.isInfixOf` errorMessage err)
          Right _ -> assertFailure "Expected empty graph compilation failure"
    , testCase "Single node graph compiles and runs to end" $ do
        let g =
              addEdge "process" endNodeId $
                addNode "process" (\s -> pure $ Right (s <> "_processed")) $
                  emptyStateGraph replaceFieldReducer
        case compileGraph g of
          Left err -> assertFailure ("Compilation failed: " ++ show err)
          Right cg -> do
            res <- runExceptT $ runGraph cg "process" ("item" :: T.Text)
            res @?= Right "item_processed"
    , testCase "3-node linear pipeline compiles and preserves state flow" $ do
        let g =
              addEdge "n1" "n2" $
                addEdge "n2" "n3" $
                  addEdge "n3" endNodeId $
                    addNode "n1" (\s -> pure $ Right (s <> " -> step1")) $
                      addNode "n2" (\s -> pure $ Right (s <> " -> step2")) $
                        addNode "n3" (\s -> pure $ Right (s <> " -> step3")) $
                          emptyStateGraph replaceFieldReducer
        case compileGraph g of
          Left err -> assertFailure ("Compilation failed: " ++ show err)
          Right cg -> do
            res <- runExceptT $ runGraph cg "n1" ("start" :: T.Text)
            res @?= Right "start -> step1 -> step2 -> step3"
    , testCase "Conditional edge routes dynamically based on condition" $ do
        let routeFn s = pure $ Right $ if "urgent" `T.isInfixOf` s then "fastTrack" else "normalTrack"
            g =
              addConditionalEdge "dispatch" routeFn $
                addEdge "fastTrack" endNodeId $
                  addEdge "normalTrack" endNodeId $
                    addNode "dispatch" (pure . Right) $
                      addNode "fastTrack" (\s -> pure $ Right (s <> " [FAST]")) $
                        addNode "normalTrack" (\s -> pure $ Right (s <> " [NORMAL]")) $
                          emptyStateGraph replaceFieldReducer
        case compileGraph g of
          Left err -> assertFailure ("Compilation failed: " ++ show err)
          Right cg -> do
            resFast <- runExceptT $ runGraph cg "dispatch" "urgent invoice"
            resFast @?= Right "urgent invoice [FAST]"
            resNormal <- runExceptT $ runGraph cg "dispatch" "general query"
            resNormal @?= Right "general query [NORMAL]"
    , testCase "Node overwrite replaces node function in state graph" $ do
        let g =
              addEdge "n1" endNodeId $
                addNode "n1" (\s -> pure $ Right (s <> " v2")) $
                  addNode "n1" (\s -> pure $ Right (s <> " v1")) $
                    emptyStateGraph replaceFieldReducer
        case compileGraph g of
          Left err -> assertFailure ("Compilation failed: " ++ show err)
          Right cg -> do
            res <- runExceptT $ runGraph cg "n1" ("base" :: T.Text)
            res @?= Right "base v2"
    ]