seihou-core-0.15.0.0: test/Seihou/Engine/RemoveSpec.hs
module Seihou.Engine.RemoveSpec (tests) where
import Control.Lens ((%~), (&), (.~), (^.))
import Data.Generics.Labels ()
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Time (UTCTime, defaultTimeLocale, parseTimeOrError)
import Effectful
import Seihou.Core.Types
import Seihou.Effect.FilesystemPure (PureFS (..), emptyFS, runFilesystemPure)
import Seihou.Engine.Remove
import Seihou.Manifest.Hash (hashContent)
import Seihou.Manifest.Types (emptyManifest)
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)
tests :: IO TestTree
tests = testSpec "Seihou.Engine.Remove" spec
fixedTime :: UTCTime
fixedTime = parseTimeOrError True defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ" "2026-03-01T10:30:00Z"
removeTime :: UTCTime
removeTime = parseTimeOrError True defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ" "2026-03-02T09:00:00Z"
modName :: ModuleName
modName = ModuleName "test-module"
otherMod :: ModuleName
otherMod = ModuleName "other-module"
-- | Helper: create a manifest with one applied module and some files.
mkManifest :: Bool -> [(FilePath, Text)] -> Manifest
mkManifest isRemovable fileContents =
(emptyManifest fixedTime)
& #modules .~ [AppliedModule {name = modName, parentVars = emptyParentVars, origin = LocalOrigin "test-module", moduleVersion = Nothing, appliedAt = fixedTime, removal = if isRemovable then Just (Removal [] []) else Nothing}]
& #files .~ Map.fromList [(path, FileRecord {hash = hashContent content, moduleName = modName, strategy = Template, generatedAt = fixedTime, baseline = Nothing, applicationIds = mempty, sharedWriteMode = SharedWriteUnknown}) | (path, content) <- fileContents]
-- | Helper: create a manifest with a specific removal spec.
mkManifestWithRemoval :: Removal -> [(FilePath, Text)] -> Manifest
mkManifestWithRemoval removal fileContents =
(emptyManifest fixedTime)
& #modules .~ [AppliedModule {name = modName, parentVars = emptyParentVars, origin = LocalOrigin "test-module", moduleVersion = Nothing, appliedAt = fixedTime, removal = Just removal}]
& #files .~ Map.fromList [(path, FileRecord {hash = hashContent content, moduleName = modName, strategy = Template, generatedAt = fixedTime, baseline = Nothing, applicationIds = mempty, sharedWriteMode = SharedWriteUnknown}) | (path, content) <- fileContents]
-- | Helper: create a PureFS with files.
mkFS :: [(FilePath, Text)] -> PureFS
mkFS fileContents = PureFS (Map.fromList fileContents) Set.empty Map.empty
-- | Run computeRemovalPlan in the pure filesystem.
runPlan :: PureFS -> Manifest -> ModuleName -> Either RemovalError RemovalPlan
runPlan fs manifest name =
fst $ runPureEff $ runFilesystemPure fs $ computeRemovalPlan manifest name
-- | Run executeRemoval in the pure filesystem.
runExec :: PureFS -> Manifest -> RemovalPlan -> Set.Set FilePath -> (Manifest, PureFS)
runExec fs manifest plan keepSet =
let (result, finalFS) =
runPureEff $
runFilesystemPure fs $
executeRemoval manifest plan keepSet removeTime
in (result, finalFS)
-- | Run buildRemovalOps in the pure filesystem.
runBuildOps :: PureFS -> Manifest -> ModuleName -> Removal -> Either RemovalError ExecutedRemovalPlan
runBuildOps fs manifest name removal =
fst $ runPureEff $ runFilesystemPure fs $ buildRemovalOps manifest name removal
-- | Run executeRemovalOps in the pure filesystem.
runExecOps :: PureFS -> Manifest -> ExecutedRemovalPlan -> Set.Set FilePath -> (Manifest, PureFS)
runExecOps fs manifest plan keepSet =
let (result, finalFS) =
runPureEff $
runFilesystemPure fs $
executeRemovalOps manifest plan keepSet removeTime
in (result, finalFS)
spec :: Spec
spec = do
describe "seed preservation and exact selection" $ do
let appId = ApplicationId "a"
app = AppliedComposition appId (AppliedModuleTarget otherMod) (LocalOrigin "test-module") Nothing [] Nothing Nothing [AppliedInstanceState name emptyParentVars (LocalOrigin text) Nothing Map.empty Nothing | (name, text) <- [(modName, "test-module"), (otherMod, "other-module")]] Map.empty [] Nothing Nothing fixedTime
receipt = SeedRecord modName (KnownSeedOwners (Map.singleton appId (Set.fromList [modName, otherMod]))) SeedCreated fixedTime
manifest = mkManifest False [] & #applications .~ [app] & #seeds .~ Map.singleton "seed.txt" receipt
it "retires a no-spec instance without losing a co-seeder or original supplier" $ do
Right (Just selection) <- pure (selectRecordedRemoval manifest modName Nothing)
let candidate = selection ^. #candidateManifest
fmap (^. #ownership) (Map.lookup "seed.txt" (candidate ^. #seeds)) `shouldBe` Just (KnownSeedOwners (Map.singleton appId (Set.singleton otherMod)))
fmap (^. #moduleName) (Map.lookup "seed.txt" (candidate ^. #seeds)) `shouldBe` Just modName
it "keeps every declarative removal form without needing to read seed content" $ do
let removal = Removal [RemovalStep action "seed.txt" (Just "rewrite.dhall") Nothing | action <- [RemoveFileAction, RemoveSectionAction, RewriteFileAction]] []
Right plan <- pure (runBuildOps emptyFS manifest modName removal)
plan ^. #ops `shouldBe` replicate 3 (KeepSeedOp "seed.txt")
it "defends direct supplied operations, including path aliases, at execution too" $ do
let fs = mkFS [("seed.txt", "project bytes")]
plan = ExecutedRemovalPlan modName [DeleteFileOp "seed.txt" RFSafe, StripSectionOp "./seed.txt", RewriteOp "seed.txt" "rewrite.dhall"]
(_, after) = runExecOps fs manifest plan Set.empty
after ^. #files `shouldBe` fs ^. #files
it "refuses unresolved legacy subtraction rather than guessing from supplier" $ do
let legacy = manifest & #seeds .~ Map.singleton "seed.txt" (receipt & #ownership .~ LegacySeedOwners (Set.singleton appId))
selectRecordedRemoval legacy modName Nothing `shouldSatisfy` either (const True) (const False)
describe "recorded root closure and conditional removal" $ do
let appId = ApplicationId "root"
variables = Map.singleton (VarName "base.starters") "false"
rootApp = AppliedComposition appId (AppliedModuleTarget modName) (LocalOrigin "root") Nothing [] Nothing Nothing [AppliedInstanceState name emptyParentVars (LocalOrigin "fixture") Nothing variables (Just (if name == otherMod then settleRemovalConditions (Map.singleton (VarName "base.starters") (VBool False)) baseSpec else rootSpec)) | name <- [otherMod, modName]] Map.empty [] Nothing Nothing fixedTime
baseSpec = Removal [RemovalStep RemoveFileAction "base.txt" Nothing Nothing, RemovalStep RemoveFileAction "package.json" Nothing (Just (ExprEq (VarName "base.starters") (VBool True)))] []
rootSpec = Removal [RemovalStep RemoveFileAction "client.txt" Nothing Nothing] []
applied name spec = AppliedModule name emptyParentVars (LocalOrigin "fixture") Nothing fixedTime (Just spec)
file name owners = FileRecord (hashContent "bytes") name Template fixedTime Nothing owners SharedWriteUnknown
manifest =
(emptyManifest fixedTime)
& #applications .~ [rootApp]
& #modules .~ [applied otherMod (settleRemovalConditions (Map.singleton (VarName "base.starters") (VBool False)) baseSpec), applied modName rootSpec]
& #files .~ Map.fromList [("base.txt", file otherMod (Set.singleton appId)), ("client.txt", file modName (Set.singleton appId))]
& #seeds .~ Map.singleton "package.json" (SeedRecord modName (KnownSeedOwners (Map.singleton appId (Set.singleton modName))) SeedCreated fixedTime)
it "retires an unshared inherited closure and uses recorded per-instance conditions" $ do
Right (Just selected) <- pure (selectRecordedRemoval manifest modName Nothing)
selected ^. #candidateManifest . #applications `shouldBe` []
selected ^. #candidateManifest . #modules `shouldBe` []
selected ^. #candidateManifest . #files `shouldBe` Map.empty
fmap (map (^. #dest) . (^. #steps)) (selected ^. #removal) `shouldBe` Just ["client.txt", "base.txt"]
it "uses application dependency order when shared module summaries were reordered" $ do
Right (Just selected) <- pure (selectRecordedRemoval (manifest & #modules %~ reverse) modName Nothing)
fmap (map (^. #dest) . (^. #steps)) (selected ^. #removal) `shouldBe` Just ["client.txt", "base.txt"]
it "selects a recorded managed root even without seed or checkout receipts" $ do
Right (Just selected) <- pure (selectRecordedRemoval (manifest & #seeds .~ Map.empty) modName Nothing)
selected ^. #candidateManifest . #applications `shouldBe` []
selected ^. #candidateManifest . #modules `shouldBe` []
it "keeps an inherited module claimed by another application" $ do
let otherId = ApplicationId "survivor"
survivor = rootApp & #applicationId .~ otherId & #target .~ AppliedModuleTarget otherMod & #instances %~ filter ((== otherMod) . (^. #name))
shared = manifest & #applications %~ (<> [survivor]) & #files %~ Map.adjust (\record -> record & #applicationIds %~ Set.insert otherId) "base.txt"
Right (Just selected) <- pure (selectRecordedRemoval shared modName Nothing)
fmap (map (^. #dest) . (^. #steps)) (selected ^. #removal) `shouldBe` Just ["client.txt"]
map (^. #name) (selected ^. #candidateManifest . #modules) `shouldBe` [otherMod]
Map.keys (selected ^. #candidateManifest . #files) `shouldBe` ["base.txt"]
it "uses the departing application's receipt even when a shared module summary differs" $ do
let laterSummary = manifest & #modules %~ map (\entry -> if entry ^. #name == otherMod then entry & #removal .~ Just (settleRemovalConditions (Map.singleton (VarName "base.starters") (VBool True)) baseSpec) else entry)
Right (Just selected) <- pure (selectRecordedRemoval laterSummary modName Nothing)
fmap (map (^. #dest) . (^. #steps)) (selected ^. #removal) `shouldBe` Just ["client.txt", "base.txt"]
it "requires ordinary adoption when a conditional legacy application has no receipt" $ do
let unknown = manifest & #applications %~ map (\app -> app & #instances %~ map (\state -> state & #removalPolicy .~ Nothing))
selectRecordedRemoval unknown modName Nothing `shouldSatisfy` either (const True) (const False)
it "preserves type-sensitive conditions and refuses unresolved policy" $ do
let fs = mkFS [("package.json", "bytes"), ("base.txt", "bytes")]
legacy = mkManifestWithRemoval baseSpec [("package.json", "bytes"), ("base.txt", "bytes")]
settle value = settleRemovalConditions (Map.singleton (VarName "base.starters") value) baseSpec
Right yes <- pure (runBuildOps fs legacy modName (settle (VBool True)))
Right no <- pure (runBuildOps fs legacy modName (settle (VBool False)))
Right text <- pure (runBuildOps fs legacy modName (settle (VText "true")))
length (yes ^. #ops) `shouldBe` 2
length (no ^. #ops) `shouldBe` 1
length (text ^. #ops) `shouldBe` 1
runBuildOps fs legacy modName baseSpec `shouldSatisfy` either (const True) (const False)
describe "computeRemovalPlan" $ do
it "returns ModuleNotApplied when module is not in manifest" $ do
let manifest = emptyManifest fixedTime
result = runPlan emptyFS manifest modName
result `shouldBe` Left (ModuleNotApplied modName)
it "returns ModuleNotRemovable when removal is Nothing" $ do
let manifest = mkManifest False [("README.md", "hello")]
fs = mkFS [("README.md", "hello")]
result = runPlan fs manifest modName
result `shouldBe` Left (ModuleNotRemovable modName)
it "classifies unchanged files as RemovalSafe" $ do
let manifest = mkManifest True [("README.md", "hello")]
fs = mkFS [("README.md", "hello")]
result = runPlan fs manifest modName
case result of
Left err -> expectationFailure ("unexpected error: " <> show err)
Right plan -> (plan ^. #files) `shouldBe` [RemovalSafe "README.md"]
it "classifies user-modified files as RemovalConflict" $ do
let manifest = mkManifest True [("README.md", "original")]
fs = mkFS [("README.md", "user changed this")]
result = runPlan fs manifest modName
case result of
Left err -> expectationFailure ("unexpected error: " <> show err)
Right plan -> (plan ^. #files) `shouldBe` [RemovalConflict "README.md"]
it "classifies deleted files as RemovalGone" $ do
let manifest = mkManifest True [("README.md", "hello")]
result = runPlan emptyFS manifest modName
case result of
Left err -> expectationFailure ("unexpected error: " <> show err)
Right plan -> (plan ^. #files) `shouldBe` [RemovalGone "README.md"]
it "handles mix of safe, conflict, and gone files" $ do
let manifest = mkManifest True [("a.txt", "aaa"), ("b.txt", "bbb"), ("c.txt", "ccc")]
fs = mkFS [("a.txt", "aaa"), ("b.txt", "modified")]
result = runPlan fs manifest modName
case result of
Left err -> expectationFailure ("unexpected error: " <> show err)
Right plan -> do
length (plan ^. #files) `shouldBe` 3
RemovalSafe "a.txt" `elem` (plan ^. #files) `shouldBe` True
RemovalConflict "b.txt" `elem` (plan ^. #files) `shouldBe` True
RemovalGone "c.txt" `elem` (plan ^. #files) `shouldBe` True
describe "executeRemoval" $ do
it "deletes safe files from the filesystem" $ do
let manifest = mkManifest True [("README.md", "hello")]
fs = mkFS [("README.md", "hello")]
plan = RemovalPlan {targetModule = modName, files = [RemovalSafe "README.md"]}
(_, finalFS) = runExec fs manifest plan Set.empty
Map.member "README.md" (finalFS ^. #files) `shouldBe` False
it "preserves files in the keep-set" $ do
let manifest = mkManifest True [("a.txt", "aaa")]
fs = mkFS [("a.txt", "modified")]
plan = RemovalPlan {targetModule = modName, files = [RemovalConflict "a.txt"]}
keepSet = Set.singleton "a.txt"
(_, finalFS) = runExec fs manifest plan keepSet
Map.member "a.txt" (finalFS ^. #files) `shouldBe` True
it "removes the module from manifest.modules" $ do
let manifest = mkManifest True [("README.md", "hello")]
fs = mkFS [("README.md", "hello")]
plan = RemovalPlan {targetModule = modName, files = [RemovalSafe "README.md"]}
(updated, _) = runExec fs manifest plan Set.empty
(updated ^. #modules) `shouldBe` []
it "removes module's files from manifest.files" $ do
let manifest = mkManifest True [("a.txt", "aaa"), ("b.txt", "bbb")]
fs = mkFS [("a.txt", "aaa"), ("b.txt", "bbb")]
plan = RemovalPlan {targetModule = modName, files = [RemovalSafe "a.txt", RemovalSafe "b.txt"]}
(updated, _) = runExec fs manifest plan Set.empty
Map.null (updated ^. #files) `shouldBe` True
it "preserves files from other modules in manifest" $ do
let base = mkManifest True [("mine.txt", "mine")]
otherRec = FileRecord (hashContent "other") otherMod Template fixedTime Nothing mempty SharedWriteUnknown
manifest =
Manifest
{ version = base ^. #version,
genAt = base ^. #genAt,
modules = base ^. #modules,
vars = base ^. #vars,
files = Map.insert "other.txt" otherRec (base ^. #files),
applications = base ^. #applications,
recipe = base ^. #recipe,
blueprint = base ^. #blueprint,
seeds = base ^. #seeds,
checkoutOutputs = base ^. #checkoutOutputs,
blueprintMigrations = base ^. #blueprintMigrations
}
fs = mkFS [("mine.txt", "mine"), ("other.txt", "other")]
plan = RemovalPlan {targetModule = modName, files = [RemovalSafe "mine.txt"]}
(updated, _) = runExec fs manifest plan Set.empty
Map.member "other.txt" (updated ^. #files) `shouldBe` True
Map.member "mine.txt" (updated ^. #files) `shouldBe` False
it "updates genAt timestamp in manifest" $ do
let manifest = mkManifest True [("a.txt", "aaa")]
fs = mkFS [("a.txt", "aaa")]
plan = RemovalPlan {targetModule = modName, files = [RemovalSafe "a.txt"]}
(updated, _) = runExec fs manifest plan Set.empty
(updated ^. #genAt) `shouldBe` removeTime
it "full round-trip: manifest returns to clean state after removal" $ do
let manifest = mkManifest True [("a.txt", "aaa"), ("b.txt", "bbb")]
fs = mkFS [("a.txt", "aaa"), ("b.txt", "bbb")]
plan = RemovalPlan {targetModule = modName, files = [RemovalSafe "a.txt", RemovalSafe "b.txt"]}
(updated, finalFS) = runExec fs manifest plan Set.empty
(updated ^. #modules) `shouldBe` []
Map.null (updated ^. #files) `shouldBe` True
Map.member "a.txt" (finalFS ^. #files) `shouldBe` False
Map.member "b.txt" (finalFS ^. #files) `shouldBe` False
describe "buildRemovalOps" $ do
it "returns ModuleNotApplied when module is not in manifest" $ do
let manifest = emptyManifest fixedTime
removal = Removal [] []
result = runBuildOps emptyFS manifest modName removal
result `shouldBe` Left (ModuleNotApplied modName)
it "builds DeleteFileOp with RFSafe for unchanged files" $ do
let removal = Removal [RemovalStep RemoveFileAction "README.md" Nothing Nothing] []
manifest = mkManifestWithRemoval removal [("README.md", "hello")]
fs = mkFS [("README.md", "hello")]
result = runBuildOps fs manifest modName removal
case result of
Left err -> expectationFailure ("unexpected error: " <> show err)
Right plan -> (plan ^. #ops) `shouldBe` [DeleteFileOp "README.md" RFSafe]
it "builds DeleteFileOp with RFConflict for modified files" $ do
let removal = Removal [RemovalStep RemoveFileAction "README.md" Nothing Nothing] []
manifest = mkManifestWithRemoval removal [("README.md", "original")]
fs = mkFS [("README.md", "user changed this")]
result = runBuildOps fs manifest modName removal
case result of
Left err -> expectationFailure ("unexpected error: " <> show err)
Right plan -> (plan ^. #ops) `shouldBe` [DeleteFileOp "README.md" RFConflict]
it "builds DeleteFileOp with RFGone for already-deleted files" $ do
let removal = Removal [RemovalStep RemoveFileAction "README.md" Nothing Nothing] []
manifest = mkManifestWithRemoval removal [("README.md", "hello")]
result = runBuildOps emptyFS manifest modName removal
case result of
Left err -> expectationFailure ("unexpected error: " <> show err)
Right plan -> (plan ^. #ops) `shouldBe` [DeleteFileOp "README.md" RFGone]
it "builds StripSectionOp for remove-section steps" $ do
let removal = Removal [RemovalStep RemoveSectionAction ".gitignore" Nothing Nothing] []
manifest = mkManifestWithRemoval removal [(".gitignore", "content")]
fs = mkFS [(".gitignore", "content")]
result = runBuildOps fs manifest modName removal
case result of
Left err -> expectationFailure ("unexpected error: " <> show err)
Right plan -> (plan ^. #ops) `shouldBe` [StripSectionOp ".gitignore"]
it "builds RemovalCommandOp for removal commands" $ do
let removal = Removal [] [Command "cabal clean" Nothing Nothing ApplicationCommand]
manifest = mkManifestWithRemoval removal []
result = runBuildOps emptyFS manifest modName removal
case result of
Left err -> expectationFailure ("unexpected error: " <> show err)
Right plan -> (plan ^. #ops) `shouldBe` [RemovalCommandOp "cabal clean" Nothing]
it "rejects a removal step destination with a parent directory segment" $ do
let removal = Removal [RemovalStep RemoveFileAction "../outside" Nothing Nothing] []
manifest = mkManifestWithRemoval removal []
fs = mkFS [("../outside", "do not delete")]
result = runBuildOps fs manifest modName removal
result
`shouldBe` Left
( RemovalUnsafePath
"remove-file destination"
"../outside"
"path must not contain '..' segment: ../outside"
)
Map.member "../outside" (fs ^. #files) `shouldBe` True
it "rejects a removal command workDir with a parent directory segment" $ do
let removal = Removal [] [Command "echo unsafe" (Just "../outside") Nothing ApplicationCommand]
manifest = mkManifestWithRemoval removal []
result = runBuildOps emptyFS manifest modName removal
result
`shouldBe` Left
( RemovalUnsafePath
"remove-command workDir"
"../outside"
"path must not contain '..' segment: ../outside"
)
it "combines steps and commands in order" $ do
let removal =
Removal
[RemovalStep RemoveFileAction "a.txt" Nothing Nothing, RemovalStep RemoveSectionAction ".gitignore" Nothing Nothing]
[Command "echo done" Nothing Nothing ApplicationCommand]
manifest = mkManifestWithRemoval removal [("a.txt", "aaa")]
fs = mkFS [("a.txt", "aaa"), (".gitignore", "stuff")]
result = runBuildOps fs manifest modName removal
case result of
Left err -> expectationFailure ("unexpected error: " <> show err)
Right plan -> do
length (plan ^. #ops) `shouldBe` 3
case plan ^. #ops of
[DeleteFileOp _ _, StripSectionOp _, RemovalCommandOp _ _] -> pure ()
other -> expectationFailure ("unexpected ops: " <> show other)
describe "executeRemovalOps" $ do
it "deletes files with DeleteFileOp" $ do
let removal = Removal [RemovalStep RemoveFileAction "README.md" Nothing Nothing] []
manifest = mkManifestWithRemoval removal [("README.md", "hello")]
fs = mkFS [("README.md", "hello")]
plan = ExecutedRemovalPlan modName [DeleteFileOp "README.md" RFSafe]
(_, finalFS) = runExecOps fs manifest plan Set.empty
Map.member "README.md" (finalFS ^. #files) `shouldBe` False
it "preserves files in keep-set for DeleteFileOp" $ do
let removal = Removal [RemovalStep RemoveFileAction "a.txt" Nothing Nothing] []
manifest = mkManifestWithRemoval removal [("a.txt", "aaa")]
fs = mkFS [("a.txt", "modified")]
plan = ExecutedRemovalPlan modName [DeleteFileOp "a.txt" RFConflict]
keepSet = Set.singleton "a.txt"
(_, finalFS) = runExecOps fs manifest plan keepSet
Map.member "a.txt" (finalFS ^. #files) `shouldBe` True
it "skips gone files" $ do
let manifest = mkManifest True [("a.txt", "aaa")]
plan = ExecutedRemovalPlan modName [DeleteFileOp "a.txt" RFGone]
(updated, _) = runExecOps emptyFS manifest plan Set.empty
(updated ^. #modules) `shouldBe` []
it "strips section from file with StripSectionOp" $ do
let content = "before\n# --- seihou:test-module ---\nmodule content\n# --- /seihou:test-module ---\nafter\n"
manifest = mkManifest True [(".gitignore", content)]
fs = mkFS [(".gitignore", content)]
plan = ExecutedRemovalPlan modName [StripSectionOp ".gitignore"]
(_, finalFS) = runExecOps fs manifest plan Set.empty
case Map.lookup ".gitignore" (finalFS ^. #files) of
Nothing -> expectationFailure ".gitignore should still exist"
Just result -> do
result `shouldSatisfy` \t ->
"before" `elem` lines (show t) || not ("seihou:test-module" `elem` lines (show t))
it "leaves file unchanged when no section markers found" $ do
let content = "no markers here\n"
manifest = mkManifest True [("file.txt", content)]
fs = mkFS [("file.txt", content)]
plan = ExecutedRemovalPlan modName [StripSectionOp "file.txt"]
(_, finalFS) = runExecOps fs manifest plan Set.empty
Map.lookup "file.txt" (finalFS ^. #files) `shouldBe` Just content
it "removes module from manifest after all steps" $ do
let removal = Removal [RemovalStep RemoveFileAction "a.txt" Nothing Nothing] []
manifest = mkManifestWithRemoval removal [("a.txt", "aaa")]
fs = mkFS [("a.txt", "aaa")]
plan = ExecutedRemovalPlan modName [DeleteFileOp "a.txt" RFSafe]
(updated, _) = runExecOps fs manifest plan Set.empty
(updated ^. #modules) `shouldBe` []
Map.null (updated ^. #files) `shouldBe` True
it "preserves other modules' files in manifest" $ do
let base = mkManifest True [("mine.txt", "mine")]
otherRec = FileRecord (hashContent "other") otherMod Template fixedTime Nothing mempty SharedWriteUnknown
manifest =
Manifest
{ version = base ^. #version,
genAt = base ^. #genAt,
modules = base ^. #modules,
vars = base ^. #vars,
files = Map.insert "other.txt" otherRec (base ^. #files),
applications = base ^. #applications,
recipe = base ^. #recipe,
blueprint = base ^. #blueprint,
seeds = base ^. #seeds,
checkoutOutputs = base ^. #checkoutOutputs,
blueprintMigrations = base ^. #blueprintMigrations
}
fs = mkFS [("mine.txt", "mine"), ("other.txt", "other")]
plan = ExecutedRemovalPlan modName [DeleteFileOp "mine.txt" RFSafe]
(updated, _) = runExecOps fs manifest plan Set.empty
Map.member "other.txt" (updated ^. #files) `shouldBe` True
Map.member "mine.txt" (updated ^. #files) `shouldBe` False