packages feed

seihou-cli-0.7.0.0: test/Seihou/CLI/InstallCollisionSpec.hs

module Seihou.CLI.InstallCollisionSpec (tests) where

import Control.Lens ((^.))
import Data.Generics.Labels ()
import Data.Text qualified as T
import Seihou.CLI.InstallShared
  ( InstallCollision (..),
    InstallOutcome (..),
    OriginInfo (..),
    classifyInstallCollision,
    formatInstallRefusal,
    installModuleDirInto,
    readOriginInfo,
  )
import System.Directory (createDirectoryIfMissing, doesFileExist)
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)

tests :: IO TestTree
tests = testSpec "Seihou.CLI.InstallCollision" spec

repoOne :: T.Text
repoOne = "https://github.com/acme/one"

repoTwo :: T.Text
repoTwo = "https://github.com/acme/two"

-- | Lay out a directory that looks like an installed artifact: one content
-- file plus the provenance file @seihou install@ writes beside it. A
-- 'Nothing' source means "installed by hand, no provenance", which is a real
-- state a user's cache can be in.
seedInstalled :: FilePath -> Maybe T.Text -> Maybe T.Text -> IO ()
seedInstalled dir mSource mVersion = do
  createDirectoryIfMissing True dir
  writeFile (dir </> "marker.txt") "the copy that was already here"
  case mSource of
    Nothing -> pure ()
    Just source ->
      writeFile (dir </> ".seihou-origin.json") $
        "{\"sourceUrl\":"
          <> show (T.unpack source)
          <> ",\"repoName\":null,\"installedAt\":\"2026-08-16T00:00:00Z\",\"version\":"
          <> maybe "null" (show . T.unpack) mVersion
          <> ",\"tags\":[]}"

-- | The directory an install copies *from*.
seedIncoming :: FilePath -> IO ()
seedIncoming dir = do
  createDirectoryIfMissing True dir
  writeFile (dir </> "incoming.txt") "the copy being installed"

spec :: Spec
spec = do
  describe "classifyInstallCollision" $ do
    it "classifies an absent directory as NoExistingInstall" $
      withSystemTempDirectory "seihou-collision" $ \dir -> do
        collision <- classifyInstallCollision (dir </> "nothing-here") repoOne
        collision `shouldBe` NoExistingInstall

    it "classifies a matching source URL as SameSource, carrying the recorded version" $
      withSystemTempDirectory "seihou-collision" $ \dir -> do
        let installed = dir </> "shared-thing"
        seedInstalled installed (Just repoOne) (Just "0.4.0")
        collision <- classifyInstallCollision installed repoOne
        collision `shouldBe` SameSource (Just "0.4.0")

    -- Two spellings of one git URL are one repository. A user who typed the
    -- .git suffix last week and omitted it today must not be told they have a
    -- different artifact.
    it "treats a trailing .git as the same source" $
      withSystemTempDirectory "seihou-collision" $ \dir -> do
        let installed = dir </> "shared-thing"
        seedInstalled installed (Just (repoOne <> ".git")) Nothing
        collision <- classifyInstallCollision installed repoOne
        collision `shouldBe` SameSource Nothing

    it "treats a trailing slash as the same source" $
      withSystemTempDirectory "seihou-collision" $ \dir -> do
        let installed = dir </> "shared-thing"
        seedInstalled installed (Just (repoOne <> "/")) Nothing
        collision <- classifyInstallCollision installed repoOne
        collision `shouldBe` SameSource Nothing

    it "classifies a different source URL as DifferentSource, carrying the recorded URL" $
      withSystemTempDirectory "seihou-collision" $ \dir -> do
        let installed = dir </> "shared-thing"
        seedInstalled installed (Just repoOne) Nothing
        collision <- classifyInstallCollision installed repoTwo
        collision `shouldBe` DifferentSource repoOne

    it "classifies a directory with no provenance file as UnknownSource" $
      withSystemTempDirectory "seihou-collision" $ \dir -> do
        let installed = dir </> "handmade"
        seedInstalled installed Nothing Nothing
        collision <- classifyInstallCollision installed repoOne
        collision `shouldBe` UnknownSource

    it "classifies an unparseable provenance file as UnknownSource" $
      withSystemTempDirectory "seihou-collision" $ \dir -> do
        let installed = dir </> "corrupt"
        seedInstalled installed Nothing Nothing
        writeFile (installed </> ".seihou-origin.json") "{ this is not valid json"
        collision <- classifyInstallCollision installed repoOne
        collision `shouldBe` UnknownSource

  describe "installModuleDirInto" $ do
    it "installs into an empty cache without comment" $
      withSystemTempDirectory "seihou-collision" $ \dir -> do
        let root = dir </> "installed"
            incoming = dir </> "incoming"
        seedIncoming incoming
        outcome <- installModuleDirInto root False incoming "shared-thing" repoOne Nothing (Just "1.0.0") []
        outcome `shouldBe` InstallPerformed
        doesFileExist (root </> "shared-thing" </> "incoming.txt") `shouldReturn` True
        recorded <- readOriginInfo (root </> "shared-thing")
        fmap (^. #sourceUrl) recorded `shouldBe` Just repoOne

    it "replaces an installation from the same source" $
      withSystemTempDirectory "seihou-collision" $ \dir -> do
        let root = dir </> "installed"
            incoming = dir </> "incoming"
        seedInstalled (root </> "shared-thing") (Just repoOne) (Just "0.4.0")
        seedIncoming incoming
        outcome <- installModuleDirInto root False incoming "shared-thing" repoOne Nothing (Just "0.5.0") []
        outcome `shouldBe` InstallPerformed
        doesFileExist (root </> "shared-thing" </> "incoming.txt") `shouldReturn` True
        doesFileExist (root </> "shared-thing" </> "marker.txt") `shouldReturn` False

    -- The property that matters: the refusal happens before anything is
    -- removed, so a refused install leaves the cache exactly as it was. The
    -- marker file is the evidence.
    it "refuses a different source and leaves the existing installation untouched" $
      withSystemTempDirectory "seihou-collision" $ \dir -> do
        let root = dir </> "installed"
            incoming = dir </> "incoming"
        seedInstalled (root </> "shared-thing") (Just repoOne) Nothing
        seedIncoming incoming
        outcome <- installModuleDirInto root False incoming "shared-thing" repoTwo Nothing Nothing []
        outcome `shouldBe` InstallRefused (DifferentSource repoOne)
        readFile (root </> "shared-thing" </> "marker.txt")
          `shouldReturn` "the copy that was already here"
        doesFileExist (root </> "shared-thing" </> "incoming.txt") `shouldReturn` False
        recorded <- readOriginInfo (root </> "shared-thing")
        fmap (^. #sourceUrl) recorded `shouldBe` Just repoOne

    it "refuses an installation with no provenance and leaves it untouched" $
      withSystemTempDirectory "seihou-collision" $ \dir -> do
        let root = dir </> "installed"
            incoming = dir </> "incoming"
        seedInstalled (root </> "handmade") Nothing Nothing
        seedIncoming incoming
        outcome <- installModuleDirInto root False incoming "handmade" repoOne Nothing Nothing []
        outcome `shouldBe` InstallRefused UnknownSource
        readFile (root </> "handmade" </> "marker.txt")
          `shouldReturn` "the copy that was already here"

    it "replaces a different source when force is passed" $
      withSystemTempDirectory "seihou-collision" $ \dir -> do
        let root = dir </> "installed"
            incoming = dir </> "incoming"
        seedInstalled (root </> "shared-thing") (Just repoOne) Nothing
        seedIncoming incoming
        outcome <- installModuleDirInto root True incoming "shared-thing" repoTwo Nothing Nothing []
        outcome `shouldBe` InstallPerformed
        doesFileExist (root </> "shared-thing" </> "incoming.txt") `shouldReturn` True
        doesFileExist (root </> "shared-thing" </> "marker.txt") `shouldReturn` False
        recorded <- readOriginInfo (root </> "shared-thing")
        fmap (^. #sourceUrl) recorded `shouldBe` Just repoTwo

  describe "formatInstallRefusal" $ do
    it "names both sources and the override flag" $ do
      let rendered = formatInstallRefusal "shared-thing" repoTwo (DifferentSource repoOne)
      rendered `shouldSatisfy` T.isInfixOf repoOne
      rendered `shouldSatisfy` T.isInfixOf repoTwo
      rendered `shouldSatisfy` T.isInfixOf "--force"
      rendered `shouldSatisfy` T.isInfixOf "shared-thing"

    it "says provenance is missing rather than naming a recorded URL" $ do
      let rendered = formatInstallRefusal "handmade" repoOne UnknownSource
      rendered `shouldSatisfy` T.isInfixOf "no .seihou-origin.json"
      rendered `shouldSatisfy` T.isInfixOf "--force"

    it "has nothing to say about a case that is not refused" $ do
      formatInstallRefusal "shared-thing" repoOne NoExistingInstall `shouldBe` ""
      formatInstallRefusal "shared-thing" repoOne (SameSource (Just "1.0.0")) `shouldBe` ""