packages feed

baikai-0.3.0.0: test/ContextSpec.hs

module ContextSpec (tests) where

import Baikai
import Control.Lens ((&), (.~), (^.))
import Data.Aeson qualified as Aeson
import Data.Time (UTCTime)
import Data.Vector qualified as V
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))

tests :: TestTree
tests =
  testGroup
    "Context helpers"
    [ monoidTests,
      constructorTests,
      timestampTests,
      flattenTextTests
    ]

monoidTests :: TestTree
monoidTests =
  testGroup
    "Monoid"
    [ testCase "has left and right identity" $ do
        let ctx =
              emptyContext
                { systemPrompt = Just "sys",
                  messages = V.fromList [user "one"],
                  tools = V.singleton sampleTool
                }
        mempty <> ctx @?= ctx
        ctx <> mempty @?= ctx,
      testCase "is associative and keeps the first system prompt" $ do
        let a = systemUser "a-system" "a"
            b = addUser "b" emptyContext
            c =
              emptyContext
                { systemPrompt = Just "c-system",
                  messages = V.fromList [assistant "c"],
                  tools = V.singleton sampleTool
                }
        (a <> b) <> c @?= a <> (b <> c)
        (a <> b <> c) ^. #systemPrompt @?= Just "a-system"
        (b <> c) ^. #systemPrompt @?= Just "c-system"
    ]

constructorTests :: TestTree
constructorTests =
  testGroup
    "constructors"
    [ testCase "contextOf preserves message order" $ do
        contextOf [user "one", assistant "two"] ^. #messages
          @?= V.fromList [user "one", assistant "two"],
      testCase "systemUser creates a system prompt and one user message" $ do
        let ctx = systemUser "system" "prompt"
        ctx ^. #systemPrompt @?= Just "system"
        ctx ^. #messages @?= V.singleton (user "prompt"),
      testCase "addMessage, addUser, and addResponse append in order" $ do
        let resp =
              emptyResponse
                & #message
                  .~ AssistantPayload
                    { content = V.singleton (AssistantText (TextContent "response")),
                      usage = zeroUsage,
                      stopReason = Stop,
                      errorMessage = Nothing,
                      timestamp = Nothing
                    }
            ctx =
              emptyContext
                |> addUser "first"
                |> addMessage (assistant "second")
                |> addResponse resp
        ctx ^. #messages
          @?= V.fromList
            [ user "first",
              assistant "second",
              responseMessage resp
            ]
    ]

timestampTests :: TestTree
timestampTests =
  testGroup
    "timestamps"
    [ testCase "pure constructors do not invent timestamps" $ do
        payloadTimestamp (user "plain") @?= Nothing
        payloadTimestamp (assistant "plain") @?= Nothing
        payloadTimestamp (toolResult "call" "tool" "ok" False) @?= Nothing,
      testCase "explicit constructors preserve timestamps" $ do
        let ts = read "2026-06-05 01:02:03 UTC"
        payloadTimestamp (userAt ts "plain") @?= Just ts
        payloadTimestamp (assistantAt ts "plain") @?= Just ts
        payloadTimestamp (toolResultAt ts "call" "tool" "ok" False) @?= Just ts
    ]

flattenTextTests :: TestTree
flattenTextTests =
  testGroup
    "flattenAssistantText"
    [ testCase "concatenates only text blocks" $ do
        flattenAssistantText
          ( V.fromList
              [ AssistantText (TextContent "hello"),
                AssistantThinking
                  ThinkingContent
                    { thinking = "hidden",
                      signature = Nothing,
                      redacted = False
                    },
                AssistantToolCall emptyToolCall {name = "lookup", arguments = Aeson.object []},
                AssistantText (TextContent " world")
              ]
          )
          @?= "hello world"
    ]

sampleTool :: Tool
sampleTool =
  emptyTool
    { name = "lookup",
      description = "Lookup a value",
      parameters = Aeson.object []
    }

payloadTimestamp :: Message -> Maybe UTCTime
payloadTimestamp (UserMessage UserPayload {timestamp = ts}) = ts
payloadTimestamp (AssistantMessage AssistantPayload {timestamp = ts}) = ts
payloadTimestamp (ToolResultMessage ToolResultPayload {timestamp = ts}) = ts

(|>) :: a -> (a -> b) -> b
(|>) x f = f x