heni-cli (empty) → 0.1.0.0
raw patch · 21 files changed
+1076/−0 lines, 21 filesdep +aesondep +basedep +bytestring
Dependencies added: aeson, base, bytestring, containers, directory, filepath, heni, process, text, unix
Files
- CHANGELOG.md +5/−0
- LICENSE +27/−0
- README.md +10/−0
- app/Heni/Cli.hs +179/−0
- app/Main.hs +6/−0
- heni-cli.cabal +84/−0
- test/Main.hs +168/−0
- test/corpus/invalid-adapter-version.json +38/−0
- test/corpus/invalid-budget.json +43/−0
- test/corpus/invalid-duplicate-key.json +1/−0
- test/corpus/invalid-named-without-adapter.json +37/−0
- test/corpus/invalid-nested-key.json +43/−0
- test/corpus/invalid-null-arguments.json +39/−0
- test/corpus/invalid-resource-unicode-name.json +63/−0
- test/corpus/invalid-unknown-key.json +43/−0
- test/corpus/invalid-unsafe-path.json +42/−0
- test/corpus/invalid-version.json +42/−0
- test/corpus/invalid-workers.json +63/−0
- test/corpus/valid-minimal.json +42/−0
- test/corpus/valid-named.json +38/−0
- test/corpus/valid-resources.json +63/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# 0.1.0.0 — 2026-10-24++## New Features++Strict JSON manifests and check/list/run/report over the shared Heni library.
+ LICENSE view
@@ -0,0 +1,27 @@+BSD 3-Clause License++Copyright (c) 2026, Nadeem Bitar+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+ list of conditions and the following disclaimer.+2. Redistributions in binary form must reproduce the above copyright notice,+ this list of conditions and the following disclaimer in the documentation+ and/or other materials provided with the distribution.+3. Neither the name of the copyright holder nor the names of its contributors+ may be used to endorse or promote products derived from this software+ without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,10 @@+# heni-cli++The `heni` executable validates manifests, lists cases, runs mutation campaigns+and renders stored reports through the matching Heni library. Version 0.1.0.0 is+unreleased. Use `heni --help` and the [CLI reference](https://github.com/shinzui/heni/blob/master/docs/cli.md).++`run` exits 0 for a met trustworthy contract, 1 for a failed contract, 2 for+configuration/infrastructure failure, and 130 after SIGINT finalization. Generic+commands need exact diagnostic/exit evidence; optional reporters provide named+assertion evidence. Use fresh output directories; report artifacts move together.
+ app/Heni/Cli.hs view
@@ -0,0 +1,179 @@+-- | Command application: parsing and presentation only; the library owns behavior.+module Heni.Cli (mainCli, selectDefinitions, exitCodeFor) where++import Control.Exception+import Control.Monad (forM_, unless)+import Data.Aeson hiding (Options)+import Data.ByteString.Lazy.Char8 qualified as LBS+import Data.Map.Strict qualified as Map+import Data.Set qualified as Set+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Version (showVersion)+import Paths_heni_cli (version)+import System.Environment (getArgs)+import System.Exit+import System.FilePath ((</>))+import System.IO+import Test.Heni+import Text.Read (readMaybe)++data Options = Options !Bool !String ![Text] ![Text] !(Maybe FilePath) !Int !Bool++defaults :: Options+defaults = Options False "auto" [] [] Nothing 0 False++mainCli :: IO ()+mainCli = do+ args <- getArgs+ case args of+ ["--version"] -> putStrLn ("heni " ++ showVersion version)+ [] -> putStrLn help >> exitWith (ExitFailure 2)+ _ | "--help" `elem` args -> putStrLn help+ command : path : rest | command `elem` ["check", "list", "run", "report"] -> case parseOptions command defaults rest of+ Left why -> hPutStrLn stderr why >> exitWith (ExitFailure 2)+ Right options -> do+ attempted <- try (dispatch command path options)+ case attempted of+ Right code -> exitCode code+ Left (e :: SomeException) -> case fromException e :: Maybe AsyncException of+ Just UserInterrupt -> hPutStrLn stderr "Interrupted; inspect the partial report or recover the journal." >> exitWith (ExitFailure 130)+ Just ThreadKilled -> exitWith (ExitFailure 130)+ _ -> hPutStrLn stderr (diagnostic e) >> exitWith (ExitFailure 2)+ _ -> hPutStrLn stderr "Expected check, list, run, or report and a path. Use --help." >> exitWith (ExitFailure 2)++parseOptions :: String -> Options -> [String] -> Either String Options+parseOptions _ options [] = Right options+parseOptions command (Options _ color cases tags output jobs recover) ("--json" : rest) = parseOptions command (Options True color cases tags output jobs recover) rest+parseOptions command (Options json _ cases tags output jobs recover) ("--color" : color : rest)+ | color `elem` ["auto", "always", "never"] = parseOptions command (Options json color cases tags output jobs recover) rest+parseOptions "run" (Options json color cases tags output jobs recover) ("--case" : ident : rest) = parseOptions "run" (Options json color (cases ++ [Text.pack ident]) tags output jobs recover) rest+parseOptions "run" (Options json color cases tags output jobs recover) ("--tag" : tag : rest) = parseOptions "run" (Options json color cases (tags ++ [Text.pack tag]) output jobs recover) rest+parseOptions "run" (Options json color cases tags _ jobs recover) ("--output" : path : rest) = parseOptions "run" (Options json color cases tags (Just path) jobs recover) rest+parseOptions "run" (Options json color cases tags output _ recover) ("--jobs" : value : rest) = case readMaybe value of+ Just jobs | jobs > 0 && jobs <= 256 -> parseOptions "run" (Options json color cases tags output jobs recover) rest+ _ -> Left "--jobs must be between 1 and 256"+parseOptions "report" (Options json color cases tags output jobs _) ("--recover" : rest) = parseOptions "report" (Options json color cases tags output jobs True) rest+parseOptions _ _ (unknown : _) = Left ("Unknown, incomplete, or unsupported option: " ++ unknown)++dispatch :: String -> FilePath -> Options -> IO Int+dispatch "report" path (Options json color _ _ _ _ recover) = do+ stored <- (if recover then recoverReport path else readReport path) >>= either throwIO pure+ if json then LBS.putStrLn (encode (reportJSON stored)) else case stored of StoredReport {result} -> render color result+ pure 0+dispatch command path (Options json color cases tags output jobs _) = do+ manifest <- readManifest path >>= either (throwIO . ConfigurationFailure) pure+ let campaign = manifestCampaign manifest+ definitions = campaignCases campaign+ case command of+ "list" -> do+ if json+ then LBS.putStrLn (encode (object ["schemaVersion" .= (1 :: Int), "cases" .= map caseDefinitionJSON definitions]))+ else forM_ definitions $ \CaseDefinition {caseId, tags = caseTags, selector, expectation, description} ->+ putStrLn (Text.unpack (caseIdText caseId) ++ " tags=" ++ show (Set.toAscList caseTags) ++ " selector=" ++ show selector ++ " expect=" ++ show expectation ++ "\n " ++ Text.unpack description)+ pure 0+ "check" -> do+ checks <- checkManifest manifest >>= either (throwIO . ConfigurationFailure) pure+ if json+ then+ LBS.putStrLn+ ( encode+ ( object+ [ "schemaVersion" .= (1 :: Int),+ "checksPerformed" .= (["static-validation", "command-availability", "isolated-edit-applicability"] :: [Text]),+ "testsExecuted" .= False,+ "cases" .= [object ["id" .= caseIdText caseId, "applicable" .= applicable, "diagnostics" .= diagnostics] | Applicability {caseId, applicable, diagnostics} <- checks]+ ]+ )+ )+ else do+ putStrLn "Checked structure, command availability and isolated edit applicability. Tests and discovery were not run."+ forM_ checks $ \Applicability {caseId, applicable, diagnostics} -> putStrLn (Text.unpack (caseIdText caseId) ++ if applicable then ": applicable" else ": " ++ show diagnostics)+ pure (if all (\Applicability {applicable} -> applicable) checks then 0 else 2)+ "run" -> do+ selected <- either (throwIO . ConfigurationFailure) pure (selectDefinitions cases tags definitions)+ let directory = maybe (manifestSourceRoot manifest </> "mutation-results") id output+ inheritedOptions = manifestRunOptions directory manifest+ options =+ inheritedOptions+ { selectedCases = Just selected,+ workers = if jobs == 0 then workers inheritedOptions else jobs,+ progress = \case CaseStarted i -> hPutStrLn stderr ("running " ++ Text.unpack (caseIdText i)); _ -> pure ()+ }+ result <- runCampaign options campaign+ if json+ then LBS.putStrLn (encode (object ["schemaVersion" .= (1 :: Int), "reportPath" .= (directory </> "report.json"), "selectionRule" .= String "union within filter kind, intersection across case/tag kinds", "result" .= runResultJSON result]))+ else render color result >> putStrLn ("Evidence: " ++ directory </> "report.json")+ pure (exitCodeFor result)+ _ -> error "validated command dispatch"++-- | Union repeated values within a filter kind; intersect the case and tag sets.+-- Unknown explicit IDs and empty intersections are configuration errors.+selectDefinitions :: [Text] -> [Text] -> [CaseDefinition] -> Either ConfigError SelectedCases+selectDefinitions requested tags definitions = do+ ids <- mapM mkCaseId requested+ let known = [caseId | CaseDefinition {caseId} <- definitions]+ unless (all (`elem` known) ids) (Left (ConfigError "case" "unknown requested case ID"))+ mkSelectedCases+ [ caseId+ | CaseDefinition {caseId, tags = caseTags} <- definitions,+ (null ids || caseId `elem` ids) && (null tags || not (Set.null (Set.intersection (Set.fromList tags) caseTags)))+ ]++-- | Infrastructure has precedence because RunStatus is library-owned.+exitCodeFor :: RunResult -> Int+exitCodeFor RunResult {status} = case status of RunPassed -> 0; RunFailed -> 1; RunUntrustworthy -> 2; RunInterrupted -> 130++exitCode :: Int -> IO ()+exitCode 0 = exitSuccess+exitCode n = exitWith (ExitFailure n)++render :: String -> RunResult -> IO ()+render color RunResult {results, status, score, failures} = do+ terminal <- hIsTerminalDevice stdout+ let colored = color == "always" || color == "auto" && terminal+ paint text = if colored then "\ESC[" ++ (if status == RunPassed then "32" else "31") ++ "m" ++ text ++ "\ESC[0m" else text+ counts = Map.fromListWith (+) [(show verdict, 1 :: Int) | CaseResult {verdict} <- results]+ putStrLn (paint (show status) ++ " " ++ show (Map.toAscList counts))+ forM_ results $ \CaseResult {caseId, verdict, reason, evidence = CaseEvidence {stages}} -> do+ putStrLn (Text.unpack (caseIdText caseId) ++ ": " ++ show verdict ++ " — " ++ Text.unpack reason)+ forM_ stages $ \StageEvidence {transcript} -> unless (Text.null transcript) (putStrLn (" witness: " ++ Text.unpack transcript))+ forM_ score $ \(SemanticScore caught total complete) -> putStrLn ("Semantic detections: " ++ show caught ++ "/" ++ show total ++ if complete then "" else " (incomplete)")+ forM_ failures (putStrLn . ("Run failure: " ++) . show)++help :: String+help =+ unlines+ [ "heni — deliberate mutation testing with attributable evidence",+ "",+ "Usage:",+ " heni check MANIFEST [--json]",+ " heni list MANIFEST [--json]",+ " heni run MANIFEST [--case ID] [--tag TAG] [--output DIRECTORY] [--jobs N] [--json]",+ " heni report REPORT.json [--json]",+ " heni report DIRECTORY --recover [--json]",+ "",+ "Selection:",+ " Repeated filters use union within each kind and intersection across case/tag kinds.",+ " Unknown explicit IDs and empty selection fail. Manifest paths resolve from its location.",+ " --output is an explicit caller-relative path; the default is SOURCE/mutation-results.",+ "",+ "Presentation:",+ " --color auto|always|never Applies to human output only; redirected auto output has no ANSI.",+ " --json JSON on stdout, progress and errors on stderr.",+ " --help Show this help.",+ " --version Show the source-package version without Git.",+ "",+ "Checks and exits:",+ " check runs static/availability/applicability checks, never tests or discovery.",+ " run: 0 met contract; 1 failed contract; 2 configuration/infrastructure; SIGINT 130.",+ " report renders stored evidence only; recovery never resumes commands.",+ " Runs require a new output directory. Inspect report.json or recover journal.jsonl after interruption."+ ]++diagnostic :: SomeException -> String+diagnostic e = case fromException e of+ Just (ConfigurationFailure (ConfigError field message)) -> Text.unpack field ++ ": " ++ Text.unpack message+ Nothing -> case fromException e of+ Just (ReportError message) -> Text.unpack message+ Nothing -> displayException e
+ app/Main.hs view
@@ -0,0 +1,6 @@+module Main (main) where++import Heni.Cli (mainCli)++main :: IO ()+main = mainCli
+ heni-cli.cabal view
@@ -0,0 +1,84 @@+cabal-version: 3.4+name: heni-cli+version: 0.1.0.0+synopsis: Command line interface to trustworthy deliberate mutation testing+description:+ Validate manifests, run checked mutations, and render portable+ Heni evidence through the shared library runner.++homepage: https://github.com/shinzui/heni+bug-reports: https://github.com/shinzui/heni/issues+license: BSD-3-Clause+license-file: LICENSE+author: Nadeem Bitar+maintainer: nadeem@gmail.com+copyright: 2026 Nadeem Bitar+category: Testing+build-type: Simple+tested-with: ghc ==9.12.4+extra-doc-files:+ CHANGELOG.md+ README.md++data-files: test/corpus/*.json++source-repository head+ type: git+ location: https://github.com/shinzui/heni.git+ subdir: heni-cli++common common+ default-language: GHC2024+ default-extensions:+ DeriveAnyClass+ DuplicateRecordFields+ OverloadedLabels+ OverloadedStrings++ ghc-options:+ -Wall+ -Wcompat++ build-depends:+ aeson >=2.3.2 && <2.4,+ base >=4.21.2 && <4.22,+ bytestring >=0.12.2 && <0.13,+ containers >=0.7 && <0.9,+ directory >=1.3.10.1 && <1.4,+ filepath >=1.5.5 && <1.6,+ heni ==0.1.0.0,+ text >=2.1.4 && <2.2,++executable heni+ import: common+ hs-source-dirs: app+ main-is: Main.hs+ other-modules:+ Heni.Cli+ Paths_heni_cli++ autogen-modules: Paths_heni_cli+ ghc-options: -threaded++test-suite heni-cli-test+ import: common+ type: exitcode-stdio-1.0+ hs-source-dirs:+ test+ app++ main-is: Main.hs+ other-modules:+ Heni.Cli+ Paths_heni_cli++ autogen-modules: Paths_heni_cli+ build-depends:+ process >=1.6.26.1 && <1.7,+ unix >=2.8.8 && <2.9,++ build-tool-depends:+ heni:heni-fixture,+ heni-cli:heni,++ ghc-options: -threaded
+ test/Main.hs view
@@ -0,0 +1,168 @@+module Main (main) where++import Control.Concurrent (threadDelay)+import Control.Exception hiding (handle)+import Control.Monad (forM_, unless, when)+import Data.Aeson+import Data.Aeson.KeyMap qualified as KM+import Data.ByteString qualified as BS+import Data.ByteString.Lazy qualified as LBS+import Data.Foldable (toList)+import Data.List (isInfixOf, isPrefixOf)+import Data.Text qualified as Text+import Data.Text.Encoding qualified as Text+import Heni.Cli (exitCodeFor, selectDefinitions)+import Paths_heni_cli (getDataFileName)+import System.Directory qualified as Dir+import System.Environment (lookupEnv)+import System.Exit (ExitCode (..))+import System.FilePath (takeDirectory, (</>))+import System.Posix.Signals (sigINT, sigKILL, signalProcess)+import System.Posix.Temp (mkdtemp)+import System.Process (readProcessWithExitCode)+import System.Process qualified as P+import System.Timeout (timeout)+import Test.Heni++assert' :: String -> Bool -> IO ()+assert' name good = unless good (error name)++checked :: (Show e) => Either e a -> a+checked = either (error . show) id++main :: IO ()+main = bracket (Dir.getTemporaryDirectory >>= \tmp -> mkdtemp (tmp </> "heni-cli-test-")) Dir.removePathForcibly $ \root -> do+ corpus <- getDataFileName "test/corpus"+ names <- Dir.listDirectory corpus+ forM_ names $ \name -> do+ content <- BS.readFile (corpus </> name)+ assert' ("shared schema/decoder corpus: " ++ name) ((case decodeManifest content of Right _ -> True; Left _ -> False) == ("valid-" `isPrefixOf` name))+ let definition ident tags old new = object ["id" .= (ident :: String), "description" .= String "Detect the intended fault", "tags" .= (tags :: [String]), "edits" .= [object ["kind" .= String "replace", "path" .= String "input", "old" .= (old :: String), "new" .= (new :: String)]], "build" .= String "build", "test" .= String "test", "expect" .= object ["kind" .= String "diagnostic", "stage" .= String "test", "exitCode" .= (1 :: Int), "outputContains" .= String "capacity rule violated"]]+ value =+ object+ [ "schemaVersion" .= (1 :: Int),+ "sourceRoot" .= String "source",+ "commands"+ .= object+ [ "build" .= object ["executable" .= String "heni-fixture", "arguments" .= (["exit", "0"] :: [String])],+ "test" .= object ["executable" .= String "heni-fixture", "arguments" .= (["check", "input", "diagnostic"] :: [String])]+ ],+ "cases" .= [definition "caught" ["regression"] "anchor" "mutant", definition "survivor" ["example"] "anchor" "harmless"]+ ]+ bytes = LBS.toStrict (encode value)+ manifestPath = root </> "heni.json"+ cli args = readProcessWithExitCode "heni" args ""+ Dir.createDirectory (root </> "source")+ BS.writeFile (root </> "source/input") "anchor\n"+ BS.writeFile manifestPath bytes+ manifest <- readManifest manifestPath >>= either (error . show) pure+ let definitions = campaignCases (manifestCampaign manifest)+ assert' "case union and tag intersection" (fmap selectedCaseIds (selectDefinitions ["caught", "caught", "survivor"] ["regression"] definitions) == Right [checked (mkCaseId "caught")])+ assert' "unknown explicit case" (case selectDefinitions ["absent"] [] definitions of Left _ -> True; _ -> False)+ assert' "empty selection" (case selectDefinitions [] ["absent"] definitions of Left _ -> True; _ -> False)+ (versionExit, version, _) <- cli ["--version"]+ assert' "archive-safe version" (versionExit == ExitSuccess && "heni 0.1.0.0" `isInfixOf` version)+ (helpExit, help, _) <- cli ["--help"]+ assert' "help selection/exit contract" (helpExit == ExitSuccess && "intersection" `isInfixOf` help && "SIGINT 130" `isInfixOf` help)+ (checkExit, checkOutput, _) <- cli ["check", manifestPath, "--json"]+ assert' "check states it did not execute tests" (checkExit == ExitSuccess && case eitherDecodeStrict (Text.encodeUtf8 (Text.pack checkOutput)) :: Either String Value of Right (Object o) -> KM.lookup "testsExecuted" o == Just (Bool False); _ -> False)+ (listExit, listing, _) <- cli ["list", manifestPath, "--json"]+ assert' "list is JSON" (listExit == ExitSuccess && case eitherDecodeStrict (Text.encodeUtf8 (Text.pack listing)) :: Either String Value of Right _ -> True; _ -> False)+ library <- runCampaign ((manifestRunOptions (root </> "library") manifest) {selectedCases = Just (checked (selectDefinitions ["caught"] [] definitions))}) (manifestCampaign manifest)+ (caughtExit, runOutput, progress) <- cli ["run", manifestPath, "--case", "caught", "--output", root </> "cli", "--json", "--color", "always"]+ assert' "caught exit and clean machine stdout" (caughtExit == ExitSuccess && not ('\ESC' `elem` runOutput) && "running caught" `isInfixOf` progress)+ cliStored <- readReport (root </> "cli/report.json") >>= either (error . show) pure+ let signature RunResult {results} = [(caseIdText caseId, verdict, [(stage, outcome) | StageEvidence {stage, outcome} <- stages]) | CaseResult {caseId, verdict, evidence = CaseEvidence {stages}} <- results]+ assert' "library/CLI share stage decisions and verdicts" (case cliStored of StoredReport {result} -> signature result == signature library && exitCodeFor result == 0)+ let resourceValue = case value of+ Object o ->+ Object+ ( KM.insert+ "execution"+ (object ["workers" .= (2 :: Int), "baselineReuse" .= object ["toolchain" .= String "self-contained-fixture/v1"]])+ ( KM.insert+ "resources"+ (toJSON [object ["name" .= String "DB", "scope" .= String "case", "identity" .= String "local-fixture", "version" .= String "v1", "epoch" .= String "clean", "environment" .= object ["HENI_RESOURCE_DB_TOKEN" .= String "resource-private-secret"], "setup" .= String "build", "reset" .= String "build", "teardown" .= String "build"]])+ (KM.insert "cases" (case KM.lookup "cases" o of Just (Array cases) -> toJSON [case c of Object fields -> Object (KM.insert "resources" (toJSON (["DB"] :: [String])) fields); other -> other | c <- toList cases]; _ -> Null) o)+ )+ )+ other -> other+ resourceManifest = root </> "resources.json"+ LBS.writeFile resourceManifest (encode resourceValue)+ configured <- readManifest resourceManifest >>= either (error . show) pure+ configuredLibrary <- runCampaign (manifestRunOptions (root </> "resource-library") configured) (manifestCampaign configured)+ (resourceExit, _, _) <- cli ["run", resourceManifest, "--jobs", "2", "--output", root </> "resource-cli", "--json"]+ resourceStored <- readReport (root </> "resource-cli/report.json") >>= either (error . show) pure+ assert' "resource commands/workers/reuse share library and CLI outcomes" (resourceExit == ExitFailure 1 && case resourceStored of StoredReport {result} -> signature result == signature configuredLibrary)+ reportWithResources <- BS.readFile (root </> "resource-cli/report.json")+ assert' "resource command configuration is redacted" (not ("resource-private-secret" `BS.isInfixOf` reportWithResources))+ fixtureOutput <- lookupEnv "HENI_SCHEMA_RESOURCE"+ forM_ fixtureOutput $ \path -> do+ Dir.createDirectoryIfMissing True (takeDirectory path)+ BS.writeFile path reportWithResources+ Dir.removeFile (root </> "resource-cli/resource-DB-case-caught-0.stdout.log")+ missingResourceLog <- readReport (root </> "resource-cli/report.json")+ assert' "missing setup command artifact invalidates portable report" (case missingResourceLog of Left _ -> True; _ -> False)+ (survivorExit, human, _) <- cli ["run", manifestPath, "--case", "survivor", "--output", root </> "survivor"]+ assert' "survivor exit and redirected output without ANSI" (survivorExit == ExitFailure 1 && not ('\ESC' `elem` human))+ (emptyExit, _, _) <- cli ["run", manifestPath, "--tag", "absent", "--output", root </> "empty"]+ assert' "empty run fails by default" (emptyExit == ExitFailure 2)+ Dir.renameDirectory (root </> "cli") (root </> "moved")+ (reportExit, storedOutput, _) <- cli ["report", root </> "moved/report.json", "--json"]+ assert' "moved evidence renders" (reportExit == ExitSuccess && not (null storedOutput))+ (recoveryExit, _, _) <- cli ["report", root </> "moved", "--recover"]+ assert' "explicit recovery renders without execution" (recoveryExit == ExitSuccess)+ let invalids =+ [ Text.replace "\"schemaVersion\":1" "\"schemaVersion\":999" (Text.decodeUtf8 bytes),+ Text.replace "\"schemaVersion\":1" "\"schemaVersion\":1,\"schemaVersion\":1" (Text.decodeUtf8 bytes),+ Text.replace "\"schemaVersion\":1" "\"schemaVersion\":1,\"\\u0073chemaVersion\":1" (Text.decodeUtf8 bytes),+ Text.replace "\"executable\":\"heni-fixture\"" "\"executable\":\"heni-fixture\",\"timeuotSeconds\":2" (Text.decodeUtf8 bytes),+ Text.replace "\"executable\":\"heni-fixture\"" "\"executable\":\"heni-fixture\",\"timeoutSeconds\":0" (Text.decodeUtf8 bytes),+ Text.replace "\"executable\":\"heni-fixture\"" "\"executable\":\"heni-fixture\",\"arguments\":null" (Text.decodeUtf8 bytes),+ Text.replace "\"path\":\"input\"" "\"path\":\"../escape\"" (Text.decodeUtf8 bytes),+ Text.replace "\"build\":\"build\"" "\"build\":\"absent\"" (Text.decodeUtf8 bytes)+ ]+ forM_ invalids $ \invalid -> assert' "invalid manifest rejected" (case decodeManifest (Text.encodeUtf8 invalid) of Left _ -> True; _ -> False)+ -- Applicability checks and listing do not execute a command with visible effects.+ let sentinel = root </> "must-not-execute"+ noExecute = object ["schemaVersion" .= (1 :: Int), "sourceRoot" .= String "source", "commands" .= object ["build" .= object ["executable" .= String "heni-fixture", "arguments" .= ["counter", sentinel]], "test" .= object ["executable" .= String "heni-fixture", "arguments" .= (["exit", "0"] :: [String])]], "cases" .= [definition "caught" [] "anchor" "mutant"]]+ LBS.writeFile (root </> "no-execute.json") (encode noExecute)+ _ <- cli ["check", root </> "no-execute.json"]+ _ <- cli ["list", root </> "no-execute.json"]+ Dir.doesFileExist sentinel >>= assert' "check/list never execute tests or builds" . not+ let interruptMarker = root </> "interrupted-child.pid"+ interruptManifest =+ object+ [ "schemaVersion" .= (1 :: Int),+ "sourceRoot" .= String "source",+ "commands"+ .= object+ [ "build" .= object ["executable" .= String "heni-fixture", "arguments" .= (["exit", "0"] :: [String])],+ "test" .= object ["executable" .= String "heni-fixture", "arguments" .= ["check", "input", "timeout", interruptMarker]]+ ],+ "cases" .= [definition "caught" [] "anchor" "mutant"]+ ]+ LBS.writeFile (root </> "interrupt.json") (encode interruptManifest)+ let process = (P.proc "heni" ["run", root </> "interrupt.json", "--output", root </> "interrupted"]) {P.std_out = P.Inherit, P.std_err = P.Inherit}+ P.withCreateProcess process $ \_ _ _ handle -> do+ let waitReady = do+ exists <- Dir.doesFileExist interruptMarker+ unless exists (threadDelay 5000 >> waitReady)+ stopOwned = do+ markerExists <- Dir.doesFileExist interruptMarker+ when markerExists $ do+ pid <- read <$> readFile interruptMarker+ _ <- try (signalProcess sigKILL pid) :: IO (Either IOException ())+ pure ()+ ( do+ ready <- timeout 5000000 waitReady+ assert' "CLI interruption handshake" (ready == Just ())+ pid <- P.getPid handle >>= maybe (error "missing owned CLI PID") pure+ signalProcess sigINT pid+ exited <- timeout 5000000 (P.waitForProcess handle)+ assert' "SIGINT uses 130 after finalization" (exited == Just (ExitFailure 130))+ partial <- readReport (root </> "interrupted/report.json")+ assert' "SIGINT retains complete selected ledger" (case partial of Right StoredReport {result = RunResult {status = RunInterrupted, results = [CaseResult {verdict = NotRun}]}} -> True; _ -> False)+ )+ `finally` stopOwned+ putStrLn "Strict manifest boundary, CLI exits, selection and library parity passed"
+ test/corpus/invalid-adapter-version.json view
@@ -0,0 +1,38 @@+{+ "schemaVersion": 1,+ "sourceRoot": ".",+ "commands": {+ "build": {+ "executable": "heni-hspec-fixture",+ "arguments": [+ "--heni-build"+ ]+ },+ "test": {+ "executable": "heni-hspec-fixture"+ }+ },+ "cases": [+ {+ "id": "named",+ "description": "Detect the intended assertion",+ "adapter": "v999",+ "edits": [+ {+ "kind": "replace",+ "path": "input",+ "old": "anchor",+ "new": "intended"+ }+ ],+ "build": "build",+ "test": "test",+ "expect": {+ "kind": "named-tests",+ "failures": [+ "suite/intended"+ ]+ }+ }+ ]+}
+ test/corpus/invalid-budget.json view
@@ -0,0 +1,43 @@+{+ "schemaVersion": 1,+ "commands": {+ "build": {+ "executable": "heni-fixture",+ "arguments": [+ "exit",+ "0"+ ]+ },+ "test": {+ "executable": "heni-fixture",+ "arguments": [+ "check",+ "input",+ "diagnostic"+ ],+ "timeoutSeconds": 0+ }+ },+ "cases": [+ {+ "id": "caught",+ "description": "Detect the intended fault",+ "edits": [+ {+ "kind": "replace",+ "path": "input",+ "old": "anchor",+ "new": "mutant"+ }+ ],+ "build": "build",+ "test": "test",+ "expect": {+ "kind": "diagnostic",+ "stage": "test",+ "exitCode": 1,+ "outputContains": "capacity rule violated"+ }+ }+ ]+}
+ test/corpus/invalid-duplicate-key.json view
@@ -0,0 +1,1 @@+{"schemaVersion": 1, "schemaVersion": 1, "commands": {"build": {"executable": "heni-fixture", "arguments": ["exit", "0"]}, "test": {"executable": "heni-fixture", "arguments": ["check", "input", "diagnostic"]}}, "cases": [{"id": "caught", "description": "Detect the intended fault", "edits": [{"kind": "replace", "path": "input", "old": "anchor", "new": "mutant"}], "build": "build", "test": "test", "expect": {"kind": "diagnostic", "stage": "test", "exitCode": 1, "outputContains": "capacity rule violated"}}]}
+ test/corpus/invalid-named-without-adapter.json view
@@ -0,0 +1,37 @@+{+ "schemaVersion": 1,+ "sourceRoot": ".",+ "commands": {+ "build": {+ "executable": "heni-hspec-fixture",+ "arguments": [+ "--heni-build"+ ]+ },+ "test": {+ "executable": "heni-hspec-fixture"+ }+ },+ "cases": [+ {+ "id": "named",+ "description": "Detect the intended assertion",+ "edits": [+ {+ "kind": "replace",+ "path": "input",+ "old": "anchor",+ "new": "intended"+ }+ ],+ "build": "build",+ "test": "test",+ "expect": {+ "kind": "named-tests",+ "failures": [+ "suite/intended"+ ]+ }+ }+ ]+}
+ test/corpus/invalid-nested-key.json view
@@ -0,0 +1,43 @@+{+ "schemaVersion": 1,+ "commands": {+ "build": {+ "executable": "heni-fixture",+ "arguments": [+ "exit",+ "0"+ ]+ },+ "test": {+ "executable": "heni-fixture",+ "arguments": [+ "check",+ "input",+ "diagnostic"+ ]+ }+ },+ "cases": [+ {+ "id": "caught",+ "description": "Detect the intended fault",+ "edits": [+ {+ "kind": "replace",+ "path": "input",+ "old": "anchor",+ "new": "mutant"+ }+ ],+ "build": "build",+ "test": "test",+ "expect": {+ "kind": "diagnostic",+ "stage": "test",+ "exitCode": 1,+ "outputContains": "capacity rule violated",+ "witness": "typo"+ }+ }+ ]+}
+ test/corpus/invalid-null-arguments.json view
@@ -0,0 +1,39 @@+{+ "schemaVersion": 1,+ "commands": {+ "build": {+ "executable": "heni-fixture",+ "arguments": null+ },+ "test": {+ "executable": "heni-fixture",+ "arguments": [+ "check",+ "input",+ "diagnostic"+ ]+ }+ },+ "cases": [+ {+ "id": "caught",+ "description": "Detect the intended fault",+ "edits": [+ {+ "kind": "replace",+ "path": "input",+ "old": "anchor",+ "new": "mutant"+ }+ ],+ "build": "build",+ "test": "test",+ "expect": {+ "kind": "diagnostic",+ "stage": "test",+ "exitCode": 1,+ "outputContains": "capacity rule violated"+ }+ }+ ]+}
+ test/corpus/invalid-resource-unicode-name.json view
@@ -0,0 +1,63 @@+{+ "schemaVersion": 1,+ "commands": {+ "build": {+ "executable": "heni-fixture",+ "arguments": [+ "exit",+ "0"+ ]+ },+ "test": {+ "executable": "heni-fixture",+ "arguments": [+ "check",+ "input",+ "diagnostic"+ ]+ }+ },+ "cases": [+ {+ "id": "caught",+ "description": "Detect the intended fault",+ "edits": [+ {+ "kind": "replace",+ "path": "input",+ "old": "anchor",+ "new": "mutant"+ }+ ],+ "build": "build",+ "test": "test",+ "expect": {+ "kind": "diagnostic",+ "stage": "test",+ "exitCode": 1,+ "outputContains": "capacity rule violated"+ },+ "resources": [+ "DB١"+ ]+ }+ ],+ "resources": [+ {+ "name": "DB١",+ "scope": "case",+ "identity": "fixture",+ "version": "v1",+ "epoch": "clean",+ "setup": "build",+ "reset": "build",+ "teardown": "build"+ }+ ],+ "execution": {+ "workers": 2,+ "baselineReuse": {+ "toolchain": "fixture/v1"+ }+ }+}
+ test/corpus/invalid-unknown-key.json view
@@ -0,0 +1,43 @@+{+ "schemaVersion": 1,+ "commands": {+ "build": {+ "executable": "heni-fixture",+ "arguments": [+ "exit",+ "0"+ ]+ },+ "test": {+ "executable": "heni-fixture",+ "arguments": [+ "check",+ "input",+ "diagnostic"+ ]+ }+ },+ "cases": [+ {+ "id": "caught",+ "description": "Detect the intended fault",+ "edits": [+ {+ "kind": "replace",+ "path": "input",+ "old": "anchor",+ "new": "mutant"+ }+ ],+ "build": "build",+ "test": "test",+ "expect": {+ "kind": "diagnostic",+ "stage": "test",+ "exitCode": 1,+ "outputContains": "capacity rule violated"+ }+ }+ ],+ "surprise": true+}
+ test/corpus/invalid-unsafe-path.json view
@@ -0,0 +1,42 @@+{+ "schemaVersion": 1,+ "commands": {+ "build": {+ "executable": "heni-fixture",+ "arguments": [+ "exit",+ "0"+ ]+ },+ "test": {+ "executable": "heni-fixture",+ "arguments": [+ "check",+ "input",+ "diagnostic"+ ]+ }+ },+ "cases": [+ {+ "id": "caught",+ "description": "Detect the intended fault",+ "edits": [+ {+ "kind": "replace",+ "path": "../escape",+ "old": "anchor",+ "new": "mutant"+ }+ ],+ "build": "build",+ "test": "test",+ "expect": {+ "kind": "diagnostic",+ "stage": "test",+ "exitCode": 1,+ "outputContains": "capacity rule violated"+ }+ }+ ]+}
+ test/corpus/invalid-version.json view
@@ -0,0 +1,42 @@+{+ "schemaVersion": 99,+ "commands": {+ "build": {+ "executable": "heni-fixture",+ "arguments": [+ "exit",+ "0"+ ]+ },+ "test": {+ "executable": "heni-fixture",+ "arguments": [+ "check",+ "input",+ "diagnostic"+ ]+ }+ },+ "cases": [+ {+ "id": "caught",+ "description": "Detect the intended fault",+ "edits": [+ {+ "kind": "replace",+ "path": "input",+ "old": "anchor",+ "new": "mutant"+ }+ ],+ "build": "build",+ "test": "test",+ "expect": {+ "kind": "diagnostic",+ "stage": "test",+ "exitCode": 1,+ "outputContains": "capacity rule violated"+ }+ }+ ]+}
+ test/corpus/invalid-workers.json view
@@ -0,0 +1,63 @@+{+ "schemaVersion": 1,+ "commands": {+ "build": {+ "executable": "heni-fixture",+ "arguments": [+ "exit",+ "0"+ ]+ },+ "test": {+ "executable": "heni-fixture",+ "arguments": [+ "check",+ "input",+ "diagnostic"+ ]+ }+ },+ "cases": [+ {+ "id": "caught",+ "description": "Detect the intended fault",+ "edits": [+ {+ "kind": "replace",+ "path": "input",+ "old": "anchor",+ "new": "mutant"+ }+ ],+ "build": "build",+ "test": "test",+ "expect": {+ "kind": "diagnostic",+ "stage": "test",+ "exitCode": 1,+ "outputContains": "capacity rule violated"+ },+ "resources": [+ "DB"+ ]+ }+ ],+ "resources": [+ {+ "name": "DB",+ "scope": "case",+ "identity": "fixture",+ "version": "v1",+ "epoch": "clean",+ "setup": "build",+ "reset": "build",+ "teardown": "build"+ }+ ],+ "execution": {+ "workers": 0,+ "baselineReuse": {+ "toolchain": "fixture/v1"+ }+ }+}
+ test/corpus/valid-minimal.json view
@@ -0,0 +1,42 @@+{+ "schemaVersion": 1,+ "commands": {+ "build": {+ "executable": "heni-fixture",+ "arguments": [+ "exit",+ "0"+ ]+ },+ "test": {+ "executable": "heni-fixture",+ "arguments": [+ "check",+ "input",+ "diagnostic"+ ]+ }+ },+ "cases": [+ {+ "id": "caught",+ "description": "Detect the intended fault",+ "edits": [+ {+ "kind": "replace",+ "path": "input",+ "old": "anchor",+ "new": "mutant"+ }+ ],+ "build": "build",+ "test": "test",+ "expect": {+ "kind": "diagnostic",+ "stage": "test",+ "exitCode": 1,+ "outputContains": "capacity rule violated"+ }+ }+ ]+}
+ test/corpus/valid-named.json view
@@ -0,0 +1,38 @@+{+ "schemaVersion": 1,+ "sourceRoot": ".",+ "commands": {+ "build": {+ "executable": "heni-hspec-fixture",+ "arguments": [+ "--heni-build"+ ]+ },+ "test": {+ "executable": "heni-hspec-fixture"+ }+ },+ "cases": [+ {+ "id": "named",+ "description": "Detect the intended assertion",+ "adapter": "heni-v1",+ "edits": [+ {+ "kind": "replace",+ "path": "input",+ "old": "anchor",+ "new": "intended"+ }+ ],+ "build": "build",+ "test": "test",+ "expect": {+ "kind": "named-tests",+ "failures": [+ "suite/intended"+ ]+ }+ }+ ]+}
+ test/corpus/valid-resources.json view
@@ -0,0 +1,63 @@+{+ "schemaVersion": 1,+ "commands": {+ "build": {+ "executable": "heni-fixture",+ "arguments": [+ "exit",+ "0"+ ]+ },+ "test": {+ "executable": "heni-fixture",+ "arguments": [+ "check",+ "input",+ "diagnostic"+ ]+ }+ },+ "cases": [+ {+ "id": "caught",+ "description": "Detect the intended fault",+ "edits": [+ {+ "kind": "replace",+ "path": "input",+ "old": "anchor",+ "new": "mutant"+ }+ ],+ "build": "build",+ "test": "test",+ "expect": {+ "kind": "diagnostic",+ "stage": "test",+ "exitCode": 1,+ "outputContains": "capacity rule violated"+ },+ "resources": [+ "DB"+ ]+ }+ ],+ "resources": [+ {+ "name": "DB",+ "scope": "case",+ "identity": "fixture",+ "version": "v1",+ "epoch": "clean",+ "setup": "build",+ "reset": "build",+ "teardown": "build"+ }+ ],+ "execution": {+ "workers": 2,+ "baselineReuse": {+ "toolchain": "fixture/v1"+ }+ }+}