typesafe-hs-0.1.0.0: test/TypeSafe/Wire/ResponseTest.hs
module TypeSafe.Wire.ResponseTest (tests, fixture) where
import Data.Aeson
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Aeson.Types (parseEither)
import Data.ByteString.Lazy qualified as LBS
import Data.Either (isLeft)
import Data.Map.Strict qualified as Map
import Data.Scientific (scientific)
import Data.Time.Calendar (fromGregorian)
import Data.Vector qualified as Vector
import Test.Tasty
import Test.Tasty.HUnit
import TypeSafe.Wire
fixture :: SystemOneResponse
fixture =
SystemOneResponse
"jev-fixture-version"
( Map.fromList
[ ("urgent", AnswerNoul (NoulAnswer 0.83)),
("category", AnswerChoice (ChoiceAnswer "technical" 0.65 (Map.fromList [("billing", 0.2), ("technical", 0.7)]))),
( "severity",
AnswerScore
( ScoreAnswer
1.5
0.55
(Map.fromList [("0", ContentText "Minor"), ("1", ContentText "Disruptive"), ("2", ContentText "Blocking")])
(Map.fromList [("0", 0.1), ("1", 0.3), ("2", 0.6)])
)
)
]
)
(Usage 120 3)
tests :: TestTree
tests =
testGroup
"Wire.Response"
[ testGroup
"success bodies"
[ testCase "baikai golden decodes to all expected values" $ do
bytes <- LBS.readFile "test/golden/baikai-response.json"
decodeSystemOneResponse bytes @?= Right fixture
fixture.usage.inputTokens @?= 120,
testCase "negative goldens fail" $
mapM_
( \name -> do
bytes <- LBS.readFile ("test/golden/baikai-response-missing-" <> name <> ".json")
assertBool name (isLeft (decodeSystemOneResponse bytes))
)
["confidence", "score"],
testCase "huge numbers survive without semantic validation" $ do
eitherDecode @NoulAnswer "{\"type\":\"noul\",\"noul\":1e400}" @?= Right (NoulAnswer (scientific 1 400))
assertBool "overflow" (isLeft (finiteDouble (scientific 1 400)))
assertBool "unit interval" (isLeft (unitInterval 1.5))
unitInterval 0.5 @?= Right 0.5
eitherDecode @ChoiceAnswer "{\"type\":\"choice\",\"choice\":\"x\",\"confidence\":1.5,\"probabilities\":{}}" @?= Right (ChoiceAnswer "x" 1.5 Map.empty),
testCase "token counts are bounded integral Ints" $ do
assertBool "fraction" (isLeft (eitherDecode @Usage "{\"input_tokens\":3.5,\"output_tokens\":1}"))
assertBool "range" (isLeft (eitherDecode @Usage "{\"input_tokens\":1e30,\"output_tokens\":1}"))
eitherDecode @Usage "{\"input_tokens\":1.2e3,\"output_tokens\":0}" @?= Right (Usage 1200 0),
testCase "unknown answers retain their complete object" $ do
let v = object ["type" .= String "polygon", "points" .= ([1, 2] :: [Int])]
a <- either fail pure (parseEither parseJSON v :: Either String Answer)
case a of
AnswerOther tag o -> do
tag @?= "polygon"
Object o @?= v
_ -> assertFailure "unknown answer coerced"
toJSON a @?= v
eitherDecode @Value (encode a) @?= Right v,
testCase "required fields of each success object" $ do
missing fixture ["model", "answers", "usage"]
missing (ChoiceAnswer "x" 0.2 Map.empty) ["type", "choice", "confidence", "probabilities"]
missing (ScoreAnswer 1 0.2 Map.empty Map.empty) ["type", "score", "confidence", "legend", "probabilities"]
missing (Usage 1 2) ["input_tokens", "output_tokens"]
missing (BoxAnswer (BoxCoordinates 0 1 0 1) Nothing) ["type", "box", "confidence"]
assertBool "bad tag" (isLeft (eitherDecode @Answer "{\"type\":7}")),
testCase "extra fields ignored and nullable confidence retained" $ do
eitherDecode @NoulAnswer "{\"type\":\"noul\",\"noul\":2,\"rationale\":\"x\"}" @?= Right (NoulAnswer 2)
let box = BoxAnswer (BoxCoordinates 0 1 0 1) Nothing
eitherDecode @BoxAnswer (encode box) @?= Right box
eitherDecode @SystemOneResponse (encode fixture) @?= Right fixture
eitherDecode @Value (encode fixture) @?= Right (toJSON fixture)
],
testGroup
"model listing"
[ testCase "listing decodes, ignores tags and parses day" $ do
m <- either fail pure (eitherDecode @ModelMetadataList "{\"models\":[{\"name\":\"jev-latest\",\"description\":\"General\",\"release_date\":\"2026-09-15\",\"tags\":[\"default\"]}]}")
releaseDay (Vector.head m.models) @?= Just (fromGregorian 2026 9 15)
eitherDecode @ModelMetadataList (encode m) @?= Right m,
testCase "required metadata fields and verbatim unusual date" $ do
missing (ModelMetadata "m" "d" "soon") ["name", "description", "release_date"]
missing (ModelMetadataList Vector.empty) ["models"]
releaseDay (ModelMetadata "m" "d" "soon") @?= Nothing
releaseDay (ModelMetadata "m" "d" "2026-13-01") @?= Nothing
],
testGroup
"error bodies"
[ testCase "validation detail with bounded location index and string preview" $ do
let v = ValidationError (Vector.fromList [LocationField "body", LocationIndex 0]) "required" "missing" (Just "null") (Just (KeyMap.singleton "min_length" (Number 1)))
body = HTTPValidationError (Just (Vector.singleton v))
parseErrorBody (encode body) @?= ErrorBodyValidation body
eitherDecode @ValidationError (encode v) @?= Right v
eitherDecode @HTTPValidationError "{}" @?= Right (HTTPValidationError Nothing),
testCase "current HTTPBadRequestError union" $ do
eitherDecode @HTTPBadRequestError "{\"detail\":\"bad request\"}" @?= Right (HTTPBadRequestError (BadRequestDetailText "bad request"))
let b = HTTPBadRequestError (BadRequestDetailObject (KeyMap.singleton "message" (String "x")))
eitherDecode @HTTPBadRequestError (encode b) @?= Right b
missing b ["detail"]
assertBool "null rejected" (isLeft (eitherDecode @HTTPBadRequestError "{\"detail\":null}")),
testCase "structured and text detail" $ do
parseErrorBody "{\"detail\":{\"error_type\":\"api_usage_error\",\"message\":\"Unknown model: x\"}}" @?= ErrorBodyDetail (ErrorDetail (Just "api_usage_error") "Unknown model: x")
parseErrorBody "{\"detail\":\"fastapi style\"}" @?= ErrorBodyDetailText "fastapi style",
testCase "unrecognized or malformed JSON stays raw" $
mapM_
( \bytes -> do
v <- either fail pure (eitherDecode @Value bytes)
parseErrorBody bytes @?= ErrorBodyJson v
)
[ "{}",
"{\"error\":{\"message\":\"invalid api key\"}}",
"{\"error\":\"plain string\"}",
"{\"message\":\"top-level message\"}",
"{\"code\":7}",
"{\"detail\":[{\"msg\":\"no loc\"}]}",
"{\"detail\":[{\"loc\":[1.5],\"msg\":\"m\",\"type\":\"t\"}]}",
"{\"detail\":[{\"loc\":[1e30],\"msg\":\"m\",\"type\":\"t\"}]}",
"{\"detail\":[{\"loc\":[],\"msg\":\"m\",\"type\":\"t\",\"input\":null}]}"
],
testCase "text, invalid UTF8 and empty" $ do
parseErrorBody "<h1>bad gateway</h1>" @?= ErrorBodyText "<h1>bad gateway</h1>"
parseErrorBody (LBS.pack [255, 254]) @?= ErrorBodyText "\xfffd\xfffd"
parseErrorBody "" @?= ErrorBodyEmpty
]
]
missing :: forall a. (ToJSON a, FromJSON a) => a -> [Key] -> Assertion
missing value keys = case toJSON value of
Object o ->
mapM_
( \key ->
assertBool
("required " <> show key)
(isLeft (parseEither (parseJSON @a) (Object (KeyMap.delete key o))))
)
keys
_ -> assertFailure "expected object fixture"