packages feed

keiki-0.9.0.0: test/Keiki/ValidationReplayAlignmentSpec.hs

{-# LANGUAGE TypeFamilies #-}

module Keiki.ValidationReplayAlignmentSpec (spec) where

import Control.Exception (evaluate)
import Control.Monad (foldM, forM_)
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import Data.Time (UTCTime (..), fromGregorian, secondsToDiffTime)
import GHC.Generics (Generic)
import Keiki.Core
import Keiki.Fixtures.EmailDelivery
import Keiki.Fixtures.RegisterEmission
import Keiki.Fixtures.SplitCoverage
import Keiki.Fixtures.UserRegistration
import Keiki.Generics (FieldsOf, RegFieldsOf, mkInCtorVia, mkWireCtorVia)
import Numeric.Natural (Natural)
import Test.Hspec
import Test.QuickCheck (property)

runCommands ::
  (BoolAlg phi (RegFile rs, ci)) =>
  SymTransducer phi rs s ci co ->
  [ci] ->
  Maybe (s, RegFile rs, [co])
runCommands t = foldM advance (initial t, initialRegs t, [])
  where
    advance (s, regs, logSoFar) cmd = do
      (s', regs', emitted) <- step t (s, regs) cmd
      pure (s', regs', logSoFar ++ emitted)

atTime :: Integer -> UTCTime
atTime n = UTCTime (fromGregorian 2026 7 12) (secondsToDiffTime n)

data AmbiguousCmd = CmdX Int | CmdY Int
  deriving stock (Eq, Show)

data AmbiguousEvent = Logged Int | LoggedY Int
  deriving stock (Eq, Show)

type AmbiguousFields = '[ '("value", Int)]

inCtorX :: InCtor AmbiguousCmd AmbiguousFields
inCtorX =
  unavailableInCtor
    "CmdX"
    (\case CmdX value -> Just (RCons (Proxy @"value") value RNil); _ -> Nothing)
    (\(RCons _ value RNil) -> CmdX value)

inCtorY :: InCtor AmbiguousCmd AmbiguousFields
inCtorY =
  unavailableInCtor
    "CmdY"
    (\case CmdY value -> Just (RCons (Proxy @"value") value RNil); _ -> Nothing)
    (\(RCons _ value RNil) -> CmdY value)

wireLogged :: WireCtor AmbiguousEvent (Int, ())
wireLogged =
  unavailableWireCtor
    "Logged"
    (\case Logged value -> Just (value, ()); _ -> Nothing)
    (\(value, ()) -> Logged value)

wireLoggedY :: WireCtor AmbiguousEvent (Int, ())
wireLoggedY =
  unavailableWireCtor
    "LoggedY"
    (\case LoggedY value -> Just (value, ()); _ -> Nothing)
    (\(value, ()) -> LoggedY value)

ambiguousTransducerWith ::
  WireCtor AmbiguousEvent (Int, ()) ->
  SymTransducer (HsPred '[] AmbiguousCmd) '[] Bool AmbiguousCmd AmbiguousEvent
ambiguousTransducerWith secondWire =
  SymTransducer
    { edgesOut = \case
        False ->
          [ Edge
              { guard = matchInCtor inCtorX,
                update = UKeep,
                output =
                  [ pack
                      inCtorX
                      wireLogged
                      (TInpCtorField inCtorX (#value :: Index AmbiguousFields Int) *: oNil)
                  ],
                target = True,
                mode = Live
              },
            Edge
              { guard = matchInCtor inCtorY,
                update = UKeep,
                output =
                  [ pack
                      inCtorY
                      secondWire
                      (TInpCtorField inCtorY (#value :: Index AmbiguousFields Int) *: oNil)
                  ],
                target = True,
                mode = Live
              }
          ]
        True -> [],
      initial = False,
      initialRegs = RNil,
      isFinal = id
    }

ambiguousTransducer :: SymTransducer (HsPred '[] AmbiguousCmd) '[] Bool AmbiguousCmd AmbiguousEvent
ambiguousTransducer = ambiguousTransducerWith wireLogged

distinctHeadTransducer :: SymTransducer (HsPred '[] AmbiguousCmd) '[] Bool AmbiguousCmd AmbiguousEvent
distinctHeadTransducer = ambiguousTransducerWith wireLoggedY

data ReplayCompletionData = ReplayCompletionData
  { completionId :: Int
  }
  deriving stock (Eq, Show, Generic)

data RegisterReplayCmd
  = CompleteNonFinal ReplayCompletionData
  | CompleteFinal ReplayCompletionData
  deriving stock (Eq, Show, Generic)

data RegisterReplayEvent = StepCompleted ReplayCompletionData
  deriving stock (Eq, Show, Generic)

commandCompletionId :: RegisterReplayCmd -> Int
commandCompletionId (CompleteNonFinal value) = value.completionId
commandCompletionId (CompleteFinal value) = value.completionId

type ReplayCompletionFields = RegFieldsOf ReplayCompletionData

type RegisterReplayRegs = '[ '("openSteps", Natural)]

inCompleteNonFinal :: InCtor RegisterReplayCmd ReplayCompletionFields
inCompleteNonFinal = mkInCtorVia @"CompleteNonFinal"

inCompleteFinal :: InCtor RegisterReplayCmd ReplayCompletionFields
inCompleteFinal = mkInCtorVia @"CompleteFinal"

wireStepCompleted :: WireCtor RegisterReplayEvent (FieldsOf ReplayCompletionData)
wireStepCompleted = mkWireCtorVia @"StepCompleted"

openSteps :: Term RegisterReplayRegs RegisterReplayCmd ifs Natural
openSteps = TReg (#openSteps :: Index RegisterReplayRegs Natural)

opaqueCommandIdentity ::
  InCtor RegisterReplayCmd ReplayCompletionFields ->
  HsPred rs RegisterReplayCmd
opaqueCommandIdentity inputCtor =
  PEq
    ( TApp1
        id
        (TInpCtorField inputCtor (#completionId :: Index ReplayCompletionFields Int))
    )
    (TInpCtorField inputCtor (#completionId :: Index ReplayCompletionFields Int))

registerReplayOutput ::
  InCtor RegisterReplayCmd ReplayCompletionFields ->
  OutTerm rs RegisterReplayCmd RegisterReplayEvent
registerReplayOutput inputCtor =
  pack
    inputCtor
    wireStepCompleted
    (TInpCtorField inputCtor (#completionId :: Index ReplayCompletionFields Int) *: oNil)

registerReplayEdge ::
  EdgeMode ->
  InCtor RegisterReplayCmd ReplayCompletionFields ->
  HsPred RegisterReplayRegs RegisterReplayCmd ->
  Edge
    (HsPred RegisterReplayRegs RegisterReplayCmd)
    RegisterReplayRegs
    RegisterReplayCmd
    RegisterReplayEvent
    Bool
registerReplayEdge edgeMode inputCtor registerCondition =
  customRegisterReplayEdge
    edgeMode
    inputCtor
    (PAnd (opaqueCommandIdentity inputCtor) registerCondition)

customRegisterReplayEdge ::
  EdgeMode ->
  InCtor RegisterReplayCmd ReplayCompletionFields ->
  HsPred RegisterReplayRegs RegisterReplayCmd ->
  Edge
    (HsPred RegisterReplayRegs RegisterReplayCmd)
    RegisterReplayRegs
    RegisterReplayCmd
    RegisterReplayEvent
    Bool
customRegisterReplayEdge edgeMode inputCtor condition =
  Edge
    { guard = PAnd (PInCtor inputCtor) condition,
      update =
        USet
          (#openSteps :: IndexN "openSteps" RegisterReplayRegs Natural)
          (TLit 0),
      output = [registerReplayOutput inputCtor],
      target = True,
      mode = edgeMode
    }

registerReplayFixture ::
  Natural ->
  EdgeMode ->
  HsPred RegisterReplayRegs RegisterReplayCmd ->
  HsPred RegisterReplayRegs RegisterReplayCmd ->
  SymTransducer
    (HsPred RegisterReplayRegs RegisterReplayCmd)
    RegisterReplayRegs
    Bool
    RegisterReplayCmd
    RegisterReplayEvent
registerReplayFixture initialOpenSteps edgeMode nonFinalCondition finalCondition =
  SymTransducer
    { edgesOut = \case
        False ->
          [ registerReplayEdge edgeMode inCompleteNonFinal nonFinalCondition,
            registerReplayEdge edgeMode inCompleteFinal finalCondition
          ]
        True -> [],
      initial = False,
      initialRegs = RCons (Proxy @"openSteps") initialOpenSteps RNil,
      isFinal = id
    }

registerDisjointFixture ::
  Natural ->
  EdgeMode ->
  SymTransducer
    (HsPred RegisterReplayRegs RegisterReplayCmd)
    RegisterReplayRegs
    Bool
    RegisterReplayCmd
    RegisterReplayEvent
registerDisjointFixture initialOpenSteps edgeMode =
  registerReplayFixture
    initialOpenSteps
    edgeMode
    (PCmp CmpGt openSteps (TLit 1))
    (PEq openSteps (TLit 1))

registerOverlappingFixture ::
  SymTransducer
    (HsPred RegisterReplayRegs RegisterReplayCmd)
    RegisterReplayRegs
    Bool
    RegisterReplayCmd
    RegisterReplayEvent
registerOverlappingFixture =
  registerReplayFixture
    2
    Live
    (PCmp CmpGt openSteps (TLit 1))
    (PCmp CmpGt openSteps (TLit 0))

opaqueOnlyFixture ::
  SymTransducer
    (HsPred RegisterReplayRegs RegisterReplayCmd)
    RegisterReplayRegs
    Bool
    RegisterReplayCmd
    RegisterReplayEvent
opaqueOnlyFixture = registerReplayFixture 2 Live PTop PTop

type RegisterReplayCondition =
  InCtor RegisterReplayCmd ReplayCompletionFields ->
  HsPred RegisterReplayRegs RegisterReplayCmd

customRegisterReplayFixture ::
  Natural ->
  EdgeMode ->
  RegisterReplayCondition ->
  RegisterReplayCondition ->
  SymTransducer
    (HsPred RegisterReplayRegs RegisterReplayCmd)
    RegisterReplayRegs
    Bool
    RegisterReplayCmd
    RegisterReplayEvent
customRegisterReplayFixture initialOpenSteps edgeMode nonFinalCondition finalCondition =
  SymTransducer
    { edgesOut = \case
        False ->
          [ customRegisterReplayEdge edgeMode inCompleteNonFinal (nonFinalCondition inCompleteNonFinal),
            customRegisterReplayEdge edgeMode inCompleteFinal (finalCondition inCompleteFinal)
          ]
        True -> [],
      initial = False,
      initialRegs = registerReplayRegs initialOpenSteps,
      isFinal = id
    }

registerReplayRegs :: Natural -> RegFile RegisterReplayRegs
registerReplayRegs value = RCons (Proxy @"openSteps") value RNil

data OpenStepsIdentity

instance FieldProjection OpenStepsIdentity where
  type FieldName OpenStepsIdentity = "value"
  type FieldOwner OpenStepsIdentity = Natural
  type FieldResult OpenStepsIdentity = Natural
  fieldShapeId _ = "natural/identity"
  projectFieldValue _ = id

openStepsProjection :: Term RegisterReplayRegs RegisterReplayCmd ifs Natural
openStepsProjection =
  regProj
    (fieldWitness @OpenStepsIdentity)
    (#openSteps :: Index RegisterReplayRegs Natural)

unsupportedRegisterConditions :: [(String, String, RegisterReplayCondition)]
unsupportedRegisterConditions =
  [ ( "disjunction",
      "POr",
      const
        ( POr
            (PCmp CmpGt openSteps (TLit 1))
            (PEq openSteps (TLit 1))
        )
    ),
    ( "negation",
      "PNot",
      const (PNot (PCmp CmpGt openSteps (TLit 1)))
    ),
    ( "arithmetic",
      "TArith",
      const
        ( PCmp
            CmpGt
            (TArith OpAdd openSteps (TLit 1))
            (TLit 0)
        )
    ),
    ( "projection",
      "TFieldProj",
      const (PCmp CmpGt openStepsProjection (TLit 0))
    ),
    ( "input field",
      "TInpCtorField",
      \inputCtor ->
        PEq
          (TInpCtorField inputCtor (#completionId :: Index ReplayCompletionFields Int))
          (TLit 7)
    ),
    ( "opaque application",
      "TApp1",
      const (PEq (TApp1 id openSteps) openSteps)
    )
  ]

type UnsupportedCarrierRegs = '[ '("enabled", Bool)]

unsupportedCarrierFixture ::
  SymTransducer
    (HsPred UnsupportedCarrierRegs RegisterReplayCmd)
    UnsupportedCarrierRegs
    Bool
    RegisterReplayCmd
    RegisterReplayEvent
unsupportedCarrierFixture =
  SymTransducer
    { edgesOut = \case
        False ->
          [ unsupportedEdge inCompleteNonFinal True,
            unsupportedEdge inCompleteFinal False
          ]
        True -> [],
      initial = False,
      initialRegs = RCons (Proxy @"enabled") True RNil,
      isFinal = id
    }
  where
    unsupportedEdge inputCtor expected =
      Edge
        { guard =
            PAnd
              (PInCtor inputCtor)
              ( PEq
                  (TReg (#enabled :: Index UnsupportedCarrierRegs Bool))
                  (TLit expected)
              ),
          update = UKeep,
          output = [registerReplayOutput inputCtor],
          target = True,
          mode = Live
        }

type DuplicateLabelRegs =
  '[ '("openSteps", Natural),
     '("openSteps", Natural)
   ]

firstDuplicateOpenSteps :: Term DuplicateLabelRegs RegisterReplayCmd ifs Natural
firstDuplicateOpenSteps = TReg ZIdx

secondDuplicateOpenSteps :: Term DuplicateLabelRegs RegisterReplayCmd ifs Natural
secondDuplicateOpenSteps = TReg (SIdx ZIdx)

duplicateLabelFixture ::
  SymTransducer
    (HsPred DuplicateLabelRegs RegisterReplayCmd)
    DuplicateLabelRegs
    Bool
    RegisterReplayCmd
    RegisterReplayEvent
duplicateLabelFixture =
  SymTransducer
    { edgesOut = \case
        False ->
          [ duplicateEdge
              inCompleteNonFinal
              (PCmp CmpGt firstDuplicateOpenSteps (TLit 1)),
            duplicateEdge
              inCompleteFinal
              (PEq secondDuplicateOpenSteps (TLit 1))
          ]
        True -> [],
      initial = False,
      initialRegs =
        RCons
          (Proxy @"openSteps")
          2
          (RCons (Proxy @"openSteps") 1 RNil),
      isFinal = id
    }
  where
    duplicateEdge inputCtor registerCondition =
      Edge
        { guard = PAnd (PInCtor inputCtor) registerCondition,
          update = UKeep,
          output =
            [ pack
                inputCtor
                wireStepCompleted
                ( TInpCtorField
                    inputCtor
                    (#completionId :: Index ReplayCompletionFields Int)
                    *: oNil
                )
            ],
          target = True,
          mode = Live
        }

concreteReplayCandidateCount ::
  (Eq co) =>
  EdgeMode ->
  SymTransducer (HsPred rs ci) rs s ci co ->
  s ->
  RegFile rs ->
  co ->
  Int
concreteReplayCandidateCount candidateMode transducer source registers observed =
  length
    [ ()
    | edge <- edgesOut transducer source,
      mode edge == candidateMode,
      headOutput : _ <- [output edge],
      Just command <- [solveOutput headOutput registers observed],
      models (guard edge) (registers, command)
    ]

data LowerBoundary = LowerStrict | LowerInclusive | LowerEquality

data UpperBoundary = UpperStrict | UpperInclusive | UpperEquality

lowerBoundaryFrom :: Int -> LowerBoundary
lowerBoundaryFrom raw = case abs (toInteger raw) `mod` 3 of
  0 -> LowerStrict
  1 -> LowerInclusive
  _ -> LowerEquality

upperBoundaryFrom :: Int -> UpperBoundary
upperBoundaryFrom raw = case abs (toInteger raw) `mod` 3 of
  0 -> UpperStrict
  1 -> UpperInclusive
  _ -> UpperEquality

boundedNatural :: Int -> Natural
boundedNatural raw = fromInteger (abs (toInteger raw) `mod` 11)

lowerBoundaryPredicate :: LowerBoundary -> Natural -> HsPred RegisterReplayRegs RegisterReplayCmd
lowerBoundaryPredicate LowerStrict value = PCmp CmpGt openSteps (TLit value)
lowerBoundaryPredicate LowerInclusive value = PCmp CmpGe openSteps (TLit value)
lowerBoundaryPredicate LowerEquality value = PEq openSteps (TLit value)

upperBoundaryPredicate :: UpperBoundary -> Natural -> HsPred RegisterReplayRegs RegisterReplayCmd
upperBoundaryPredicate UpperStrict value = PCmp CmpLt openSteps (TLit value)
upperBoundaryPredicate UpperInclusive value = PCmp CmpLe openSteps (TLit value)
upperBoundaryPredicate UpperEquality value = PEq openSteps (TLit value)

intervalAgreementProperty :: Int -> Int -> Int -> Int -> Bool
intervalAgreementProperty rawLower rawUpper rawLowerKind rawUpperKind =
  warningSuppressed == not concreteOverlapExists
  where
    lower = boundedNatural rawLower
    upper = boundedNatural rawUpper
    leftPredicate = lowerBoundaryPredicate (lowerBoundaryFrom rawLowerKind) lower
    rightPredicate = upperBoundaryPredicate (upperBoundaryFrom rawUpperKind) upper
    transducer =
      customRegisterReplayFixture
        0
        Live
        (const leftPredicate)
        (const rightPredicate)
    warningSuppressed = null (inversionAmbiguityWarnings transducer)
    concreteOverlapExists =
      any
        ( \registerValue ->
            concreteReplayCandidateCount
              Live
              transducer
              False
              (registerReplayRegs registerValue)
              (StepCompleted (ReplayCompletionData 7))
              == 2
        )
        [0 .. 12]

type ReadRegs = '[ '("seen", Int)]

readGuardTransducer :: HsPred ReadRegs AmbiguousCmd -> SymTransducer (HsPred ReadRegs AmbiguousCmd) ReadRegs Bool AmbiguousCmd ()
readGuardTransducer edgeGuard =
  SymTransducer
    { edgesOut = \case
        False ->
          [ Edge
              { guard = edgeGuard,
                update =
                  USet
                    (#seen :: IndexN "seen" ReadRegs Int)
                    (TInpCtorField inCtorX (#value :: Index AmbiguousFields Int)),
                output = [],
                target = True,
                mode = Live
              }
          ]
        True -> [],
      initial = False,
      initialRegs = RCons (Proxy @"seen") 0 RNil,
      isFinal = id
    }

unguardedReadTransducer :: SymTransducer (HsPred ReadRegs AmbiguousCmd) ReadRegs Bool AmbiguousCmd ()
unguardedReadTransducer = readGuardTransducer PTop

safeReadTransducer :: SymTransducer (HsPred ReadRegs AmbiguousCmd) ReadRegs Bool AmbiguousCmd ()
safeReadTransducer = readGuardTransducer (PAnd (matchInCtor inCtorX) PTop)

wrongOrderReadTransducer :: SymTransducer (HsPred ReadRegs AmbiguousCmd) ReadRegs Bool AmbiguousCmd ()
wrongOrderReadTransducer =
  readGuardTransducer
    ( PAnd
        (PEq (TInpCtorField inCtorX (#value :: Index AmbiguousFields Int)) (TLit 7))
        (matchInCtor inCtorX)
    )

rightOrderReadTransducer :: SymTransducer (HsPred ReadRegs AmbiguousCmd) ReadRegs Bool AmbiguousCmd ()
rightOrderReadTransducer =
  readGuardTransducer
    ( PAnd
        (matchInCtor inCtorX)
        (PEq (TInpCtorField inCtorX (#value :: Index AmbiguousFields Int)) (TLit 7))
    )

data EpsilonVertex = EpsilonStart | EpsilonEnd
  deriving stock (Eq, Ord, Show, Enum, Bounded)

data EpsilonCase
  = ChangesVertexOnly
  | WritesRegistersOnly
  | ChangesBoth
  | NoOpSelfLoop

epsilonTransducer :: EpsilonCase -> SymTransducer (HsPred ReadRegs AmbiguousCmd) ReadRegs EpsilonVertex AmbiguousCmd ()
epsilonTransducer epsilonCase =
  SymTransducer
    { edgesOut = \case
        EpsilonStart ->
          case epsilonCase of
            ChangesVertexOnly ->
              [Edge (matchInCtor inCtorX) UKeep [] EpsilonEnd Live]
            WritesRegistersOnly ->
              [Edge (matchInCtor inCtorX) setSeen [] EpsilonStart Live]
            ChangesBoth ->
              [Edge (matchInCtor inCtorX) setSeen [] EpsilonEnd Live]
            NoOpSelfLoop ->
              [Edge (matchInCtor inCtorX) UKeep [] EpsilonStart Live]
        EpsilonEnd -> [],
      initial = EpsilonStart,
      initialRegs = RCons (Proxy @"seen") 0 RNil,
      isFinal = (== EpsilonEnd)
    }
  where
    setSeen =
      USet
        (#seen :: IndexN "seen" ReadRegs Int)
        (TInpCtorField inCtorX (#value :: Index AmbiguousFields Int))

spec :: Spec
spec = do
  describe "validate-clean transducers replay their own logs" $ do
    it "splitCoverageFixed replays its own log" $ do
      Just (forwardVertex, RNil, emitted) <-
        pure (runCommands splitCoverageFixed [Begin 1 2 3])
      emitted `shouldBe` [OutABC 1 2 3, OutBC 2 3]
      validateTransducer defaultValidationOptions splitCoverageFixed `shouldBe` []
      case reconstitute splitCoverageFixed emitted of
        Just (replayVertex, RNil) -> replayVertex `shouldBe` forwardVertex
        Nothing -> expectationFailure "splitCoverageFixed did not replay its own log"

    it "registerEmission replays command fields and TReg audit fields" $ do
      Just (forwardVertex, forwardRegs, emitted) <-
        pure (runCommands registerEmission registerCommands)
      emitted
        `shouldBe` [Opened "alice", Added 7 "alice", Closed "alice", Archived "alice"]
      validateTransducer defaultValidationOptions registerEmission `shouldBe` []
      case reconstitute registerEmission emitted of
        Just (replayVertex, replayRegs) -> do
          replayVertex `shouldBe` forwardVertex
          (replayRegs ! (#owner :: Index RegisterEmissionRegs Text))
            `shouldBe` (forwardRegs ! (#owner :: Index RegisterEmissionRegs Text))
          (replayRegs ! (#total :: Index RegisterEmissionRegs Int))
            `shouldBe` (forwardRegs ! (#total :: Index RegisterEmissionRegs Int))
        Nothing -> expectationFailure "registerEmission did not replay its own log"

    it "emailDelivery validates clean and replays its own log" $ do
      let cmd =
            SendEmail
              SendEmailData
                { recipient = "alice@example.com",
                  subject = "hello",
                  at = atTime 0
                }
      Just (forwardVertex, forwardRegs, emitted) <- pure (runCommands emailDelivery [cmd])
      validateTransducer defaultValidationOptions emailDelivery `shouldBe` []
      case reconstitute emailDelivery emitted of
        Just (replayVertex, replayRegs) -> do
          replayVertex `shouldBe` forwardVertex
          (replayRegs ! (#emailRecipient :: Index EmailRegs Text))
            `shouldBe` (forwardRegs ! (#emailRecipient :: Index EmailRegs Text))
          (replayRegs ! (#emailSubject :: Index EmailRegs Text))
            `shouldBe` (forwardRegs ! (#emailSubject :: Index EmailRegs Text))
          (replayRegs ! (#emailSentAt :: Index EmailRegs UTCTime))
            `shouldBe` (forwardRegs ! (#emailSentAt :: Index EmailRegs UTCTime))
        Nothing -> expectationFailure "emailDelivery did not replay its own log"

    it "userReg's persisted canonical path replays its own log" $ do
      let commands =
            [ StartRegistration (StartRegistrationData "alice@x" "Z9F4" (atTime 0)),
              ResendConfirmation (ResendConfirmationData "K2P7" (atTime 100)),
              ConfirmAccount (ConfirmAccountData "K2P7" (atTime 200)),
              FulfillGDPRRequest (FulfillGDPRRequestData (atTime 300))
            ]
      Just (forwardVertex, forwardRegs, emitted) <- pure (runCommands userReg commands)
      validateTransducer defaultValidationOptions userReg `shouldBe` []
      case reconstitute userReg emitted of
        Just (replayVertex, replayRegs) -> do
          replayVertex `shouldBe` forwardVertex
          (replayRegs ! (#email :: Index UserRegRegs Text))
            `shouldBe` (forwardRegs ! (#email :: Index UserRegRegs Text))
          (replayRegs ! (#confirmCode :: Index UserRegRegs Text))
            `shouldBe` (forwardRegs ! (#confirmCode :: Index UserRegRegs Text))
          (replayRegs ! (#registeredAt :: Index UserRegRegs UTCTime))
            `shouldBe` (forwardRegs ! (#registeredAt :: Index UserRegRegs UTCTime))
          (replayRegs ! (#confirmedAt :: Index UserRegRegs UTCTime))
            `shouldBe` (forwardRegs ! (#confirmedAt :: Index UserRegRegs UTCTime))
          (replayRegs ! (#deletedAt :: Index UserRegRegs UTCTime))
            `shouldBe` (forwardRegs ! (#deletedAt :: Index UserRegRegs UTCTime))
        Nothing -> expectationFailure "userReg did not replay its persisted path"

  describe "split-coverage counterexample" $ do
    it "produces a log that its current validator accepts but replay rejects" $ do
      Just (True, RNil, emitted) <- pure (runCommands splitCoverageBad [Begin 1 2 3])
      emitted `shouldBe` [OutAB 1 2, OutBC 2 3]
      case reconstitute splitCoverageBad emitted of
        Nothing -> pure ()
        Just _ -> expectationFailure "splitCoverageBad unexpectedly replayed its own log"

    it "validator flags the head-unrecoverable edge" $ do
      let warnings = validateTransducer defaultValidationOptions splitCoverageBad
          isHeadWarning
            ( HeadUnrecoverable
                { tvwEdge = EdgeRef {edgeSource = False, edgeIndex = 0},
                  tvwInCtor = Just "Begin",
                  tvwTailOnlySlots = ["c"]
                }
              ) = True
          isHeadWarning _ = False
      warnings `shouldSatisfy` any isHeadWarning

  describe "cross-edge inversion ambiguity" $ do
    it "predicts the replay failure for two equal head wire constructors" $ do
      Just (True, RNil, emitted) <- pure (runCommands ambiguousTransducer [CmdX 7])
      emitted `shouldBe` [Logged 7]
      case reconstitute ambiguousTransducer emitted of
        Nothing -> pure ()
        Just _ -> expectationFailure "same-head transducer unexpectedly replayed"
      let warnings = validateTransducer defaultValidationOptions ambiguousTransducer
          isAmbiguous
            ( InversionAmbiguity
                { tvwSource = False,
                  tvwEdgeA = 0,
                  tvwEdgeB = 1,
                  tvwWireCtor = "Logged"
                }
              ) = True
          isAmbiguous _ = False
      warnings `shouldSatisfy` any isAmbiguous

    it "distinct head wire constructors validate and replay" $ do
      Just (True, RNil, emitted) <- pure (runCommands distinctHeadTransducer [CmdY 9])
      emitted `shouldBe` [LoggedY 9]
      validateTransducer defaultValidationOptions distinctHeadTransducer `shouldBe` []
      case reconstitute distinctHeadTransducer emitted of
        Just (True, RNil) -> pure ()
        _ -> expectationFailure "distinct-head transducer did not replay"

  describe "shared-register replay candidate disjointness" $ do
    it "suppresses the false positive for openSteps > 1 versus openSteps == 1" $
      inversionAmbiguityWarnings (registerDisjointFixture 2 Live)
        `shouldBe` []

    it "preserves forward/replay agreement for both non-final and final register paths" $ do
      let cases =
            [ (2, CompleteNonFinal (ReplayCompletionData 7)),
              (1, CompleteFinal (ReplayCompletionData 9))
            ]
      mapM_
        ( \(initialOpenSteps, command) -> do
            let transducer = registerDisjointFixture initialOpenSteps Live
            case runCommands transducer [command] of
              Just (forwardVertex, forwardRegs, emitted) -> do
                emitted
                  `shouldBe` [StepCompleted (ReplayCompletionData (commandCompletionId command))]
                case reconstitute transducer emitted of
                  Just (replayVertex, replayRegs) -> do
                    replayVertex `shouldBe` forwardVertex
                    replayRegs ! (#openSteps :: Index RegisterReplayRegs Natural)
                      `shouldBe` (forwardRegs ! (#openSteps :: Index RegisterReplayRegs Natural))
                  Nothing -> expectationFailure "register-disjoint fixture did not replay"
              Nothing -> expectationFailure "register-disjoint fixture did not step"
        )
        cases

    it "bounds every concrete candidate count for the suppressed pair across registers, events, and modes" $ do
      forM_ [Live, ReplayOnly] $ \candidateMode ->
        forM_ [0 .. 5] $ \registerValue -> do
          let transducer = registerDisjointFixture registerValue candidateMode
              registers = registerReplayRegs registerValue
          inversionAmbiguityWarnings transducer `shouldBe` []
          forM_ [-2 .. 2] $ \observedId ->
            concreteReplayCandidateCount
              candidateMode
              transducer
              False
              registers
              (StepCompleted (ReplayCompletionData observedId))
              `shouldSatisfy` (<= 1)

    it "agrees with concrete candidates for generated strict, inclusive, and equality boundaries" $
      property intervalAgreementProperty

    it "retains the overlapping warning with its opaque precision blocker and exhibits two concrete candidates" $ do
      case inversionAmbiguityWarnings registerOverlappingFixture of
        [InversionAmbiguity {tvwDetail = detail}] -> detail `shouldContain` "TApp1"
        other -> expectationFailure ("expected one overlap warning, got " <> show other)
      case reconstituteEither
        registerOverlappingFixture
        [StepCompleted (ReplayCompletionData 7)] of
        Left failure ->
          case replayFailureReason failure of
            ReplayEventFailed (ReplayAmbiguousInversions False matchedEdges) ->
              map (edgeIndex . matchedEdge) matchedEdges `shouldBe` [0, 1]
            other ->
              expectationFailure ("expected ReplayAmbiguousInversions, got " <> show other)
        Right result ->
          expectationFailure ("expected ambiguous replay, got " <> show (fst result))
      concreteReplayCandidateCount
        Live
        registerOverlappingFixture
        False
        (registerReplayRegs 2)
        (StepCompleted (ReplayCompletionData 7))
        `shouldBe` 2

    it "names the opaque conjunct when only command-dependent conditions remain" $
      case inversionAmbiguityWarnings opaqueOnlyFixture of
        [InversionAmbiguity {tvwDetail = detail}] -> detail `shouldContain` "TApp1"
        other -> expectationFailure ("expected one opaque-only warning, got " <> show other)

    it "names an unsupported register carrier and fails conservatively" $
      case inversionAmbiguityWarnings unsupportedCarrierFixture of
        [InversionAmbiguity {tvwDetail = detail}] -> do
          detail `shouldContain` "unsupported register carrier"
          detail `shouldContain` "Bool"
        other -> expectationFailure ("expected one unsupported-carrier warning, got " <> show other)

    it "retains every unsupported guard shape unless a supported sibling proves disjointness" $ do
      forM_ unsupportedRegisterConditions $ \(label, expectedBlocker, condition) -> do
        let blockedFixture = customRegisterReplayFixture 2 Live condition condition
        case inversionAmbiguityWarnings blockedFixture of
          [InversionAmbiguity {tvwDetail = detail}] ->
            detail `shouldContain` expectedBlocker
          other ->
            expectationFailure
              ("expected one " <> label <> " warning, got " <> show other)

        let contradictedFixture =
              customRegisterReplayFixture
                2
                Live
                ( \inputCtor ->
                    PAnd
                      (condition inputCtor)
                      (PCmp CmpGt openSteps (TLit 1))
                )
                ( \inputCtor ->
                    PAnd
                      (condition inputCtor)
                      (PEq openSteps (TLit 1))
                )
        inversionAmbiguityWarnings contradictedFixture `shouldBe` []

    it "does not merge distinct duplicate-labelled register positions" $ do
      case inversionAmbiguityWarnings duplicateLabelFixture of
        [InversionAmbiguity {tvwDetail = detail}] -> do
          detail `shouldContain` "distinct positions [0,1]"
          detail `shouldContain` "duplicate label \"openSteps\""
        other -> expectationFailure ("expected one duplicate-label warning, got " <> show other)
      case reconstituteEither
        duplicateLabelFixture
        [StepCompleted (ReplayCompletionData 7)] of
        Left failure ->
          case replayFailureReason failure of
            ReplayEventFailed (ReplayAmbiguousInversions False matchedEdges) ->
              map (edgeIndex . matchedEdge) matchedEdges `shouldBe` [0, 1]
            other ->
              expectationFailure ("expected duplicate-label ambiguity, got " <> show other)
        Right result ->
          expectationFailure ("expected duplicate-label ambiguity, got " <> show (fst result))

  describe "guard implies input reads" $ do
    let isUnguarded
          ( UnguardedInputRead
              { tvwEdge = EdgeRef {edgeSource = False, edgeIndex = 0},
                tvwInCtor = Just "CmdX"
              }
            ) = True
        isUnguarded _ = False

    it "flags a PTop-guarded update read" $
      guardImpliesInputReadWarnings unguardedReadTransducer
        `shouldSatisfy` any isUnguarded

    it "accepts a read protected by an earlier constructor guard" $ do
      guardImpliesInputReadWarnings safeReadTransducer `shouldBe` []
      case step safeReadTransducer (False, initialRegs safeReadTransducer) (CmdY 3) of
        Nothing -> pure ()
        Just _ -> expectationFailure "safe constructor guard accepted CmdY"

    it "flags a guard read that appears before its constructor guard" $
      guardImpliesInputReadWarnings wrongOrderReadTransducer
        `shouldSatisfy` any isUnguarded

    it "accepts a guard read after its constructor guard" $
      guardImpliesInputReadWarnings rightOrderReadTransducer `shouldBe` []

    it "predicts the runtime TInpCtorField crash" $
      evaluate
        ( case step unguardedReadTransducer (False, initialRegs unguardedReadTransducer) (CmdY 3) of
            Just (_, regs, _) -> regs ! (#seen :: Index ReadRegs Int)
            Nothing -> 0
        )
        `shouldThrow` errorCall "evalTerm: TInpCtorField guard violation: CmdX"

  describe "state-changing epsilon" $ do
    let warningShape transducer =
          [ (tvwChangesVertex, tvwWritesRegisters)
          | StateChangingEpsilon
              { tvwEdge = EdgeRef {edgeSource = EpsilonStart, edgeIndex = 0},
                tvwChangesVertex,
                tvwWritesRegisters
              } <-
              stateChangingEpsilonWarnings transducer
          ]

    it "reports vertex-only, register-only, and combined changes exactly" $ do
      warningShape (epsilonTransducer ChangesVertexOnly) `shouldBe` [(True, False)]
      warningShape (epsilonTransducer WritesRegistersOnly) `shouldBe` [(False, True)]
      warningShape (epsilonTransducer ChangesBoth) `shouldBe` [(True, True)]

    it "keeps a UKeep self-loop clean" $
      validateTransducer defaultValidationOptions (epsilonTransducer NoOpSelfLoop)
        `shouldBe` []

    it "allows only this check to be disabled explicitly" $
      validateTransducer
        defaultValidationOptions {checkStateChangingEpsilon = False}
        (epsilonTransducer ChangesVertexOnly)
        `shouldBe` []

    it "predicts empty-log replay divergence" $ do
      let transducer = epsilonTransducer ChangesVertexOnly
      Just (EpsilonEnd, _, emitted) <- pure (runCommands transducer [CmdX 7])
      emitted `shouldBe` []
      case reconstitute transducer emitted of
        Just (EpsilonStart, _) -> pure ()
        _ -> expectationFailure "empty log unexpectedly reproduced the forward vertex"

    it "does not let the hidden-input and state-change checks mask each other" $ do
      let warnings = validateTransducer defaultValidationOptions (epsilonTransducer ChangesBoth)
      warnings `shouldSatisfy` any (\case HiddenInput {} -> True; _ -> False)
      warnings `shouldSatisfy` any (\case StateChangingEpsilon {} -> True; _ -> False)