packages feed

seihou-core-0.4.0.0: test/Seihou/Engine/ExecuteSpec.hs

module Seihou.Engine.ExecuteSpec (tests) where

import Data.Map.Strict qualified as Map
import Data.Text qualified as T
import Data.Time (UTCTime, defaultTimeLocale, parseTimeOrError)
import Effectful
import Seihou.Core.Types
import Seihou.Effect.Filesystem (doesDirectoryExist, doesFileExist, readFileText)
import Seihou.Effect.FilesystemPure (PureFS (..), emptyFS, runFilesystemPure)
import Seihou.Engine.Execute (dryRunPlan, executePlan)
import Seihou.Manifest.Hash (hashContent)
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)

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

fixedTime :: UTCTime
fixedTime = parseTimeOrError True defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ" "2026-03-01T10:30:00Z"

modName :: ModuleName
modName = ModuleName "test-module"

-- | Run execution in pure filesystem and return results + final FS state.
runExec :: [Operation] -> ((Map.Map FilePath FileRecord, PureFS), PureFS)
runExec ops =
  runPureEff $
    runFilesystemPure emptyFS $ do
      records <- executePlan "/project" ops Map.empty modName fixedTime
      fs <- pure () -- just to get the final state via the tuple
      pure (records, emptyFS) -- placeholder; actual FS state is in the outer tuple

-- | More useful helper: run and return records + the pure FS state.
runExecFS :: PureFS -> [Operation] -> (Map.Map FilePath FileRecord, PureFS)
runExecFS initial ops =
  runPureEff $
    runFilesystemPure initial $
      executePlan "/project" ops Map.empty modName fixedTime

spec :: Spec
spec = do
  describe "executePlan" $ do
    it "writes a file via WriteFileOp" $ do
      let ops = [WriteFileOp "hello.txt" "hello world" Template]
          (records, fs) = runExecFS emptyFS ops
      Map.member "hello.txt" records `shouldBe` True
      Map.lookup "/project/hello.txt" (fs.files) `shouldBe` Just "hello world"

    it "creates a directory via CreateDirOp" $ do
      let ops = [CreateDirOp "src"]
          ((exists, _), _) =
            runPureEff $
              runFilesystemPure emptyFS $ do
                _ <- executePlan "/project" ops Map.empty modName fixedTime
                e <- doesDirectoryExist "/project/src"
                pure (e, ())
      exists `shouldBe` True

    it "produces FileRecord with correct hash" $ do
      let content = "test content"
          ops = [WriteFileOp "test.txt" content Template]
          (records, _) = runExecFS emptyFS ops
          record = records Map.! "test.txt"
      record.hash `shouldBe` hashContent content

    it "produces FileRecord with correct module name" $ do
      let ops = [WriteFileOp "test.txt" "data" Template]
          (records, _) = runExecFS emptyFS ops
          record = records Map.! "test.txt"
      record.moduleName `shouldBe` modName

    it "produces FileRecord with correct timestamp" $ do
      let ops = [WriteFileOp "test.txt" "data" Template]
          (records, _) = runExecFS emptyFS ops
          record = records Map.! "test.txt"
      record.generatedAt `shouldBe` fixedTime

    it "handles multiple operations" $ do
      let ops =
            [ CreateDirOp "src",
              WriteFileOp "README.md" "# Hello" Template,
              WriteFileOp "src/Main.hs" "module Main where" Template
            ]
          (records, fs) = runExecFS emptyFS ops
      Map.size records `shouldBe` 2
      Map.lookup "/project/README.md" (fs.files) `shouldBe` Just "# Hello"
      Map.lookup "/project/src/Main.hs" (fs.files) `shouldBe` Just "module Main where"

    it "skips RunCommandOp" $ do
      let ops = [RunCommandOp "echo hello" Nothing]
          (records, _) = runExecFS emptyFS ops
      Map.size records `shouldBe` 0

    it "handles CopyFileOp by reading source and writing dest" $ do
      let initial = PureFS (Map.singleton "/source/file.txt" "copied content") mempty
          ops = [CopyFileOp "/source/file.txt" "dest.txt"]
          (records, fs) = runExecFS initial ops
      Map.member "dest.txt" records `shouldBe` True
      Map.lookup "/project/dest.txt" (fs.files) `shouldBe` Just "copied content"

    it "records Template strategy in FileRecord" $ do
      let ops = [WriteFileOp "test.txt" "content" Template]
          (records, _) = runExecFS emptyFS ops
          record = records Map.! "test.txt"
      record.strategy `shouldBe` Template

    it "records Copy strategy in FileRecord" $ do
      let ops = [WriteFileOp "test.txt" "content" Copy]
          (records, _) = runExecFS emptyFS ops
          record = records Map.! "test.txt"
      record.strategy `shouldBe` Copy

    it "records DhallText strategy in FileRecord" $ do
      let ops = [WriteFileOp "test.txt" "content" DhallText]
          (records, _) = runExecFS emptyFS ops
          record = records Map.! "test.txt"
      record.strategy `shouldBe` DhallText

    it "records Structured strategy in FileRecord" $ do
      let ops = [WriteFileOp "test.json" "{}" Structured]
          (records, _) = runExecFS emptyFS ops
          record = records Map.! "test.json"
      record.strategy `shouldBe` Structured

    it "executes PatchFileOp AppendFile on existing file" $ do
      let initial = PureFS (Map.singleton "/project/README.md" "# Title\n") mempty
          ops = [PatchFileOp "README.md" "extra line\n" AppendFile Template modName]
          (records, fs) = runExecFS initial ops
      Map.member "README.md" records `shouldBe` True
      let content = fs.files Map.! "/project/README.md"
      T.isInfixOf "# Title" content `shouldBe` True
      T.isInfixOf "extra line" content `shouldBe` True

    it "executes PatchFileOp PrependFile on existing file" $ do
      let initial = PureFS (Map.singleton "/project/README.md" "# Title\n") mempty
          ops = [PatchFileOp "README.md" "header\n" PrependFile Template modName]
          (records, fs) = runExecFS initial ops
      Map.member "README.md" records `shouldBe` True
      let content = fs.files Map.! "/project/README.md"
      T.isInfixOf "header" content `shouldBe` True
      T.isInfixOf "# Title" content `shouldBe` True

    it "executes PatchFileOp AppendSection with section markers" $ do
      let initial = PureFS (Map.singleton "/project/README.md" "# Title\n") mempty
          ops = [PatchFileOp "README.md" "section content\n" AppendSection Template modName]
          (records, fs) = runExecFS initial ops
      Map.member "README.md" records `shouldBe` True
      let content = fs.files Map.! "/project/README.md"
      T.isInfixOf "# Title" content `shouldBe` True
      T.isInfixOf "seihou:test-module" content `shouldBe` True
      T.isInfixOf "section content" content `shouldBe` True

    it "executes PatchFileOp on nonexistent file (creates from empty)" $ do
      let ops = [PatchFileOp "new.txt" "new content\n" AppendFile Template modName]
          (records, fs) = runExecFS emptyFS ops
      Map.member "new.txt" records `shouldBe` True
      let content = fs.files Map.! "/project/new.txt"
      T.isInfixOf "new content" content `shouldBe` True

    it "executes PatchFileOp AppendLineIfAbsent, skipping existing lines" $ do
      let initial = PureFS (Map.singleton "/project/.gitignore" "node_modules/\n.env\n") mempty
          ops = [PatchFileOp ".gitignore" ".env\n.claude/\n" AppendLineIfAbsent Template modName]
          (records, fs) = runExecFS initial ops
      Map.member ".gitignore" records `shouldBe` True
      let content = fs.files Map.! "/project/.gitignore"
      content `shouldBe` "node_modules/\n.env\n.claude/\n"

    it "executes PatchFileOp AppendLineIfAbsent idempotently" $ do
      let initial = PureFS (Map.singleton "/project/.gitignore" "node_modules/\n.claude/\n") mempty
          ops = [PatchFileOp ".gitignore" ".claude/\n" AppendLineIfAbsent Template modName]
          (records, fs) = runExecFS initial ops
      Map.member ".gitignore" records `shouldBe` True
      let content = fs.files Map.! "/project/.gitignore"
      content `shouldBe` "node_modules/\n.claude/\n"

  describe "dryRunPlan" $ do
    it "formats WriteFileOp" $ do
      let result = dryRunPlan [WriteFileOp "README.md" "content" Template]
      T.isInfixOf "write" result `shouldBe` True
      T.isInfixOf "README.md" result `shouldBe` True

    it "formats CreateDirOp" $ do
      let result = dryRunPlan [CreateDirOp "src"]
      T.isInfixOf "mkdir" result `shouldBe` True
      T.isInfixOf "src" result `shouldBe` True

    it "formats CopyFileOp" $ do
      let result = dryRunPlan [CopyFileOp "src.txt" "dest.txt"]
      T.isInfixOf "copy" result `shouldBe` True

    it "formats RunCommandOp" $ do
      let result = dryRunPlan [RunCommandOp "echo hello" Nothing]
      T.isInfixOf "run" result `shouldBe` True

    it "returns message for empty plan" $ do
      let result = dryRunPlan []
      T.isInfixOf "No operations" result `shouldBe` True

    it "formats PatchFileOp" $ do
      let result = dryRunPlan [PatchFileOp "README.md" "content" AppendSection Template (ModuleName "nix-flake")]
      T.isInfixOf "patch" result `shouldBe` True
      T.isInfixOf "README.md" result `shouldBe` True
      T.isInfixOf "nix-flake" result `shouldBe` True

    it "does not include file content" $ do
      let result = dryRunPlan [WriteFileOp "secret.txt" "super secret" Template]
      T.isInfixOf "super secret" result `shouldBe` False