packages feed

keiro-dsl-0.22.0.0: test/Keiro/Dsl/MappedStructuralEnumEquality.hs

module Keiro.Dsl.MappedStructuralEnumEquality (mappedStructuralEnumEqualitySpec) where

import Control.Monad (forM_)
import Data.List.NonEmpty qualified as NE
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as TIO
import Keiro.Dsl.AggregateType
import Keiro.Dsl.Diff qualified as Diff
import Keiro.Dsl.ExplainBindings
import Keiro.Dsl.Expression
import Keiro.Dsl.FoldFingerprint
import Keiro.Dsl.Grammar hiding (Spec)
import Keiro.Dsl.LanguageVersion
import Keiro.Dsl.Parser
import Keiro.Dsl.ReplayImpact
import Keiro.Dsl.Scaffold
import Keiro.Dsl.ScaffoldRecord
import Keiro.Dsl.ScaffoldRun (scaffoldServiceModules)
import Keiro.Dsl.SemanticContract
import Keiro.Dsl.TypeGraph (MappedKey (..))
import Keiro.Dsl.Validate
import Keiro.Dsl.WorkspaceRecord qualified as WR
import Test.Hspec

mappedStructuralEnumEqualitySpec :: Spec
mappedStructuralEnumEqualitySpec = describe "mapped structural enum equality" $ do
  it "admits only Language 6 and adds no global fold segment" $ do
    let definitions = NE.toList languageRegistry
    [definition.version | definition <- definitions, runtimeProfileHasCapability definition.runtimeSemanticsProfile StructuralEnumEqualityGuards] `shouldBe` [currentAuthoringLanguageVersion]
    capabilityFoldSegment StructuralEnumEqualityGuards `shouldBe` Nothing
    source <- fixture
    forM_ ["4", "5"] $ \version -> do
      service <- parseChecked (T.replace "language keiro-dsl 6" ("language keiro-dsl " <> version) source)
      codes service `shouldContain` [AggregateGuardCapabilityUnsupported]
      wrapped <- parseChecked (T.replace "language keiro-dsl 6" ("language keiro-dsl " <> version) (replaceGuard "(cmd.requiredScope == reg.requiredScope) == true" source))
      codes wrapped `shouldContain` [AggregateGuardCapabilityUnsupported]
      literalWrite <- parseChecked (T.replace "language keiro-dsl 6" ("language keiro-dsl " <> version) (T.unlines (filter (not . T.isPrefixOf "    guard ") (T.lines source))))
      codes literalWrite `shouldContain` [AggregateGuardCapabilityUnsupported]
  it "checks both operators with roots, qualified constructors on either side, and self comparisons" $ do
    source <- fixture
    forM_ [left <> " " <> operator <> " " <> right | operator <- ["==", "!="], (left, right) <- [("cmd.requiredScope", "reg.requiredScope"), ("cmd.requiredScope", "RequiredScope.PipelineScope"), ("RequiredScope.DiagnosticScope", "reg.requiredScope"), ("cmd.requiredScope", "cmd.requiredScope"), ("RequiredScope.PipelineScope", "RequiredScope.DiagnosticScope")]] $ \guardText -> do
      service <- parseChecked (replaceGuard guardText source)
      errors service `shouldBe` []
      let modules = scaffoldServiceModules (defaultContext "mapped-structural-enum-equality") service
      firewallBreaches modules `shouldBe` []
  it "rejects invalid operands and operations at the expression" $ do
    source <- fixture
    forM_
      [ ("cmd.requiredScope == \"pipeline\"", AggregateExpressionOperandTypeMismatch),
        ("cmd.requiredScope < reg.requiredScope", AggregateExpressionOperatorUnsupported),
        ("cmd.requiredScope + reg.requiredScope == cmd.requiredScope", AggregateExpressionOperatorUnsupported),
        ("cmd.requiredScope == RequiredScope.Unknown", AggregateExpressionLiteralInvalid),
        ("cmd.requiredScope == PipelineScope", AggregateExpressionRootUnknown),
        ("cmd.request == cmd.request", AggregateExpressionOperatorUnsupported)
      ]
      $ \(guardText, code) -> do
        service <- parseChecked (replaceGuard guardText source)
        codes service `shouldContain` [code]
  it "separates structural and nominal enums despite identical strings" $ do
    source <- fixture
    service <- parseChecked (replaceGuard "cmd.requiredScope == NominalScope.Pipeline" (T.replace "context mapped-structural-enum-equality" "context mapped-structural-enum-equality\nenum NominalScope { Pipeline=pipeline Diagnostic=diagnostic }" source))
    codes service `shouldContain` [AggregateGuardTypeMismatch]
  it "keeps declaration identity even when two enums share their Haskell owner" $ do
    source <- fixture
    let (_, enumAndRest) = T.breakOn "mapped structural enum RequiredScope" source
        (enumSource, _) = T.breakOn "mapped structural record Request" enumAndRest
        second sameOwner = T.replace "type=OtherScope" (if sameOwner then "type=RequiredScope" else "type=OtherScope") (T.replace "RequiredScope" "OtherScope" enumSource)
    forM_ [False, True] $ \sameOwner -> do
      service <- parseChecked (replaceGuard "cmd.requiredScope == cmd.otherScope" (T.replace "mapped structural record Request" (second sameOwner <> "mapped structural record Request") (T.replace "command Match {" "command Match { otherScope:OtherScope" source)))
      let spec = checkedSpec service
          owner = aggregate service
          transition = head owner.transitions
          Just syntax = transition.guard
      case resolveGuardExpr (expressionEnvironment spec owner transition) syntax of
        Left diagnostics -> map (.code) (NE.toList diagnostics) `shouldContain` [ScalarOperandTypeMismatch]
        Right _ -> expectationFailure "cross-declaration equality was accepted"
  it "retains mapped codecs, samples, initials and writes while rejecting other mapped shapes" $ do
    source <- fixture
    service <- parseChecked source
    let symbols = aggregateSymbols (checkedSpec service)
    case resolveAggregateType symbols noLoc CommandFieldUse (TRef "RequiredScope") of
      Right resolved@(AggregateStructuralEnum _) -> do
        case resolved of AggregateMapped key -> key `shouldBe` MappedKey "RequiredScope"; _ -> expectationFailure "lost mapped-value view"
        aggregateCapability EqualityGuardUse resolved `shouldBe` SolverVisible
        aggregateCapability OrderingGuardUse resolved `shouldBe` Unsupported
        aggregateCapability CommandFieldUse resolved `shouldBe` OpaqueOnly
      other -> expectationFailure (show other)
    forM_
      [ T.replace "mapped structural enum RequiredScope" "mapped structural union RequiredScope" . T.replace "wire string { PipelineScope as \"pipeline\" DiagnosticScope as \"diagnostic\" }" "wire tagged-object tag=\"tag\" contents=\"contents\" unknown-fields=reject { PipelineScope as \"pipeline\" DiagnosticScope as \"diagnostic\" }",
        T.replace "mapped structural enum RequiredScope" "mapped opaque RequiredScope" . T.replace "  wire string { PipelineScope as \"pipeline\" DiagnosticScope as \"diagnostic\" }" "  codec = \"scope.json\"\n  version = \"1\"" . T.replace "  binding = \"Conformance.MappedStructuralEnumEquality.Bindings.requiredScopeBinding\"\n" "" . T.replace "  binding-version = \"1\"\n" "" . T.replace "  canonical-type = \"conformance.RequiredScope.v1\"\n" ""
      ]
      $ \transform -> do
        other <- parseChecked (transform source)
        codes other `shouldContain` [AggregateExpressionOperatorUnsupported]
  it "rejects incomplete binding metadata statically" $ do
    source <- fixture
    incomplete <- parseChecked (T.replace "  binding = \"Conformance.MappedStructuralEnumEquality.Bindings.requiredScopeBinding\"\n" "" source)
    codes incomplete `shouldContain` [MappedMissingIngredient]
  it "emits finite root witnesses and exhaustive binding laws without fixture-selected reconstruction" $ do
    source <- fixture
    service <- parseChecked source
    let modules = scaffoldServiceModules (defaultContext "mapped-structural-enum-equality") service
        projections = moduleText "StructuralProjections.hs" modules
        conformance = moduleText "StructuralConformance.hs" modules
    projections `shouldSatisfy` T.isInfixOf "finiteProjectionDomain"
    projections `shouldSatisfy` T.isInfixOf "bindingFromShape"
    projections `shouldSatisfy` (not . T.isInfixOf "NonEmpty.head")
    conformance `shouldSatisfy` T.isInfixOf "exhaustive enum shape round-trip"
    conformance `shouldSatisfy` T.isInfixOf "checkFieldProjectionOwner"
    conformance `shouldSatisfy` T.isInfixOf "checkFieldProjectionKey"
  it "records a distinct equality row and explains its total binding obligation" $ do
    source <- fixture
    service <- parseChecked source
    let identities = structuralEnumEqualityIdentitiesForService service
    identities `shouldSatisfy` (not . null)
    case bindingObligationsForService service of
      Left failure -> expectationFailure (show failure)
      Right obligations -> do
        let contracts = [identity | obligation <- obligations, Just identity <- [obligation.equalityContract]]
        contracts `shouldSatisfy` (not . null)
        contracts `shouldSatisfy` all (`elem` identities)
    contents <- TIO.readFile "test/conformance-mapped-structural-enum-equality/keiro-dsl-ledger.context.mapped-structural-enum-equality.txt"
    case parseRecord contents of
      Nothing -> expectationFailure "scaffold record did not parse"
      Just record -> do
        let updated = record {structuralEnumEqualities = identities}
        parseRecord (renderRecord updated) `shouldBe` Just updated
        parseRecord (renderRecord updated <> "structural-enum-equality " <> NE.head (NE.fromList identities) <> "\n") `shouldBe` Nothing
        renderRecord updated `shouldSatisfy` T.isInfixOf "structural-enum-equality keiro-dsl/structural-enum-equality/1:"
    workspaceContents <- TIO.readFile "test/conformance-workspace-nominals/keiro-dsl-ledger.workspace.workspace-nominal-proof.txt"
    case WR.parseWorkspaceRecord workspaceContents of
      Nothing -> expectationFailure "workspace ledger did not parse"
      Just record -> do
        let updated = record {WR.structuralEnumEqualities = identities}
        WR.parseWorkspaceRecord (WR.renderWorkspaceRecord updated) `shouldBe` Just updated
  it "scopes fold identity to equality use and keeps fixture names and order out of it" $ do
    source <- fixture
    service <- parseChecked source
    let fingerprint value = either (error . show) id (aggregateFoldFingerprintForService value (aggregate value))
        surface value = either (error . show) id (aggregateFoldSurfaceForService value (aggregate value))
    surface service `shouldSatisfy` T.isInfixOf "structural-enum-equality-use:keiro-dsl/structural-enum-equality/1:"
    forM_ [T.replace "Bindings.requiredScopeFixtures" "Bindings.changedFixtures", T.replace "PipelineScope as \"pipeline\" DiagnosticScope as \"diagnostic\"" "DiagnosticScope as \"diagnostic\" PipelineScope as \"pipeline\""] $ \transform -> do
      same <- parseChecked (transform source)
      fingerprint same `shouldBe` fingerprint service
      replayImpactServices service same `shouldBe` Right ReplayNeutral
    forM_ [T.replace "binding-version = \"1\"" "binding-version = \"2\"", T.replace "\"pipeline\"" "\"pipeline-v2\"", replaceGuard "cmd.requiredScope != reg.requiredScope", T.replace "Bindings.requiredScopeBinding" "Bindings.changedBinding", T.replace "conformance.RequiredScope.v1" "conformance.RequiredScope.v2"] $ \transform -> do
      changed <- parseChecked (transform source)
      fingerprint changed `shouldNotBe` fingerprint service
      replayImpactServices service changed `shouldSatisfy` (/= Right ReplayNeutral)
      Diff.diffServices service changed `shouldSatisfy` either (const False) (not . null)
    noEquality <- parseChecked (stripGuards source)
    surface noEquality `shouldSatisfy` (not . T.isInfixOf "structural-enum-equality-use:")
    oldLanguage <- parseChecked (T.replace "language keiro-dsl 6" "language keiro-dsl 5" (stripGuards source))
    errors oldLanguage `shouldBe` []
    fingerprint noEquality `shouldBe` fingerprint oldLanguage
    surface noEquality `shouldBe` surface oldLanguage
    noGuardDiff <- pure (Diff.diffServices noEquality service)
    noGuardDiff `shouldSatisfy` either (const False) (not . null)
    replayImpactServices noEquality service `shouldSatisfy` (/= Right ReplayNeutral)

  it "compares each supported-history revision against its own graph and preserves unrelated controls" $ do
    let load name = TIO.readFile ("test/fixtures/mapped-structural-enum-equality-history/" <> name <> ".keiro") >>= parseChecked
        fingerprints service = [(owner.name, either (error . show) id (aggregateFoldFingerprintForService service owner)) | NAggregate owner <- (checkedSpec service).nodes]
    old <- load "guarded"
    forM_ [("data-only", False), ("binding-v2", False), ("wire-v2", False), ("fixture-only", True), ("reordered", True)] $ \(name, neutral) -> do
      new <- load name
      errors new `shouldBe` []
      lookup "ScopeControl" (fingerprints new) `shouldBe` lookup "ScopeControl" (fingerprints old)
      (lookup "ScopeGate" (fingerprints new) == lookup "ScopeGate" (fingerprints old)) `shouldBe` neutral
      result <- either (\failure -> expectationFailure (show failure) >> fail "replay comparison failed") pure (replayImpactServices old new)
      (result == ReplayNeutral) `shouldBe` neutral
    dataOnly <- load "data-only"
    let surface = either (error . show) id (aggregateFoldSurfaceForService dataOnly (aggregate dataOnly))
        fingerprint = either (error . show) id (aggregateFoldFingerprintForService dataOnly (aggregate dataOnly))
    expected <- TIO.readFile "test/fixtures/mapped-structural-enum-data-only-fold.golden"
    fingerprint <> "\n" <> surface <> "\n" `shouldBe` expected
  it "samples literal-guarded commands by projected wire key instead of fixture order" $ do
    source <- fixture
    let harnessFor checked = do
          errors checked `shouldBe` []
          let modules = scaffoldServiceModules (defaultContext "mapped-structural-enum-equality") checked
          firewallBreaches modules `shouldBe` []
          pure (moduleText "ScopeGate/Harness.hs" modules)
        sampled filters = "(case [value | (_, value) <- NonEmpty.toList (fixtureCases Bindings.requiredScopeFixtures), " <> filters <> "] of value : _ -> value; [] -> error \"enum guard requires a matching supplied fixture\")"
    harness <- parseChecked source >>= harnessFor
    harness `shouldSatisfy` T.isInfixOf "import Generated.MappedStructuralEnumEquality.StructuralProjections qualified as StructuralProjections"
    harness `shouldSatisfy` T.isInfixOf "(Match (MatchData Bindings.initialScope "
    harness `shouldSatisfy` T.isInfixOf ("(Differ (DifferData " <> sampled "value /= Bindings.initialScope")
    harness `shouldSatisfy` T.isInfixOf ("(EqRight (EqRightData " <> sampled "fieldWitnessGet StructuralProjections.requiredScopeWitness value == \"pipeline\"")
    harness `shouldSatisfy` T.isInfixOf ("(EqLeft (EqLeftData " <> sampled "fieldWitnessGet StructuralProjections.requiredScopeWitness value == \"pipeline\"")
    harness `shouldSatisfy` T.isInfixOf ("(NeRight (NeRightData " <> sampled "fieldWitnessGet StructuralProjections.requiredScopeWitness value /= \"diagnostic\"")
    harness `shouldSatisfy` T.isInfixOf ("(NeLeft (NeLeftData " <> sampled "fieldWitnessGet StructuralProjections.requiredScopeWitness value /= \"diagnostic\"")
    combined <- parseChecked (replaceGuard "cmd.requiredScope != reg.requiredScope && cmd.requiredScope != RequiredScope.DiagnosticScope" source) >>= harnessFor
    combined `shouldSatisfy` T.isInfixOf ("(Match (MatchData " <> sampled "value /= Bindings.initialScope, fieldWitnessGet StructuralProjections.requiredScopeWitness value /= \"diagnostic\"")
    alternative <- parseChecked (replaceGuard "cmd.requiredScope == RequiredScope.DiagnosticScope || cmd.requiredScope == RequiredScope.PipelineScope" source) >>= harnessFor
    alternative `shouldSatisfy` T.isInfixOf ("(Match (MatchData " <> sampled "fieldWitnessGet StructuralProjections.requiredScopeWitness value == \"diagnostic\"")
    unguarded <- parseChecked (stripGuards source) >>= harnessFor
    unguarded `shouldSatisfy` (not . T.isInfixOf "StructuralProjections")
    unguarded `shouldSatisfy` (not . T.isInfixOf "fieldWitnessGet")

fixture :: IO Text
fixture = TIO.readFile "test/fixtures/mapped-structural-enum-equality.keiro"

parseChecked :: Text -> IO CheckedService
parseChecked source = case parseSource "mapped-structural-enum-equality.keiro" source of
  Left failure -> expectationFailure (T.unpack (renderParseFailure failure)) >> fail "parse failed"
  Right parsed -> pure (checkedSource parsed)

aggregate :: CheckedService -> Aggregate
aggregate service = head [value | NAggregate value <- (checkedSpec service).nodes]

errors :: CheckedService -> [Diagnostic]
errors = filter ((== Error) . (.severity)) . validateService

codes :: CheckedService -> [DiagnosticCode]
codes = map (.code) . errors

replaceGuard :: Text -> Text -> Text
replaceGuard = T.replace "cmd.requiredScope == reg.requiredScope"

stripGuards :: Text -> Text
stripGuards = T.replace "write requiredScope := RequiredScope.PipelineScope" "write requiredScope := cmd.requiredScope" . T.unlines . filter (not . T.isPrefixOf "    guard ") . T.lines

moduleText :: Text -> [ScaffoldModule] -> Text
moduleText suffix modules = T.unlines [value.text | value <- modules, suffix `T.isSuffixOf` T.pack value.path]