seihou-core-0.13.0.0: test/Seihou/Interaction/PromptSpec.hs
module Seihou.Interaction.PromptSpec (tests) where
import Control.Lens ((&), (.~), (^.))
import Control.Monad (forM_)
import Data.Generics.Labels ()
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Effectful
import Seihou.Composition.Instance (primaryInstance)
import Seihou.Composition.Resolve (PromptPermission (..), RecipeInputSpec (..), resolveRecipeWithPrompts, resolveWithPromptPermission, resolveWithPrompts)
import Seihou.Core.Types
import Seihou.Effect.ConsolePure
import Seihou.Interaction.Prompt (promptForVar, runPrompts)
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)
tests :: IO TestTree
tests = testSpec "Seihou.Interaction.Prompt" spec
-- | Helper to create a minimal module.
mkModule :: ModuleName -> [ModuleName] -> [VarDecl] -> [VarExport] -> [Prompt] -> Module
mkModule name deps vars exports prompts =
Module
{ name = name,
version = Nothing,
description = Nothing,
vars = vars,
exports = exports,
prompts = prompts,
steps = [],
commands = [],
dependencies = map simpleDep deps,
removal = Nothing,
migrations = []
}
-- | Helper to create a text variable declaration.
mkTextVar :: VarName -> Maybe VarValue -> Bool -> VarDecl
mkTextVar name defVal required =
VarDecl
{ name = name,
type_ = VTText,
default_ = defVal,
description = Nothing,
required = required,
validation = Nothing
}
-- | Helper to create a bool variable declaration.
mkBoolVar :: VarName -> Maybe VarValue -> Bool -> VarDecl
mkBoolVar name defVal required =
VarDecl
{ name = name,
type_ = VTBool,
default_ = defVal,
description = Nothing,
required = required,
validation = Nothing
}
-- | Helper to create a simple prompt for a variable.
mkPrompt :: VarName -> Text -> Prompt
mkPrompt var text =
Prompt
{ var = var,
text = text,
condition = Nothing,
choices = Nothing
}
-- | Helper to create a prompt with choices.
mkChoicePrompt :: VarName -> Text -> [Text] -> Prompt
mkChoicePrompt var text choices =
Prompt
{ var = var,
text = text,
condition = Nothing,
choices = Just choices
}
-- | Helper to create a prompt with a when condition.
mkConditionalPrompt :: VarName -> Text -> Expr -> Prompt
mkConditionalPrompt var text expr =
Prompt
{ var = var,
text = text,
condition = Just expr,
choices = Nothing
}
-- | Helper to create an export without alias.
mkExport :: VarName -> VarExport
mkExport name = VarExport {var = name, alias = Nothing}
spec :: Spec
spec = do
describe "recipe input resolution" $ do
let feature = mkBoolVar "feature" Nothing True
consumer = mkModule "consumer" [] [mkBoolVar "feature" (Just (VBool False)) False] [] []
modules = [(primaryInstance "consumer", consumer, "/fixture")]
resolve permission spec saved cli literals local =
resolveRecipeWithPrompts permission False spec modules saved cli literals Map.empty "" "" local Map.empty Map.empty Map.empty
recipe = RecipeInputSpec [feature] [mkPrompt "feature" "Feature?"] Nothing
it "requires the recipe choice before considering a consumer default" $ do
let (result, _) = runPureEff $ runConsolePureNonInteractive $ resolve PromptsForbidden recipe Map.empty Map.empty Map.empty Map.empty
result `shouldBe` Left [MissingRequiredVar "feature"]
it "broadcasts explicit false and retains prompt source" $ do
let (result, console) = runPureEff $ runConsolePure ["false"] $ resolve PromptsAllowed recipe Map.empty Map.empty Map.empty Map.empty
case result of
Left errs -> expectationFailure (show errs)
Right (inputs, values) -> do
(inputs Map.! "feature") ^. #value `shouldBe` VBool False
(values Map.! primaryInstance "consumer" Map.! "feature") ^. #source `shouldBe` FromPrompt
console ^. #outputs `shouldBe` ["Feature?"]
it "keeps CLI above literals, and literals above config" $ do
let check cli literal local = fst $ runPureEff $ runConsolePureNonInteractive $ resolve PromptsForbidden recipe Map.empty cli literal local
case check (Map.singleton "feature" "false") (Map.singleton "feature" "true") Map.empty of
Right (inputs, _) -> (inputs Map.! "feature") ^. #value `shouldBe` VBool False
Left errs -> expectationFailure (show errs)
case check Map.empty (Map.singleton "feature" "false") (Map.singleton "feature" "true") of
Right (inputs, _) -> (inputs Map.! "feature") ^. #value `shouldBe` VBool False
Left errs -> expectationFailure (show errs)
it "lets optional conditions see resolved siblings despite another missing input" $ do
let details = mkTextVar "details" Nothing False
spec =
RecipeInputSpec
[feature, mkBoolVar "other" Nothing True, details]
[mkPrompt "other" "Other?", mkConditionalPrompt "details" "Details?" (ExprEq "feature" (VBool True))]
Nothing
(result, console) = runPureEff $ runConsolePure ["false", "owner"] $ resolve PromptsAllowed spec Map.empty Map.empty Map.empty (Map.singleton "feature" "true")
case result of
Right (inputs, _) -> (inputs Map.! "details") ^. #value `shouldBe` VText "owner"
Left errs -> expectationFailure (show errs)
console ^. #outputs `shouldBe` ["Other?", "Details? [skip]:"]
it "rejects exhausted invalid optional answers" $ do
let optional = feature & #required .~ False
spec = RecipeInputSpec [optional] [mkPrompt "feature" "Feature?"] Nothing
(result, _) = runPureEff $ runConsolePure ["bad", "bad", "bad"] $ resolve PromptsAllowed spec Map.empty Map.empty Map.empty Map.empty
result `shouldBe` Left [CoercionFailed "feature" VTBool "bad"]
it "adopts historical false through recipe coercion and rejects disagreeing consumers" $ do
let second = primaryInstance "second"
both = modules <> [(second, consumer, "/second")]
check saved cli =
fst $
runPureEff $
runConsolePureNonInteractive $
resolveRecipeWithPrompts PromptsForbidden False recipe both saved cli Map.empty Map.empty "" "" Map.empty Map.empty Map.empty Map.empty
saved a b = Map.fromList [(primaryInstance "consumer", Map.singleton "feature" a), (second, Map.singleton "feature" b)]
case check (saved "false" "no") Map.empty of
Right (inputs, _) -> (inputs Map.! "feature") ^. #value `shouldBe` VBool False
Left errs -> expectationFailure (show errs)
case check (saved "false" "true") Map.empty of
Left [ValidationFailed "feature" _] -> pure ()
other -> expectationFailure (show other)
case check (saved "false" "true") (Map.singleton "feature" "true") of
Right (inputs, _) -> (inputs Map.! "feature") ^. #value `shouldBe` VBool True
Left errs -> expectationFailure (show errs)
it "validates the broadcast against each consumer declaration" $ do
let invalid = consumer & #vars .~ [mkTextVar "feature" Nothing True & #validation .~ Just (ValMinLength 10)]
(result, _) =
runPureEff $
runConsolePureNonInteractive $
resolveRecipeWithPrompts PromptsForbidden False recipe [(primaryInstance "consumer", invalid, "/fixture")] Map.empty (Map.singleton "feature" "true") Map.empty Map.empty "" "" Map.empty Map.empty Map.empty Map.empty
case result of
Left [ValidationFailed "feature" _] -> pure ()
other -> expectationFailure (show other)
it "replays recorded choices before config and skips accepted optional omissions" $ do
let optional = mkTextVar "details" Nothing False
spec = RecipeInputSpec [feature, optional] [mkPrompt "details" "Details?"] (Just (Map.singleton "feature" "false"))
(result, console) = runPureEff $ runConsolePure [] $ resolve PromptsAllowed spec Map.empty Map.empty Map.empty (Map.singleton "feature" "true")
case result of
Right (inputs, _) -> (inputs Map.! "feature") ^. #value `shouldBe` VBool False
Left errs -> expectationFailure (show errs)
console ^. #outputs `shouldBe` []
describe "owner policy" $ do
let owner = mkTextVar "git.githubOwner" Nothing True
policy = owner & #validation .~ Just (ValPattern "^topagentnetwork$")
prompt = mkPrompt "git.githubOwner" "Owner?"
check source (Left [ValidationFailed "git.githubOwner" msg]) = msg `shouldSatisfy` T.isInfixOf source
check _ other = expectationFailure (show other)
resolve recipeDecl memberDecl recorded cli literals global answers confirm =
fst $
runPureEff $
runConsolePure answers $
resolveRecipeWithPrompts
PromptsAllowed
confirm
(RecipeInputSpec [recipeDecl] [prompt] recorded)
[(primaryInstance "consumer", mkModule "consumer" [] [memberDecl] [] [], "/fixture")]
Map.empty
cli
literals
Map.empty
""
""
Map.empty
Map.empty
Map.empty
global
forM_ [True, False] $ \recipeOwnsPolicy -> do
let r = if recipeOwnsPolicy then policy else owner
m = if recipeOwnsPolicy then owner else policy
it ("rejects global input with its source; recipe policy=" <> show recipeOwnsPolicy) $
check "global config" (resolve r m Nothing Map.empty Map.empty (Map.singleton "git.githubOwner" "personal") [] False)
it ("revalidates history with its source; recipe policy=" <> show recipeOwnsPolicy) $
check "application" (resolve r m (Just (Map.singleton "git.githubOwner" "personal")) Map.empty Map.empty Map.empty [] False)
it ("rejects literals with their source; recipe policy=" <> show recipeOwnsPolicy) $
check "literal recipe binding" (resolve r m Nothing Map.empty (Map.singleton "git.githubOwner" "personal") Map.empty [] False)
it ("rejects prompt input with its source; recipe policy=" <> show recipeOwnsPolicy) $
check "prompt" (resolve r m Nothing Map.empty Map.empty Map.empty ["personal", "personal", "personal"] False)
it "accepts a valid retry after a rejected answer" $ do
case resolve policy owner Nothing Map.empty Map.empty Map.empty ["personal", "topagentnetwork"] False of
Right (inputs, _) -> (inputs Map.! "git.githubOwner") ^. #source `shouldBe` FromPrompt
other -> expectationFailure (show other)
it "validates numbered prompt choices" $ do
let (result, _) = runPureEff $ runConsolePure ["1"] $ promptForVar (mkChoicePrompt "git.githubOwner" "Owner?" ["personal", "topagentnetwork"]) policy Map.empty
case result of
Left (ValidationFailed _ msg) -> msg `shouldSatisfy` T.isInfixOf "prompt"
other -> expectationFailure (show other)
it "validates a changed answer during confirm-defaults" $
check "prompt" (resolve (policy & #default_ .~ Just (VText "topagentnetwork")) owner Nothing Map.empty Map.empty Map.empty ["personal", "personal", "personal"] True)
it "validates an accepted prompt default" $ do
let (result, _) = runPureEff $ runConsolePure [""] $ promptForVar prompt (policy & #default_ .~ Just (VText "personal")) Map.empty
case result of
Left (ValidationFailed _ msg) -> msg `shouldSatisfy` T.isInfixOf "prompt"
other -> expectationFailure (show other)
describe "runPrompts" $ do
it "fills a required text variable with FromPrompt source" $ do
let decl = mkTextVar "project.name" Nothing True
prompt = mkPrompt "project.name" "What is the project name?"
bindings = Map.empty
(result, st) <-
runEff $
runConsolePure ["my-app"] $
runPrompts [prompt] [decl] bindings
Map.member "project.name" result `shouldBe` True
let rv = result Map.! "project.name"
(rv ^. #value) `shouldBe` VText "my-app"
(rv ^. #source) `shouldBe` FromPrompt
-- The prompt text should have been output
(st ^. #outputs) `shouldSatisfy` any (== "What is the project name?")
it "fills a prompt with choices via selection number" $ do
let decl = mkTextVar "license" Nothing True
prompt = mkChoicePrompt "license" "Choose a license:" ["MIT", "Apache-2.0", "BSD-3-Clause"]
bindings = Map.empty
(result, _st) <-
runEff $
runConsolePure ["2"] $
runPrompts [prompt] [decl] bindings
Map.member "license" result `shouldBe` True
((result Map.! "license") ^. #value) `shouldBe` VText "Apache-2.0"
((result Map.! "license") ^. #source) `shouldBe` FromPrompt
it "skips a prompt whose when condition evaluates to False" $ do
let decl = mkTextVar "extra.flag" Nothing True
-- Condition: IsSet license — but license is not in bindings
prompt = mkConditionalPrompt "extra.flag" "Extra flag?" (ExprIsSet "license")
bindings = Map.empty
(result, st) <-
runEff $
runConsolePure ["some-value"] $
runPrompts [prompt] [decl] bindings
-- Prompt was skipped, so the variable is not resolved
Map.member "extra.flag" result `shouldBe` False
-- No prompt text was output
(st ^. #outputs) `shouldSatisfy` all (/= "Extra flag?")
it "shows a prompt whose when condition evaluates to True" $ do
let decl = mkTextVar "extra.flag" Nothing True
-- Condition: IsSet license — license IS in bindings
prompt = mkConditionalPrompt "extra.flag" "Extra flag?" (ExprIsSet "license")
bindings = Map.singleton "license" (VText "MIT")
(result, _st) <-
runEff $
runConsolePure ["some-value"] $
runPrompts [prompt] [decl] bindings
Map.member "extra.flag" result `shouldBe` True
((result Map.! "extra.flag") ^. #value) `shouldBe` VText "some-value"
it "skips a prompt for a variable not in the unresolved set" $ do
let decl = mkTextVar "project.name" Nothing True
-- Prompt references a different variable
prompt = mkPrompt "other.var" "Other?"
bindings = Map.empty
(result, st) <-
runEff $
runConsolePure ["anything"] $
runPrompts [prompt] [decl] bindings
Map.null result `shouldBe` True
(st ^. #outputs) `shouldSatisfy` all (/= "Other?")
describe "default value display" $ do
it "shows default value in prompt text and accepts Enter" $ do
let decl = mkTextVar "project.version" (Just (VText "0.1.0.0")) True
prompt = mkPrompt "project.version" "Project version"
(result, st) <-
runEff $
runConsolePure [""] $
promptForVar prompt decl Map.empty
case result of
Left err -> expectationFailure $ "Expected Right, got: " ++ show err
Right rv -> do
(rv ^. #value) `shouldBe` VText "0.1.0.0"
(rv ^. #source) `shouldBe` FromPrompt
-- Prompt text should include the default in brackets
(st ^. #outputs) `shouldSatisfy` any (== "Project version [0.1.0.0]:")
it "accepts user input over default when provided" $ do
let decl = mkTextVar "project.version" (Just (VText "0.1.0.0")) True
prompt = mkPrompt "project.version" "Project version"
(result, st) <-
runEff $
runConsolePure ["1.0.0"] $
promptForVar prompt decl Map.empty
case result of
Left err -> expectationFailure $ "Expected Right, got: " ++ show err
Right rv ->
(rv ^. #value) `shouldBe` VText "1.0.0"
(st ^. #outputs) `shouldSatisfy` any (== "Project version [0.1.0.0]:")
it "shows [skip] for optional variable without default" $ do
let decl = mkTextVar "license" Nothing False
prompt = mkPrompt "license" "License"
(_result, st) <-
runEff $
runConsolePure [""] $
promptForVar prompt decl Map.empty
(st ^. #outputs) `shouldSatisfy` any (== "License [skip]:")
it "shows bool default as yes/no" $ do
let decl = mkBoolVar "enable.ci" (Just (VBool True)) False
prompt = mkPrompt "enable.ci" "Enable CI?"
(result, st) <-
runEff $
runConsolePure [""] $
promptForVar prompt decl Map.empty
case result of
Left err -> expectationFailure $ "Expected Right, got: " ++ show err
Right rv ->
(rv ^. #value) `shouldBe` VBool True
(st ^. #outputs) `shouldSatisfy` any (== "Enable CI? [yes]:")
describe "promptForVar" $ do
it "coerces boolean input correctly" $ do
let decl = mkBoolVar "use.ci" Nothing True
prompt = mkPrompt "use.ci" "Enable CI?"
(result, _st) <-
runEff $
runConsolePure ["yes"] $
promptForVar prompt decl Map.empty
case result of
Left err -> expectationFailure $ "Expected Right, got: " ++ show err
Right rv -> do
(rv ^. #value) `shouldBe` VBool True
(rv ^. #source) `shouldBe` FromPrompt
it "coerces 'no' to False for boolean variable" $ do
let decl = mkBoolVar "use.ci" Nothing True
prompt = mkPrompt "use.ci" "Enable CI?"
(result, _st) <-
runEff $
runConsolePure ["no"] $
promptForVar prompt decl Map.empty
case result of
Left err -> expectationFailure $ "Expected Right, got: " ++ show err
Right rv ->
(rv ^. #value) `shouldBe` VBool False
it "retries on empty input then succeeds" $ do
let decl = mkTextVar "project.name" Nothing True
prompt = mkPrompt "project.name" "Project name?"
(result, st) <-
runEff $
runConsolePure ["", "my-app"] $
promptForVar prompt decl Map.empty
case result of
Left err -> expectationFailure $ "Expected Right, got: " ++ show err
Right rv ->
(rv ^. #value) `shouldBe` VText "my-app"
-- Should have output a retry message
(st ^. #outputs) `shouldSatisfy` any (== "Value cannot be empty. Please try again.")
it "fails after exhausting retries on empty input" $ do
let decl = mkTextVar "project.name" Nothing True
prompt = mkPrompt "project.name" "Project name?"
(result, _st) <-
runEff $
runConsolePure ["", "", ""] $
promptForVar prompt decl Map.empty
case result of
Left (MissingRequiredVar _) -> pure ()
Left err -> expectationFailure $ "Expected MissingRequiredVar, got: " ++ show err
Right _ -> expectationFailure "Expected Left, got Right"
describe "resolveWithPrompts" $ do
it "prompts for missing required variables in interactive mode" $ do
let m =
mkModule
"base"
[]
[mkTextVar "project.name" Nothing True]
[]
[mkPrompt "project.name" "What is the project name?"]
modules = [(primaryInstance (m ^. #name), m, "/fake/base")]
(result, st) <-
runEff $
runConsolePure ["my-app"] $
resolveWithPrompts modules Map.empty Map.empty "" "" Map.empty Map.empty Map.empty Map.empty
case result of
Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs
Right resolved -> do
let baseVars = resolved Map.! primaryInstance "base"
((baseVars Map.! "project.name") ^. #value) `shouldBe` VText "my-app"
((baseVars Map.! "project.name") ^. #source) `shouldBe` FromPrompt
(st ^. #outputs) `shouldSatisfy` any (== "What is the project name?")
it "does not prompt when all variables are provided via CLI" $ do
let m =
mkModule
"base"
[]
[mkTextVar "project.name" Nothing True]
[]
[mkPrompt "project.name" "What is the project name?"]
modules = [(primaryInstance (m ^. #name), m, "/fake/base")]
cliOverrides = Map.singleton "project.name" "from-cli"
(result, st) <-
runEff $
runConsolePure [] $
resolveWithPrompts modules cliOverrides Map.empty "" "" Map.empty Map.empty Map.empty Map.empty
case result of
Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs
Right resolved -> do
let baseVars = resolved Map.! primaryInstance "base"
((baseVars Map.! "project.name") ^. #value) `shouldBe` VText "from-cli"
((baseVars Map.! "project.name") ^. #source) `shouldBe` FromCLI
-- No prompts should have been displayed
(st ^. #outputs) `shouldSatisfy` all (/= "What is the project name?")
it "skips prompts and errors in non-interactive mode" $ do
let m =
mkModule
"base"
[]
[mkTextVar "project.name" Nothing True]
[]
[mkPrompt "project.name" "What is the project name?"]
modules = [(primaryInstance (m ^. #name), m, "/fake/base")]
(result, st) <-
runEff $
runConsolePureNonInteractive $
resolveWithPrompts modules Map.empty Map.empty "" "" Map.empty Map.empty Map.empty Map.empty
case result of
Left errs -> case errs of
[MissingRequiredVar name] -> name `shouldBe` "project.name"
[other] -> expectationFailure $ "Expected MissingRequiredVar, got: " ++ show other
_ -> expectationFailure $ "Expected exactly 1 error, got: " ++ show (length errs)
Right _ -> expectationFailure "Expected Left (errors), got Right"
-- No prompts should have been displayed
(st ^. #outputs) `shouldSatisfy` all (/= "What is the project name?")
it "forbids prompts even when the Console interpreter is interactive" $ do
let m =
mkModule
"base"
[]
[mkTextVar "project.name" Nothing True]
[]
[mkPrompt "project.name" "What is the project name?"]
modules = [(primaryInstance (m ^. #name), m, "/fake/base")]
(result, st) <-
runEff $
runConsolePure ["must-not-be-read"] $
resolveWithPromptPermission PromptsForbidden modules Map.empty Map.empty Map.empty "" "" Map.empty Map.empty Map.empty Map.empty
result `shouldBe` Left [MissingRequiredVar "project.name"]
(st ^. #inputs) `shouldBe` ["must-not-be-read"]
(st ^. #outputs) `shouldSatisfy` all (/= "What is the project name?")
it "prompts for optional variables after required resolution" $ do
let m =
mkModule
"base"
[]
[ mkTextVar "project.name" Nothing True,
mkTextVar "license" Nothing False
]
[]
[ mkPrompt "project.name" "What is the project name?",
mkPrompt "license" "License"
]
modules = [(primaryInstance (m ^. #name), m, "/fake/base")]
(result, st) <-
runEff $
runConsolePure ["my-app", "MIT"] $
resolveWithPrompts modules Map.empty Map.empty "" "" Map.empty Map.empty Map.empty Map.empty
case result of
Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs
Right resolved -> do
let baseVars = resolved Map.! primaryInstance "base"
((baseVars Map.! "project.name") ^. #value) `shouldBe` VText "my-app"
((baseVars Map.! "project.name") ^. #source) `shouldBe` FromPrompt
((baseVars Map.! "license") ^. #value) `shouldBe` VText "MIT"
((baseVars Map.! "license") ^. #source) `shouldBe` FromPrompt
(st ^. #outputs) `shouldSatisfy` any (== "Optional configuration:")
it "skips optional variable when user presses Enter" $ do
let m =
mkModule
"base"
[]
[ mkTextVar "project.name" Nothing True,
mkTextVar "license" Nothing False
]
[]
[ mkPrompt "project.name" "What is the project name?",
mkPrompt "license" "License"
]
modules = [(primaryInstance (m ^. #name), m, "/fake/base")]
(result, _st) <-
runEff $
runConsolePure ["my-app", ""] $
resolveWithPrompts modules Map.empty Map.empty "" "" Map.empty Map.empty Map.empty Map.empty
case result of
Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs
Right resolved -> do
let baseVars = resolved Map.! primaryInstance "base"
Map.member "project.name" baseVars `shouldBe` True
Map.member "license" baseVars `shouldBe` False
it "shows Optional configuration separator for optional-only modules" $ do
let m =
mkModule
"base"
[]
[mkTextVar "license" Nothing False]
[]
[mkPrompt "license" "License"]
modules = [(primaryInstance (m ^. #name), m, "/fake/base")]
(result, st) <-
runEff $
runConsolePure ["MIT"] $
resolveWithPrompts modules Map.empty Map.empty "" "" Map.empty Map.empty Map.empty Map.empty
case result of
Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs
Right resolved -> do
let baseVars = resolved Map.! primaryInstance "base"
((baseVars Map.! "license") ^. #value) `shouldBe` VText "MIT"
(st ^. #outputs) `shouldSatisfy` any (== "Optional configuration:")
it "does not show optional prompts in non-interactive mode" $ do
let m =
mkModule
"base"
[]
[mkTextVar "license" Nothing False]
[]
[mkPrompt "license" "License"]
modules = [(primaryInstance (m ^. #name), m, "/fake/base")]
(result, st) <-
runEff $
runConsolePureNonInteractive $
resolveWithPrompts modules Map.empty Map.empty "" "" Map.empty Map.empty Map.empty Map.empty
case result of
Left _ -> expectationFailure "Expected Right (no required vars)"
Right resolved -> do
let baseVars = resolved Map.! primaryInstance "base"
Map.member "license" baseVars `shouldBe` False
(st ^. #outputs) `shouldSatisfy` all (/= "Optional configuration:")
it "respects when condition on optional prompts" $ do
let m =
mkModule
"base"
[]
[ mkTextVar "project.name" Nothing True,
mkTextVar "extra" Nothing False
]
[]
[ mkPrompt "project.name" "Name?",
mkConditionalPrompt "extra" "Extra?" (ExprIsSet "nonexistent")
]
modules = [(primaryInstance (m ^. #name), m, "/fake/base")]
(result, st) <-
runEff $
runConsolePure ["my-app"] $
resolveWithPrompts modules Map.empty Map.empty "" "" Map.empty Map.empty Map.empty Map.empty
case result of
Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs
Right resolved -> do
let baseVars = resolved Map.! primaryInstance "base"
Map.member "extra" baseVars `shouldBe` False
-- The condition was false so Optional configuration header should not appear
-- (no optional prompts actually fired)
(st ^. #outputs) `shouldSatisfy` all (/= "Extra?")
it "does not prompt for optional variables already resolved via config" $ do
let m =
mkModule
"base"
[]
[mkTextVar "license" Nothing False]
[]
[mkPrompt "license" "License"]
modules = [(primaryInstance (m ^. #name), m, "/fake/base")]
globalConfig = Map.singleton "license" "MIT"
(result, st) <-
runEff $
runConsolePure [] $
resolveWithPrompts modules Map.empty Map.empty "" "" Map.empty Map.empty Map.empty globalConfig
case result of
Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs
Right resolved -> do
let baseVars = resolved Map.! primaryInstance "base"
((baseVars Map.! "license") ^. #value) `shouldBe` VText "MIT"
((baseVars Map.! "license") ^. #source) `shouldBe` FromGlobalConfig
(st ^. #outputs) `shouldSatisfy` all (/= "Optional configuration:")
it "flows prompted value from first module to second via exports" $ do
let base =
mkModule
"base"
[]
[mkTextVar "project.name" Nothing True]
[mkExport "project.name"]
[mkPrompt "project.name" "What is the project name?"]
app =
mkModule
"app"
["base"]
[mkTextVar "project.name" Nothing True]
[]
[]
modules = [(primaryInstance (base ^. #name), base, "/fake/base"), (primaryInstance (app ^. #name), app, "/fake/app")]
(result, st) <-
runEff $
runConsolePure ["my-app"] $
resolveWithPrompts modules Map.empty Map.empty "" "" Map.empty Map.empty Map.empty Map.empty
case result of
Left errs -> expectationFailure $ "Expected Right, got: " ++ show errs
Right resolved -> do
-- Base module was prompted
let baseVars = resolved Map.! primaryInstance "base"
((baseVars Map.! "project.name") ^. #value) `shouldBe` VText "my-app"
((baseVars Map.! "project.name") ^. #source) `shouldBe` FromPrompt
-- App module received the value via export (no additional prompt needed)
let appVars = resolved Map.! primaryInstance "app"
((appVars Map.! "project.name") ^. #value) `shouldBe` VText "my-app"
-- Only one prompt should have fired (for base), not two
let promptOutputs = filter (== "What is the project name?") (st ^. #outputs)
length promptOutputs `shouldBe` 1