langchain-hs-0.0.6.0: test/Test/Langchain/Tool/FileSystem.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.Langchain.Tool.FileSystem (tests) where
import Data.Aeson (object, (.=))
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Langchain.Tool (toolExecute)
import Langchain.Tool.Checkpoint (newCheckpointRegistry, undoFileEditTool)
import Langchain.Tool.FileSystem
import Langchain.Tool.Security (defaultSecurityPolicy)
import System.Directory (createDirectoryIfMissing)
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Tasty
import Test.Tasty.HUnit
tests :: TestTree
tests =
testGroup
"Langchain.Tool.FileSystem"
[ testCase "writeFileTool and readFileTool perform paginated I/O correctly" $ do
withSystemTempDirectory "tool-test" $ \dir -> do
reg <- newCheckpointRegistry
let policy = defaultSecurityPolicy
filePath = T.pack (dir </> "test.txt")
content = "Line 1\nLine 2\nLine 3\nLine 4" :: Text
-- Write initial file
wRes <-
toolExecute (writeFileTool (Just reg) policy) (object ["path" .= filePath, "content" .= content])
assertBool "Write should succeed" (case wRes of Right _ -> True; _ -> False)
-- Read with pagination (lines 2 to 3)
rRes <-
toolExecute
(readFileTool (Just reg) policy)
(object ["path" .= filePath, "start_line" .= (2 :: Int), "end_line" .= (3 :: Int)])
case rRes of
Left err -> assertFailure $ "Failed reading paginated lines: " ++ show err
Right txt -> do
assertBool "Contains Line 2" ("2: Line 2" `T.isInfixOf` txt)
assertBool "Contains Line 3" ("3: Line 3" `T.isInfixOf` txt)
assertBool "Does not contain Line 1" (not $ "1: Line 1" `T.isInfixOf` txt)
, testCase "writeFileTool prevents accidental overwrite without overwrite=true" $ do
withSystemTempDirectory "tool-test" $ \dir -> do
reg <- newCheckpointRegistry
let policy = defaultSecurityPolicy
filePath = T.pack (dir </> "immutable.txt")
_ <-
toolExecute
(writeFileTool (Just reg) policy)
(object ["path" .= filePath, "content" .= ("initial" :: Text)])
wRes2 <-
toolExecute
(writeFileTool (Just reg) policy)
(object ["path" .= filePath, "content" .= ("new" :: Text)])
case wRes2 of
Left _ -> pure () -- Expect failure
Right _ -> assertFailure "Expected failure when overwriting without overwrite=true"
, testCase "editFileTool enforces read-before-edit and unique replacement with undo" $ do
withSystemTempDirectory "tool-test" $ \dir -> do
reg <- newCheckpointRegistry
let policy = defaultSecurityPolicy
fpStr = dir </> "code.hs"
filePath = T.pack fpStr
initialCode = "module Main where\n\nval = 10\n"
-- Write directly via TIO so the registry hasn't tracked a read yet
TIO.writeFile fpStr initialCode
-- Attempt edit before reading -> Should fail read-before-edit
editRes1 <-
toolExecute
(editFileTool (Just reg) policy)
( object
["path" .= filePath, "old_string" .= ("val = 10" :: Text), "new_string" .= ("val = 42" :: Text)]
)
case editRes1 of
Left _ -> pure () -- Read-before-edit enforced
Right _ -> assertFailure "Expected edit_file to fail when file has not been read yet"
-- Inspect file using read_file
_ <- toolExecute (readFileTool (Just reg) policy) (object ["path" .= filePath])
-- Now edit should succeed
editRes2 <-
toolExecute
(editFileTool (Just reg) policy)
( object
["path" .= filePath, "old_string" .= ("val = 10" :: Text), "new_string" .= ("val = 42" :: Text)]
)
case editRes2 of
Left err -> assertFailure $ "Edit failed after reading: " ++ show err
Right msg -> assertBool "Check message" ("Successfully edited" `T.isInfixOf` msg)
-- Verify content changed
newContent <- TIO.readFile fpStr
assertBool "Value updated" ("val = 42" `T.isInfixOf` newContent)
-- Test undo checkpoint
undoRes <- toolExecute (undoFileEditTool reg) (object ["path" .= filePath])
case undoRes of
Left err -> assertFailure $ "Undo failed: " ++ show err
Right _ -> do
revertedContent <- TIO.readFile fpStr
assertBool "Reverted back" ("val = 10" `T.isInfixOf` revertedContent)
, testCase "findFilesTool finds files by glob and obeys max_depth" $ do
withSystemTempDirectory "tool-test" $ \dir -> do
let policy = defaultSecurityPolicy
subDir = dir </> "nested" </> "deep"
createDirectoryIfMissing True subDir
writeFile (dir </> "app.hs") "main = pure ()"
writeFile (subDir </> "lib.hs") "module Lib where"
writeFile (dir </> "README.md") "# Title"
-- Search for *.hs with depth 0
resDepth0 <-
toolExecute
(findFilesTool policy)
(object ["path" .= T.pack dir, "pattern" .= ("*.hs" :: Text), "max_depth" .= (0 :: Int)])
case resDepth0 of
Left err -> assertFailure $ "Find failed: " ++ show err
Right txt -> do
assertBool "Found root app.hs" ("app.hs" `T.isInfixOf` txt)
assertBool "Did not find deep lib.hs at depth 0" (not $ "lib.hs" `T.isInfixOf` txt)
-- Search for *.hs with depth 3
resDepth3 <-
toolExecute
(findFilesTool policy)
(object ["path" .= T.pack dir, "pattern" .= ("*.hs" :: Text), "max_depth" .= (3 :: Int)])
case resDepth3 of
Left err -> assertFailure $ "Find failed: " ++ show err
Right txt -> do
assertBool "Found root app.hs" ("app.hs" `T.isInfixOf` txt)
assertBool "Found nested lib.hs" ("lib.hs" `T.isInfixOf` txt)
assertBool "Excluded README.md" (not $ "README.md" `T.isInfixOf` txt)
, testCase "grepSearchTool finds content with line numbers" $ do
withSystemTempDirectory "tool-test" $ \dir -> do
let policy = defaultSecurityPolicy
writeFile (dir </> "a.txt") "apple\nbanana\ncherry"
writeFile (dir </> "b.txt") "date\nelderberry\nfig\napple pie"
res <-
toolExecute
(grepSearchTool policy)
(object ["path" .= T.pack dir, "query" .= ("apple" :: Text)])
case res of
Left err -> assertFailure $ "Grep failed: " ++ show err
Right txt -> do
assertBool "Contains match header" ("Found 2 matches" `T.isInfixOf` txt)
assertBool "Contains line number" (":1:" `T.isInfixOf` txt || ":4:" `T.isInfixOf` txt)
, testCase "listDirTool lists created files" $ do
withSystemTempDirectory "tool-test" $ \dir -> do
reg <- newCheckpointRegistry
let policy = defaultSecurityPolicy
filePath = T.pack (dir </> "sample.txt")
_ <-
toolExecute
(writeFileTool (Just reg) policy)
(object ["path" .= filePath, "content" .= ("content" :: Text)])
lRes <- toolExecute (listDirTool policy) (object ["path" .= T.pack dir])
case lRes of
Left err -> assertFailure $ "Unexpected error: " ++ show err
Right filesTxt -> assertBool "Should contain sample.txt" ("sample.txt" `T.isInfixOf` filesTxt)
]