seihou-core-0.4.0.0: test/Seihou/Engine/ConflictSpec.hs
module Seihou.Engine.ConflictSpec (tests) where
import Data.Text qualified as T
import Effectful
import Seihou.Core.Types
import Seihou.Effect.ConsolePure (ConsoleState (..), runConsolePure, runConsolePureNonInteractive)
import Seihou.Engine.Conflict (resolveConflicts, resolveConflictsInteractive)
import Seihou.Manifest.Hash (hashContent)
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)
tests :: IO TestTree
tests = testSpec "Seihou.Engine.Conflict" spec
-- | Helper to make a ConflictFile with realistic hashes.
mkConflict :: FilePath -> ConflictFile
mkConflict path =
ConflictFile
{ path = path,
moduleName = ModuleName "test-module",
manifestHash = hashContent "original content",
diskHash = hashContent "user edited content",
planContent = "new generated content"
}
spec :: Spec
spec = do
describe "resolveConflicts" $ do
it "returns Just [] for empty conflict list" $ do
let (result, _st) = runPureEff $ runConsolePure [] $ resolveConflicts False []
result `shouldBe` Just []
it "returns Just [] for empty conflict list with force" $ do
let (result, _st) = runPureEff $ runConsolePure [] $ resolveConflicts True []
result `shouldBe` Just []
it "returns all AcceptNew when force is True" $ do
let conflicts = [mkConflict "a.txt", mkConflict "b.txt", mkConflict "c.txt"]
(result, _st) = runPureEff $ runConsolePure [] $ resolveConflicts True conflicts
case result of
Just resolved -> do
length resolved `shouldBe` 3
all (\(_, r) -> r == AcceptNew) resolved `shouldBe` True
Nothing -> expectationFailure "Expected Just, got Nothing"
it "returns Nothing in non-interactive mode without force" $ do
let conflicts = [mkConflict "a.txt"]
(result, _st) = runPureEff $ runConsolePureNonInteractive $ resolveConflicts False conflicts
result `shouldBe` Nothing
it "does not produce console output when force is True" $ do
let conflicts = [mkConflict "a.txt"]
(_result, st) = runPureEff $ runConsolePure [] $ resolveConflicts True conflicts
st.consoleOutputs `shouldBe` []
describe "resolveConflictsInteractive" $ do
it "resolves accept with 'a'" $ do
let conflict = mkConflict "readme.md"
(result, _st) = runPureEff $ runConsolePure ["a"] $ resolveConflictsInteractive [conflict]
case result of
Just [(_, res)] -> res `shouldBe` AcceptNew
_ -> expectationFailure "Expected Just with one AcceptNew resolution"
it "resolves keep with 'k'" $ do
let conflict = mkConflict "readme.md"
(result, _st) = runPureEff $ runConsolePure ["k"] $ resolveConflictsInteractive [conflict]
case result of
Just [(_, res)] -> res `shouldBe` KeepCurrent
_ -> expectationFailure "Expected Just with one KeepCurrent resolution"
it "resolves skip with 's'" $ do
let conflict = mkConflict "readme.md"
(result, _st) = runPureEff $ runConsolePure ["s"] $ resolveConflictsInteractive [conflict]
case result of
Just [(_, res)] -> res `shouldBe` Skip
_ -> expectationFailure "Expected Just with one Skip resolution"
it "returns Nothing on abort with 'A'" $ do
let conflicts = [mkConflict "a.txt", mkConflict "b.txt"]
(result, _st) = runPureEff $ runConsolePure ["A"] $ resolveConflictsInteractive conflicts
result `shouldBe` Nothing
it "re-prompts on invalid input then accepts valid input" $ do
let conflict = mkConflict "readme.md"
(result, st) = runPureEff $ runConsolePure ["x", "a"] $ resolveConflictsInteractive [conflict]
case result of
Just [(_, res)] -> res `shouldBe` AcceptNew
_ -> expectationFailure "Expected Just with one AcceptNew resolution"
any (T.isInfixOf "Invalid choice") (st.consoleOutputs) `shouldBe` True
it "resolves multiple files in order" $ do
let conflicts = [mkConflict "a.txt", mkConflict "b.txt", mkConflict "c.txt"]
(result, _st) = runPureEff $ runConsolePure ["a", "k", "s"] $ resolveConflictsInteractive conflicts
case result of
Just resolved -> do
length resolved `shouldBe` 3
map snd resolved `shouldBe` [AcceptNew, KeepCurrent, Skip]
map ((.path) . fst) resolved `shouldBe` ["a.txt", "b.txt", "c.txt"]
Nothing -> expectationFailure "Expected Just, got Nothing"
it "abort on second file stops prompting" $ do
let conflicts = [mkConflict "a.txt", mkConflict "b.txt", mkConflict "c.txt"]
(result, st) = runPureEff $ runConsolePure ["a", "A"] $ resolveConflictsInteractive conflicts
result `shouldBe` Nothing
-- Should have prompted for a.txt and b.txt, but not c.txt
let outputs = T.unlines (st.consoleOutputs)
T.isInfixOf "a.txt" outputs `shouldBe` True
T.isInfixOf "b.txt" outputs `shouldBe` True
T.isInfixOf "c.txt" outputs `shouldBe` False
it "outputs file paths in prompt messages" $ do
let conflict = mkConflict "src/Main.hs"
(_result, st) = runPureEff $ runConsolePure ["a"] $ resolveConflictsInteractive [conflict]
outputs = T.unlines (st.consoleOutputs)
T.isInfixOf "src/Main.hs" outputs `shouldBe` True
T.isInfixOf "modified since last generation" outputs `shouldBe` True
it "accepts full word inputs" $ do
let conflicts = [mkConflict "a.txt", mkConflict "b.txt", mkConflict "c.txt", mkConflict "d.txt"]
(result, _st) = runPureEff $ runConsolePure ["accept", "keep", "skip", "abort"] $ resolveConflictsInteractive conflicts
-- abort on d.txt → Nothing
result `shouldBe` Nothing
describe "resolveConflicts integration" $ do
it "dispatches to interactive prompts when interactive and not force" $ do
let conflicts = [mkConflict "config.yaml", mkConflict "Makefile"]
(result, st) = runPureEff $ runConsolePure ["k", "a"] $ resolveConflicts False conflicts
case result of
Just resolved -> do
map snd resolved `shouldBe` [KeepCurrent, AcceptNew]
Nothing -> expectationFailure "Expected Just, got Nothing"
-- Verify prompt output was produced
let outputs = T.unlines (st.consoleOutputs)
T.isInfixOf "config.yaml" outputs `shouldBe` True
T.isInfixOf "Makefile" outputs `shouldBe` True
it "non-interactive mode produces no console output" $ do
let conflicts = [mkConflict "a.txt"]
(_result, st) = runPureEff $ runConsolePureNonInteractive $ resolveConflicts False conflicts
st.consoleOutputs `shouldBe` []
it "force mode preserves conflict file references in resolution" $ do
let c = mkConflict "important.txt"
(result, _st) = runPureEff $ runConsolePure [] $ resolveConflicts True [c]
case result of
Just [(resolved_c, AcceptNew)] ->
resolved_c.path `shouldBe` "important.txt"
_ -> expectationFailure "Expected Just with AcceptNew for important.txt"
it "interactive abort via resolveConflicts returns Nothing" $ do
let conflicts = [mkConflict "first.txt", mkConflict "second.txt"]
(result, st) = runPureEff $ runConsolePure ["A"] $ resolveConflicts False conflicts
result `shouldBe` Nothing
-- Only first.txt was prompted before abort
let outputs = T.unlines (st.consoleOutputs)
T.isInfixOf "first.txt" outputs `shouldBe` True
T.isInfixOf "second.txt" outputs `shouldBe` False
it "choice prompt text includes all four options" $ do
let conflict = mkConflict "test.txt"
(_result, st) = runPureEff $ runConsolePure ["s"] $ resolveConflicts False [conflict]
outputs = T.unlines (st.consoleOutputs)
T.isInfixOf "[a]ccept" outputs `shouldBe` True
T.isInfixOf "[k]eep" outputs `shouldBe` True
T.isInfixOf "[s]kip" outputs `shouldBe` True
T.isInfixOf "[A]bort" outputs `shouldBe` True