packages feed

seihou-core-0.5.0.0: test/Seihou/Engine/ThreeWayMergeSpec.hs

module Seihou.Engine.ThreeWayMergeSpec (tests) where

import Control.Monad (unless)
import Data.Maybe (isJust)
import Data.Text qualified as T
import Seihou.Engine.ThreeWayMerge
import System.Directory (findExecutable)
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)

tests :: IO TestTree
tests = testSpec "Seihou.Engine.ThreeWayMerge" spec

spec :: Spec
spec = do
  describe "pure safety cases" $ do
    it "takes newly generated content when current still equals baseline" $
      threeWayMerge "base" "base" "new" `shouldReturn` MergeClean "new"

    it "preserves current content when generation did not change" $
      threeWayMerge "base" "user" "base" `shouldReturn` MergeClean "user"

    it "returns identical current and generated content" $
      threeWayMerge "base" "same" "same" `shouldReturn` MergeClean "same"

    it "handles empty content through an identity without invoking Git" $
      threeWayMerge "" "" "generated" `shouldReturn` MergeClean "generated"

    it "rejects NUL-bearing binary content even when both sides match" $ do
      result <- threeWayMerge "a\NULb" "a\NULb" "a\NULb"
      result `shouldSatisfy` isUnavailable

    it "returns unavailable when the Git executable is missing" $ do
      result <- threeWayMergeWithGit "/definitely/missing/seihou-git" "base" "user" "generated"
      result `shouldSatisfy` isUnavailable

    it "returns unavailable for a fatal driver result without markers" $ do
      result <- threeWayMergeWithGit "/usr/bin/false" "base" "user" "generated"
      result `shouldSatisfy` isUnavailable

  describe "git merge-file driver" $ do
    it "merges non-overlapping user and module insertions" $ withGit $ do
      let baseline = "alpha\nshared\nomega\n"
          current = "alpha\nuser\nshared\nomega\n"
          generated = "alpha\nshared\nmodule\nomega\n"
      result <- threeWayMerge baseline current generated
      case result of
        MergeClean merged -> do
          merged `shouldSatisfy` T.isInfixOf "user"
          merged `shouldSatisfy` T.isInfixOf "module"
        other -> expectationFailure ("expected clean merge, got " <> show other)

    it "returns labeled diff3 markers for overlapping replacements" $ withGit $ do
      result <- threeWayMerge "alpha\nshared\nomega\n" "alpha\nuser\nomega\n" "alpha\nmodule\nomega\n"
      case result of
        MergeConflicted merged -> do
          merged `shouldSatisfy` T.isInfixOf "<<<<<<< current"
          merged `shouldSatisfy` T.isInfixOf "||||||| generated-base"
          merged `shouldSatisfy` T.isInfixOf ">>>>>>> new-generated"
        other -> expectationFailure ("expected conflict, got " <> show other)

    it "preserves a user-only deletion alongside a generated insertion" $ withGit $ do
      result <-
        threeWayMerge
          "alpha\nremove-me\nomega\n"
          "alpha\nomega\n"
          "alpha\nremove-me\nomega\nmodule\n"
      case result of
        MergeClean merged -> do
          merged `shouldNotSatisfy` T.isInfixOf "remove-me"
          merged `shouldSatisfy` T.isInfixOf "module"
        other -> expectationFailure ("expected clean merge, got " <> show other)

    it "preserves a generated deletion alongside a user insertion" $ withGit $ do
      result <-
        threeWayMerge
          "alpha\nuser-anchor\nmiddle\nremove-me\nomega\n"
          "alpha\nuser\nuser-anchor\nmiddle\nremove-me\nomega\n"
          "alpha\nuser-anchor\nmiddle\nomega\n"
      case result of
        MergeClean merged -> do
          merged `shouldNotSatisfy` T.isInfixOf "remove-me"
          merged `shouldSatisfy` T.isInfixOf "user"
        other -> expectationFailure ("expected clean merge, got " <> show other)

    it "handles missing trailing newlines" $ withGit $ do
      result <- threeWayMerge "alpha\nmiddle\nomega" "user-alpha\nmiddle\nomega" "alpha\nmiddle\nmodule-omega"
      case result of
        MergeClean merged -> do
          merged `shouldSatisfy` T.isInfixOf "user-alpha"
          merged `shouldSatisfy` T.isInfixOf "module-omega"
        other -> expectationFailure ("expected clean merge, got " <> show other)

    it "round-trips Unicode changes from both sides" $ withGit $ do
      result <-
        threeWayMerge
          "こんにちは\n共有\n終わり\n"
          "こんにちは\n利用者\n共有\n終わり\n"
          "こんにちは\n共有\nモジュール\n終わり\n"
      case result of
        MergeClean merged -> do
          merged `shouldSatisfy` T.isInfixOf "利用者"
          merged `shouldSatisfy` T.isInfixOf "モジュール"
        other -> expectationFailure ("expected clean merge, got " <> show other)

isUnavailable :: MergeOutcome -> Bool
isUnavailable (MergeUnavailable message) = not (T.null message)
isUnavailable _ = False

withGit :: Expectation -> Expectation
withGit action = do
  available <- gitAvailable
  unless available (pendingWith "git is not available")
  action

gitAvailable :: IO Bool
gitAvailable = isJust <$> findExecutable "git"