packages feed

seihou-core-0.4.0.0: test/Seihou/Core/RegistrySyncSpec.hs

module Seihou.Core.RegistrySyncSpec (tests) where

import Data.Maybe (isJust, mapMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Seihou.Core.Registry
  ( EntryKind (..),
    Registry (..),
    RegistryEntry (..),
    SyncDiff (..),
    SyncReport (..),
    SyncStatus (..),
    computeRegistrySync,
    formatDriftWarning,
  )
import Seihou.Core.Types (ModuleName (..))
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)

tests :: IO TestTree
tests = testSpec "Seihou.Core.RegistrySync" spec

spec :: Spec
spec = describe "computeRegistrySync" $ do
  it "classifies SyncMissing when registry version is Nothing and disk has a version" $ do
    let entry = mkEntry "alpha" Nothing
        reg = mkReg [entry] []
        report = computeRegistrySync reg [(ModuleEntry, ModuleName "alpha", Just "1.0.0")]
    map (.diffStatus) report.syncDiffs `shouldBe` [SyncMissing]
    map (.diffNew) report.syncDiffs `shouldBe` [Just "1.0.0"]
    (head report.syncUpdated.modules).version `shouldBe` Just "1.0.0"

  it "classifies SyncStale when registry and disk versions differ" $ do
    let entry = mkEntry "alpha" (Just "0.1.0")
        reg = mkReg [entry] []
        report = computeRegistrySync reg [(ModuleEntry, ModuleName "alpha", Just "1.0.0")]
    map (.diffStatus) report.syncDiffs `shouldBe` [SyncStale "1.0.0"]
    map (.diffOld) report.syncDiffs `shouldBe` [Just "0.1.0"]
    map (.diffNew) report.syncDiffs `shouldBe` [Just "1.0.0"]
    (head report.syncUpdated.modules).version `shouldBe` Just "1.0.0"

  it "classifies SyncInSync when registry and disk versions match" $ do
    let entry = mkEntry "alpha" (Just "1.0.0")
        reg = mkReg [entry] []
        report = computeRegistrySync reg [(ModuleEntry, ModuleName "alpha", Just "1.0.0")]
    map (.diffStatus) report.syncDiffs `shouldBe` [SyncInSync]
    (head report.syncUpdated.modules).version `shouldBe` Just "1.0.0"

  it "classifies SyncInSync when registry and disk are both Nothing" $ do
    let entry = mkEntry "alpha" Nothing
        reg = mkReg [entry] []
        report = computeRegistrySync reg [(ModuleEntry, ModuleName "alpha", Nothing)]
    map (.diffStatus) report.syncDiffs `shouldBe` [SyncInSync]
    (head report.syncUpdated.modules).version `shouldBe` Nothing

  it "classifies SyncOrphan when the entry has no lookup (module.dhall absent/unreadable)" $ do
    let entry = mkEntry "alpha" (Just "1.0.0")
        reg = mkReg [entry] []
        report = computeRegistrySync reg []
    map (.diffStatus) report.syncDiffs `shouldBe` [SyncOrphan]
    -- Orphan: version left as-is
    (head report.syncUpdated.modules).version `shouldBe` Just "1.0.0"

  it "preserves registry order in the diff output" $ do
    let reg =
          ( mkReg
              [ mkEntry "alpha" Nothing,
                mkEntry "beta" (Just "0.1.0"),
                mkEntry "gamma" (Just "2.0.0")
              ]
              [mkEntry "lib-one" Nothing]
          )
            { prompts = [mkEntry "review" Nothing]
            }
        lookups =
          [ (ModuleEntry, ModuleName "alpha", Just "1.0.0"),
            (ModuleEntry, ModuleName "beta", Just "0.2.0"),
            (ModuleEntry, ModuleName "gamma", Just "2.0.0"),
            (RecipeEntry, ModuleName "lib-one", Just "0.3.0"),
            (PromptEntry, ModuleName "review", Just "0.4.0")
          ]
        report = computeRegistrySync reg lookups
    map (.diffName) report.syncDiffs
      `shouldBe` [ ModuleName "alpha",
                   ModuleName "beta",
                   ModuleName "gamma",
                   ModuleName "lib-one",
                   ModuleName "review"
                 ]
    map (.diffKind) report.syncDiffs
      `shouldBe` [ModuleEntry, ModuleEntry, ModuleEntry, RecipeEntry, PromptEntry]
    map (.diffStatus) report.syncDiffs
      `shouldBe` [SyncMissing, SyncStale "0.2.0", SyncInSync, SyncMissing, SyncMissing]

  it "returns an empty report for an empty registry" $ do
    let reg = mkReg [] []
        report = computeRegistrySync reg []
    report.syncDiffs `shouldBe` []
    report.syncUpdated `shouldBe` reg

  it "distinguishes module and recipe entries with the same name in lookups" $ do
    -- Module and recipe namespaces share a validation check,
    -- but the sync lookup must distinguish them by kind.
    let reg =
          mkReg
            [mkEntry "alpha" Nothing]
            [mkEntry "beta" Nothing]
        lookups =
          [ (ModuleEntry, ModuleName "alpha", Just "1.0.0"),
            (RecipeEntry, ModuleName "beta", Just "2.0.0")
          ]
        report = computeRegistrySync reg lookups
    map (.diffNew) report.syncDiffs `shouldBe` [Just "1.0.0", Just "2.0.0"]

  describe "formatDriftWarning" $ do
    it "produces a warning for a stale entry" $ do
      let reg = mkReg [mkEntry "alpha" (Just "0.1.0")] []
          lookups = [(ModuleEntry, ModuleName "alpha", Just "1.0.0")]
          report = computeRegistrySync reg lookups
          warnings = mapMaybe formatDriftWarning report.syncDiffs
      length warnings `shouldBe` 1
      isJust (formatDriftWarning (head report.syncDiffs)) `shouldBe` True

    it "produces no warnings when all entries are in sync" $ do
      let reg = mkReg [mkEntry "alpha" (Just "1.0.0")] []
          lookups = [(ModuleEntry, ModuleName "alpha", Just "1.0.0")]
          report = computeRegistrySync reg lookups
          warnings = mapMaybe formatDriftWarning report.syncDiffs
      warnings `shouldBe` []

    it "produces no warnings for orphan entries (handled by validateRegistry)" $ do
      let reg = mkReg [mkEntry "alpha" (Just "1.0.0")] []
          report = computeRegistrySync reg []
          warnings = mapMaybe formatDriftWarning report.syncDiffs
      warnings `shouldBe` []

    it "mentions prompt.dhall in stale prompt warnings" $ do
      let reg = (mkReg [] []) {prompts = [mkEntry "review" (Just "0.1.0")]}
          lookups = [(PromptEntry, ModuleName "review", Just "0.2.0")]
          report = computeRegistrySync reg lookups
          warnings = mapMaybe formatDriftWarning report.syncDiffs
      warnings
        `shouldBe` [ "prompt 'review' registry version 0.1.0 differs from prompt.dhall version 0.2.0 — run `seihou registry sync-versions`"
                   ]

mkEntry :: Text -> Maybe Text -> RegistryEntry
mkEntry n v =
  RegistryEntry
    { name = ModuleName n,
      version = v,
      path = "modules/" <> T.unpack n,
      description = Nothing,
      tags = []
    }

mkReg :: [RegistryEntry] -> [RegistryEntry] -> Registry
mkReg mods recs =
  Registry
    { repoName = "Test",
      repoDescription = Nothing,
      modules = mods,
      recipes = recs,
      blueprints = [],
      prompts = []
    }