packages feed

keiki-0.5.0.0: test/Keiki/ValidationSpec.hs

module Keiki.ValidationSpec (spec) where

import Data.List (isInfixOf)
import Data.Proxy (Proxy (..))
import Data.Word (Word8)
import Keiki.Core
import Keiki.FieldProjSpec qualified as FieldProj
import Keiki.Symbolic (checkDeadEdgesSym, checkTransitionDeterminismSym)
import Numeric.Natural (Natural)
import Test.Hspec

-- A tiny two-constructor command for guards.
data Cmd = Foo | Bar
  deriving stock (Eq, Show)

inCtorFoo :: InCtor Cmd '[]
inCtorFoo =
  InCtor
    { icName = "Foo",
      icMatch = \case Foo -> Just RNil; _ -> Nothing,
      icBuild = \RNil -> Foo
    }

inCtorBar :: InCtor Cmd '[]
inCtorBar =
  InCtor
    { icName = "Bar",
      icMatch = \case Bar -> Just RNil; _ -> Nothing,
      icBuild = \RNil -> Bar
    }

data VEvent = Fooed | Bared
  deriving stock (Eq, Show)

wireFooed :: WireCtor VEvent ()
wireFooed =
  WireCtor
    { wcName = "Fooed",
      wcMatch = \case Fooed -> Just (); _ -> Nothing,
      wcBuild = \() -> Fooed
    }

wireBared :: WireCtor VEvent ()
wireBared =
  WireCtor
    { wcName = "Bared",
      wcMatch = \case Bared -> Just (); _ -> Nothing,
      wcBuild = \() -> Bared
    }

-- A three-state enum: Start (reachable), Mid (reachable), Orphan (unreachable).
data V = Start | Mid | Orphan
  deriving stock (Eq, Ord, Show, Enum, Bounded)

-- (a) overlapping guards out of Start (both PTop).
overlapT :: SymTransducer (HsPred '[] Cmd) '[] V Cmd ()
overlapT =
  SymTransducer
    { edgesOut = \case
        Start ->
          [ Edge {guard = PTop, update = UKeep, output = [], target = Mid, mode = Live},
            Edge {guard = PTop, update = UKeep, output = [], target = Mid, mode = Live}
          ]
        _ -> [],
      initial = Start,
      initialRegs = RNil,
      isFinal = (== Mid)
    }

-- (b) an edge leaving the unreachable Orphan vertex.
deadT :: SymTransducer (HsPred '[] Cmd) '[] V Cmd ()
deadT =
  SymTransducer
    { edgesOut = \case
        Start -> [Edge {guard = matchInCtor inCtorFoo, update = UKeep, output = [], target = Mid, mode = Live}]
        Orphan -> [Edge {guard = PTop, update = UKeep, output = [], target = Start, mode = Live}]
        _ -> [],
      initial = Start,
      initialRegs = RNil,
      isFinal = (== Mid)
    }

-- (c) a literal-PBot guard on a reachable edge.
botT :: SymTransducer (HsPred '[] Cmd) '[] V Cmd ()
botT =
  SymTransducer
    { edgesOut = \case
        Start -> [Edge {guard = PBot, update = UKeep, output = [], target = Mid, mode = Live}]
        _ -> [],
      initial = Start,
      initialRegs = RNil,
      isFinal = (== Mid)
    }

-- (d) a clean transducer: mutually exclusive guards, every vertex with edges
-- is reachable, no overlapping/PBot guards. (Orphan has no outgoing edges, so
-- although it is structurally unreachable it contributes no edge to flag.)
cleanT :: SymTransducer (HsPred '[] Cmd) '[] V Cmd VEvent
cleanT =
  SymTransducer
    { edgesOut = \case
        Start ->
          [ Edge {guard = matchInCtor inCtorFoo, update = UKeep, output = [pack inCtorFoo wireFooed oNil], target = Mid, mode = Live},
            Edge {guard = matchInCtor inCtorBar, update = UKeep, output = [pack inCtorBar wireBared oNil], target = Mid, mode = Live}
          ]
        _ -> [],
      initial = Start,
      initialRegs = RNil,
      isFinal = (== Mid)
    }

-- (e) sym-only overlap: one PTop and one PInCtor edge out of Start. They DO
-- overlap (PTop always holds; the Foo guard holds on Foo) but the structural
-- pure path cannot prove it (neither both-PTop nor same-ctor), so only the
-- symbolic determinism check flags it.
symOverlapT :: SymTransducer (HsPred '[] Cmd) '[] V Cmd ()
symOverlapT =
  SymTransducer
    { edgesOut = \case
        Start ->
          [ Edge {guard = matchInCtor inCtorFoo, update = UKeep, output = [], target = Mid, mode = Live},
            Edge {guard = PTop, update = UKeep, output = [], target = Mid, mode = Live}
          ]
        _ -> [],
      initial = Start,
      initialRegs = RNil,
      isFinal = (== Mid)
    }

-- (f) an opaque collection-style guard (EP-67): the guard lifts list membership
-- through a TApp closure the symbolic analyses cannot see through. The register
-- slot holds a collection; the guard asks "is 5 in items?" via `elem`, which has
-- no structural keiki node, so it is forced through TApp1.
type ItemRegs = '[ '("items", [Int])]

opaqueT :: SymTransducer (HsPred ItemRegs Cmd) ItemRegs V Cmd VEvent
opaqueT =
  SymTransducer
    { edgesOut = \case
        Start ->
          [ Edge
              { guard =
                  PEq
                    (TApp1 (5 `elem`) (TReg (ZIdx :: Index ItemRegs [Int])))
                    (TLit True),
                update = UKeep,
                output = [pack inCtorFoo wireFooed oNil],
                target = Mid,
                mode = Live
              }
          ]
        _ -> [],
      initial = Start,
      initialRegs = RCons (Proxy @"items") [] RNil,
      isFinal = (== Mid)
    }

-- Natural equality and ordering are symbolic, but generic TArith is opaque
-- because its type-wide registry would also expose partial subtraction.
type NaturalRegs = '[ '("n", Natural)]

naturalIdx :: Index NaturalRegs Natural
naturalIdx = ZIdx

naturalArithmeticOpaqueT ::
  SymTransducer (HsPred NaturalRegs Cmd) NaturalRegs V Cmd ()
naturalArithmeticOpaqueT =
  SymTransducer
    { edgesOut = \case
        Start ->
          [ Edge
              { guard =
                  PCmp
                    CmpGt
                    (tadd (proj naturalIdx) (TLit 1))
                    (TLit 0),
                update = UKeep,
                output = [],
                target = Start,
                mode = Live
              }
          ]
        _ -> [],
      initial = Start,
      initialRegs = RCons (Proxy @"n") 0 RNil,
      isFinal = const False
    }

-- A 3-slot input constructor, mirroring CoreHiddenInputsGSMSpec, used to build
-- a hidden-input edge (its output recovers only slots a, b — never c).
data MultiInput = Begin Int Int Int
  deriving stock (Eq, Show)

data MultiOutput = OutAB Int Int
  deriving stock (Eq, Show)

inCtorBegin :: InCtor MultiInput '[ '("a", Int), '("b", Int), '("c", Int)]
inCtorBegin =
  InCtor
    { icName = "Begin",
      icMatch = \case
        Begin a b c ->
          Just $
            RCons (Proxy @"a") a $
              RCons (Proxy @"b") b $
                RCons (Proxy @"c") c $
                  RNil,
      icBuild = \(RCons _ a (RCons _ b (RCons _ c RNil))) -> Begin a b c
    }

wcAB :: WireCtor MultiOutput (Int, (Int, ()))
wcAB =
  WireCtor
    { wcName = "OutAB",
      wcMatch = \case OutAB a b -> Just (a, (b, ())),
      wcBuild = \(a, (b, ())) -> OutAB a b
    }

-- A two-state transducer whose only edge recovers slots {a, b} but not {c},
-- so slot c is a hidden input.
hiddenT :: SymTransducer (HsPred '[] MultiInput) '[] Bool MultiInput MultiOutput
hiddenT =
  SymTransducer
    { edgesOut = \case
        False ->
          [ Edge
              { guard = matchInCtor inCtorBegin,
                update = UKeep,
                output =
                  [ pack
                      inCtorBegin
                      wcAB
                      ( OFCons
                          (TInpCtorField inCtorBegin (#a :: Index '[ '("a", Int), '("b", Int), '("c", Int)] Int))
                          (OFCons (TInpCtorField inCtorBegin (#b :: Index '[ '("a", Int), '("b", Int), '("c", Int)] Int)) OFNil)
                      )
                  ],
                target = True,
                mode = Live
              }
          ]
        True -> [],
      initial = False,
      initialRegs = RNil,
      isFinal = id
    }

-- Pure-overlap fixtures (EP-76). Every edge is a state-preserving self-loop so
-- the default validation result isolates determinism from epsilon-state-change
-- diagnostics.
type OverlapRegs = '[ '("x", Int)]

xIdx :: Index OverlapRegs Int
xIdx = ZIdx

overlapFixture ::
  HsPred OverlapRegs Cmd ->
  HsPred OverlapRegs Cmd ->
  SymTransducer (HsPred OverlapRegs Cmd) OverlapRegs V Cmd ()
overlapFixture leftGuard rightGuard =
  SymTransducer
    { edgesOut = \case
        Start ->
          [ Edge leftGuard UKeep [] Start Live,
            Edge rightGuard UKeep [] Start Live
          ]
        _ -> [],
      initial = Start,
      initialRegs = RCons (Proxy @"x") 0 RNil,
      isFinal = const False
    }

fooWith :: HsPred OverlapRegs Cmd -> HsPred OverlapRegs Cmd
fooWith = PAnd (PInCtor inCtorFoo)

barWith :: HsPred OverlapRegs Cmd -> HsPred OverlapRegs Cmd
barWith = PAnd (PInCtor inCtorBar)

motivatingOverlapT ::
  SymTransducer (HsPred OverlapRegs Cmd) OverlapRegs V Cmd ()
motivatingOverlapT =
  overlapFixture
    (fooWith (PCmp CmpGt (proj xIdx) (TLit 0)))
    (fooWith (PCmp CmpGt (proj xIdx) (TLit 5)))

disjointOverlapT ::
  SymTransducer (HsPred OverlapRegs Cmd) OverlapRegs V Cmd ()
disjointOverlapT =
  overlapFixture
    (fooWith (PCmp CmpGt (proj xIdx) (TLit 5)))
    (fooWith (PCmp CmpLt (proj xIdx) (TLit 3)))

unknownOrT ::
  SymTransducer (HsPred OverlapRegs Cmd) OverlapRegs V Cmd ()
unknownOrT =
  overlapFixture
    ( fooWith
        ( POr
            (PCmp CmpGt (proj xIdx) (TLit 0))
            (PCmp CmpLt (proj xIdx) (TLit 0))
        )
    )
    (fooWith (PCmp CmpGt (proj xIdx) (TLit 5)))

unknownOpaqueT ::
  SymTransducer (HsPred OverlapRegs Cmd) OverlapRegs V Cmd ()
unknownOpaqueT =
  overlapFixture
    ( fooWith
        (PCmp CmpGt (TApp1 id (proj xIdx)) (TLit 0))
    )
    (fooWith (PCmp CmpGt (proj xIdx) (TLit 5)))

intArithmeticStructuralT ::
  SymTransducer (HsPred OverlapRegs Cmd) OverlapRegs V Cmd ()
intArithmeticStructuralT =
  SymTransducer
    { edgesOut = \case
        Start ->
          [ Edge
              (PCmp CmpGt (tadd (proj xIdx) (TLit 1)) (TLit 0))
              UKeep
              []
              Start
              Live
          ]
        _ -> [],
      initial = Start,
      initialRegs = RCons (Proxy @"x") 0 RNil,
      isFinal = const False
    }

naturalInteriorOverlapT ::
  SymTransducer (HsPred NaturalRegs Cmd) NaturalRegs V Cmd ()
naturalInteriorOverlapT =
  SymTransducer
    { edgesOut = \case
        Start ->
          [ Edge
              (PCmp CmpGt (proj naturalIdx) (TLit 1))
              UKeep
              []
              Start
              Live,
            Edge
              (PCmp CmpLt (proj naturalIdx) (TLit 3))
              UKeep
              []
              Start
              Live
          ]
        _ -> [],
      initial = Start,
      initialRegs = RCons (Proxy @"n") 0 RNil,
      isFinal = const False
    }

differentCtorT ::
  SymTransducer (HsPred OverlapRegs Cmd) OverlapRegs V Cmd ()
differentCtorT =
  overlapFixture
    (fooWith (PCmp CmpGt (proj xIdx) (TLit 0)))
    (barWith (PCmp CmpGt (proj xIdx) (TLit 5)))

type ByteOverlapRegs = '[ '("x", Word8)]

byteOverlapIdx :: Index ByteOverlapRegs Word8
byteOverlapIdx = ZIdx

disjointWord8T ::
  SymTransducer (HsPred ByteOverlapRegs Cmd) ByteOverlapRegs V Cmd ()
disjointWord8T =
  SymTransducer
    { edgesOut = \case
        Start ->
          [ Edge
              ( PAnd
                  (PInCtor inCtorFoo)
                  (PCmp CmpGe (proj byteOverlapIdx) (TLit 200))
              )
              UKeep
              []
              Start
              Live,
            Edge
              ( PAnd
                  (PInCtor inCtorFoo)
                  (PCmp CmpLe (proj byteOverlapIdx) (TLit 100))
              )
              UKeep
              []
              Start
              Live
          ]
        _ -> [],
      initial = Start,
      initialRegs = RCons (Proxy @"x") 0 RNil,
      isFinal = const False
    }

type BoolOverlapRegs = '[ '("x", Bool)]

boolOverlapIdx :: Index BoolOverlapRegs Bool
boolOverlapIdx = ZIdx

boolLiteralWitnessT ::
  SymTransducer (HsPred BoolOverlapRegs Cmd) BoolOverlapRegs V Cmd ()
boolLiteralWitnessT =
  SymTransducer
    { edgesOut = \case
        Start ->
          [ Edge
              (PAnd (PInCtor inCtorFoo) (PEq (proj boolOverlapIdx) (TLit True)))
              UKeep
              []
              Start
              Live,
            Edge
              (PAnd (PInCtor inCtorFoo) (PEq (TLit True) (proj boolOverlapIdx)))
              UKeep
              []
              Start
              Live
          ]
        _ -> [],
      initial = Start,
      initialRegs = RCons (Proxy @"x") False RNil,
      isFinal = const False
    }

spec :: Spec
spec = do
  describe "validateTransducer (pure, no solver)" $ do
    it "clean transducer yields no warnings" $
      validateTransducer defaultValidationOptions cleanT `shouldBe` []

    it "overlapping pair yields a NondeterministicPair naming both indices and source" $ do
      let isOverlapStart (NondeterministicPair {tvwSource = Start, tvwEdgeA = 0, tvwEdgeB = 1}) = True
          isOverlapStart _ = False
      filter isOverlapStart (validateTransducer defaultValidationOptions overlapT)
        `shouldSatisfy` (not . null)

    it "edge from an unreachable vertex yields a PossiblyDeadEdge" $ do
      let isDeadOrphan (PossiblyDeadEdge {tvwEdge = EdgeRef {edgeSource = Orphan, edgeIndex = 0}}) = True
          isDeadOrphan _ = False
      validateTransducer defaultValidationOptions deadT
        `shouldSatisfy` any isDeadOrphan

    it "literal-PBot guard on a reachable edge yields a PossiblyDeadEdge" $ do
      let isBotDead (PossiblyDeadEdge {tvwEdge = EdgeRef {edgeSource = Start, edgeIndex = 0}, tvwDetail = d}) =
            "unsatisfiable" `isInfixOf` d
          isBotDead _ = False
      validateTransducer defaultValidationOptions botT
        `shouldSatisfy` any isBotDead

  describe "validateTransducer hidden-input (structured)" $ do
    it "flags slot c as a hidden input with structured ctor/slot data" $ do
      let warnings = validateTransducer defaultValidationOptions hiddenT
          isHiddenC (HiddenInput {tvwEdge = EdgeRef {edgeSource = False, edgeIndex = 0}, tvwInCtor = Just "Begin", tvwMissingSlots = ms}) =
            "c" `elem` ms
          isHiddenC _ = False
      warnings `shouldSatisfy` any isHiddenC

  describe "ValidationOptions toggles" $ do
    it "disabling determinism suppresses NondeterministicPair" $ do
      let opts = defaultValidationOptions {checkDeterminism = False}
          isND (NondeterministicPair {}) = True
          isND _ = False
      filter isND (validateTransducer opts overlapT) `shouldBe` []

  describe "opaque-guard audit (EP-67, opt-in)" $ do
    let optsOn = defaultValidationOptions {warnOpaqueGuards = True}
        isOpaqueStart (OpaqueGuard {tvwEdge = EdgeRef {edgeSource = Start, edgeIndex = 0}}) = True
        isOpaqueStart _ = False

    it "an opaque collection-style guard is flagged when the audit is on" $
      validateTransducer optsOn opaqueT `shouldSatisfy` any isOpaqueStart

    it "unsupported Natural arithmetic is flagged when the audit is on" $
      validateTransducer optsOn naturalArithmeticOpaqueT
        `shouldSatisfy` any isOpaqueStart

    it "supported Int arithmetic remains structural" $ do
      let isOpaque (OpaqueGuard {}) = True
          isOpaque _ = False
      validateTransducer optsOn intArithmeticStructuralT
        `shouldSatisfy` (not . any isOpaque)

    it "a fully structural transducer is never flagged, even with the audit on" $ do
      let isOpaque (OpaqueGuard {}) = True
          isOpaque _ = False
      filter isOpaque (validateTransducer optsOn cleanT) `shouldBe` []

    it "the audit is silent under defaultValidationOptions (backward compat)" $
      validateTransducer defaultValidationOptions opaqueT `shouldBe` []

  describe "typed field projection validation" $ do
    let validOutput =
          [ pack
              FieldProj.newDocCtor
              FieldProj.docAcceptedWire
              (OFCons (TInpCtorField FieldProj.newDocCtor #doc) OFNil)
          ]
        projectionWarnings fixture =
          validateTransducer defaultValidationOptions fixture

    it "accepts supported Text equality and stays out of OpaqueGuard" $ do
      let warnings =
            validateTransducer
              defaultValidationOptions {warnOpaqueGuards = True}
              FieldProj.docProjectionTransducer
      warnings `shouldBe` []

    it "rejects a projection result outside the symbolic registry" $ do
      let fixture =
            FieldProj.docProjectionTransducer
              { edgesOut = \FieldProj.DocState ->
                  [ Edge
                      { guard =
                          PAnd
                            (matchInCtor FieldProj.newDocCtor)
                            ( regProj FieldProj.docNumbersW FieldProj.docIx
                                .== inpProj FieldProj.docNumbersW FieldProj.newDocCtor #doc
                            ),
                        update = UKeep,
                        output = validOutput,
                        target = FieldProj.DocState,
                        mode = Live
                      }
                  ]
              }
          isUnsupported ProjectionResultUnsupported {} = True
          isUnsupported _ = False
      projectionWarnings fixture `shouldSatisfy` any isUnsupported

    it "rejects ordering over a Text projection while equality remains supported" $ do
      let fixture =
            FieldProj.docProjectionTransducer
              { edgesOut = \FieldProj.DocState ->
                  [ Edge
                      { guard =
                          PAnd
                            (matchInCtor FieldProj.newDocCtor)
                            ( PCmp
                                CmpLt
                                (regProj FieldProj.docHashW FieldProj.docIx)
                                (TLit "z")
                            ),
                        update = UKeep,
                        output = validOutput,
                        target = FieldProj.DocState,
                        mode = Live
                      }
                  ]
              }
          isOrdering ProjectionOrderingUnsupported {} = True
          isOrdering _ = False
          isResult ProjectionResultUnsupported {} = True
          isResult _ = False
          warnings = projectionWarnings fixture
      warnings `shouldSatisfy` any isOrdering
      warnings `shouldSatisfy` (not . any isResult)

    it "rejects a projection in an update" $ do
      let fixture =
            FieldProj.docProjectionTransducer
              { edgesOut = \FieldProj.DocState ->
                  [ Edge
                      { guard = matchInCtor FieldProj.newDocCtor,
                        update =
                          USet
                            FieldProj.docN
                            (regProj FieldProj.docIdentityW FieldProj.docIx),
                        output = validOutput,
                        target = FieldProj.DocState,
                        mode = Live
                      }
                  ]
              }
          isUpdate ProjectionOutsideGuard {tvwProjectionLocation = "update"} = True
          isUpdate _ = False
      projectionWarnings fixture `shouldSatisfy` any isUpdate

    it "rejects an output projection and still reports its owner field hidden" $ do
      let projectedWire =
            WireCtor
              { wcName = "ProjectedHash",
                wcMatch = \case
                  FieldProj.DocAccepted doc -> Just (FieldProj.diHash doc, ()),
                wcBuild = \(hash, ()) ->
                  FieldProj.DocAccepted (FieldProj.DocInfo hash "" [])
              }
          fixture =
            FieldProj.docProjectionTransducer
              { edgesOut = \FieldProj.DocState ->
                  [ Edge
                      { guard = matchInCtor FieldProj.newDocCtor,
                        update = UKeep,
                        output =
                          [ pack
                              FieldProj.newDocCtor
                              projectedWire
                              ( OFCons
                                  (inpProj FieldProj.docHashW FieldProj.newDocCtor #doc)
                                  OFNil
                              )
                          ],
                        target = FieldProj.DocState,
                        mode = Live
                      }
                  ]
              }
          isOutput ProjectionOutsideGuard {tvwProjectionLocation = "output"} = True
          isOutput _ = False
          isHiddenDoc HiddenInput {tvwMissingSlots = missing} = "doc" `elem` missing
          isHiddenDoc _ = False
          warnings = projectionWarnings fixture
      warnings `shouldSatisfy` any isOutput
      warnings `shouldSatisfy` any isHiddenDoc

    it "requires PInCtor before an input-based projection read" $ do
      let fixture =
            FieldProj.docProjectionTransducer
              { edgesOut = \FieldProj.DocState ->
                  [ Edge
                      { guard =
                          inpProj FieldProj.docHashW FieldProj.newDocCtor #doc
                            .== TLit "hash",
                        update = UKeep,
                        output = validOutput,
                        target = FieldProj.DocState,
                        mode = Live
                      }
                  ]
              }
          isUnguarded UnguardedInputRead {} = True
          isUnguarded _ = False
      projectionWarnings fixture `shouldSatisfy` any isUnguarded

  describe "checkTransitionDeterminismSym (z3-backed)" $ do
    it "mutually-exclusive PInCtor guards yield no determinism warning" $
      checkTransitionDeterminismSym cleanT `shouldBe` []

    it "agrees with the pure path on a PTop-vs-PInCtor overlap" $ do
      checkTransitionDeterminismPure symOverlapT `shouldSatisfy` (not . null)
      checkTransitionDeterminismSym symOverlapT `shouldSatisfy` (not . null)

  describe "provable overlap through PAnd spines" $ do
    let determinismWarningsOnly = filter isDeterminismWarning
        isDeterminismWarning (NondeterministicPair {}) = True
        isDeterminismWarning _ = False
        warningPair warning = (dwSource warning, dwEdgeA warning, dwEdgeB warning)
        pureIsSubsetOfSymbolic fixture = do
          let purePairs = map warningPair (checkTransitionDeterminismPure fixture)
              symbolicPairs = map warningPair (checkTransitionDeterminismSym fixture)
          purePairs `shouldSatisfy` all (`elem` symbolicPairs)

    it "finds the motivating same-constructor integral overlap" $ do
      determinismWarningsOnly
        (validateTransducer defaultValidationOptions motivatingOverlapT)
        `shouldBe` [ NondeterministicPair
                       { tvwSource = Start,
                         tvwEdgeA = 0,
                         tvwEdgeB = 1,
                         tvwInCtor = Just "Foo",
                         tvwDetail =
                           "edges #0 and #1 out of Start have overlapping guards"
                       }
                   ]

    it "does not warn for disjoint integral intervals" $ do
      checkTransitionDeterminismPure disjointOverlapT `shouldBe` []
      checkTransitionDeterminismPure disjointWord8T `shouldBe` []

    it "uses a mentioned non-integral literal as a concrete witness" $
      checkTransitionDeterminismPure boolLiteralWitnessT
        `shouldSatisfy` (not . null)

    it "finds an interior overlap in Natural's zero-bounded domain" $ do
      let purePairs = map warningPair (checkTransitionDeterminismPure naturalInteriorOverlapT)
          symbolicPairs = map warningPair (checkTransitionDeterminismSym naturalInteriorOverlapT)
      purePairs `shouldBe` [(Start, 0, 1)]
      purePairs `shouldSatisfy` all (`elem` symbolicPairs)

    it "does not guess through POr or an opaque TApp term" $ do
      checkTransitionDeterminismPure unknownOrT `shouldBe` []
      checkTransitionDeterminismPure unknownOpaqueT `shouldBe` []

    it "does not warn across different input constructors" $
      checkTransitionDeterminismPure differentCtorT `shouldBe` []

    it "keeps every pure warning inside the symbolic result" $ do
      mapM_
        pureIsSubsetOfSymbolic
        [ motivatingOverlapT,
          disjointOverlapT,
          unknownOrT,
          unknownOpaqueT,
          differentCtorT
        ]

  describe "checkDeadEdgesSym (z3-backed)" $ do
    it "flags a literal-PBot guard as unsatisfiable in isolation" $ do
      let isBotEdge (DeadEdgeWarning {dewEdge = EdgeRef {edgeSource = Start, edgeIndex = 0}}) = True
          isBotEdge _ = False
      checkDeadEdgesSym botT `shouldSatisfy` any isBotEdge