seihou-core-0.4.0.0: test/Seihou/Core/VariableSpec.hs
module Seihou.Core.VariableSpec (tests) where
import Data.Map.Strict qualified as Map
import Data.Text qualified as T
import Seihou.Core.Expr (evalExpr, parseExpr)
import Seihou.Core.Types
import Seihou.Core.Variable
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)
tests :: IO TestTree
tests = testSpec "Seihou.Core.Variable" spec
-- | Helper to build a simple text var declaration.
textVar :: VarName -> Bool -> Maybe VarValue -> Maybe Validation -> VarDecl
textVar name req def val =
VarDecl
{ name = name,
type_ = VTText,
default_ = def,
description = Nothing,
required = req,
validation = val
}
-- | Helper to build a bool var declaration.
boolVar :: VarName -> Bool -> Maybe VarValue -> VarDecl
boolVar name req def =
VarDecl
{ name = name,
type_ = VTBool,
default_ = def,
description = Nothing,
required = req,
validation = Nothing
}
-- | Helper to build an int var declaration.
intVar :: VarName -> Bool -> Maybe VarValue -> Maybe Validation -> VarDecl
intVar name req def val =
VarDecl
{ name = name,
type_ = VTInt,
default_ = def,
description = Nothing,
required = req,
validation = val
}
-- | Helper to build a choice var declaration.
choiceVar :: VarName -> Bool -> [T.Text] -> Maybe VarValue -> VarDecl
choiceVar name req options def =
VarDecl
{ name = name,
type_ = VTChoice options,
default_ = def,
description = Nothing,
required = req,
validation = Nothing
}
spec :: Spec
spec = do
describe "envVarName" $ do
it "converts project.name to SEIHOU_VAR_PROJECT_NAME" $ do
envVarName "project.name" `shouldBe` "SEIHOU_VAR_PROJECT_NAME"
it "converts license to SEIHOU_VAR_LICENSE" $ do
envVarName "license" `shouldBe` "SEIHOU_VAR_LICENSE"
it "converts project.version to SEIHOU_VAR_PROJECT_VERSION" $ do
envVarName "project.version" `shouldBe` "SEIHOU_VAR_PROJECT_VERSION"
describe "coerceValue" $ do
it "coerces text as-is" $ do
coerceValue "x" VTText "hello" `shouldBe` Right (VText "hello")
it "coerces true string to VBool True" $ do
coerceValue "x" VTBool "true" `shouldBe` Right (VBool True)
it "coerces yes string to VBool True" $ do
coerceValue "x" VTBool "yes" `shouldBe` Right (VBool True)
it "coerces 1 string to VBool True" $ do
coerceValue "x" VTBool "1" `shouldBe` Right (VBool True)
it "coerces false string to VBool False" $ do
coerceValue "x" VTBool "false" `shouldBe` Right (VBool False)
it "coerces no string to VBool False" $ do
coerceValue "x" VTBool "no" `shouldBe` Right (VBool False)
it "rejects invalid bool string" $ do
coerceValue "x" VTBool "maybe" `shouldBe` Left (CoercionFailed "x" VTBool "maybe")
it "coerces integer string" $ do
coerceValue "x" VTInt "42" `shouldBe` Right (VInt 42)
it "coerces negative integer" $ do
coerceValue "x" VTInt "-5" `shouldBe` Right (VInt (-5))
it "rejects non-integer string" $ do
coerceValue "x" VTInt "abc" `shouldBe` Left (CoercionFailed "x" VTInt "abc")
it "coerces comma-separated list" $ do
coerceValue "x" (VTList VTText) "a,b,c"
`shouldBe` Right (VList [VText "a", VText "b", VText "c"])
it "strips whitespace in list elements" $ do
coerceValue "x" (VTList VTText) "a , b , c"
`shouldBe` Right (VList [VText "a", VText "b", VText "c"])
it "coerces valid choice" $ do
coerceValue "x" (VTChoice ["MIT", "BSD3", "Apache"]) "MIT"
`shouldBe` Right (VText "MIT")
it "rejects invalid choice" $ do
coerceValue "x" (VTChoice ["MIT", "BSD3"]) "GPL"
`shouldBe` Left (CoercionFailed "x" (VTChoice ["MIT", "BSD3"]) "GPL")
describe "coerceDefault" $ do
it "coerces a raw-text bool default to VBool" $ do
coerceDefault "x" VTBool (VText "true") `shouldBe` Right (VBool True)
it "coerces a raw-text int default to VInt" $ do
coerceDefault "x" VTInt (VText "42") `shouldBe` Right (VInt 42)
it "keeps a text default as VText" $ do
coerceDefault "x" VTText (VText "hello") `shouldBe` Right (VText "hello")
it "validates a constrained choice default" $ do
coerceDefault "x" (VTChoice ["MIT", "BSD3"]) (VText "MIT")
`shouldBe` Right (VText "MIT")
it "keeps an unconstrained (empty) choice default as VText" $ do
coerceDefault "x" (VTChoice []) (VText "MIT") `shouldBe` Right (VText "MIT")
it "passes an already-typed default through unchanged" $ do
coerceDefault "x" VTBool (VBool True) `shouldBe` Right (VBool True)
it "fails on a malformed bool default" $ do
coerceDefault "x" VTBool (VText "treu")
`shouldBe` Left (CoercionFailed "x" VTBool "treu")
describe "validateVarValue" $ do
it "passes when no validation is set" $ do
let decl = textVar "x" True Nothing Nothing
validateVarValue decl (VText "anything") `shouldBe` Right ()
it "passes pattern validation" $ do
let decl = textVar "x" True Nothing (Just (ValPattern "[a-z][a-z0-9-]*"))
validateVarValue decl (VText "my-app") `shouldBe` Right ()
it "rejects pattern validation" $ do
let decl = textVar "x" True Nothing (Just (ValPattern "[a-z][a-z0-9-]*"))
case validateVarValue decl (VText "MyApp") of
Left (ValidationFailed _ _) -> pure ()
other -> expectationFailure ("Expected ValidationFailed, got: " <> show other)
it "passes range validation" $ do
let decl = intVar "x" True Nothing (Just (ValRange 1 100))
validateVarValue decl (VInt 50) `shouldBe` Right ()
it "rejects out-of-range value" $ do
let decl = intVar "x" True Nothing (Just (ValRange 1 100))
case validateVarValue decl (VInt 200) of
Left (ValidationFailed _ _) -> pure ()
other -> expectationFailure ("Expected ValidationFailed, got: " <> show other)
it "passes min-length validation" $ do
let decl = textVar "x" True Nothing (Just (ValMinLength 3))
validateVarValue decl (VText "hello") `shouldBe` Right ()
it "rejects too-short value" $ do
let decl = textVar "x" True Nothing (Just (ValMinLength 3))
case validateVarValue decl (VText "ab") of
Left (ValidationFailed _ _) -> pure ()
other -> expectationFailure ("Expected ValidationFailed, got: " <> show other)
it "passes max-length validation" $ do
let decl = textVar "x" True Nothing (Just (ValMaxLength 5))
validateVarValue decl (VText "hello") `shouldBe` Right ()
it "rejects too-long value" $ do
let decl = textVar "x" True Nothing (Just (ValMaxLength 3))
case validateVarValue decl (VText "hello") of
Left (ValidationFailed _ _) -> pure ()
other -> expectationFailure ("Expected ValidationFailed, got: " <> show other)
describe "resolveVariables" $ do
it "resolves from CLI overrides" $ do
let decls = [textVar "project.name" True Nothing Nothing]
cli = Map.fromList [("project.name", "my-app")]
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved -> do
let rv = resolved Map.! "project.name"
rv.value `shouldBe` VText "my-app"
rv.source `shouldBe` FromCLI
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "resolves from environment variables" $ do
let decls = [textVar "project.name" True Nothing Nothing]
cli = Map.empty
env = Map.fromList [("SEIHOU_VAR_PROJECT_NAME", "env-app")]
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved -> do
let rv = resolved Map.! "project.name"
rv.value `shouldBe` VText "env-app"
rv.source `shouldBe` FromEnv "SEIHOU_VAR_PROJECT_NAME"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "resolves from module defaults" $ do
let decls = [textVar "project.version" False (Just (VText "0.1.0.0")) Nothing]
cli = Map.empty
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved -> do
let rv = resolved Map.! "project.version"
rv.value `shouldBe` VText "0.1.0.0"
rv.source `shouldBe` FromDefault
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "rejects missing required variable" $ do
let decls = [textVar "project.name" True Nothing Nothing]
cli = Map.empty
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Left errs -> errs `shouldBe` [MissingRequiredVar "project.name"]
Right _ -> expectationFailure "Expected Left"
it "CLI override beats environment variable" $ do
let decls = [textVar "project.name" True Nothing Nothing]
cli = Map.fromList [("project.name", "cli-app")]
env = Map.fromList [("SEIHOU_VAR_PROJECT_NAME", "env-app")]
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved -> do
(.value) (resolved Map.! "project.name") `shouldBe` VText "cli-app"
(.source) (resolved Map.! "project.name") `shouldBe` FromCLI
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "environment variable beats module default" $ do
let decls = [textVar "project.name" True (Just (VText "default-app")) Nothing]
cli = Map.empty
env = Map.fromList [("SEIHOU_VAR_PROJECT_NAME", "env-app")]
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved -> do
(.value) (resolved Map.! "project.name") `shouldBe` VText "env-app"
(.source) (resolved Map.! "project.name") `shouldBe` FromEnv "SEIHOU_VAR_PROJECT_NAME"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "CLI override beats module default" $ do
let decls = [textVar "project.name" True (Just (VText "default-app")) Nothing]
cli = Map.fromList [("project.name", "cli-app")]
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved ->
(.value) (resolved Map.! "project.name") `shouldBe` VText "cli-app"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "resolves multiple variables with mixed sources" $ do
let decls =
[ textVar "project.name" True Nothing Nothing,
textVar "project.version" False (Just (VText "0.1.0.0")) Nothing,
textVar "license" False (Just (VText "MIT")) Nothing
]
cli = Map.fromList [("project.name", "my-app")]
env = Map.fromList [("SEIHOU_VAR_LICENSE", "BSD3")]
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved -> do
(.value) (resolved Map.! "project.name") `shouldBe` VText "my-app"
(.source) (resolved Map.! "project.name") `shouldBe` FromCLI
(.value) (resolved Map.! "project.version") `shouldBe` VText "0.1.0.0"
(.source) (resolved Map.! "project.version") `shouldBe` FromDefault
(.value) (resolved Map.! "license") `shouldBe` VText "BSD3"
(.source) (resolved Map.! "license") `shouldBe` FromEnv "SEIHOU_VAR_LICENSE"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "coerces CLI bool override" $ do
let decls = [boolVar "enable.tests" True Nothing]
cli = Map.fromList [("enable.tests", "true")]
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved ->
(.value) (resolved Map.! "enable.tests") `shouldBe` VBool True
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "coerces env int override" $ do
let decls = [intVar "port" True Nothing Nothing]
cli = Map.empty
env = Map.fromList [("SEIHOU_VAR_PORT", "8080")]
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved ->
(.value) (resolved Map.! "port") `shouldBe` VInt 8080
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "returns coercion error for bad int" $ do
let decls = [intVar "port" True Nothing Nothing]
cli = Map.fromList [("port", "abc")]
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Left errs -> errs `shouldBe` [CoercionFailed "port" VTInt "abc"]
Right _ -> expectationFailure "Expected Left"
it "validates after coercion" $ do
let decls = [textVar "project.name" True Nothing (Just (ValPattern "[a-z][a-z0-9-]*"))]
cli = Map.fromList [("project.name", "MyBad")]
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Left (err : _) -> case err of
ValidationFailed _ _ -> pure ()
other -> expectationFailure ("Expected ValidationFailed, got: " <> show other)
Left [] -> expectationFailure "Expected non-empty error list"
Right _ -> expectationFailure "Expected Left"
it "resolves choice variable from CLI" $ do
let decls = [choiceVar "license" True ["MIT", "BSD3", "Apache"] Nothing]
cli = Map.fromList [("license", "MIT")]
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved ->
(.value) (resolved Map.! "license") `shouldBe` VText "MIT"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "coerces a raw-text bool default to VBool when falling through to default" $ do
-- Mirrors the Dhall decode of @default = Some "true"@ on a @bool@ var:
-- the default reaches resolution as 'VText' and must arrive typed.
let decls = [boolVar "feature.on" False (Just (VText "true"))]
cli = Map.empty
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved -> do
(.value) (resolved Map.! "feature.on") `shouldBe` VBool True
(.source) (resolved Map.! "feature.on") `shouldBe` FromDefault
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "coerces a raw-text int default to VInt when falling through to default" $ do
let decls = [intVar "retries" False (Just (VText "3")) Nothing]
cli = Map.empty
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved ->
(.value) (resolved Map.! "retries") `shouldBe` VInt 3
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "errors when a bool default cannot be coerced" $ do
let decls = [boolVar "feature.on" False (Just (VText "treu"))]
cli = Map.empty
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Left errs -> errs `shouldBe` [CoercionFailed "feature.on" VTBool "treu"]
Right _ -> expectationFailure "Expected Left"
it "re-coerces a manifest-round-tripped bool value to VBool" $ do
-- The manifest stores resolved variables as raw text (a 'VBool' is
-- serialized to "true"). On a re-run such a value can only re-enter
-- resolution through a config-style 'Map VarName Text' source, which
-- routes through 'coerceValue' — so a stored bool round-trips back to
-- 'VBool', never reaching evaluation as 'VText "true"'.
let manifestStoredText = "true" -- i.e. varValueToText (VBool True)
decls = [boolVar "feature.on" True Nothing]
cli = Map.empty
env = Map.empty
local = Map.fromList [("feature.on", manifestStoredText)]
case resolveVariables decls cli env "" "" local Map.empty Map.empty Map.empty Map.empty of
Right resolved ->
(.value) (resolved Map.! "feature.on") `shouldBe` VBool True
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "Eq <var> true evaluates True for a defaulted bool" $ do
-- The end-to-end bug: a bool default must make @Eq feature.on true@ match.
let decls = [boolVar "feature.on" False (Just (VText "true"))]
cli = Map.empty
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved -> do
let varMap = Map.map (.value) resolved
case parseExpr "Eq feature.on true" of
Right expr -> evalExpr varMap expr `shouldBe` True
Left err -> expectationFailure ("Expected parse, got: " <> show err)
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
describe "formatExplain" $ do
it "formats a provenance report with bracket notation" $ do
let decl1 = textVar "project.name" True Nothing Nothing
decl2 = textVar "project.version" False (Just (VText "0.1.0.0")) Nothing
resolved =
Map.fromList
[ ("project.name", ResolvedVar (VText "my-app") FromCLI decl1),
("project.version", ResolvedVar (VText "0.1.0.0") FromDefault decl2)
]
output = formatExplain resolved
T.isInfixOf "project.name" output `shouldBe` True
T.isInfixOf "\"my-app\"" output `shouldBe` True
T.isInfixOf "[--var]" output `shouldBe` True
T.isInfixOf "project.version" output `shouldBe` True
T.isInfixOf "\"0.1.0.0\"" output `shouldBe` True
T.isInfixOf "[default]" output `shouldBe` True
it "formats env source with bracket notation" $ do
let decl = textVar "license" True Nothing Nothing
resolved =
Map.fromList
[("license", ResolvedVar (VText "MIT") (FromEnv "SEIHOU_VAR_LICENSE") decl)]
output = formatExplain resolved
T.isInfixOf "[env SEIHOU_VAR_LICENSE]" output `shouldBe` True
it "formats local config source with bracket notation" $ do
let decl = textVar "license" True Nothing Nothing
resolved =
Map.fromList
[("license", ResolvedVar (VText "MIT") FromLocalConfig decl)]
output = formatExplain resolved
T.isInfixOf "[local config]" output `shouldBe` True
it "formats namespace config source with bracket notation" $ do
let decl = textVar "haskell.ghc" True Nothing Nothing
resolved =
Map.fromList
[("haskell.ghc", ResolvedVar (VText "9.12.2") (FromNamespaceConfig "haskell") decl)]
output = formatExplain resolved
T.isInfixOf "[namespace: haskell]" output `shouldBe` True
it "formats context config source with bracket notation" $ do
let decl = textVar "user.email" True Nothing Nothing
resolved =
Map.fromList
[("user.email", ResolvedVar (VText "me@work.com") (FromContextConfig "work") decl)]
output = formatExplain resolved
T.isInfixOf "[context: work]" output `shouldBe` True
it "formats global config source with bracket notation" $ do
let decl = textVar "license" True Nothing Nothing
resolved =
Map.fromList
[("license", ResolvedVar (VText "MIT") FromGlobalConfig decl)]
output = formatExplain resolved
T.isInfixOf "[global config]" output `shouldBe` True
it "formats parent source with bracket notation" $ do
let decl = textVar "skill.name" True Nothing Nothing
resolved =
Map.fromList
[("skill.name", ResolvedVar (VText "exec-plan") (FromParent "exec-plan") decl)]
output = formatExplain resolved
T.isInfixOf "[parent: exec-plan]" output `shouldBe` True
it "uses 2-space indentation" $ do
let decl = textVar "license" True Nothing Nothing
resolved =
Map.fromList
[("license", ResolvedVar (VText "MIT") FromCLI decl)]
output = formatExplain resolved
T.isInfixOf " license" output `shouldBe` True
describe "formatDeclarations" $ do
it "formats required variables without defaults" $ do
let decls = [VarDecl "project.name" VTText Nothing (Just "Name") True Nothing]
output = formatDeclarations decls
T.isInfixOf "project.name" output `shouldBe` True
T.isInfixOf "(required, no default)" output `shouldBe` True
it "formats variables with default values" $ do
let decls = [VarDecl "project.version" VTText (Just (VText "0.1.0.0")) Nothing False Nothing]
output = formatDeclarations decls
T.isInfixOf "\"0.1.0.0\"" output `shouldBe` True
T.isInfixOf "(required, no default)" output `shouldBe` False
it "formats variables with defaults using = sign" $ do
let decls =
[ VarDecl
{ name = "project.version",
type_ = VTText,
default_ = Just (VText "0.1.0.0"),
description = Nothing,
required = False,
validation = Nothing
}
]
output = formatDeclarations decls
T.isInfixOf "project.version" output `shouldBe` True
T.isInfixOf "= \"0.1.0.0\"" output `shouldBe` True
it "aligns columns for multiple variables" $ do
let decls =
[ VarDecl "project.name" VTText Nothing (Just "Name") True Nothing,
VarDecl "project.version" VTText (Just (VText "0.1.0.0")) Nothing False Nothing
]
output = formatDeclarations decls
outputLines = T.lines output
-- Both lines should have the same position for "="
length (filter (T.isInfixOf "=") outputLines) `shouldBe` 2
it "uses 2-space indentation" $ do
let decls = [VarDecl "x" VTText Nothing Nothing True Nothing]
output = formatDeclarations decls
T.isInfixOf " x" output `shouldBe` True
describe "resolveVariables (six-layer precedence)" $ do
it "resolves from local config" $ do
let decls = [textVar "license" True Nothing Nothing]
cli = Map.empty
env = Map.empty
local = Map.fromList [("license", "MIT")]
case resolveVariables decls cli env "" "" local Map.empty Map.empty Map.empty Map.empty of
Right resolved -> do
(.value) (resolved Map.! "license") `shouldBe` VText "MIT"
(.source) (resolved Map.! "license") `shouldBe` FromLocalConfig
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "resolves from namespace config" $ do
let decls = [textVar "haskell.ghc" True Nothing Nothing]
cli = Map.empty
env = Map.empty
nsCfg = Map.fromList [("haskell.ghc", "9.12.2")]
case resolveVariables decls cli env "haskell" "" Map.empty nsCfg Map.empty Map.empty Map.empty of
Right resolved -> do
(.value) (resolved Map.! "haskell.ghc") `shouldBe` VText "9.12.2"
(.source) (resolved Map.! "haskell.ghc") `shouldBe` FromNamespaceConfig "haskell"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "resolves from global config" $ do
let decls = [textVar "license" True Nothing Nothing]
cli = Map.empty
env = Map.empty
global = Map.fromList [("license", "MIT")]
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty global Map.empty of
Right resolved -> do
(.value) (resolved Map.! "license") `shouldBe` VText "MIT"
(.source) (resolved Map.! "license") `shouldBe` FromGlobalConfig
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "local config overrides global config" $ do
let decls = [textVar "license" True Nothing Nothing]
cli = Map.empty
env = Map.empty
local = Map.fromList [("license", "BSD3")]
global = Map.fromList [("license", "MIT")]
case resolveVariables decls cli env "" "" local Map.empty Map.empty global Map.empty of
Right resolved -> do
(.value) (resolved Map.! "license") `shouldBe` VText "BSD3"
(.source) (resolved Map.! "license") `shouldBe` FromLocalConfig
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "namespace config overrides global config" $ do
let decls = [textVar "license" True Nothing Nothing]
cli = Map.empty
env = Map.empty
nsCfg = Map.fromList [("license", "Apache")]
global = Map.fromList [("license", "MIT")]
case resolveVariables decls cli env "haskell" "" Map.empty nsCfg Map.empty global Map.empty of
Right resolved -> do
(.value) (resolved Map.! "license") `shouldBe` VText "Apache"
(.source) (resolved Map.! "license") `shouldBe` FromNamespaceConfig "haskell"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "context config resolves when namespace and local don't have variable" $ do
let decls = [textVar "user.email" True Nothing Nothing]
cli = Map.empty
env = Map.empty
ctxCfg = Map.fromList [("user.email", "me@work.com")]
case resolveVariables decls cli env "" "work" Map.empty Map.empty ctxCfg Map.empty Map.empty of
Right resolved -> do
(.value) (resolved Map.! "user.email") `shouldBe` VText "me@work.com"
(.source) (resolved Map.! "user.email") `shouldBe` FromContextConfig "work"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "context config is lower priority than namespace config" $ do
let decls = [textVar "user.email" True Nothing Nothing]
cli = Map.empty
env = Map.empty
nsCfg = Map.fromList [("user.email", "ns@example.com")]
ctxCfg = Map.fromList [("user.email", "ctx@example.com")]
case resolveVariables decls cli env "haskell" "work" Map.empty nsCfg ctxCfg Map.empty Map.empty of
Right resolved -> do
(.value) (resolved Map.! "user.email") `shouldBe` VText "ns@example.com"
(.source) (resolved Map.! "user.email") `shouldBe` FromNamespaceConfig "haskell"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "context config is higher priority than global config" $ do
let decls = [textVar "user.email" True Nothing Nothing]
cli = Map.empty
env = Map.empty
ctxCfg = Map.fromList [("user.email", "ctx@example.com")]
global = Map.fromList [("user.email", "global@example.com")]
case resolveVariables decls cli env "" "work" Map.empty Map.empty ctxCfg global Map.empty of
Right resolved -> do
(.value) (resolved Map.! "user.email") `shouldBe` VText "ctx@example.com"
(.source) (resolved Map.! "user.email") `shouldBe` FromContextConfig "work"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "local config overrides namespace config" $ do
let decls = [textVar "license" True Nothing Nothing]
cli = Map.empty
env = Map.empty
local = Map.fromList [("license", "GPL")]
nsCfg = Map.fromList [("license", "Apache")]
case resolveVariables decls cli env "haskell" "" local nsCfg Map.empty Map.empty Map.empty of
Right resolved -> do
(.value) (resolved Map.! "license") `shouldBe` VText "GPL"
(.source) (resolved Map.! "license") `shouldBe` FromLocalConfig
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "env overrides local config" $ do
let decls = [textVar "license" True Nothing Nothing]
cli = Map.empty
env = Map.fromList [("SEIHOU_VAR_LICENSE", "env-license")]
local = Map.fromList [("license", "local-license")]
case resolveVariables decls cli env "" "" local Map.empty Map.empty Map.empty Map.empty of
Right resolved -> do
(.value) (resolved Map.! "license") `shouldBe` VText "env-license"
(.source) (resolved Map.! "license") `shouldBe` FromEnv "SEIHOU_VAR_LICENSE"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "CLI overrides all config layers" $ do
let decls = [textVar "license" True Nothing Nothing]
cli = Map.fromList [("license", "cli-license")]
env = Map.fromList [("SEIHOU_VAR_LICENSE", "env-license")]
local = Map.fromList [("license", "local-license")]
nsCfg = Map.fromList [("license", "ns-license")]
global = Map.fromList [("license", "global-license")]
case resolveVariables decls cli env "haskell" "" local nsCfg Map.empty global Map.empty of
Right resolved -> do
(.value) (resolved Map.! "license") `shouldBe` VText "cli-license"
(.source) (resolved Map.! "license") `shouldBe` FromCLI
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "global config overrides module default" $ do
let decls = [textVar "license" False (Just (VText "default-license")) Nothing]
cli = Map.empty
env = Map.empty
global = Map.fromList [("license", "global-license")]
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty global Map.empty of
Right resolved -> do
(.value) (resolved Map.! "license") `shouldBe` VText "global-license"
(.source) (resolved Map.! "license") `shouldBe` FromGlobalConfig
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "resolves mixed sources across multiple variables" $ do
let decls =
[ textVar "project.name" True Nothing Nothing,
textVar "license" False (Just (VText "MIT")) Nothing,
textVar "haskell.ghc" False (Just (VText "9.8.1")) Nothing
]
cli = Map.fromList [("project.name", "cli-app")]
env = Map.empty
local = Map.fromList [("license", "BSD3")]
nsCfg = Map.fromList [("haskell.ghc", "9.12.2")]
case resolveVariables decls cli env "haskell" "" local nsCfg Map.empty Map.empty Map.empty of
Right resolved -> do
(.value) (resolved Map.! "project.name") `shouldBe` VText "cli-app"
(.source) (resolved Map.! "project.name") `shouldBe` FromCLI
(.value) (resolved Map.! "license") `shouldBe` VText "BSD3"
(.source) (resolved Map.! "license") `shouldBe` FromLocalConfig
(.value) (resolved Map.! "haskell.ghc") `shouldBe` VText "9.12.2"
(.source) (resolved Map.! "haskell.ghc") `shouldBe` FromNamespaceConfig "haskell"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "omits non-required variable with no value from any source" $ do
let decls = [textVar "optional.var" False Nothing Nothing]
cli = Map.empty
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Right resolved -> Map.member "optional.var" resolved `shouldBe` False
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "still errors on missing required variable with no value" $ do
let decls = [textVar "required.var" True Nothing Nothing]
cli = Map.empty
env = Map.empty
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty Map.empty of
Left errs -> errs `shouldBe` [MissingRequiredVar "required.var"]
Right _ -> expectationFailure "Expected Left"
it "resolves non-required variable when value is provided" $ do
let decls = [textVar "optional.var" False Nothing Nothing]
cli = Map.empty
env = Map.empty
global = Map.fromList [("optional.var", "from-global")]
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty global Map.empty of
Right resolved -> do
(.value) (resolved Map.! "optional.var") `shouldBe` VText "from-global"
(.source) (resolved Map.! "optional.var") `shouldBe` FromGlobalConfig
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "resolves mix of required, optional-with-value, and optional-without-value" $ do
let decls =
[ textVar "project.name" True Nothing Nothing,
textVar "optional.missing" False Nothing Nothing,
textVar "optional.present" False Nothing Nothing
]
cli = Map.fromList [("project.name", "my-app")]
env = Map.empty
global = Map.fromList [("optional.present", "found")]
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty global Map.empty of
Right resolved -> do
(.value) (resolved Map.! "project.name") `shouldBe` VText "my-app"
Map.member "optional.missing" resolved `shouldBe` False
(.value) (resolved Map.! "optional.present") `shouldBe` VText "found"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "coerces config values through type system" $ do
let decls = [boolVar "enable.tests" True Nothing]
cli = Map.empty
env = Map.empty
local = Map.fromList [("enable.tests", "true")]
case resolveVariables decls cli env "" "" local Map.empty Map.empty Map.empty Map.empty of
Right resolved ->
(.value) (resolved Map.! "enable.tests") `shouldBe` VBool True
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "resolves from parent-supplied vars" $ do
let decls = [textVar "skill.name" True Nothing Nothing]
cli = Map.empty
env = Map.empty
parentVars = Map.fromList [("skill.name", ("exec-plan", "parent-mod"))]
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty parentVars of
Right resolved -> do
(.value) (resolved Map.! "skill.name") `shouldBe` VText "exec-plan"
(.source) (resolved Map.! "skill.name") `shouldBe` FromParent "parent-mod"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "parent-supplied var overrides module default" $ do
let decls = [textVar "skill.name" False (Just (VText "default-val")) Nothing]
cli = Map.empty
env = Map.empty
parentVars = Map.fromList [("skill.name", ("parent-val", "parent-mod"))]
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty Map.empty parentVars of
Right resolved -> do
(.value) (resolved Map.! "skill.name") `shouldBe` VText "parent-val"
(.source) (resolved Map.! "skill.name") `shouldBe` FromParent "parent-mod"
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
it "global config overrides parent-supplied var" $ do
let decls = [textVar "skill.name" True Nothing Nothing]
cli = Map.empty
env = Map.empty
global = Map.fromList [("skill.name", "global-val")]
parentVars = Map.fromList [("skill.name", ("parent-val", "parent-mod"))]
case resolveVariables decls cli env "" "" Map.empty Map.empty Map.empty global parentVars of
Right resolved -> do
(.value) (resolved Map.! "skill.name") `shouldBe` VText "global-val"
(.source) (resolved Map.! "skill.name") `shouldBe` FromGlobalConfig
Left errs -> expectationFailure ("Expected Right, got: " <> show errs)
describe "diagnoseResolution" $ do
it "reports unused config keys" $ do
let decl = textVar "project.name" True Nothing Nothing
resolved = Map.fromList [("project.name", ResolvedVar (VText "app") FromCLI decl)]
global = Map.fromList [("project.name", "app"), ("auther.name", "typo")]
(unused, _) = diagnoseResolution resolved [decl] Map.empty Map.empty Map.empty global
unused `shouldBe` [VarName "auther.name"]
it "reports unresolved optional variables" $ do
let decls =
[ textVar "project.name" True Nothing Nothing,
textVar "optional.var" False Nothing Nothing
]
resolved = Map.fromList [("project.name", ResolvedVar (VText "app") FromCLI (head decls))]
(_, unresolved) = diagnoseResolution resolved decls Map.empty Map.empty Map.empty Map.empty
unresolved `shouldBe` [VarName "optional.var"]
it "returns empty lists when everything matches" $ do
let decl = textVar "license" True Nothing Nothing
resolved = Map.fromList [("license", ResolvedVar (VText "MIT") FromGlobalConfig decl)]
global = Map.fromList [("license", "MIT")]
(unused, unresolved) = diagnoseResolution resolved [decl] Map.empty Map.empty Map.empty global
unused `shouldBe` []
unresolved `shouldBe` []
it "detects unused keys across all config layers" $ do
let decl = textVar "project.name" True Nothing Nothing
resolved = Map.fromList [("project.name", ResolvedVar (VText "app") FromCLI decl)]
local = Map.fromList [("local.extra", "x")]
nsCfg = Map.fromList [("ns.extra", "y")]
global = Map.fromList [("global.extra", "z")]
(unused, _) = diagnoseResolution resolved [decl] local nsCfg Map.empty global
unused `shouldBe` [VarName "global.extra", VarName "local.extra", VarName "ns.extra"]