seihou-core-0.4.0.0: test/Seihou/Engine/ValidateSpec.hs
module Seihou.Engine.ValidateSpec (tests) where
import Data.Text qualified as T
import Seihou.Core.Types
import Seihou.Engine.Validate
import System.Directory (createDirectoryIfMissing)
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)
tests :: IO TestTree
tests = testSpec "Seihou.Engine.Validate" spec
-- | A well-formed module for validation tests.
goodModule :: Module
goodModule =
Module
{ name = "test-module",
version = Just "1.0.0",
description = Just "A test module",
vars =
[ VarDecl
{ name = "project.name",
type_ = VTText,
default_ = Nothing,
description = Just "Project name",
required = True,
validation = Nothing
}
],
exports = [VarExport {var = "project.name", alias = Nothing}],
prompts = [Prompt {var = "project.name", text = "Name?", condition = Nothing, choices = Nothing}],
steps =
[ Step
{ strategy = Template,
src = "README.md.tpl",
dest = "README.md",
condition = Nothing,
patch = Nothing
}
],
commands = [],
dependencies = [],
removal = Nothing,
migrations = []
}
-- | A module with multiple validation errors.
badModule :: Module
badModule =
Module
{ name = "BadName",
version = Nothing,
description = Nothing,
vars =
[ VarDecl "x" VTText Nothing Nothing True Nothing,
VarDecl "x" VTBool Nothing Nothing False Nothing
],
exports = [VarExport {var = "nonexistent", alias = Nothing}],
prompts = [Prompt {var = "undeclared", text = "?", condition = Nothing, choices = Nothing}],
steps = [Step Template "missing.tpl" "/absolute/path" Nothing Nothing],
commands = [],
dependencies = [],
removal = Nothing,
migrations = []
}
-- | Helper to update Module fields without ambiguity.
withVars :: [VarDecl] -> Module -> Module
withVars v m = Module m.name m.version m.description v m.exports m.prompts m.steps m.commands m.dependencies m.removal m.migrations
withSteps :: [Step] -> Module -> Module
withSteps s m = Module m.name m.version m.description m.vars m.exports m.prompts s m.commands m.dependencies m.removal m.migrations
withPrompts :: [Prompt] -> Module -> Module
withPrompts p m = Module m.name m.version m.description m.vars m.exports p m.steps m.commands m.dependencies m.removal m.migrations
withCommands :: [Command] -> Module -> Module
withCommands c m = Module m.name m.version m.description m.vars m.exports m.prompts m.steps c m.dependencies m.removal m.migrations
withVarsAndPrompts :: [VarDecl] -> [Prompt] -> Module -> Module
withVarsAndPrompts v p m = Module m.name m.version m.description v m.exports p m.steps m.commands m.dependencies m.removal m.migrations
-- | Helper: check if any DiagCheck has the given label and non-empty details.
hasFailedCheck :: T.Text -> [DiagCheck] -> Bool
hasFailedCheck label = any (\c -> c.diagLabel == label && not (null (c.diagDetails)))
-- | Helper: check if any DiagCheck has the given label and empty details (pass).
hasPassedCheck :: T.Text -> [DiagCheck] -> Bool
hasPassedCheck label = any (\c -> c.diagLabel == label && null (c.diagDetails))
-- | Helper: count checks with non-empty details of a given severity.
countFailures :: DiagSeverity -> [DiagCheck] -> Int
countFailures sev = length . filter (\c -> c.diagSeverity == sev && not (null (c.diagDetails)))
spec :: Spec
spec = do
describe "buildReport" $ do
it "produces all-pass checks for a valid module" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
report <- buildReport False tmpDir goodModule
report.reportDhallOk `shouldBe` True
reportHasErrors report `shouldBe` False
countFailures DiagError report.reportChecks `shouldBe` 0
it "detects module name format errors" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
report <- buildReport False tmpDir badModule
hasFailedCheck "Module name format" report.reportChecks `shouldBe` True
it "detects duplicate variable names" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
report <- buildReport False tmpDir badModule
hasFailedCheck "Unique variable names" report.reportChecks `shouldBe` True
it "detects export referencing undeclared variable" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
report <- buildReport False tmpDir badModule
hasFailedCheck "Export references" report.reportChecks `shouldBe` True
it "detects prompt referencing undeclared variable" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
report <- buildReport False tmpDir badModule
hasFailedCheck "Prompt references" report.reportChecks `shouldBe` True
it "detects missing source files" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
report <- buildReport False tmpDir badModule
hasFailedCheck "Source file existence" report.reportChecks `shouldBe` True
it "detects unsafe step destinations" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
report <- buildReport False tmpDir badModule
hasFailedCheck "Safe step destinations" report.reportChecks `shouldBe` True
it "detects missing module version" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
report <- buildReport False tmpDir badModule
hasFailedCheck "Module version declared" report.reportChecks `shouldBe` True
it "passes when module has a version" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
report <- buildReport False tmpDir goodModule
hasPassedCheck "Module version declared" report.reportChecks `shouldBe` True
it "reports multiple errors at once" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
report <- buildReport False tmpDir badModule
reportHasErrors report `shouldBe` True
countFailures DiagError report.reportChecks `shouldSatisfy` (>= 5)
it "does not include lint checks when lint is False" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
report <- buildReport False tmpDir goodModule
let hasWarning = any (\c -> c.diagSeverity == DiagWarning) report.reportChecks
hasWarning `shouldBe` False
it "includes lint checks when lint is True" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
report <- buildReport True tmpDir goodModule
let hasWarning = any (\c -> c.diagSeverity == DiagWarning) report.reportChecks
hasWarning `shouldBe` True
describe "lint checks" $ do
it "detects unused variables" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m =
withVars
[ VarDecl "project.name" VTText Nothing (Just "Name") True Nothing,
VarDecl "unused.var" VTText Nothing (Just "Unused") False Nothing
]
goodModule
report <- buildReport True tmpDir m
hasFailedCheck "Unused variables" report.reportChecks `shouldBe` True
let details = concatMap (.diagDetails) $ filter (\c -> c.diagLabel == "Unused variables") report.reportChecks
any (T.isInfixOf "unused.var") details `shouldBe` True
it "does not flag used variables as unused" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
report <- buildReport True tmpDir goodModule
hasFailedCheck "Unused variables" report.reportChecks `shouldBe` False
it "detects required variables without prompts" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m =
withVarsAndPrompts
[VarDecl "project.name" VTText Nothing (Just "Name") True Nothing]
[]
goodModule
report <- buildReport True tmpDir m
hasFailedCheck "Required variables without prompts" report.reportChecks `shouldBe` True
it "does not flag required variables that have prompts" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
report <- buildReport True tmpDir goodModule
hasFailedCheck "Required variables without prompts" report.reportChecks `shouldBe` False
it "detects duplicate step destinations" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "a.tpl") "stub"
writeFile (tmpDir </> "files" </> "b.tpl") "stub"
let m =
withSteps
[ Step Template "a.tpl" "out.txt" Nothing Nothing,
Step Template "b.tpl" "out.txt" Nothing Nothing
]
goodModule
report <- buildReport True tmpDir m
hasFailedCheck "Duplicate step destinations" report.reportChecks `shouldBe` True
it "does not flag patch ops as duplicate destinations" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "a.tpl") "stub"
writeFile (tmpDir </> "files" </> "b.tpl") "stub"
let m =
withSteps
[ Step Template "a.tpl" "out.txt" Nothing Nothing,
Step Template "b.tpl" "out.txt" Nothing (Just AppendFile)
]
goodModule
report <- buildReport True tmpDir m
hasFailedCheck "Duplicate step destinations" report.reportChecks `shouldBe` False
it "detects empty choice lists" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m =
withVars
[VarDecl "pick" (VTChoice []) Nothing (Just "Pick") True Nothing]
goodModule
report <- buildReport True tmpDir m
hasFailedCheck "Empty choice lists" report.reportChecks `shouldBe` True
it "detects missing variable descriptions" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m =
withVars
[VarDecl "project.name" VTText Nothing Nothing True Nothing]
goodModule
report <- buildReport True tmpDir m
hasFailedCheck "Missing variable descriptions" report.reportChecks `shouldBe` True
it "does not flag variables with descriptions" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
report <- buildReport True tmpDir goodModule
hasFailedCheck "Missing variable descriptions" report.reportChecks `shouldBe` False
describe "conditional lint" $ do
-- Base vars: keep project.name (referenced by goodModule's export/prompt)
-- and add a bool feature flag for the comparison tests.
let baseVars =
[ VarDecl "project.name" VTText Nothing (Just "Name") True Nothing,
VarDecl "feature.on" VTBool Nothing (Just "Feature flag") False Nothing
]
stepWithCondition cond =
withSteps
[Step Template "README.md.tpl" "README.md" cond Nothing]
(withVars baseVars goodModule)
it "flags a bool variable compared against a string literal in a when clause" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m = stepWithCondition (Just (ExprEq "feature.on" (VText "true")))
report <- buildReport True tmpDir m
hasFailedCheck "Conditional comparison types" report.reportChecks `shouldBe` True
reportHasErrors report `shouldBe` True
let details =
concatMap (.diagDetails) $
filter (\c -> c.diagLabel == "Conditional comparison types") report.reportChecks
any (T.isInfixOf "feature.on") details `shouldBe` True
any (T.isInfixOf "bareword true") details `shouldBe` True
it "does not flag a bool variable compared against a bareword true" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m = stepWithCondition (Just (ExprEq "feature.on" (VBool True)))
report <- buildReport True tmpDir m
hasFailedCheck "Conditional comparison types" report.reportChecks `shouldBe` False
hasPassedCheck "Conditional comparison types" report.reportChecks `shouldBe` True
it "flags a when clause referencing an undeclared variable" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m = stepWithCondition (Just (ExprIsSet "nix.treefmtt"))
report <- buildReport True tmpDir m
hasFailedCheck "Conditional variable references" report.reportChecks `shouldBe` True
let details =
concatMap (.diagDetails) $
filter (\c -> c.diagLabel == "Conditional variable references") report.reportChecks
any (T.isInfixOf "nix.treefmtt") details `shouldBe` True
it "passes both conditional checks for a correct module" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m = stepWithCondition (Just (ExprEq "feature.on" (VBool True)))
report <- buildReport True tmpDir m
hasPassedCheck "Conditional variable references" report.reportChecks `shouldBe` True
hasPassedCheck "Conditional comparison types" report.reportChecks `shouldBe` True
it "does not run conditional checks when lint is False" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m = stepWithCondition (Just (ExprEq "feature.on" (VText "true")))
report <- buildReport False tmpDir m
any (\c -> c.diagLabel == "Conditional comparison types") report.reportChecks
`shouldBe` False
reportHasErrors report `shouldBe` False
it "flags an undeclared variable referenced in a template {{#if}} conditional" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile
(tmpDir </> "files" </> "README.md.tpl")
"{{#if Eq ghost true}}\nhi\n{{/if}}\n"
let m = withVars baseVars goodModule
report <- buildReport True tmpDir m
hasFailedCheck "Conditional variable references" report.reportChecks `shouldBe` True
let details =
concatMap (.diagDetails) $
filter (\c -> c.diagLabel == "Conditional variable references") report.reportChecks
any (T.isInfixOf "ghost") details `shouldBe` True
any (T.isInfixOf "README.md.tpl") details `shouldBe` True
it "flags a bool var compared to a string literal inside a template {{#if}}" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile
(tmpDir </> "files" </> "README.md.tpl")
"{{#if Eq feature.on \"true\"}}\nhi\n{{/if}}\n"
let m = withVars baseVars goodModule
report <- buildReport True tmpDir m
hasFailedCheck "Conditional comparison types" report.reportChecks `shouldBe` True
describe "renderReportPlain" $ do
it "renders a valid module report with check marks" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
report <- buildReport False tmpDir goodModule
let rendered = renderReportPlain report
T.isInfixOf "\x2713 module.dhall evaluates successfully" rendered `shouldBe` True
T.isInfixOf "\x2713 Module name: test-module" rendered `shouldBe` True
T.isInfixOf "Module 'test-module' is valid." rendered `shouldBe` True
it "renders an invalid module report with cross marks and error count" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
report <- buildReport False tmpDir badModule
let rendered = renderReportPlain report
T.isInfixOf "\x2717 Module name format" rendered `shouldBe` True
T.isInfixOf "error(s) found. Module is invalid." rendered `shouldBe` True
it "renders a Dhall-failure report" $ do
let report =
ValidateReport
{ reportModule = goodModule,
reportPath = "/some/path",
reportDhallOk = False,
reportDhallError = Just "test error message",
reportChecks = []
}
rendered = renderReportPlain report
T.isInfixOf "\x2717 module.dhall failed to evaluate" rendered `shouldBe` True
T.isInfixOf "test error message" rendered `shouldBe` True
T.isInfixOf "1 error(s) found. Module is invalid." rendered `shouldBe` True
it "renders a Dhall-failure report without error details when absent" $ do
let report =
ValidateReport
{ reportModule = goodModule,
reportPath = "/some/path",
reportDhallOk = False,
reportDhallError = Nothing,
reportChecks = []
}
rendered = renderReportPlain report
T.isInfixOf "\x2717 module.dhall failed to evaluate" rendered `shouldBe` True
T.isInfixOf "1 error(s) found. Module is invalid." rendered `shouldBe` True
it "renders lint warnings with warning symbol" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m =
withVars
[ VarDecl "project.name" VTText Nothing (Just "Name") True Nothing,
VarDecl "unused" VTText Nothing (Just "Unused") False Nothing
]
goodModule
report <- buildReport True tmpDir m
let rendered = renderReportPlain report
T.isInfixOf "\x26A0 Unused variables" rendered `shouldBe` True
describe "reportHasErrors" $ do
it "returns False for a valid report" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
report <- buildReport False tmpDir goodModule
reportHasErrors report `shouldBe` False
it "returns True for a report with errors" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
report <- buildReport False tmpDir badModule
reportHasErrors report `shouldBe` True
it "returns False when only warnings are present" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m =
withVars
[ VarDecl "project.name" VTText Nothing (Just "Name") True Nothing,
VarDecl "unused" VTText Nothing (Just "Unused") False Nothing
]
goodModule
report <- buildReport True tmpDir m
reportHasErrors report `shouldBe` False
it "returns True when Dhall failed" $ do
let report =
ValidateReport
{ reportModule = goodModule,
reportPath = "/some/path",
reportDhallOk = False,
reportDhallError = Just "some dhall error",
reportChecks = []
}
reportHasErrors report `shouldBe` True
describe "command safety" $ do
it "passes for valid commands" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m = withCommands [Command "echo hello" Nothing Nothing] goodModule
report <- buildReport False tmpDir m
hasPassedCheck "Command safety" report.reportChecks `shouldBe` True
it "fails for empty command text" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m = withCommands [Command " " Nothing Nothing] goodModule
report <- buildReport False tmpDir m
hasFailedCheck "Command safety" report.reportChecks `shouldBe` True
it "fails for absolute workDir" $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m = withCommands [Command "echo hi" (Just "/usr/local") Nothing] goodModule
report <- buildReport False tmpDir m
hasFailedCheck "Command safety" report.reportChecks `shouldBe` True
it "fails for workDir containing .." $ do
withSystemTempDirectory "seihou-validate" $ \tmpDir -> do
createDirectoryIfMissing True (tmpDir </> "files")
writeFile (tmpDir </> "files" </> "README.md.tpl") "stub"
let m = withCommands [Command "echo hi" (Just "../escape") Nothing] goodModule
report <- buildReport False tmpDir m
hasFailedCheck "Command safety" report.reportChecks `shouldBe` True