packages feed

shikumi-trace-0.3.0.0: test/ObservationSpec.hs

module ObservationSpec (tests) where

import Data.Aeson (ToJSON, toJSON)
import Data.IORef (newIORef)
import Data.Text (Text)
import Effectful (liftIO, runEff)
import Effectful.Concurrent (runConcurrent)
import Effectful.Concurrent.Async (mapConcurrently)
import Effectful.Error.Static (runErrorNoCallStack)
import Effectful.Prim (runPrim)
import GHC.Generics (Generic)
import Shikumi.Adapter (ToPrompt)
import Shikumi.Error (ShikumiError)
import Shikumi.Module (predict, predictCaptured)
import Shikumi.Program (Program (..))
import Shikumi.Schema (FromModel, ToSchema, Validatable)
import Shikumi.Signature (mkSignature)
import Shikumi.Trace.Observation
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertFailure, testCase, (@?=))
import TraceFixtures (mkResponse, runSequencedLLM)

newtype Cell = Cell {cell :: Text} deriving stock (Eq, Show, Generic)

instance ToJSON Cell

instance ToSchema Cell

instance FromModel Cell

instance ToPrompt Cell

instance Validatable Cell

leaf :: Program Cell Cell
leaf = predictCaptured (mkSignature "Echo")

run :: Program i o -> i -> [Text] -> IO (Either ShikumiError (Either ShikumiError o, [NodeObservation]))
run p i replies = do
  ref <- newIORef (map mkResponse replies)
  runEff . runPrim . runErrorNoCallStack @ShikumiError . runSequencedLLM ref $ runProgramObserved p i

tests :: TestTree
tests =
  testGroup
    "observations"
    [ testCase "concurrent outer examples retain independent inputs and ordinals" $ do
        results <-
          runEff . runConcurrent $
            mapConcurrently
              (\value -> liftIO (run leaf (Cell value) ["{\"cell\":\"ok\"}"]))
              ["one", "two"]
        let observations = [obs | Right (_, [obs]) <- results]
        map observationInput observations @?= map (Just . toJSON . Cell) ["one", "two"]
        map observationInvocation observations @?= [0, 0],
      testCase "retry keeps evidence but rejects successful leaves in failed validation" $ do
        result <- run (Retry 2 (Validate (\x -> if x == Cell "bad" then Left "reject" else Right x) leaf)) (Cell "input") ["{\"cell\":\"bad\"}", "{\"cell\":\"good\"}"]
        case result of
          Right (out, obs) -> do
            out @?= Right (Cell "good")
            map observationInvocation obs @?= [0, 1]
            map observationEligible obs @?= [False, True]
            map observationOutput obs @?= map (Just . toJSON . Cell) ["bad", "good"]
          Left e -> assertFailure (show e),
      testCase "map repeats a structural path with distinct invocations" $ do
        result <- run (Map 2 leaf) [Cell "a", Cell "b"] ["{\"cell\":\"a\"}", "{\"cell\":\"b\"}"]
        case result of
          Right (_, [a, b]) -> do
            observationPath a @?= observationPath b
            map observationInvocation [a, b] @?= [0, 1]
          other -> assertFailure (show other),
      testCase "root failure returns evidence; missing codec is distinct from failure" $ do
        result <- run (predict (mkSignature "Echo") :: Program Cell Cell) (Cell "a") ["broken"]
        case result of
          Right (Left _, [obs]) -> do
            observationInput obs @?= Nothing
            observationEligible obs @?= False
          other -> assertFailure (show other)
    ]