seihou-cli 0.14.0.0 → 0.15.0.0
raw patch · 24 files changed
+152/−45 lines, 24 filesdep ~seihou-corePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: seihou-core
API changes (from Hackage documentation)
+ Seihou.CLI.ManifestCapabilityUpgrade: prepareRemovalPolicies :: Manifest -> Either Text Manifest
Files
- data/module-reference.md +9/−0
- data/setup-prompt.md +2/−1
- help/manifest.md +18/−1
- help/modules.md +9/−0
- help/update.md +6/−1
- help/workflow.md +3/−1
- seihou-cli.cabal +4/−4
- src-exe/Seihou/CLI/Commands.hs +3/−1
- src-exe/Seihou/CLI/Remove.hs +11/−3
- src-exe/Seihou/CLI/Run.hs +5/−3
- src/Seihou/CLI/ManifestCapabilityUpgrade.hs +10/−1
- src/Seihou/CLI/SchemaVersion.hs +2/−2
- src/Seihou/CLI/Update.hs +5/−3
- src/Seihou/CLI/Update/Render.hs +6/−0
- src/Seihou/CLI/Update/Types.hs +1/−0
- test/Seihou/CLI/ManifestCapabilityUpgradeSpec.hs +2/−2
- test/Seihou/CLI/ManifestRepairOriginsSpec.hs +1/−1
- test/Seihou/CLI/ManifestUpgradeSpec.hs +8/−8
- test/Seihou/CLI/RecipeExtensionE2ESpec.hs +1/−1
- test/Seihou/CLI/SeedRunE2ESpec.hs +23/−0
- test/Seihou/CLI/StatusSpec.hs +2/−1
- test/Seihou/CLI/UpdateE2ESpec.hs +4/−4
- test/Seihou/CLI/UpdateSpec.hs +14/−4
- test/Seihou/CLI/UpgradeDiagnosisSpec.hs +3/−3
data/module-reference.md view
@@ -220,3 +220,12 @@ Recipe inheritance producers require Seihou 0.14.0.0 or newer. Update the runtime before adopting descriptors that use `extends` or inherited overrides.++## Conditional removal in composed modules++`RemovalStep.when` uses the generation expression grammar and is evaluated against+the owning instance's recorded resolved variables. A base suppressing starter+outputs must apply the same condition to their cleanup steps. Omitted conditions+remain unconditional. Require Seihou 0.15.0.0+ before publishing these descriptors.+A recorded single-module root removes its unshared inherited instances in the same+operation; shared qualified instances remain owned by surviving applications.
data/setup-prompt.md view
@@ -123,9 +123,10 @@ - `-c CTX`: override context for config lookup - `--no-commands`: skip shell command steps - `seihou remove MODULE [--dry-run] [--force]` — remove an applied module by executing its declared removal steps+ - A recorded single-module root also retires its unshared inherited modules; other applications retain shared instances. - Managed-file removal needs a `removal` section; recorded seed-only/checkout-only owners can be removed without a custom spec. Seed retirement keeps project files. Removal stops future setup and does not undo arbitrary shell effects. - `--dry-run`: preview the removal plan (Delete, Strip, Rewrite, Run, and protected Keep operations)- - `--force`: skip confirmation prompts+ - `--force`: accept conflicted managed-file deletions; seeds remain protected - `seihou agent run BLUEPRINT [PROMPT] [--var K=V] [--no-baseline]` — run a blueprint - Discovers the blueprint, resolves variables (same precedence chain as `seihou run`), optionally applies base modules, then starts the configured provider - `--no-baseline`: skip applying declared base modules
help/manifest.md view
@@ -65,8 +65,9 @@ 10 -> 11 precise seed ownership claims 11 -> 12 recipe inputs 12 -> 13 unknown recipe-chain history+ 13 -> 14 conditional removal policies and recorded root retirement - ✓ Upgraded .seihou/manifest.json to schema version 13.+ ✓ Upgraded .seihou/manifest.json to schema version 14. Review the diff and commit it: git diff .seihou/manifest.json Running it on a manifest that is already current reports that there is@@ -164,3 +165,19 @@ the old manifest. Metadata-only base changes need acceptance, and unchanged application command receipts remain valid. Use the normal recipe update to adopt unknown history; no manual manifest editing is required.++CONDITIONAL REMOVAL POLICIES++ Schema 14 records the conditional-removal/root-retirement boundary. Omitted+ removal conditions remain unconditional. Each application instance records+ removalPolicy with typed Boolean condition results at accepted run/update.+ The adjacent upgrade records null for unknown historical policies, changes no+ project files, and invents no result. A schema-13 document carrying policy+ evidence is rejected. Ordinary run/update/removal prepare this+ lossless boundary in memory; previews do not publish it. A conditional legacy+ application without accepted policy evidence needs its ordinary update first.++ A recorded single-module root retires unshared inherited modules in the same+ removal. Other applications keep shared qualified instances. Explicit+ additional-root compositions and recipe applications retain exact-instance+ selection. See seihou help modules.
help/modules.md view
@@ -127,3 +127,12 @@ working directories real and project-relative. Ordinary receipts never suppress checkout commands. `--no-commands` suppresses both lifecycles while retaining setup; a true no-op update executes neither. Removal accepts application commands only.++Removing a recorded application with one module root also retires its unshared+inherited modules in the same operation. Other applications keep shared qualified+instances. Seeds remain protected throughout inherited cleanup. Explicit compositions+with additional roots and recipe applications keep exact-instance selection.++Removal steps may declare `when`, using the generation expression grammar. It is+evaluated against that instance's saved resolved values. Older omitted conditions+remain unconditional. This policy requires the schema-14 runtime.
help/update.md view
@@ -93,9 +93,14 @@ Recipe applications also reuse recorded recipe inputs before member variables. Inputs used only by conditions survive fresh clones. Ordinary preview prepares- schema 13 as needed and adopts agreeing historical values; conflicting or newly+ schema 14 as needed and adopts agreeing historical values; conflicting or newly required choices need --var KEY=VALUE or --reconfigure. Recipe-only changes are recorded even when no generated file changes.++ Accepted update also records each instance's removal policy with typed Boolean+ condition results. Policy-only adoption appears in the preview and is a real+ update; repeating it with matching policies is a no-op. No manual manifest+ edits are needed. MIGRATIONS
help/workflow.md view
@@ -58,7 +58,9 @@ seihou remove NAME Managed-file removal follows the module's removal declaration and protects- conflicts. Recorded seed-only/checkout-only owners need no custom spec; seed+ conflicts. Removing a recorded single-module root also retires its unshared+ inherited modules; other applications keep shared instances. Recorded+ seed-only/checkout-only owners need no custom spec; seed retirement preserves files. Status/diff count seeds without content comparisons. Review changes and commit portable manifest/config, real source directories and ignore rules. Accepted link adoption can stage tracked-link removals.
seihou-cli.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: seihou-cli-version: 0.14.0.0+version: 0.15.0.0 synopsis: CLI for Seihou project scaffolding description: Command-line interface for Seihou, a composable project scaffolding@@ -144,7 +144,7 @@ generic-lens >=2.2 && <3, lens >=5.2 && <6, process >=1.6 && <2,- seihou-core ^>=0.14.0.0,+ seihou-core ^>=0.15.0.0, streamly-core >=0.3 && <0.5, temporary >=1.3 && <2, text >=2.0 && <3,@@ -233,7 +233,7 @@ optparse-applicative >=0.18 && <1, process >=1.6 && <2, seihou-cli-internal,- seihou-core ^>=0.14.0.0,+ seihou-core ^>=0.15.0.0, temporary >=1.3 && <2, text >=2.0 && <3, time >=1.12 && <2,@@ -330,7 +330,7 @@ lens >=5.2 && <6, process >=1.6 && <2, seihou-cli-internal,- seihou-core ^>=0.14.0.0,+ seihou-core ^>=0.15.0.0, streamly-core >=0.3 && <0.5, tasty >=1.4 && <2, tasty-hspec >=1.2 && <2,
src-exe/Seihou/CLI/Commands.hs view
@@ -653,7 +653,9 @@ ( Just $ vsep [ pretty ("Removes a module that was previously applied via 'seihou run' and" :: String),- pretty ("reverses its declared effects. Managed files require a removal" :: String),+ pretty ("reverses its declared effects. A recorded single-module root also" :: String),+ pretty ("retires its unshared inherited modules. Shared instances remain." :: String),+ pretty ("Managed files require a removal" :: String), pretty ("specification. Seed-only ownership can be retired without one; seed" :: String), pretty ("files and their deliberate deletion are preserved, even with --force." :: String), line,
src-exe/Seihou/CLI/Remove.hs view
@@ -13,7 +13,7 @@ import Data.Time.Clock (getCurrentTime) import Seihou.CLI.CheckoutSync (renderCheckoutPlan) import Seihou.CLI.Commands (RemoveOpts (..))-import Seihou.CLI.ManifestCapabilityUpgrade (prepareRecordedSeedOwnership)+import Seihou.CLI.ManifestCapabilityUpgrade (prepareRecordedSeedOwnership, prepareRemovalPolicies) import Seihou.CLI.Style (bold, dim, green, useColor, yellow) import Seihou.Composition.Instance (ModuleInstance (..), qualifiedName) import Seihou.Core.Checkout@@ -51,7 +51,8 @@ let matchingApps = Set.fromList [app ^. #applicationId | app <- originalManifest ^. #applications, maybe True (== app ^. #applicationId) (opts ^. #application), state <- app ^. #instances, modName == state ^. #name || modName == qualifiedName (ModuleInstance (state ^. #name) (state ^. #parentVars))] legacyPaths = Set.fromList [path | (path, receipt) <- Map.toAscList (originalManifest ^. #seeds), LegacySeedOwners apps <- [receipt ^. #ownership], not (Set.null (apps `Set.intersection` matchingApps))] prepared <- prepareRecordedSeedOwnership root paths legacyPaths originalManifest- manifest <- case prepared of Left err -> TIO.putStrLn err >> exitFailure; Right candidate -> pure candidate+ seedPrepared <- case prepared of Left err -> TIO.putStrLn err >> exitFailure; Right candidate -> pure candidate+ manifest <- case prepareRemovalPolicies seedPrepared of Left err -> TIO.putStrLn err >> exitFailure; Right candidate -> pure candidate case selectRecordedRemoval manifest modName (opts ^. #application) of Left err -> TIO.putStrLn (err ^. #message) >> exitFailure Right (Just selected) -> handleCheckoutRemoval opts manifest selected >> exitWith ExitSuccess@@ -221,7 +222,14 @@ handleCheckoutRemoval opts manifest selected = do root <- getCurrentDirectory let candidate = selected ^. #candidateManifest- retiredSeeds = [path | (path, receipt) <- Map.toAscList (manifest ^. #seeds), case receipt ^. #ownership of KnownSeedOwners owners -> Set.member (selected ^. #instanceName) (Map.findWithDefault Set.empty (selected ^. #applicationId) owners); _ -> False]+ ownersFor path source = case Map.lookup path (source ^. #seeds) of+ Just receipt -> case receipt ^. #ownership of KnownSeedOwners owners -> Map.findWithDefault Set.empty (selected ^. #applicationId) owners; _ -> Set.empty+ Nothing -> Set.empty+ retiredSeeds = [path | path <- Map.keys (manifest ^. #seeds), not (Set.null (ownersFor path manifest `Set.difference` ownersFor path candidate))]+ qualified applied = qualifiedName (ModuleInstance (applied ^. #name) (applied ^. #parentVars))+ retainedModules = Set.fromList (map qualified (candidate ^. #modules))+ inherited = [qualified applied | applied <- manifest ^. #modules, not (Set.member (qualified applied) retainedModules), qualified applied /= selected ^. #instanceName]+ mapM_ (\name -> TIO.putStrLn (" Retire inherited module " <> name ^. #unModuleName <> ".")) inherited mapM_ (\path -> TIO.putStrLn (" Keep " <> T.pack path <> " (seed file; owned by the project)")) (take 5 retiredSeeds) checked <- runEff $ runFilesystem $ runProcessIO $ prepareCheckoutPlan Reconcile ApplyDeclaration root manifest candidate [] plan <- case checked of
src-exe/Seihou/CLI/Run.hs view
@@ -64,6 +64,7 @@ import Seihou.Core.Checkout import Seihou.Core.CommandFingerprint (checkoutCommandsFromOperations, isCheckoutCommand) import Seihou.Core.Context (resolveContext)+import Seihou.Core.Expr (settleRemovalConditions) import Seihou.Core.Migration (MigrationPlan (..)) import Seihou.Core.Module (defaultSearchPaths, discoverRunnable) import Seihou.Core.Seed (renderSeedOwnershipError, replaceSelectedSeedClaims)@@ -513,7 +514,7 @@ -- Build updated manifest with all composed modules. let orphanedPaths = map (^. #path) (diff ^. #orphaned) cleanedFiles = foldr Map.delete (manifest ^. #files) orphanedPaths- allModuleEntries = updateAllModules (manifest ^. #modules) originedModules now+ allModuleEntries = updateAllModules (manifest ^. #modules) originedModules resolved now allResolvedVals = Map.unions [Map.map (^. #value) vs | vs <- Map.elems resolved]@@ -996,9 +997,10 @@ updateAllModules :: [AppliedModule] -> [(ModuleInstance, Module, ArtifactOrigin)] ->+ Map ModuleInstance (Map VarName ResolvedVar) -> UTCTime -> [AppliedModule]-updateAllModules existing modulesInOrder now =+updateAllModules existing modulesInOrder resolved now = let composedKeys = Set.fromList [ (inst ^. #module_, inst ^. #parentVars)@@ -1012,7 +1014,7 @@ origin = origin, moduleVersion = m ^. #version, appliedAt = now,- removal = m ^. #removal+ removal = settleRemovalConditions (Map.map (^. #value) (resolved Map.! inst)) <$> m ^. #removal } | (inst, m, origin) <- modulesInOrder ]
src/Seihou/CLI/ManifestCapabilityUpgrade.hs view
@@ -31,6 +31,7 @@ prepareSeedOwnership, prepareRecordedSeedOwnership, prepareRecipeInheritance,+ prepareRemovalPolicies, gatherSeedOwnershipEvidence, CertificationScope (..), EvidencePolicy (..),@@ -539,5 +540,13 @@ prepareRecipeInheritance manifest = do (_, document) <- first renderManifestUpgradeError $- upgradeDocumentLosslessly (max (manifest ^. #version) (minimumManifestVersion RecipeInheritance)) (Aeson.toJSON manifest)+ upgradeDocumentLosslessly (max (manifest ^. #version) (max (minimumManifestVersion RecipeInheritance) (minimumManifestVersion ConditionalRemovalPolicies))) (Aeson.toJSON manifest)+ first T.pack (manifestFromJSON (Aeson.encode document))++-- | Stage the lossless policy boundary; dry-run callers never publish this candidate.+prepareRemovalPolicies :: Manifest -> Either Text Manifest+prepareRemovalPolicies manifest = do+ (_, document) <-+ first renderManifestUpgradeError $+ upgradeDocumentLosslessly (max (manifest ^. #version) (minimumManifestVersion ConditionalRemovalPolicies)) (Aeson.toJSON manifest) first T.pack (manifestFromJSON (Aeson.encode document))
src/Seihou/CLI/SchemaVersion.hs view
@@ -9,11 +9,11 @@ -- | Raw URL for the seihou-schema package.dhall at a pinned commit schemaUrl :: Text-schemaUrl = "https://raw.githubusercontent.com/shinzui/seihou-schema/dbeff0dd67c15b7a1c57f90b23103afdc2fa8056/package.dhall"+schemaUrl = "https://raw.githubusercontent.com/shinzui/seihou-schema/e9f91438f081fba90f40f987de93df9f74c7117d/package.dhall" -- | SHA256 integrity hash for the schema import schemaHash :: Text-schemaHash = "sha256:065c27b4d26059a7641e8ee7ee9d23ed32a698052c6aa8966426e6a678ee0e82"+schemaHash = "sha256:00f822087302b53f7ffe74e6505a633c59a0c2f206d12bfadb58a2fe211c0e40" -- | Complete Dhall import line for use in generated modules schemaImportLine :: Text
src/Seihou/CLI/Update.hs view
@@ -65,6 +65,7 @@ import Seihou.Core.ArtifactRef (renderArtifactRefError, resolveArtifactOrigin) import Seihou.Core.Checkout import Seihou.Core.CommandFingerprint (checkoutCommandsFromOperations)+import Seihou.Core.Expr (settleRemovalConditions) import Seihou.Core.Module (defaultSearchPaths, discoverRunnable) import Seihou.Core.Seed (seedApplicationIds) import Seihou.Core.Types@@ -164,7 +165,7 @@ selectedIdsForSeeds = Set.fromList (map (^. #candidate . #applicationId) plannedApplications) neededLegacy = Set.fromList [path | (path, receipt) <- Map.toAscList (base ^. #seeds), LegacySeedOwners apps <- [receipt ^. #ownership], Map.member path claims || not (Set.null (apps `Set.intersection` selectedIdsForSeeds))] relevant = not (Map.null claims) || any (\receipt -> not (Set.null (seedApplicationIds receipt `Set.intersection` selectedIdsForSeeds))) (Map.elems (base ^. #seeds))- structurallyPrepared = prepareManifestSchema manifestPath RecipeInheritance document base >>= \prepared -> if relevant then prepareManifestSchema manifestPath SeedOwnerClaims (Aeson.toJSON prepared) prepared else Right prepared+ structurallyPrepared = prepareManifestSchema manifestPath ConditionalRemovalPolicies document base >>= \prepared -> if relevant then prepareManifestSchema manifestPath SeedOwnerClaims (Aeson.toJSON prepared) prepared else Right prepared paths <- defaultSearchPaths preparedResult <- case structurallyPrepared of Left err -> pure (Left err)@@ -908,7 +909,8 @@ parentVars = state ^. #parentVars, origin = applied ^. #origin, moduleVersion = applied ^. #moduleVersion,- resolvedVars = state ^. #resolvedVars+ resolvedVars = state ^. #resolvedVars,+ removalPolicy = state ^. #removalPolicy } savedInstanceValues :: AppliedComposition -> SavedInstanceValues@@ -1313,7 +1315,7 @@ origin = publishedInstance application instanceId ^. #origin, moduleVersion = modul ^. #version, appliedAt = now,- removal = modul ^. #removal+ removal = settleRemovalConditions (Map.map (^. #value) ((application ^. #resolvedValues) Map.! instanceId)) <$> modul ^. #removal } | (application, instanceId, modul) <- deduplicateInstances applications ]
src/Seihou/CLI/Update/Render.hs view
@@ -50,6 +50,8 @@ ModuleLoadError (..), ModuleName (..), Operation (..),+ ParentVars,+ Removal, SHA256 (..), VarName (..), )@@ -119,6 +121,9 @@ <> fromMaybe "<module>" target <> "' to have an agent repair the manifest state and finish the upgrade.\n" +instancePolicies :: AppliedComposition -> [(ModuleName, ParentVars, Maybe Removal)]+instancePolicies application = [(state ^. #name, state ^. #parentVars, state ^. #removalPolicy) | state <- application ^. #instances]+ renderUpdateHuman :: Bool -> UpdateOutput -> Text renderUpdateHuman _ (UpdatePlanOutput (UpdatePlanView plan)) = T.unlines $@@ -132,6 +137,7 @@ renderFiles (reconciliationSummary (plan ^. #reconciliation)), if isUpdateNoOp plan then "Commands: 0 will run (no project update)" else renderCommands (summarizeCommandPlan (plan ^. #commandPlan)) ]+ <> ["Removal policies changed: " <> appliedTargetName (application ^. #candidate . #target) | application <- plan ^. #plannedApplications, maybe [] instancePolicies (application ^. #previous) /= instancePolicies (application ^. #candidate)] <> ["Checkout declarations changed: " <> application ^. #candidate . #applicationId . #unApplicationId | application <- plan ^. #plannedApplications, maybe [] (^. #checkoutCommands) (application ^. #previous) /= application ^. #candidate . #checkoutCommands] <> [" checkout " <> (if isUpdateNoOp plan then "not-run-no-update" else dispositionText (command ^. #disposition)) <> ": " <> fromMaybe "" (commandText (command ^. #operation)) | command <- plan ^. #commandPlan . #commands, isCheckoutCommand (command ^. #operation)] <> T.lines (renderCheckoutPlan True (plan ^. #checkoutPlan))
src/Seihou/CLI/Update/Types.hs view
@@ -365,3 +365,4 @@ && previous ^. #origin == candidate ^. #origin && previous ^. #moduleVersion == candidate ^. #moduleVersion && previous ^. #resolvedVars == candidate ^. #resolvedVars+ && previous ^. #removalPolicy == candidate ^. #removalPolicy
test/Seihou/CLI/ManifestCapabilityUpgradeSpec.hs view
@@ -114,7 +114,7 @@ spec :: Spec spec = do describe "legacy seed ownership evidence" $ do- let instanceState name = AppliedInstanceState name emptyParentVars (LocalOrigin (name ^. #unModuleName)) (Just "1.0.0") Map.empty+ let instanceState name = AppliedInstanceState name emptyParentVars (LocalOrigin (name ^. #unModuleName)) (Just "1.0.0") Map.empty Nothing app = recordedApplication appA & #instances .~ map instanceState ["first", "second"] receipt = SeedRecord "first" (LegacySeedOwners (Set.singleton appA)) SeedCreated testTime old = emptyManifest testTime & #applications .~ [app] & #seeds .~ Map.singleton "a.txt" receipt@@ -294,7 +294,7 @@ case manifestFromJSON written of Left err -> expectationFailure err Right upgraded -> do- (upgraded ^. #version) `shouldBe` ManifestSchemaVersion 13+ (upgraded ^. #version) `shouldBe` ManifestSchemaVersion 14 (^. #sharedWriteMode) <$> Map.lookup ".gitignore" (upgraded ^. #files) `shouldBe` Just SharedWriteAdditiveOnly TIO.readFile (fixture ^. #gitignorePath) `shouldReturn` gitignoreBefore
test/Seihou/CLI/ManifestRepairOriginsSpec.hs view
@@ -56,7 +56,7 @@ additionalModules = [], namespace = Nothing, context = Nothing,- instances = [AppliedInstanceState "beta" emptyParentVars (origin localPath "beta") (Just "1.0.0") Map.empty],+ instances = [AppliedInstanceState "beta" emptyParentVars (origin localPath "beta") (Just "1.0.0") Map.empty Nothing], commandReceipts = Map.empty, checkoutCommands = [], recipeInputs = Nothing,
test/Seihou/CLI/ManifestUpgradeSpec.hs view
@@ -264,12 +264,12 @@ outcome <- runManifestUpgrade (opts False False Nothing) case outcome of UpgradeWritten result -> do- map stepVersions (result ^. #steps) `shouldBe` [(6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13)]+ map stepVersions (result ^. #steps) `shouldBe` [(6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13), (13, 14)] (result ^. #entries) `shouldBe` []- (result ^. #toVersion) `shouldBe` ManifestSchemaVersion 13+ (result ^. #toVersion) `shouldBe` ManifestSchemaVersion 14 other -> expectationFailure ("expected a write, got " <> show other) document <- readDocument manifestPath- lookupPath ["version"] document `shouldBe` Just (Aeson.Number 13)+ lookupPath ["version"] document `shouldBe` Just (Aeson.Number 14) lookupPath ["files", ".gitignore", "sharedWriteMode"] document `shouldBe` Just (Aeson.String "additive-only") lookupPath ["files", "README.md", "sharedWriteMode"] document `shouldBe` Just (Aeson.String "unknown") lookupPath ["files", ".gitignore", "additiveOnly"] document `shouldBe` Nothing@@ -281,7 +281,7 @@ before <- LBS.readFile manifestPath outcome <- runManifestUpgrade (opts False False Nothing) case outcome of- UpgradeNotNeeded version _ -> version `shouldBe` ManifestSchemaVersion 13+ UpgradeNotNeeded version _ -> version `shouldBe` ManifestSchemaVersion 14 other -> expectationFailure ("expected nothing to do, got " <> show other) LBS.readFile manifestPath `shouldReturn` before @@ -310,7 +310,7 @@ outcome <- runManifestUpgrade (opts True True Nothing) case outcome of UpgradeWouldWrite result [] -> do- map stepVersions (result ^. #steps) `shouldBe` [(5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13)]+ map stepVersions (result ^. #steps) `shouldBe` [(5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13), (13, 14)] map (^. #artifactName) (result ^. #entries) `shouldBe` ["haskell-base", "project-lint", "haskell-service", "scratch-helper"] let report = formatUpgradeReport result@@ -335,10 +335,10 @@ outcome <- runManifestUpgrade (opts False True Nothing) case outcome of UpgradeWritten result ->- map stepVersions (result ^. #steps) `shouldBe` [(2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13)]+ map stepVersions (result ^. #steps) `shouldBe` [(2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13), (13, 14)] other -> expectationFailure ("expected a write, got " <> show other) document <- readDocument manifestPath- lookupPath ["version"] document `shouldBe` Just (Aeson.Number 13)+ lookupPath ["version"] document `shouldBe` Just (Aeson.Number 14) it "rejects a target newer than this build or older than the document" $ do withUpgradeProject schema6Manifest $ \_ ->@@ -366,7 +366,7 @@ output `shouldSatisfy` T.isInfixOf "5 -> 6" output `shouldSatisfy` T.isInfixOf "6 -> 7" document <- readDocument manifestPath- lookupPath ["version"] document `shouldBe` Just (Aeson.Number 13)+ lookupPath ["version"] document `shouldBe` Just (Aeson.Number 14) lookupPath ["modules", "0", "origin"] document `shouldBe` Just (Aeson.toJSON (RemoteOrigin "https://example.com/demo-modules.git" "demo" (Just "demo-modules"))) filter absoluteLooking (documentStrings document) `shouldBe` []
test/Seihou/CLI/RecipeExtensionE2ESpec.hs view
@@ -77,7 +77,7 @@ before <- readManifest root case Aeson.eitherDecode before :: Either String Aeson.Value of Right (Aeson.Object fields) -> do- KM.lookup "version" fields `shouldBe` Just (Aeson.toJSON (13 :: Int))+ KM.lookup "version" fields `shouldBe` Just (Aeson.toJSON (14 :: Int)) T.pack (show fields) `shouldSatisfy` T.isInfixOf "recipeChain" T.pack (show fields) `shouldSatisfy` T.isInfixOf "descriptorHash" other -> expectationFailure (show other)
test/Seihou/CLI/SeedRunE2ESpec.hs view
@@ -204,3 +204,26 @@ D.removeFile (root </> "nested/start.txt") _ <- success binary root ["run", "demo"] TIO.readFile (root </> "nested/start.txt") `shouldReturn` "starter\n"++ it "removes a recorded seed root and its unshared inherited managed files in one command" $+ withSystemTempDirectory "seed-root-closure" $ \root -> do+ (binary, modul) <- prepare root+ let base = root </> ".seihou/modules/base"+ D.createDirectoryIfMissing True (base </> "files")+ TIO.writeFile (base </> "files/base") "managed base\n"+ TIO.writeFile (base </> "files/starter") "wrong base starter\n"+ TIO.writeFile (base </> "module.dhall") "let S = ../demo/schema/package.dhall in S.Module::{name=\"base\",version=Some \"1.0.0\",vars=[S.VarDecl::{name=\"base.starters\",type=\"bool\",default=Some \"true\"}],steps=[S.Step::{strategy=\"copy\",src=\"base\",dest=\"base.txt\"},S.Step::{strategy=\"copy\",src=\"starter\",dest=\"nested/start.txt\",when=Some \"Eq base.starters true\"}],removal=Some S.Removal::{steps=[S.RemovalStep::{action=\"remove-file\",dest=\"base.txt\"},S.RemovalStep::{action=\"remove-file\",dest=\"nested/start.txt\",when=Some \"Eq base.starters true\"}]}}"+ TIO.writeFile (modul </> "module.dhall") (body "seed" ",dependencies=[S.Dependency::{module=\"base\",vars=[{name=\"base.starters\",value=\"false\"}]}],removal=Some S.Removal::{steps=[S.RemovalStep::{action=\"remove-file\",dest=\"LICENSE\"}]}")+ _ <- success binary root ["run", "demo"]+ TIO.writeFile (root </> "nested/start.txt") "authored seed\n"+ before <- BS.readFile (root </> ".seihou/manifest.json")+ preview <- success binary root ["remove", "demo", "--dry-run"]+ preview `shouldSatisfy` T.isInfixOf "Retire inherited module base"+ BS.readFile (root </> ".seihou/manifest.json") `shouldReturn` before+ _ <- success binary root ["remove", "demo"]+ TIO.readFile (root </> "nested/start.txt") `shouldReturn` "authored seed\n"+ D.doesFileExist (root </> "base.txt") `shouldReturn` False+ D.doesFileExist (root </> "LICENSE") `shouldReturn` False+ after <- readManifest root+ after ^. #applications `shouldBe` []+ after ^. #modules `shouldBe` []
test/Seihou/CLI/StatusSpec.hs view
@@ -101,7 +101,8 @@ parentVars = emptyParentVars, origin = LocalOrigin name, moduleVersion = Just "1.0.0",- resolvedVars = Map.empty+ resolvedVars = Map.empty,+ removalPolicy = Nothing } -- | Build a 'MigrationPlan' fixture for use with formatStatus.
test/Seihou/CLI/UpdateE2ESpec.hs view
@@ -237,7 +237,7 @@ expectSuccess "targeted update" exitCode stdoutText stderrText stdoutText `shouldSatisfy` T.isInfixOf "\"outcome\":\"applied\"" manifestText <- TIO.readFile (fixture ^. #manifestPath)- manifestText `shouldSatisfy` T.isInfixOf "\"version\":13"+ manifestText `shouldSatisfy` T.isInfixOf "\"version\":14" manifestText `shouldSatisfy` T.isInfixOf "\"sharedWriteMode\":\"additive-only\"" -- Recording an answer must not touch a single byte of the project. TIO.readFile (fixture ^. #gitignorePath) `shouldReturn` beforeGitignore@@ -276,7 +276,7 @@ dryOut `shouldSatisfy` T.isInfixOf "\"outcome\":\"plan\"" dryOut `shouldSatisfy` T.isInfixOf "\"alreadyUpToDate\":false" dryOut `shouldSatisfy` T.isInfixOf "\"fromSchema\":6"- dryOut `shouldSatisfy` T.isInfixOf "\"toSchema\":13"+ dryOut `shouldSatisfy` T.isInfixOf "\"toSchema\":14" dryOut `shouldSatisfy` T.isInfixOf "{\"from\":\"unknown\",\"path\":\".gitignore\",\"to\":\"additive-only\"}" dryOut `shouldNotSatisfy` T.isInfixOf "shared_path_requires_applications" LBS.readFile (fixture ^. #manifestPath) `shouldReturn` beforeManifest@@ -297,7 +297,7 @@ case decoded of Left err -> expectationFailure err Right manifest -> do- (manifest ^. #version) `shouldBe` ManifestSchemaVersion 13+ (manifest ^. #version) `shouldBe` ManifestSchemaVersion 14 case Map.lookup ".gitignore" (manifest ^. #files) of Nothing -> expectationFailure "the manifest lost .gitignore" Just record -> do@@ -317,7 +317,7 @@ binary <- seihouBinary (dryExit, dryOut, dryErr) <- runSeihouShared binary bug1 ["update", "alpha", "--dry-run"] expectSuccess "human dry run" dryExit dryOut dryErr- dryOut `shouldSatisfy` T.isInfixOf "Manifest: schema 6 -> 13"+ dryOut `shouldSatisfy` T.isInfixOf "Manifest: schema 6 -> 14" dryOut `shouldSatisfy` T.isInfixOf ".gitignore evidence unknown -> additive-only" (dryOut <> dryErr) `shouldNotSatisfy` leaksInternals
test/Seihou/CLI/UpdateSpec.hs view
@@ -12,7 +12,7 @@ where import Control.Exception (bracket)-import Control.Lens ((&), (.~), (^.))+import Control.Lens ((%~), (&), (.~), (^.)) import Control.Monad (forM_) import Data.ByteString.Lazy qualified as LBS import Data.Foldable (traverse_)@@ -372,6 +372,15 @@ isUpdateNoOp (unchangedFilePlan SharedWriteAdditiveOnly True) `shouldBe` True isUpdateNoOp (unchangedFilePlan SharedWriteRequiresOwnershipClosure False) `shouldBe` True + it "accepts removal-policy-only adoption and then returns to a true no-op" $ do+ let previous = application (AppliedModuleTarget "base") [instanceState "base"]+ policy = Removal [RemovalStep RemoveFileAction "starter" Nothing (Just (ExprLit False))] []+ candidate = previous & #instances %~ map (\state -> state & #removalPolicy .~ Just policy)+ planned before after = PlannedApplication (Just before) after [] Map.empty [] Map.empty+ plan before after = unchangedFilePlan SharedWriteAdditiveOnly True & #plannedApplications .~ [planned before after] & #applications .~ [after]+ isUpdateNoOp (plan previous candidate) `shouldBe` False+ isUpdateNoOp (plan candidate candidate) `shouldBe` True+ it "rejects a plan when its manifest snapshot changes" $ withSystemTempDirectory "seihou-update-stale" $ \root -> do fixture <- prepareUpdateFixture root@@ -584,7 +593,7 @@ withSystemTempDirectory "seihou-update-schema-5" $ \root -> do fixture <- prepareSharedPathFixture CoOwnerAppends root manifestText <- TIO.readFile (fixture ^. #manifestPath)- TIO.writeFile (fixture ^. #manifestPath) (T.replace "\"version\":13" "\"version\":5" manifestText)+ TIO.writeFile (fixture ^. #manifestPath) (T.replace "\"version\":14" "\"version\":5" manifestText) withSavedEnv "XDG_CONFIG_HOME" (Just (fixture ^. #xdgHome)) $ withCurrentDirectory (fixture ^. #projectRoot) $ do forM_ [NamedUpdateTargets ["alpha"], AllRecordedApplications] $ \selection -> do@@ -617,7 +626,7 @@ parentTwo = ParentVars (Map.singleton "tenant" "two") instanceOne = ModuleInstance "shared" parentOne instanceTwo = ModuleInstance "shared" parentTwo- stateFor parent = AppliedInstanceState "shared" parent (LocalOrigin "shared") (Just "1.0.0") Map.empty+ stateFor parent = AppliedInstanceState "shared" parent (LocalOrigin "shared") (Just "1.0.0") Map.empty Nothing previous = application (AppliedModuleTarget "shared") [stateFor parentOne, stateFor parentTwo] candidate = Module@@ -1195,7 +1204,8 @@ parentVars = emptyParentVars, origin, moduleVersion = Just "1.0.0",- resolvedVars = Map.empty+ resolvedVars = Map.empty,+ removalPolicy = Nothing } moduleDhall :: Text -> Text -> Text
test/Seihou/CLI/UpgradeDiagnosisSpec.hs view
@@ -43,7 +43,7 @@ withFixture CoOwnerAppendsPredatingEvidence noDamage $ \_ _ diagnose -> do checks <- readiness <$> diagnose "alpha" statusOf "manifest-schema-current" checks `shouldBe` NeedsAttention- detailOf "manifest-schema-current" checks `shouldSatisfy` T.isInfixOf "schema 6 is older than 13"+ detailOf "manifest-schema-current" checks `shouldSatisfy` T.isInfixOf "schema 6 is older than 14" statusOf "shared-evidence-known" checks `shouldBe` NeedsAttention detailOf "shared-evidence-known" checks `shouldBe` ".gitignore is shared with beta and its write mode is unknown" statusOf "update-plans-cleanly" checks `shouldBe` Ready@@ -59,7 +59,7 @@ ((^. #errorCode) <$> diagnosis ^. #updateDryRun) `shouldBe` ProbeOk (Just "shared_write_evidence_unavailable") it "reports a schema-5 manifest as needing seihou manifest upgrade" $- withFixture CoOwnerAppends (withManifestText (T.replace "\"version\":13" "\"version\":5")) $ \_ _ diagnose -> do+ withFixture CoOwnerAppends (withManifestText (T.replace "\"version\":14" "\"version\":5")) $ \_ _ diagnose -> do diagnosis <- diagnose "alpha" let checks = readiness diagnosis statusOf "manifest-readable" checks `shouldBe` NeedsAttention@@ -154,7 +154,7 @@ [ ("healthy" :: Text, CoOwnerAppends, const (pure ())), ("schema 6", CoOwnerAppendsPredatingEvidence, const (pure ())), ("evidence unavailable", CoOwnerAppendsPredatingEvidence, evidenceUnavailable),- ("schema 5", CoOwnerAppends, withManifestText (T.replace "\"version\":13" "\"version\":5")),+ ("schema 5", CoOwnerAppends, withManifestText (T.replace "\"version\":14" "\"version\":5")), ("not json", CoOwnerAppends, withManifestText (const "{not json")), ("local origin", CoOwnerAppends, localBetaOrigin), ("pending transaction", CoOwnerAppends, pendingTransaction)