seihou-core-0.4.0.0: test/Seihou/Engine/PreviewSpec.hs
module Seihou.Engine.PreviewSpec (tests) where
import Data.Map.Strict qualified as Map
import Data.Text qualified as T
import Seihou.Core.Types
import Seihou.Engine.Preview
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)
tests :: IO TestTree
tests = testSpec "Seihou.Engine.Preview" spec
modName :: ModuleName
modName = ModuleName "test-module"
modName2 :: ModuleName
modName2 = ModuleName "other-module"
-- | A DiffResult with all fields empty.
emptyDiff :: DiffResult
emptyDiff =
DiffResult
{ new = [],
modified = [],
unchanged = [],
conflicts = [],
orphaned = []
}
spec :: Spec
spec = do
describe "buildPreview" $ do
it "treats all file ops as FsNew when no DiffResult is provided" $ do
let ops =
[ WriteFileOp "README.md" "# Hello" Template,
WriteFileOp "src/Lib.hs" "module Lib" Template,
CopyFileOp "templates/LICENSE" "LICENSE"
]
result = buildPreview ops Nothing Map.empty
length result `shouldBe` 3
(result !! 0).previewStatus `shouldBe` FsNew
(result !! 1).previewStatus `shouldBe` FsNew
(result !! 2).previewStatus `shouldBe` FsNew
it "classifies a new file as FsNew" $ do
let ops = [WriteFileOp "README.md" "# Hello" Template]
diff = emptyDiff {new = [PlannedFile "README.md" modName "# Hello"]}
result = buildPreview ops (Just diff) Map.empty
length result `shouldBe` 1
(head result).previewStatus `shouldBe` FsNew
it "classifies a modified file as FsModified" $ do
let ops = [WriteFileOp "README.md" "# Updated" Template]
diff = emptyDiff {modified = [ModifiedFile "README.md" modName (SHA256 "old") "# Updated"]}
result = buildPreview ops (Just diff) Map.empty
length result `shouldBe` 1
(head result).previewStatus `shouldBe` FsModified
it "classifies an unchanged file as FsUnchanged" $ do
let ops = [WriteFileOp "README.md" "# Same" Template]
diff = emptyDiff {unchanged = ["README.md"]}
result = buildPreview ops (Just diff) Map.empty
length result `shouldBe` 1
(head result).previewStatus `shouldBe` FsUnchanged
it "classifies a conflicting file as FsConflict" $ do
let ops = [WriteFileOp "README.md" "# New" Template]
diff =
emptyDiff
{ conflicts =
[ ConflictFile
{ path = "README.md",
moduleName = modName,
manifestHash = SHA256 "man",
diskHash = SHA256 "disk",
planContent = "# New"
}
]
}
result = buildPreview ops (Just diff) Map.empty
length result `shouldBe` 1
(head result).previewStatus `shouldBe` FsConflict
it "classifies an orphaned file as FsOrphaned" $ do
let ops = [WriteFileOp "other.txt" "content" Template]
diff =
emptyDiff
{ new = [PlannedFile "other.txt" modName "content"],
orphaned = [OrphanedFile "old.txt" modName]
}
result = buildPreview ops (Just diff) Map.empty
-- One file preview + one orphan preview
length result `shouldBe` 2
let orphan = result !! 1
case orphan of
OrphanPreview path mn -> do
path `shouldBe` "old.txt"
mn `shouldBe` modName
_ -> expectationFailure "Expected OrphanPreview"
it "does not include orphaned files that are produced by an operation" $ do
let ops = [WriteFileOp "reused.txt" "content" Template]
diff =
emptyDiff
{ new = [PlannedFile "reused.txt" modName "content"],
orphaned = [OrphanedFile "reused.txt" modName2]
}
result = buildPreview ops (Just diff) Map.empty
-- Only the file preview, orphan is suppressed because path matches an operation
length result `shouldBe` 1
case head result of
FilePreview {} -> pure ()
_ -> expectationFailure "Expected FilePreview"
it "produces DirPreview for CreateDirOp" $ do
let ops = [CreateDirOp "src"]
result = buildPreview ops Nothing Map.empty
length result `shouldBe` 1
case head result of
DirPreview path -> path `shouldBe` "src"
_ -> expectationFailure "Expected DirPreview"
it "produces CommandPreview for RunCommandOp" $ do
let ops = [RunCommandOp "cabal build" Nothing]
result = buildPreview ops Nothing Map.empty
length result `shouldBe` 1
case head result of
CommandPreview cmd -> cmd `shouldBe` "cabal build"
_ -> expectationFailure "Expected CommandPreview"
it "maps PatchFileOp to FilePreview with patch annotation" $ do
let ops = [PatchFileOp "README.md" "extra content" AppendSection Template modName]
result = buildPreview ops Nothing Map.empty
length result `shouldBe` 1
case head result of
FilePreview status path annotation mMod -> do
status `shouldBe` FsNew
path `shouldBe` "README.md"
annotation `shouldBe` "patch"
mMod `shouldBe` Just modName
_ -> expectationFailure "Expected FilePreview"
it "maps CopyFileOp to FilePreview with copy annotation" $ do
let ops = [CopyFileOp "templates/LICENSE" "LICENSE"]
result = buildPreview ops Nothing Map.empty
length result `shouldBe` 1
case head result of
FilePreview _ path annotation _ -> do
path `shouldBe` "LICENSE"
annotation `shouldBe` "copy"
_ -> expectationFailure "Expected FilePreview"
it "includes correct strategy annotation for WriteFileOp" $ do
let ops =
[ WriteFileOp "a.txt" "" Copy,
WriteFileOp "b.txt" "" Template,
WriteFileOp "c.txt" "" DhallText,
WriteFileOp "d.txt" "" Structured
]
result = buildPreview ops Nothing Map.empty
map (.previewAnnotation) (filter isFilePreview result)
`shouldBe` ["copy", "template", "dhall-text", "structured"]
describe "renderPreviewPlain" $ do
it "renders empty list as 'No operations' message" $ do
renderPreviewPlain [] `shouldBe` "No operations to perform.\n"
it "renders FilePreview lines with status tags" $ do
let lines' =
[ FilePreview FsNew "README.md" "template" Nothing,
FilePreview FsModified "src/Lib.hs" "template" Nothing,
FilePreview FsUnchanged "LICENSE" "copy" Nothing,
FilePreview FsConflict "config.yml" "structured" Nothing,
FilePreview FsOrphaned "old.txt" "template" Nothing
]
rendered = renderPreviewPlain lines'
renderedLines = T.lines rendered
length renderedLines `shouldBe` 5
(renderedLines !! 0) `shouldBe` " [new] README.md (template)"
(renderedLines !! 1) `shouldBe` " [modified] src/Lib.hs (template)"
(renderedLines !! 2) `shouldBe` " [unchanged] LICENSE (copy)"
(renderedLines !! 3) `shouldBe` " [conflict] config.yml (structured)"
(renderedLines !! 4) `shouldBe` " [orphaned] old.txt (template)"
it "renders DirPreview, CommandPreview, and OrphanPreview" $ do
let lines' =
[ DirPreview "src",
CommandPreview "cabal build",
OrphanPreview "gone.txt" modName
]
rendered = renderPreviewPlain lines'
renderedLines = T.lines rendered
length renderedLines `shouldBe` 3
(renderedLines !! 0) `shouldBe` " mkdir src"
(renderedLines !! 1) `shouldBe` " run cabal build"
(renderedLines !! 2) `shouldBe` " [orphaned] gone.txt (orphaned from test-module)"
describe "formatPlanView" $ do
it "includes header with module names" $ do
let preview = [FilePreview FsNew "README.md" "template" (Just modName)]
diff = emptyDiff {new = [PlannedFile "README.md" modName "# Hello"]}
rendered = formatPlanView [modName] Map.empty preview diff
T.isInfixOf "Generation Plan (test-module):" rendered `shouldBe` True
it "includes header with multiple module names joined by +" $ do
let preview = []
diff = emptyDiff
rendered = formatPlanView [modName, modName2] Map.empty preview diff
T.isInfixOf "Generation Plan (test-module + other-module):" rendered `shouldBe` True
it "includes Variables section when variables are present" $ do
let vars = Map.fromList [(VarName "project.name", VText "hello")]
preview = []
diff = emptyDiff
rendered = formatPlanView [modName] vars preview diff
T.isInfixOf "Variables:" rendered `shouldBe` True
T.isInfixOf "project.name" rendered `shouldBe` True
T.isInfixOf "\"hello\"" rendered `shouldBe` True
it "omits Variables section when no variables" $ do
let rendered = formatPlanView [modName] Map.empty [] emptyDiff
T.isInfixOf "Variables:" rendered `shouldBe` False
it "includes summary with file and conflict counts" $ do
let diff =
emptyDiff
{ new = [PlannedFile "a.txt" modName ""],
modified = [ModifiedFile "b.txt" modName (SHA256 "old") "new"]
}
rendered = formatPlanView [modName] Map.empty [] diff
T.isInfixOf "2 files to write, 0 conflicts" rendered `shouldBe` True
-- | Helper to test if a PreviewLine is a FilePreview.
isFilePreview :: PreviewLine -> Bool
isFilePreview (FilePreview {}) = True
isFilePreview _ = False