packages feed

seihou-cli-0.4.0.0: test/Seihou/CLI/SavePromptedSpec.hs

module Seihou.CLI.SavePromptedSpec (tests) where

import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Seihou.CLI.SavePrompted (collectPromptedValues, offerSavePrompted)
import Seihou.Composition.Instance (primaryInstance)
import Seihou.Core.Types
import Seihou.Effect.ConfigWriterPure (ConfigWriterState (..), emptyConfigWriterState, runConfigWriterPure)
import Seihou.Effect.ConsolePure (ConsoleState (..), runConsolePure)
import Seihou.Prelude (runEff)
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)

tests :: IO TestTree
tests = testSpec "Seihou.CLI.SavePrompted" spec

mkDecl :: Text -> VarDecl
mkDecl n =
  VarDecl
    { name = VarName n,
      type_ = VTText,
      default_ = Nothing,
      description = Nothing,
      required = True,
      validation = Nothing
    }

mkResolved :: VarSource -> Text -> VarDecl -> ResolvedVar
mkResolved src val decl =
  ResolvedVar
    { value = VText val,
      source = src,
      decl = decl
    }

spec :: Spec
spec = do
  describe "collectPromptedValues" $ do
    it "extracts variables with FromPrompt source" $ do
      let decl = mkDecl "project.name"
          resolved =
            Map.singleton (primaryInstance (ModuleName "mod1")) $
              Map.singleton (VarName "project.name") (mkResolved FromPrompt "my-app" decl)
          result = collectPromptedValues resolved Map.empty
      result `shouldBe` [(VarName "project.name", "my-app", Nothing)]

    it "ignores variables from non-prompt sources" $ do
      let decl = mkDecl "license"
          resolved =
            Map.singleton (primaryInstance (ModuleName "mod1")) $
              Map.fromList
                [ (VarName "license", mkResolved FromGlobalConfig "MIT" decl),
                  (VarName "project.name", mkResolved FromCLI "foo" (mkDecl "project.name"))
                ]
          result = collectPromptedValues resolved Map.empty
      result `shouldBe` []

    it "skips values already in local config with the same value" $ do
      let decl = mkDecl "project.name"
          resolved =
            Map.singleton (primaryInstance (ModuleName "mod1")) $
              Map.singleton (VarName "project.name") (mkResolved FromPrompt "my-app" decl)
          localConfig = Map.singleton (VarName "project.name") "my-app"
          result = collectPromptedValues resolved localConfig
      result `shouldBe` []

    it "includes values that differ from local config with existing value" $ do
      let decl = mkDecl "project.name"
          resolved =
            Map.singleton (primaryInstance (ModuleName "mod1")) $
              Map.singleton (VarName "project.name") (mkResolved FromPrompt "new-app" decl)
          localConfig = Map.singleton (VarName "project.name") "old-app"
          result = collectPromptedValues resolved localConfig
      result `shouldBe` [(VarName "project.name", "new-app", Just "old-app")]

    it "deduplicates across modules" $ do
      let decl = mkDecl "project.name"
          resolved =
            Map.fromList
              [ ( primaryInstance (ModuleName "mod1"),
                  Map.singleton (VarName "project.name") (mkResolved FromPrompt "app1" decl)
                ),
                ( primaryInstance (ModuleName "mod2"),
                  Map.singleton (VarName "project.name") (mkResolved FromPrompt "app2" decl)
                )
              ]
          result = collectPromptedValues resolved Map.empty
      length result `shouldBe` 1

    it "handles multiple prompted variables" $ do
      let resolved =
            Map.singleton (primaryInstance (ModuleName "mod1")) $
              Map.fromList
                [ (VarName "license", mkResolved FromPrompt "MIT" (mkDecl "license")),
                  (VarName "project.name", mkResolved FromPrompt "my-app" (mkDecl "project.name"))
                ]
          result = collectPromptedValues resolved Map.empty
      length result `shouldBe` 2

    it "returns empty list when no prompted values exist" $ do
      let resolved =
            Map.singleton (primaryInstance (ModuleName "mod1")) $
              Map.singleton (VarName "x") (mkResolved FromDefault "val" (mkDecl "x"))
          result = collectPromptedValues resolved Map.empty
      result `shouldBe` []

  describe "offerSavePrompted" $ do
    let entries =
          [ (VarName "project.name", "my-app", Nothing),
            (VarName "license", "MIT", Nothing)
          ]

    it "saves values when user confirms with 'y'" $ do
      (((), cwState), _consoleSt) <-
        runEff $
          runConsolePure ["y"] $
            runConfigWriterPure emptyConfigWriterState $
              offerSavePrompted Nothing True entries
      cwState.cwLocal `shouldBe` Map.fromList [("project.name", "my-app"), ("license", "MIT")]

    it "does not save when user declines with 'n'" $ do
      (((), cwState), _consoleSt) <-
        runEff $
          runConsolePure ["n"] $
            runConfigWriterPure emptyConfigWriterState $
              offerSavePrompted Nothing True entries
      cwState.cwLocal `shouldBe` Map.empty

    it "saves without asking when --save-prompted (Just True)" $ do
      (((), cwState), consoleSt) <-
        runEff $
          runConsolePure [] $
            runConfigWriterPure emptyConfigWriterState $
              offerSavePrompted (Just True) True entries
      cwState.cwLocal `shouldBe` Map.fromList [("project.name", "my-app"), ("license", "MIT")]
      -- Should not contain the confirmation prompt
      any (T.isInfixOf "Save prompted values") consoleSt.consoleOutputs `shouldBe` False

    it "skips entirely when --no-save-prompted (Just False)" $ do
      (((), cwState), consoleSt) <-
        runEff $
          runConsolePure [] $
            runConfigWriterPure emptyConfigWriterState $
              offerSavePrompted (Just False) True entries
      cwState.cwLocal `shouldBe` Map.empty
      consoleSt.consoleOutputs `shouldBe` []

    it "skips in non-interactive mode when no flag given" $ do
      (((), cwState), _consoleSt) <-
        runEff $
          runConsolePure [] $
            runConfigWriterPure emptyConfigWriterState $
              offerSavePrompted Nothing False entries
      cwState.cwLocal `shouldBe` Map.empty

    it "shows overwrite note for existing values" $ do
      let entriesWithOverwrite =
            [ (VarName "project.name", "new-app", Just "old-app")
            ]
      ((_result, _cwState), consoleSt) <-
        runEff $
          runConsolePure ["y"] $
            runConfigWriterPure emptyConfigWriterState $
              offerSavePrompted Nothing True entriesWithOverwrite
      any (T.isInfixOf "overwrites current") consoleSt.consoleOutputs `shouldBe` True

    it "does nothing when entries list is empty" $ do
      (((), cwState), consoleSt) <-
        runEff $
          runConsolePure [] $
            runConfigWriterPure emptyConfigWriterState $
              offerSavePrompted Nothing True []
      cwState.cwLocal `shouldBe` Map.empty
      consoleSt.consoleOutputs `shouldBe` []

    it "displays confirmation message after saving" $ do
      (((), _cwState), consoleSt) <-
        runEff $
          runConsolePure ["y"] $
            runConfigWriterPure emptyConfigWriterState $
              offerSavePrompted Nothing True entries
      any (T.isInfixOf "Saved 2 value(s)") consoleSt.consoleOutputs `shouldBe` True