llm-simple-0.4.2.0: test/LLM/FsLimitsSpec.hs
{-# LANGUAGE CPP #-}
module LLM.FsLimitsSpec (spec) where
import Control.Exception (ErrorCall (..), throwIO, try)
import Data.ByteString qualified as BS
import Data.Text qualified as T
import LLM.Core.Types (TypedTool (..))
import LLM.Tools.FsConfig (mkFsConfig)
import LLM.Tools.FsLimits
( atomicWriteWith,
maxPaginatedFileBytes,
maxReadBytes,
readBoundedTextFile,
writeUtf8File,
)
import LLM.Tools.ReadFilePaginated
( ReadFilePaginatedArgs (..),
readFilePaginatedToolTyped,
)
import System.Directory (createDirectory, listDirectory)
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
#if !defined(mingw32_HOST_OS)
import System.Posix.Files (fileMode, getFileStatus, setFileMode)
import System.Posix.Types (FileMode)
#endif
import Test.Hspec
( Spec,
describe,
it,
shouldBe,
shouldContain,
shouldReturn,
shouldSatisfy,
)
spec :: Spec
spec = describe "FsLimits" $ do
describe "readBoundedTextFile" $ do
it "reads a file within the cap" $ withTempFile $ \dir -> do
let path = dir </> "small.txt"
writeFile path "hello"
result <- readBoundedTextFile path "test.txt" maxReadBytes
result `shouldBe` Right "hello"
it "rejects a file larger than the cap" $ withTempFile $ \dir -> do
let path = dir </> "big.txt"
BS.writeFile path (BS.replicate (maxReadBytes + 1) 0x61)
result <- readBoundedTextFile path "big.txt" maxReadBytes
result `shouldSatisfy` \case
Left err -> "exceeds the" `T.isInfixOf` err
Right _ -> False
describe "atomicWriteWith" $ do
it "leaves the original unchanged and no temp file when the action throws" $ withTempFile $ \dir -> do
let path = dir </> "f.txt"
BS.writeFile path "original"
r <- try $ atomicWriteWith path $ \h -> do
BS.hPut h "partial"
throwIO (ErrorCall "boom")
r `shouldBe` Left (ErrorCall "boom")
BS.readFile path `shouldReturn` "original"
listDirectory dir `shouldReturn` ["f.txt"]
it "replaces the content and leaves no temp file" $ withTempFile $ \dir -> do
let path = dir </> "f.txt"
BS.writeFile path "original"
writeUtf8File path "new"
BS.readFile path `shouldReturn` "new"
listDirectory dir `shouldReturn` ["f.txt"]
#if !defined(mingw32_HOST_OS)
it "keeps the exact mode of an existing 0644 file" $ withTempFile $ \dir ->
modeAfterWrite dir 0o644 `shouldReturn` 0o644
it "keeps the exact mode of an existing 0755 file" $ withTempFile $ \dir ->
modeAfterWrite dir 0o755 `shouldReturn` 0o755
it "gives a new file the default mode, not 0600" $ withTempFile $ \dir -> do
-- A file created the ordinary way gets 0666 minus the umask.
BS.writeFile (dir </> "plain.txt") ""
expected <- modeOf (dir </> "plain.txt")
writeUtf8File (dir </> "new.txt") "x"
modeOf (dir </> "new.txt") `shouldReturn` expected
#endif
describe "read_file_paginated" $ do
it "rejects source files larger than maxPaginatedFileBytes before reading" $
withSandbox $ \sb -> do
cfg <- mkFsConfig sb
let path = sb </> "huge.txt"
BS.writeFile path (BS.replicate (fromIntegral maxPaginatedFileBytes + 1) 0x61)
let TypedTool {ttoolExecute = exec} = readFilePaginatedToolTyped cfg
result <-
exec
()
ReadFilePaginatedArgs
{ _rfpPath = "huge.txt",
_rfpOffset = 1,
_rfpLimit = 10
}
T.unpack result `shouldContain` "exceeds the"
withTempFile :: (FilePath -> IO a) -> IO a
withTempFile = withSystemTempDirectory "llm-simple-fslimits"
withSandbox :: (FilePath -> IO a) -> IO a
withSandbox act = withSystemTempDirectory "llm-simple-fslimits" $ \root -> do
let sb = root </> "sandbox"
createDirectory sb
act sb
#if !defined(mingw32_HOST_OS)
modeOf :: FilePath -> IO FileMode
modeOf p = (`mod` 0o1000) . fileMode <$> getFileStatus p
modeAfterWrite :: FilePath -> FileMode -> IO FileMode
modeAfterWrite dir mode = do
let path = dir </> "f.txt"
BS.writeFile path "old"
setFileMode path mode
writeUtf8File path "new"
modeOf path
#endif