seihou-core-0.16.0.0: test/Seihou/Core/JudgementArtifactSpec.hs
-- | Judgements as ordinary artifacts: registry entries, registry validation
-- and synchronization, discovery, enumeration, and the ambiguity rule for a
-- @judgement.dhall@ that shares its directory with another definition file.
module Seihou.Core.JudgementArtifactSpec (tests) where
import Data.Generics.Labels ()
import Data.List (isInfixOf)
import Data.Text qualified as T
import Seihou.Core.Module (DiscoveredRunnable (..), RunnableKind (..), discoverAllRunnables, discoverRunnable)
import Seihou.Core.Registry
import Seihou.Core.Types (ModuleLoadError (..), ModuleName (..), Runnable (..))
import Seihou.Dhall.Eval (evalRegistryFromFile)
import Seihou.Prelude
import System.Directory (createDirectoryIfMissing, getCurrentDirectory)
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)
tests :: IO TestTree
tests = testSpec "Seihou.Core.JudgementArtifact" spec
entry :: Text -> FilePath -> RegistryEntry
entry n p = RegistryEntry (ModuleName n) Nothing p Nothing []
registry :: [RegistryEntry] -> [RegistryEntry] -> Registry
registry mods judgements =
Registry
{ repoName = "fixture",
repoDescription = Nothing,
modules = mods,
recipes = [],
blueprints = [],
prompts = [],
judgements = judgements
}
-- | A minimal judgement definition importing the local schema.
writeJudgement :: FilePath -> Text -> Text -> IO ()
writeJudgement dir name version = do
cwd <- getCurrentDirectory
createDirectoryIfMissing True dir
writeFile (dir </> "judgement.dhall") $
"let S = "
<> cwd
<> "/../schema/package.dhall in S.Judgement::{ name = \""
<> T.unpack name
<> "\", version = Some \""
<> T.unpack version
<> "\", description = Some \"Guards data\", checks = [ S.Check::{ name = \"c\", question = \"q?\" } ] }"
writeModule :: FilePath -> Text -> IO ()
writeModule dir name = do
cwd <- getCurrentDirectory
createDirectoryIfMissing True dir
writeFile (dir </> "module.dhall") ("let S = " <> cwd <> "/../schema/package.dhall in S.Module::{ name = \"" <> T.unpack name <> "\" }")
spec :: Spec
spec = do
describe "registry judgements" $ do
it "decodes a registry with a judgement entry" $
withSystemTempDirectory "judgement-registry" $ \tmp -> do
writeFile (tmp </> "seihou-registry.dhall") $
"{ repoName = \"r\", repoDescription = None Text\n\
\, modules = [] : List { name : Text, version : Optional Text, path : Text, description : Optional Text, tags : List Text }\n\
\, judgements = [ { name = \"guard\", version = Some \"0.1.0\", path = \"judgements/guard\", description = Some \"Guards data\", tags = [ \"review\" ] } ]\n\
\}"
result <- evalRegistryFromFile (tmp </> "seihou-registry.dhall")
case result of
Left err -> expectationFailure (show err)
Right reg -> do
reg ^. #judgements `shouldBe` [RegistryEntry (ModuleName "guard") (Just "0.1.0") "judgements/guard" (Just "Guards data") ["review"]]
reg ^. #prompts `shouldBe` []
it "decodes a legacy registry without a judgements field as an empty list" $
withSystemTempDirectory "judgement-registry-legacy" $ \tmp -> do
writeFile (tmp </> "seihou-registry.dhall") $
"{ repoName = \"r\", repoDescription = None Text\n\
\, modules = [ { name = \"m\", version = None Text, path = \"m\", description = None Text, tags = [] : List Text } ]\n\
\, recipes = [] : List { name : Text, version : Optional Text, path : Text, description : Optional Text, tags : List Text }\n\
\, blueprints = [] : List { name : Text, version : Optional Text, path : Text, description : Optional Text, tags : List Text }\n\
\, prompts = [] : List { name : Text, version : Optional Text, path : Text, description : Optional Text, tags : List Text }\n\
\}"
result <- evalRegistryFromFile (tmp </> "seihou-registry.dhall")
fmap (^. #judgements) result `shouldBe` Right []
it "round-trips judgement entries through renderRegistryDhall" $
withSystemTempDirectory "judgement-registry-emit" $ \tmp -> do
let reg = registry [entry "m" "m"] [RegistryEntry (ModuleName "guard") (Just "0.1.0") "guard" (Just "Guards") ["a"]]
writeFile (tmp </> "seihou-registry.dhall") (T.unpack (renderRegistryDhall reg))
evalRegistryFromFile (tmp </> "seihou-registry.dhall") `shouldReturn` Right reg
it "accepts a well-formed judgement entry" $
withSystemTempDirectory "judgement-validate-ok" $ \tmp -> do
writeJudgement (tmp </> "guard") "guard" "0.1.0"
validateRegistry tmp (registry [] [entry "guard" "guard"]) `shouldReturn` []
it "reports a judgement entry whose judgement.dhall is missing" $
withSystemTempDirectory "judgement-validate-missing" $ \tmp -> do
errs <- validateRegistry tmp (registry [] [entry "ghost" "ghost"])
errs `shouldBe` ["registry judgement entry 'ghost' points to missing judgement.dhall at ghost"]
it "reports an invalid name and an unsafe path" $
withSystemTempDirectory "judgement-validate-unsafe" $ \tmp -> do
errs <- validateRegistry tmp (registry [] [entry "Bad_Name" "../escape"])
errs `shouldSatisfy` elem "registry judgement name must match [a-z][a-z0-9-]*, got: Bad_Name"
errs `shouldSatisfy` elem "registry judgement path must not contain '..': ../escape"
it "reports a judgement whose name collides with a module" $
withSystemTempDirectory "judgement-validate-collision" $ \tmp -> do
writeModule (tmp </> "modules" </> "shared") "shared"
writeJudgement (tmp </> "judgements" </> "shared") "shared" "0.1.0"
errs <- validateRegistry tmp (registry [entry "shared" "modules/shared"] [entry "shared" "judgements/shared"])
errs `shouldBe` ["name collision: 'shared' appears as both a module and a judgement"]
it "reports an entry directory where judgement.dhall sits next to module.dhall" $
withSystemTempDirectory "judgement-validate-ambiguous" $ \tmp -> do
writeModule (tmp </> "mixed") "mixed"
writeJudgement (tmp </> "mixed") "mixed" "0.1.0"
errs <- validateRegistry tmp (registry [] [entry "mixed" "mixed"])
errs `shouldSatisfy` any (T.isInfixOf "ambiguous artifact 'mixed'")
errs `shouldSatisfy` any (T.isInfixOf "module.dhall, judgement.dhall")
it "classifies judgement version drift and counts judgements" $
withSystemTempDirectory "judgement-validate-full" $ \tmp -> do
writeJudgement (tmp </> "guard") "guard" "0.2.0"
let reg = registry [] [RegistryEntry (ModuleName "guard") (Just "0.1.0") "guard" Nothing []]
lookups = [(JudgementEntry, ModuleName "guard", Just "0.2.0")]
report = computeRegistrySync reg lookups
map (^. #status) (report ^. #diffs) `shouldBe` [SyncStale "0.2.0"]
map (^. #kind) (report ^. #diffs) `shouldBe` [JudgementEntry]
fmap (^. #version) (report ^. #updated . #judgements) `shouldBe` [Just "0.2.0"]
full <- validateRegistryFull tmp reg lookups
full ^. #judgementCount `shouldBe` 1
map formatValidationIssue (full ^. #issues)
`shouldBe` ["judgements.guard: registry version 0.1.0 does not match judgement.dhall version 0.2.0"]
it "recognises a single-judgement repository root" $
withSystemTempDirectory "judgement-single-root" $ \tmp -> do
writeJudgement tmp "guard" "0.1.0"
contents <- discoverRepoContents evalRegistryFromFile tmp
case contents of
SingleJudgement root -> root `shouldBe` tmp
other -> expectationFailure ("expected SingleJudgement, got " <> show other)
describe "judgement discovery" $ do
it "discovers a judgement directory by name" $
withSystemTempDirectory "judgement-discover" $ \tmp -> do
writeJudgement (tmp </> "guard") "guard" "0.1.0"
result <- discoverRunnable [tmp] (ModuleName "guard")
case result of
Right (RunnableJudgement def dir) -> do
def ^. #name `shouldBe` "guard"
dir `shouldBe` tmp </> "guard"
Right _ -> expectationFailure "expected a judgement"
Left err -> expectationFailure (show err)
it "rejects a judgement co-located with a module as AmbiguousArtifact" $
withSystemTempDirectory "judgement-ambiguous" $ \tmp -> do
writeModule (tmp </> "mixed") "mixed"
writeJudgement (tmp </> "mixed") "mixed" "0.1.0"
result <- discoverRunnable [tmp] (ModuleName "mixed")
case result of
Left (AmbiguousArtifact name dir files) -> do
name `shouldBe` ModuleName "mixed"
dir `shouldBe` tmp </> "mixed"
files `shouldBe` ["module.dhall", "judgement.dhall"]
Left err -> expectationFailure ("expected AmbiguousArtifact, got " <> show err)
Right _ -> expectationFailure "expected AmbiguousArtifact, got a runnable"
it "keeps module precedence for combinations without a judgement" $
withSystemTempDirectory "judgement-no-ambiguity" $ \tmp -> do
writeModule (tmp </> "plain") "plain"
writeFile (tmp </> "plain" </> "prompt.dhall") "this is never read"
result <- discoverRunnable [tmp] (ModuleName "plain")
case result of
Right (RunnableModule _ _) -> pure ()
Right _ -> expectationFailure "expected the module"
Left err -> expectationFailure (show err)
it "enumerates judgements with their kind, and an ambiguous directory as an error" $
withSystemTempDirectory "judgement-enumerate" $ \tmp -> do
writeJudgement (tmp </> "guard") "guard" "0.1.0"
writeModule (tmp </> "mixed") "mixed"
writeJudgement (tmp </> "mixed") "mixed" "0.1.0"
found <- discoverAllRunnables [tmp]
let rows = [(r ^. #name, r ^. #kind, r ^. #isError, r ^. #description) | r <- found]
rows `shouldSatisfy` elem ("guard", KindJudgement, False, Just "Guards data")
case [r | r <- found, r ^. #name == "mixed"] of
[r] -> do
r ^. #kind `shouldBe` KindJudgement
r ^. #isError `shouldBe` True
fmap T.unpack (r ^. #error) `shouldSatisfy` maybe False ("judgement.dhall" `isInfixOf`)
other -> expectationFailure ("expected one row for mixed, got " <> show (length other))