packages feed

langchain-hs-gemini-0.0.6.0: test/Test/Gemini/Client/InteractionsSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wno-unused-matches -Wno-incomplete-patterns #-}

{- |
Module      : Test.Gemini.Client.InteractionsSpec
Description : Test every single example from https://ai.google.dev/api/interactions-api-v1
-}
module Test.Gemini.Client.InteractionsSpec (tests) where

import Data.Aeson (encode)
import Data.Aeson.QQ (aesonQQ)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Network.HTTP.Types (status200, status404)
import Network.Wai (Application, rawPathInfo, requestMethod, responseLBS, strictRequestBody)
import Network.Wai.Handler.Warp (testWithApplication)
import System.Directory (doesFileExist, getHomeDirectory)
import System.Environment (lookupEnv)
import System.FilePath ((</>))
import Test.Tasty
import Test.Tasty.HUnit

import Gemini.Client

-- | Helper to start a local mock Warp server on a free port
withMockServer :: Application -> (Text -> IO a) -> IO a
withMockServer app action =
  testWithApplication (pure app) $ \port ->
    action ("http://127.0.0.1:" <> T.pack (show port))

-- | Read GEMINI API KEY from env or ~/.gemini_api_key
getLiveApiKey :: IO (Maybe Text)
getLiveApiKey = do
  mbEnv <- lookupEnv "GEMINI_API_KEY"
  case mbEnv of
    Just k | not (null k) -> pure (Just (T.pack k))
    _ -> do
      home <- getHomeDirectory
      let p = home </> ".gemini_api_key"
      exists <- doesFileExist p
      if exists
        then do
          txt <- T.strip <$> TIO.readFile p
          pure $ if T.null txt then Nothing else Just txt
        else pure Nothing

tests :: TestTree
tests =
  testGroup
    "Gemini.Client.InteractionsSpec (All Documentation Examples)"
    [ mockExamplesTests
    , liveExamplesTests
    ]

-- ---------------------------------------------------------------------------
-- 1. Mock Server Tests (Verifies all 14 examples against exact API contracts)
-- ---------------------------------------------------------------------------

mockExamplesTests :: TestTree
mockExamplesTests =
  testGroup
    "Mock API Contract Tests"
    [ testCase "Example 1: Simple Request" $ do
        let app req respond = do
              body <- strictRequestBody req
              let expectedResp =
                    [aesonQQ|{
                      "id": "v1_mock_simple",
                      "object": "interaction",
                      "status": "completed",
                      "model": "gemini-3.6-flash",
                      "steps": [
                        {
                          "type": "model_output",
                          "content": [{"type": "text", "text": "Why did the chicken cross the road? To get to the other side!"}]
                        }
                      ]
                    }|]
              respond $ responseLBS status200 [("Content-Type", "application/json")] (encode expectedResp)
        withMockServer app $ \base -> do
          env <- newGeminiClientEnvWithBaseUrl base "fake-key"
          let req = createModelInteraction "gemini-3.6-flash" (TextInput "Tell me a joke")
          res <- runGeminiClient env (createInteraction "fake-key" req)
          case res of
            Left err -> assertFailure (show err)
            Right inter -> do
              interactionId inter @?= "v1_mock_simple"
              interactionStatus inter @?= StatusCompleted
              case interactionSteps inter of
                [ModelOutputStep [TextContent txt]] ->
                  assertBool "contains joke" ("chicken" `T.isInfixOf` txt)
                other -> assertFailure ("Unexpected steps: " ++ show other)
    , testCase "Example 2: Multi-turn Conversation" $ do
        let app req respond = do
              body <- strictRequestBody req
              let expectedResp =
                    [aesonQQ|{
                      "id": "v1_mock_multiturn",
                      "object": "interaction",
                      "status": "completed",
                      "steps": [
                        {
                          "type": "model_output",
                          "content": [{"type": "text", "text": "Your name is Bob."}]
                        }
                      ]
                    }|]
              respond $ responseLBS status200 [("Content-Type", "application/json")] (encode expectedResp)
        withMockServer app $ \base -> do
          env <- newGeminiClientEnvWithBaseUrl base "fake-key"
          let steps =
                [ UserInputStep [TextContent "Hi, I am Bob"]
                , ModelOutputStep [TextContent "Hello Bob! Nice to meet you."]
                , UserInputStep [TextContent "What is my name?"]
                ]
              req = createModelInteraction "gemini-3.6-flash" (StepListInput steps)
          res <- runGeminiClient env (createInteraction "fake-key" req)
          case res of
            Left err -> assertFailure (show err)
            Right inter -> interactionStatus inter @?= StatusCompleted
    , testCase "Example 3: Multimodal - Image Input" $ do
        let app req respond = do
              body <- strictRequestBody req
              let expectedResp =
                    [aesonQQ|{
                      "id": "v1_mock_image",
                      "status": "completed",
                      "steps": [
                        {
                          "type": "model_output",
                          "content": [{"type": "text", "text": "This is a red circle."}]
                        }
                      ]
                    }|]
              respond $ responseLBS status200 [("Content-Type", "application/json")] (encode expectedResp)
        withMockServer app $ \base -> do
          env <- newGeminiClientEnvWithBaseUrl base "fake-key"
          let contents =
                [ TextContent "What is in this image?"
                , ImageContent
                    "image/png"
                    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
                ]
              req = createModelInteraction "gemini-3.6-flash" (ContentListInput contents)
          res <- runGeminiClient env (createInteraction "fake-key" req)
          case res of
            Left err -> assertFailure (show err)
            Right inter -> interactionStatus inter @?= StatusCompleted
    , testCase "Example 4: Function Calling (requires_action -> function_result)" $ do
        let app req respond = do
              body <- strictRequestBody req
              let isFollowUp = "\"function_result\"" `BS.isInfixOf` LBS.toStrict body
                  resp =
                    if isFollowUp
                      then
                        [aesonQQ|{
                          "id": "v1_mock_fc_step2",
                          "status": "completed",
                          "steps": [
                            {
                              "type": "model_output",
                              "content": [{"type": "text", "text": "The weather in Tokyo is 20°C and sunny."}]
                            }
                          ]
                        }|]
                      else
                        [aesonQQ|{
                          "id": "v1_mock_fc_step1",
                          "status": "requires_action",
                          "steps": [
                            {
                              "type": "function_call",
                              "id": "call_tokyo",
                              "name": "get_weather",
                              "arguments": {"location": "Tokyo"}
                            }
                          ]
                        }|]
              respond $ responseLBS status200 [("Content-Type", "application/json")] (encode resp)
        withMockServer app $ \base -> do
          env <- newGeminiClientEnvWithBaseUrl base "fake-key"
          let tool = functionTool "get_weather" (Just "Get current weather") (Just [aesonQQ|{"type": "object"}|])
              req1 =
                (createModelInteraction "gemini-3.6-flash" (TextInput "What is the weather in Tokyo?"))
                  { reqTools = Just [tool]
                  }
          res1 <- runGeminiClient env (createInteraction "fake-key" req1)
          case res1 of
            Left err -> assertFailure (show err)
            Right inter1 -> do
              interactionStatus inter1 @?= StatusRequiresAction
              case interactionSteps inter1 of
                [FunctionCallStep {callId = cid, callName = cname}] -> do
                  cid @?= "call_tokyo"
                  cname @?= "get_weather"
                  -- Follow up with function_result
                  let resultStep = FunctionResultStep cid (Just cname) [aesonQQ|{"temperature": 20, "condition": "sunny"}|] Nothing
                      req2 =
                        (defaultCreateInteractionRequest (StepListInput [resultStep]))
                          { reqModel = Just "gemini-3.6-flash"
                          , reqPreviousInteractionId = Just (interactionId inter1)
                          }
                  res2 <- runGeminiClient env (createInteraction "fake-key" req2)
                  case res2 of
                    Left err2 -> assertFailure (show err2)
                    Right inter2 -> interactionStatus inter2 @?= StatusCompleted
                other -> assertFailure ("Expected FunctionCallStep, got: " ++ show other)
    , testCase "Example 5: Deep Research Agent" $ do
        let app req respond = do
              body <- strictRequestBody req
              let expectedResp =
                    [aesonQQ|{
                      "id": "v1_mock_deep_research",
                      "agent": "deep-research",
                      "status": "completed",
                      "steps": [
                        {
                          "type": "model_output",
                          "content": [{"type": "text", "text": "Deep research findings on quantum computing."}]
                        }
                      ]
                    }|]
              respond $ responseLBS status200 [("Content-Type", "application/json")] (encode expectedResp)
        withMockServer app $ \base -> do
          env <- newGeminiClientEnvWithBaseUrl base "fake-key"
          let req = createAgentInteraction "deep-research" (TextInput "Research quantum computing breakthroughs")
          res <- runGeminiClient env (createInteraction "fake-key" req)
          case res of
            Left err -> assertFailure (show err)
            Right inter -> do
              interactionAgent inter @?= Just "deep-research"
              interactionStatus inter @?= StatusCompleted
    , testCase "Example 6: Antigravity Agent" $ do
        let app req respond = do
              body <- strictRequestBody req
              let expectedResp =
                    [aesonQQ|{
                      "id": "v1_mock_antigravity",
                      "agent": "antigravity",
                      "status": "completed",
                      "steps": [
                        {
                          "type": "model_output",
                          "content": [{"type": "text", "text": "Antigravity task completed."}]
                        }
                      ]
                    }|]
              respond $ responseLBS status200 [("Content-Type", "application/json")] (encode expectedResp)
        withMockServer app $ \base -> do
          env <- newGeminiClientEnvWithBaseUrl base "fake-key"
          let req = createAgentInteraction "antigravity" (TextInput "Solve the algorithmic puzzle")
          res <- runGeminiClient env (createInteraction "fake-key" req)
          case res of
            Left err -> assertFailure (show err)
            Right inter -> do
              interactionAgent inter @?= Just "antigravity"
              interactionStatus inter @?= StatusCompleted
    , testCase "Example 7: Reuse Env (previous_interaction_id)" $ do
        let app req respond = do
              body <- strictRequestBody req
              let expectedResp =
                    [aesonQQ|{
                      "id": "v1_mock_reuse",
                      "previous_interaction_id": "v1_mock_prev",
                      "status": "completed",
                      "steps": [
                        {
                          "type": "model_output",
                          "content": [{"type": "text", "text": "Continuing previous session."}]
                        }
                      ]
                    }|]
              respond $ responseLBS status200 [("Content-Type", "application/json")] (encode expectedResp)
        withMockServer app $ \base -> do
          env <- newGeminiClientEnvWithBaseUrl base "fake-key"
          let req =
                (createModelInteraction "gemini-3.6-flash" (TextInput "Continue our conversation"))
                  { reqPreviousInteractionId = Just "v1_mock_prev"
                  }
          res <- runGeminiClient env (createInteraction "fake-key" req)
          case res of
            Left err -> assertFailure (show err)
            Right inter -> interactionPreviousInteractionId inter @?= Just "v1_mock_prev"
    , testCase "Example 8: With Sources / Grounding" $ do
        let app req respond = do
              body <- strictRequestBody req
              let expectedResp =
                    [aesonQQ|{
                      "id": "v1_mock_sources",
                      "status": "completed",
                      "steps": [
                        {
                          "type": "google_search_result",
                          "call_id": "call_search_1",
                          "result": [
                            {
                              "title": "Example Source",
                              "url": "https://example.com/source",
                              "snippet": "Information from source"
                            }
                          ]
                        },
                        {
                          "type": "model_output",
                          "content": [{"type": "text", "text": "According to Example Source, ..."}]
                        }
                      ]
                    }|]
              respond $ responseLBS status200 [("Content-Type", "application/json")] (encode expectedResp)
        withMockServer app $ \base -> do
          env <- newGeminiClientEnvWithBaseUrl base "fake-key"
          let req =
                (createModelInteraction "gemini-3.6-flash" (TextInput "Find recent news"))
                  { reqTools = Just [GoogleSearchTool]
                  }
          res <- runGeminiClient env (createInteraction "fake-key" req)
          case res of
            Left err -> assertFailure (show err)
            Right inter -> do
              let searchSteps = [s | s@(GoogleSearchResultStep {}) <- interactionSteps inter]
              assertBool "Contains google_search_result step" (not (null searchSteps))
    , testCase "Example 9: Custom Agent / System Instruction" $ do
        let app req respond = do
              body <- strictRequestBody req
              let expectedResp =
                    [aesonQQ|{
                      "id": "v1_mock_pirate",
                      "status": "completed",
                      "system_instruction": "You are a pirate.",
                      "steps": [
                        {
                          "type": "model_output",
                          "content": [{"type": "text", "text": "Ahoy matey!"}]
                        }
                      ]
                    }|]
              respond $ responseLBS status200 [("Content-Type", "application/json")] (encode expectedResp)
        withMockServer app $ \base -> do
          env <- newGeminiClientEnvWithBaseUrl base "fake-key"
          let req =
                (createModelInteraction "gemini-3.6-flash" (TextInput "Hello"))
                  { reqSystemInstruction = Just "You are a pirate."
                  }
          res <- runGeminiClient env (createInteraction "fake-key" req)
          case res of
            Left err -> assertFailure (show err)
            Right inter -> interactionSystemInstruction inter @?= Just "You are a pirate."
    , testCase
        "Example 10: Built-in Tools (CodeExecution, GoogleSearch, GoogleMaps, FileSearch, UrlContext)"
        $ do
          let app req respond = do
                body <- strictRequestBody req
                let expectedResp =
                      [aesonQQ|{
                      "id": "v1_mock_tools",
                      "status": "completed",
                      "steps": [
                        {
                          "type": "code_execution_call",
                          "id": "call_py",
                          "arguments": {"code": "print(2+2)"}
                        },
                        {
                          "type": "code_execution_result",
                          "call_id": "call_py",
                          "result": "4\n"
                        },
                        {
                          "type": "model_output",
                          "content": [{"type": "text", "text": "2 + 2 = 4"}]
                        }
                      ]
                    }|]
                respond $ responseLBS status200 [("Content-Type", "application/json")] (encode expectedResp)
          withMockServer app $ \base -> do
            env <- newGeminiClientEnvWithBaseUrl base "fake-key"
            let tools =
                  [ CodeExecutionTool
                  , GoogleSearchTool
                  , GoogleMapsTool (Just 37.77) (Just (-122.41))
                  , FileSearchTool (Just ["my-store"])
                  , UrlContextTool
                  ]
                req =
                  (createModelInteraction "gemini-3.6-flash" (TextInput "Run math code"))
                    { reqTools = Just tools
                    }
            res <- runGeminiClient env (createInteraction "fake-key" req)
            case res of
              Left err -> assertFailure (show err)
              Right inter -> do
                let pyCalls = [s | s@(CodeExecutionCallStep {}) <- interactionSteps inter]
                assertBool "Contains CodeExecutionCallStep" (not (null pyCalls))
    , testCase "Example 11: Cancel Interaction" $ do
        let app req respond = do
              let p = rawPathInfo req
                  m = requestMethod req
              if m == "POST" && "/cancel" `BS.isSuffixOf` p
                then do
                  let expectedResp = [aesonQQ|{"id": "v1_cancel_me", "status": "cancelled", "steps": []}|]
                  respond $ responseLBS status200 [("Content-Type", "application/json")] (encode expectedResp)
                else respond $ responseLBS status404 [] "not found"
        withMockServer app $ \base -> do
          env <- newGeminiClientEnvWithBaseUrl base "fake-key"
          res <- runGeminiClient env (cancelInteraction "fake-key" "v1_cancel_me")
          case res of
            Left err -> assertFailure (show err)
            Right inter -> interactionStatus inter @?= StatusCancelled
    , testCase "Example 12: Get Interaction" $ do
        let app req respond = do
              let expectedResp = [aesonQQ|{"id": "v1_got_it", "status": "completed", "steps": []}|]
              respond $ responseLBS status200 [("Content-Type", "application/json")] (encode expectedResp)
        withMockServer app $ \base -> do
          env <- newGeminiClientEnvWithBaseUrl base "fake-key"
          res <- runGeminiClient env (getInteraction "fake-key" "v1_got_it" Nothing Nothing)
          case res of
            Left err -> assertFailure (show err)
            Right inter -> interactionId inter @?= "v1_got_it"
    , testCase "Example 13: Delete Interaction" $ do
        let app req respond = do
              respond $ responseLBS status200 [("Content-Type", "application/json")] "{}"
        withMockServer app $ \base -> do
          env <- newGeminiClientEnvWithBaseUrl base "fake-key"
          res <- runGeminiClient env (deleteInteraction "fake-key" "v1_del_me")
          case res of
            Left err -> assertFailure (show err)
            Right val -> val @?= [aesonQQ|{}|]
    , testCase "Example 14: Embeddings (embedContent and batchEmbedContents)" $ do
        let app req respond = do
              let p = rawPathInfo req
              if ":embedContent" `BS.isSuffixOf` p
                then do
                  let resp = [aesonQQ|{"embedding": {"values": [0.1, 0.2, 0.3]}}|]
                  respond $ responseLBS status200 [("Content-Type", "application/json")] (encode resp)
                else
                  if ":batchEmbedContents" `BS.isSuffixOf` p
                    then do
                      let resp = [aesonQQ|{"embeddings": [{"values": [0.1, 0.2]}, {"values": [0.3, 0.4]}]}|]
                      respond $ responseLBS status200 [("Content-Type", "application/json")] (encode resp)
                    else respond $ responseLBS status404 [] "not found"
        withMockServer app $ \base -> do
          env <- newGeminiClientEnvWithBaseUrl base "fake-key"
          -- Single
          let singleReq = EmbedContentRequest (EmbedContentPayload [EmbedPart "Query text"])
          res1 <- runGeminiClient env (embedContent "fake-key" "gemini-embedding-001" singleReq)
          case res1 of
            Left err -> assertFailure (show err)
            Right r -> embeddingValues (responseEmbedding r) @?= [0.1, 0.2, 0.3]
          -- Batch
          let batchReq =
                BatchEmbedContentsRequest
                  [ BatchEmbedRequestItem "models/gemini-embedding-001" (EmbedContentPayload [EmbedPart "Doc 1"])
                  , BatchEmbedRequestItem "models/gemini-embedding-001" (EmbedContentPayload [EmbedPart "Doc 2"])
                  ]
          res2 <- runGeminiClient env (batchEmbedContents "fake-key" "gemini-embedding-001" batchReq)
          case res2 of
            Left err -> assertFailure (show err)
            Right r -> length (batchEmbeddings r) @?= 2
    ]

-- ---------------------------------------------------------------------------
-- 2. Live API Tests (executed when ~/.gemini_api_key or GEMINI_API_KEY is present)
-- ---------------------------------------------------------------------------

liveExamplesTests :: TestTree
liveExamplesTests =
  testGroup
    "Live Google Gemini API Tests (~/.gemini_api_key)"
    [ testCase "Live: Simple Request, Get, and Delete Interaction lifecycle" $ do
        mbKey <- getLiveApiKey
        case mbKey of
          Nothing -> pure () -- Skip if no live key available
          Just key -> do
            env <- newGeminiClientEnv key
            let req = createModelInteraction "gemini-3.6-flash" (TextInput "Reply with the exact word PONG")
            resCreate <- runGeminiClient env (createInteraction key req)
            case resCreate of
              Left err -> assertFailure ("Live createInteraction failed: " ++ show err)
              Right inter -> do
                let interId = interactionId inter
                assertBool "Interaction ID is non-empty" (not (T.null interId))
                -- Test GET interaction
                resGet <- runGeminiClient env (getInteraction key interId Nothing Nothing)
                case resGet of
                  Left err -> assertFailure ("Live getInteraction failed: " ++ show err)
                  Right gotInter -> interactionId gotInter @?= interId
                -- Test DELETE interaction
                resDel <- runGeminiClient env (deleteInteraction key interId)
                case resDel of
                  Left err -> assertFailure ("Live deleteInteraction failed: " ++ show err)
                  Right _ -> pure ()
    , testCase "Live: Multi-turn Conversation" $ do
        mbKey <- getLiveApiKey
        case mbKey of
          Nothing -> pure ()
          Just key -> do
            env <- newGeminiClientEnv key
            let steps =
                  [ UserInputStep [TextContent "My favorite fruit is Mango."]
                  , ModelOutputStep [TextContent "Got it! Your favorite fruit is Mango."]
                  , UserInputStep [TextContent "What is my favorite fruit? Reply in one word."]
                  ]
                req = createModelInteraction "gemini-3.6-flash" (StepListInput steps)
            res <- runGeminiClient env (createInteraction key req)
            case res of
              Left err -> assertFailure ("Live multi-turn failed: " ++ show err)
              Right inter -> do
                interactionStatus inter @?= StatusCompleted
                -- Clean up
                _ <- runGeminiClient env (deleteInteraction key (interactionId inter))
                pure ()
    , testCase "Live: Function Calling (Tool use)" $ do
        mbKey <- getLiveApiKey
        case mbKey of
          Nothing -> pure ()
          Just key -> do
            env <- newGeminiClientEnv key
            let weatherTool =
                  functionTool
                    "get_weather"
                    (Just "Get the weather for a location")
                    ( Just
                        [aesonQQ|{
                      "type": "object",
                      "properties": {
                        "location": {"type": "string"}
                      },
                      "required": ["location"]
                    }|]
                    )
                req =
                  (createModelInteraction "gemini-3.6-flash" (TextInput "What is the weather in Tokyo?"))
                    { reqTools = Just [weatherTool]
                    }
            res <- runGeminiClient env (createInteraction key req)
            case res of
              Left err -> assertFailure ("Live tool call failed: " ++ show err)
              Right inter -> do
                interactionStatus inter @?= StatusRequiresAction
                case [s | s@(FunctionCallStep {}) <- interactionSteps inter] of
                  (FunctionCallStep {..} : _) -> do
                    callName @?= "get_weather"
                    -- Follow up with tool result
                    let resultStep =
                          FunctionResultStep
                            callId
                            (Just callName)
                            [aesonQQ|{"temperature": 22, "condition": "Sunny"}|]
                            Nothing
                        followUp =
                          (defaultCreateInteractionRequest (StepListInput [resultStep]))
                            { reqModel = Just "gemini-3.6-flash"
                            , reqPreviousInteractionId = Just (interactionId inter)
                            }
                    resFollowUp <- runGeminiClient env (createInteraction key followUp)
                    case resFollowUp of
                      Left fErr -> assertFailure ("Live follow-up failed: " ++ show fErr)
                      Right completedInter -> do
                        interactionStatus completedInter @?= StatusCompleted
                        -- Clean up
                        _ <- runGeminiClient env (deleteInteraction key (interactionId completedInter))
                        pure ()
                  [] -> assertFailure "Expected FunctionCallStep in interaction steps"
                _ <- runGeminiClient env (deleteInteraction key (interactionId inter))
                pure ()
    , testCase "Live: Built-in Code Execution Tool" $ do
        mbKey <- getLiveApiKey
        case mbKey of
          Nothing -> pure ()
          Just key -> do
            env <- newGeminiClientEnv key
            let req =
                  (createModelInteraction "gemini-3.6-flash" (TextInput "Run python code to compute 999 * 333"))
                    { reqTools = Just [CodeExecutionTool]
                    }
            res <- runGeminiClient env (createInteraction key req)
            case res of
              Left err -> assertFailure ("Live code execution failed: " ++ show err)
              Right inter -> do
                interactionStatus inter @?= StatusCompleted
                _ <- runGeminiClient env (deleteInteraction key (interactionId inter))
                pure ()
    , testCase "Live: Built-in Google Search Tool" $ do
        mbKey <- getLiveApiKey
        case mbKey of
          Nothing -> pure ()
          Just key -> do
            env <- newGeminiClientEnv key
            let req =
                  (createModelInteraction "gemini-3.6-flash" (TextInput "Who won the cricket world cup in 2023?"))
                    { reqTools = Just [GoogleSearchTool]
                    }
            res <- runGeminiClient env (createInteraction key req)
            case res of
              Left err -> assertFailure ("Live google search failed: " ++ show err)
              Right inter -> do
                interactionStatus inter @?= StatusCompleted
                _ <- runGeminiClient env (deleteInteraction key (interactionId inter))
                pure ()
    , testCase "Live: Background Interaction and Cancel" $ do
        mbKey <- getLiveApiKey
        case mbKey of
          Nothing -> pure ()
          Just key -> do
            env <- newGeminiClientEnv key
            let req =
                  (createModelInteraction "gemini-3.6-flash" (TextInput "Write a detailed history of the universe"))
                    { reqBackground = Just True
                    }
            resCreate <- runGeminiClient env (createInteraction key req)
            case resCreate of
              Left err -> assertFailure ("Live background create failed: " ++ show err)
              Right inter -> do
                let interId = interactionId inter
                resCancel <- runGeminiClient env (cancelInteraction key interId)
                case resCancel of
                  Left err -> assertFailure ("Live cancel failed: " ++ show err)
                  Right cancelled -> do
                    interactionStatus cancelled @?= StatusCancelled
                    _ <- runGeminiClient env (deleteInteraction key interId)
                    pure ()
    , testCase "Live: Embeddings (gemini-embedding-001)" $ do
        mbKey <- getLiveApiKey
        case mbKey of
          Nothing -> pure ()
          Just key -> do
            env <- newGeminiClientEnv key
            -- Single embed
            let singleReq = EmbedContentRequest (EmbedContentPayload [EmbedPart "Haskell LangChain"])
            resSingle <- runGeminiClient env (embedContent key "gemini-embedding-001" singleReq)
            case resSingle of
              Left err -> assertFailure ("Live embedContent failed: " ++ show err)
              Right resp -> do
                let vec = embeddingValues (responseEmbedding resp)
                assertBool "Embedding dimension > 0" (not (null vec))
            -- Batch embed
            let batchReq =
                  BatchEmbedContentsRequest
                    [ BatchEmbedRequestItem "models/gemini-embedding-001" (EmbedContentPayload [EmbedPart "Hello"])
                    , BatchEmbedRequestItem "models/gemini-embedding-001" (EmbedContentPayload [EmbedPart "World"])
                    ]
            resBatch <- runGeminiClient env (batchEmbedContents key "gemini-embedding-001" batchReq)
            case resBatch of
              Left err -> assertFailure ("Live batchEmbedContents failed: " ++ show err)
              Right resp -> length (batchEmbeddings resp) @?= 2
    ]