packages feed

shikumi-tools-0.4.0.0: test/ToolOutputSpec.hs

module ToolOutputSpec (tests) where

import Baikai qualified as B
import Control.Lens ((&), (.~), (^.))
import Data.Aeson (object, (.=))
import Data.Generics.Labels ()
import Data.Text qualified as T
import Data.Vector qualified as V
import Effectful.Error.Static (throwError)
import Fixtures (weatherArgs, weatherRegistry)
import Shikumi.Error (ShikumiError (..))
import Shikumi.Testing (runEffScript)
import Shikumi.Tool
import Shikumi.Tool.Output
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit

tests :: TestTree
tests =
  testGroup
    "ToolOutput"
    [ testCase "typed text compatibility" $ do
        let call = B.ToolCall "A" "get_weather" weatherArgs
        rich <- runEffScript [] (runToolCallOutput weatherRegistry call)
        plain <- runEffScript [] (runToolCall weatherRegistry call)
        fmap (fmap renderToolOutput) rich @?= plain,
      testCase "structured-only dynamic output is preserved and visible" $ do
        let value = object ["answer" .= (42 :: Int)]
            output = ToolOutput (B.ToolResult V.empty False) (Just value) [value]
            reg = mkRegistry [mkDynTool "dynamic" "runtime" (object []) (\_ -> pure (Right output))]
            call = B.ToolCall "A" "dynamic" (object [])
        actual <- runEffScript [] (runToolCallOutput reg call)
        actual @?= Right (Right output)
        assertBool "JSON labels" ("Structured JSON:" `T.isInfixOf` renderToolOutput output && "Extension JSON:" `T.isInfixOf` renderToolOutput output)
        case toolOutputMessage call output of
          B.ToolResultMessage p -> do
            p ^. #toolCallId @?= "A"
            V.length (p ^. #content) @?= 2
          _ -> assertFailure "expected tool message",
      testCase "mixed native blocks retain images and error flag" $ do
        let img = B.emptyImageContent & #imageData .~ "bytes" & #mimeType .~ "image/png"
            blocks = V.fromList [B.ToolResultText (B.TextContent "caption"), B.ToolResultImage img]
            output = ToolOutput (B.ToolResult blocks True) Nothing []
        case toolOutputMessage (B.ToolCall "A" "tool" (object [])) output of
          B.ToolResultMessage p -> do
            p ^. #content @?= blocks
            p ^. #isError @?= True
          _ -> assertFailure "expected tool message"
        assertBool "error flag visible in prompt/text projection" ("Tool error:" `T.isPrefixOf` renderToolOutput output)
        assertBool "image not dropped in text projection" ("Image JSON:" `T.isInfixOf` renderToolOutput output),
      testCase "dynamic recoverable failure is model-visible" $ do
        let tool = mkDynTool "bad" "" (object []) (\_ -> throwError (ValidationFailure "bad result"))
        result <- runEffScript [] (runErasedOutput tool (object []))
        result @?= Right (Left (ToolRunFailed "bad" "bad result"))
    ]