seihou-cli-0.4.0.0: test/Seihou/CLI/AgentLaunchSpec.hs
module Seihou.CLI.AgentLaunchSpec (tests) where
import Data.List (nub)
import Data.Text qualified as T
import Seihou.CLI.AgentLaunch
( AgentContext (..),
BaselineStatus (..),
formatAvailableModules,
formatBaselineStatus,
formatBlueprintIdentity,
formatLocalModules,
formatManifestState,
formatModuleDhallState,
formatReferenceFiles,
formatReferenceFilesDir,
formatSeihouProjectState,
resolveBlueprintTools,
setupAllowedTools,
substitute,
)
import Seihou.Core.Types
( Blueprint (..),
BlueprintFile (..),
ModuleName (..),
)
import Test.Hspec
import Test.Tasty (TestTree)
import Test.Tasty.Hspec (testSpec)
tests :: IO TestTree
tests = testSpec "Seihou.CLI.AgentLaunch" $ do
describe "substitute" $ do
it "replaces a single key" $
substitute [("name", "Alice")] "Hello {{name}}"
`shouldBe` "Hello Alice"
it "replaces multiple keys" $
substitute [("a", "1"), ("b", "2")] "{{a}}-{{b}}"
`shouldBe` "1-2"
it "leaves unknown keys untouched" $
substitute [("a", "1")] "{{a}} and {{c}}"
`shouldBe` "1 and {{c}}"
it "is a no-op on a template with no placeholders" $
substitute [("a", "1")] "no placeholders here"
`shouldBe` "no placeholders here"
describe "AgentContext formatters" $ do
let baseCtx =
AgentContext
{ cwd = "/tmp/test",
seihouInitialized = False,
hasManifest = False,
localModuleDhall = False,
localModules = [],
availableModules = []
}
describe "formatSeihouProjectState" $ do
it "names .seihou/ when initialised" $
formatSeihouProjectState (baseCtx {seihouInitialized = True})
`shouldBe` "Seihou project: .seihou/ directory exists (this is a seihou-managed project)"
it "states 'No .seihou/' when not initialised" $
formatSeihouProjectState baseCtx
`shouldBe` "Seihou project: No .seihou/ directory (not yet a seihou project in this directory)"
describe "formatManifestState" $ do
it "names manifest.json when present" $
formatManifestState (baseCtx {hasManifest = True})
`shouldBe` "Manifest: .seihou/manifest.json exists (modules have been applied here)"
it "reports no manifest otherwise" $
formatManifestState baseCtx
`shouldBe` "Manifest: No manifest (no modules applied yet)"
describe "formatModuleDhallState" $ do
it "names module.dhall when present in cwd" $
formatModuleDhallState (baseCtx {localModuleDhall = True})
`shouldBe` "Module in cwd: module.dhall found in current directory (user is authoring a module here)"
it "is empty when module.dhall is absent" $
formatModuleDhallState baseCtx `shouldBe` ""
describe "formatLocalModules" $ do
it "is empty when there are no local modules" $
formatLocalModules baseCtx `shouldBe` ""
it "lists local modules with bullet prefixes" $
formatLocalModules (baseCtx {localModules = ["foo", "bar"]})
`shouldBe` "Local modules:\n - foo\n - bar"
describe "formatAvailableModules" $ do
it "states 'None discovered' when empty" $
formatAvailableModules baseCtx
`shouldBe` "Available modules: None discovered"
it "renders entries as 'name — description (source)' lines" $
formatAvailableModules
(baseCtx {availableModules = [("foo", "the foo module", "user")]})
`shouldBe` "Available modules across search paths:\n - foo — the foo module (user)"
describe "formatBaselineStatus" $ do
it "names --no-baseline as the reason when skipped" $
formatBaselineStatus BaselineSkipped
`shouldBe` "(no baseline applied — `--no-baseline` was passed)"
it "calls out an empty baseModules list" $
formatBaselineStatus BaselineEmpty
`shouldBe` "(this blueprint declares no base modules)"
it "renders an applied versioned module as a bullet" $
formatBaselineStatus (BaselineApplied [(ModuleName "foo", Just "1.0.0")])
`shouldBe` " - foo (v1.0.0)"
it "renders an applied unversioned module as a bullet" $
formatBaselineStatus (BaselineApplied [(ModuleName "foo", Nothing)])
`shouldBe` " - foo (unversioned)"
describe "formatReferenceFiles" $ do
it "states '(no reference files)' when empty" $
formatReferenceFiles [] `shouldBe` "(no reference files)"
it "renders an entry with a description as 'path — description'" $
formatReferenceFiles [BlueprintFile {src = "x.txt", description = Just "an example"}]
`shouldBe` " - x.txt — an example"
it "renders an entry without a description as just the path" $
formatReferenceFiles [BlueprintFile {src = "y.txt", description = Nothing}]
`shouldBe` " - y.txt"
describe "formatReferenceFilesDir" $ do
it "renders the mounted path with read-directly guidance" $ do
let rendered = formatReferenceFilesDir (Just "/tmp/bp/files")
rendered `shouldSatisfy` T.isInfixOf "/tmp/bp/files"
rendered `shouldSatisfy` T.isInfixOf "open them directly"
it "renders the ask-the-user fallback when no directory is mounted" $ do
let rendered = formatReferenceFilesDir Nothing
rendered `shouldSatisfy` T.isInfixOf "ask the user"
rendered `shouldSatisfy` (not . T.isInfixOf "readable at")
describe "resolveBlueprintTools" $ do
it "returns exactly the base set when nothing is declared" $
resolveBlueprintTools Nothing `shouldBe` setupAllowedTools
it "unions declared tools onto the base set without duplicates" $ do
let result = resolveBlueprintTools (Just ["Bash(mori *)"])
result `shouldSatisfy` (\tools -> all (`elem` tools) setupAllowedTools)
result `shouldSatisfy` (elem "Bash(mori *)")
length result `shouldBe` length (nub result)
it "does not duplicate a declared base tool" $
resolveBlueprintTools (Just ["Read"]) `shouldBe` setupAllowedTools
describe "formatBlueprintIdentity" $ do
let mk v d =
Blueprint
{ name = ModuleName "bp",
version = v,
description = d,
prompt = "",
vars = [],
prompts = [],
baseModules = [],
files = [],
allowedTools = Nothing,
tags = []
}
it "renders name, version, description as a three-line block" $
formatBlueprintIdentity (mk (Just "0.1") (Just "a thing"))
`shouldBe` "Name: bp\nVersion: 0.1\nDescription: a thing"
it "names '(unspecified)' when version is missing" $
formatBlueprintIdentity (mk Nothing (Just "a thing"))
`shouldBe` "Name: bp\nVersion: (unspecified)\nDescription: a thing"
it "names '(no description)' when description is missing" $
formatBlueprintIdentity (mk (Just "0.1") Nothing)
`shouldBe` "Name: bp\nVersion: 0.1\nDescription: (no description)"