packages feed

keiki-0.1.0.0: test/Keiki/CoreInFlightSpec.hs

module Keiki.CoreInFlightSpec (spec) where

import Data.Proxy (Proxy (..))
import Keiki.Core
import Test.Hspec

-- | A synthetic 2-event command for one transition. The input has one
-- constructor; the output alphabet has two: 'Started' and 'Echoed'.
-- A single edge from 'False' to 'True' emits both, in that order.
data MultiInput = Begin Int deriving (Eq, Show)

data MultiOutput = Started Int | Echoed Int deriving (Eq, Show)

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

wcStarted :: WireCtor MultiOutput (Int, ())
wcStarted =
  WireCtor
    { wcName = "Started",
      wcMatch = \case
        Started n -> Just (n, ())
        _ -> Nothing,
      wcBuild = \(n, ()) -> Started n
    }

wcEchoed :: WireCtor MultiOutput (Int, ())
wcEchoed =
  WireCtor
    { wcName = "Echoed",
      wcMatch = \case
        Echoed n -> Just (n, ())
        _ -> Nothing,
      wcBuild = \(n, ()) -> Echoed n
    }

-- | A minimal 2-vertex transducer with one length-2 edge:
--
--   False --[guard ci=Begin / output [Started n, Echoed n]]--> True
multi :: SymTransducer (HsPred '[] MultiInput) '[] Bool MultiInput MultiOutput
multi =
  SymTransducer
    { edgesOut = \case
        False ->
          [ Edge
              { guard = matchInCtor inCtorBegin,
                update = UKeep,
                output =
                  [ pack
                      inCtorBegin
                      wcStarted
                      (OFCons (TInpCtorField inCtorBegin (#payload :: Index '[ '("payload", Int)] Int)) OFNil),
                    pack
                      inCtorBegin
                      wcEchoed
                      (OFCons (TInpCtorField inCtorBegin (#payload :: Index '[ '("payload", Int)] Int)) OFNil)
                  ],
                target = True
              }
          ]
        True -> [],
      initial = False,
      initialRegs = RNil,
      isFinal = id
    }

spec :: Spec
spec = do
  describe "omega returns a length-2 event list for a multi-event edge" $ do
    it "Begin 42 yields [Started 42, Echoed 42]" $
      omega multi False RNil (Begin 42) `shouldBe` [Started 42, Echoed 42]

  describe "step's third component is the same length-2 list" $ do
    it "step (False, RNil) (Begin 7) returns (True, _, [Started 7, Echoed 7])" $
      case step multi (False, RNil) (Begin 7) of
        Just (True, _, [Started 7, Echoed 7]) -> pure ()
        other -> expectationFailure ("unexpected: " <> show3 other)

  describe "applyEvents (chunked replay)" $ do
    it "round-trips [Started 42, Echoed 42] from Settled False" $
      case applyEvents multi (False, RNil) [Started 42, Echoed 42] of
        Just (s, _) -> s `shouldBe` True
        Nothing -> expectationFailure "applyEvents returned Nothing"

    it "rejects a truncated chunk [Started 42] (queue non-empty at end)" $
      case applyEvents multi (False, RNil) [Started 42] of
        Nothing -> pure ()
        Just _ ->
          expectationFailure
            "applyEvents accepted a chunk that ends mid-flight"

    it "rejects an out-of-order chunk [Echoed 42, Started 42]" $
      case applyEvents multi (False, RNil) [Echoed 42, Started 42] of
        Nothing -> pure ()
        Just _ -> expectationFailure "applyEvents accepted out-of-order events"

  describe "applyEventStreaming (event-by-event)" $ do
    it "Settled False ⊢ Started 42 → InFlight True [Echoed 42]" $
      case applyEventStreaming multi (Settled False) RNil (Started 42) of
        Just (InFlight True [Echoed 42], _) -> pure ()
        other -> expectationFailure ("unexpected: " <> showInFlight other)

    it "InFlight True [Echoed 42] ⊢ Echoed 42 → Settled True" $
      case applyEventStreaming multi (InFlight True [Echoed 42]) RNil (Echoed 42) of
        Just (Settled True, _) -> pure ()
        other -> expectationFailure ("unexpected: " <> showInFlight other)

    it "Settled False ⊢ Echoed 42 → Nothing (out-of-order)" $
      case applyEventStreaming multi (Settled False) RNil (Echoed 42) of
        Nothing -> pure ()
        Just _ -> expectationFailure "accepted out-of-order first event"

    it "InFlight True [Echoed 42] ⊢ Started 42 → Nothing (queue mismatch)" $
      case applyEventStreaming multi (InFlight True [Echoed 42]) RNil (Started 42) of
        Nothing -> pure ()
        Just _ ->
          expectationFailure
            "applyEventStreaming accepted an out-of-order tail event"

  describe "streaming and chunked replay agree on the final state" $ do
    it "step-by-step InFlight threading reaches the same Settled True" $ do
      let chunked = applyEvents multi (False, RNil) [Started 42, Echoed 42]
          streamed = do
            (s1, regs1) <- applyEventStreaming multi (Settled False) RNil (Started 42)
            (s2, regs2) <- applyEventStreaming multi s1 regs1 (Echoed 42)
            case s2 of
              Settled v -> Just (v, regs2)
              _ -> Nothing
      case (chunked, streamed) of
        (Just (cs, _), Just (ss, _)) -> cs `shouldBe` ss
        _ ->
          expectationFailure
            "chunked and streaming replay disagreed on the final state"
  where
    show3 :: Maybe (Bool, x, [MultiOutput]) -> String
    show3 Nothing = "Nothing"
    show3 (Just (s, _, cos_)) =
      "Just (" <> show s <> ", _, " <> show cos_ <> ")"

    showInFlight :: Maybe (InFlight Bool MultiOutput, x) -> String
    showInFlight Nothing = "Nothing"
    showInFlight (Just (w, _)) = "Just (" <> show w <> ", _)"