keiki-0.1.0.0: test/Keiki/ChoiceSpec.hs
{-# LANGUAGE BlockArguments #-}
-- | Acceptance tests for the 'Data.Profunctor.Choice.Choice' instance
-- on 'SomeSymTransducer' (EP-29 of MasterPlan 9, M1).
--
-- The Choice instance routes @Either@-shaped inputs to one of two
-- arms: @left'@ wraps the underlying transducer into the @Left@ arm
-- and lifts an identity transducer into the @Right@ arm; @right'@ is
-- the symmetric routing. The implementation delegates to
-- 'Keiki.Composition.alternative' with 'identityTransducer' on the
-- pass-through arm.
--
-- Fixture: 'Keiki.Fixtures.EmailDelivery' wrapped in
-- 'someSymTransducer'. The tests cover:
--
-- * Forward routing — a @Left@ input lands on the wrapped arm's
-- edges and emits a @Left@ wire event; a @Right@ input passes
-- straight through unchanged.
-- * Survival of 'Keiki.Symbolic.isSingleValuedSym'.
-- * Sentinel preservation: @left' Cat.id == Cat.id@ on the wrapper
-- by construction (the instance returns 'SomeSymIdentity' on the
-- sentinel arm).
module Keiki.ChoiceSpec (spec) where
import Control.Category qualified as Cat
import Data.Profunctor (Choice (..))
import Data.Time.Calendar (fromGregorian)
import Data.Time.Clock (UTCTime (..), secondsToDiffTime)
import Keiki.Core
import Keiki.Fixtures.EmailDelivery
import Keiki.Profunctor
import Keiki.Symbolic (isSingleValuedSym, withSymPred)
import Test.Hspec
-- * Fixtures ----------------------------------------------------------------
sampleAt :: UTCTime
sampleAt = UTCTime (fromGregorian 2026 5 9) (secondsToDiffTime 0)
sampleSendEmail :: EmailCmd
sampleSendEmail =
SendEmail
SendEmailData
{ recipient = "alice@example.com",
subject = "hello",
at = sampleAt
}
sampleEmailEvent :: EmailEvent
sampleEmailEvent =
EmailSent
EmailSentData
{ recipient = "alice@example.com",
subject = "hello",
at = sampleAt
}
someEmail :: SomeSymTransducer EmailCmd EmailEvent
someEmail = someSymTransducer emailDelivery
-- * Specs -------------------------------------------------------------------
spec :: Spec
spec = do
describe "left'" $ do
it "Left input routes through the wrapped transducer" $ do
let routedLeft :: SomeSymTransducer (Either EmailCmd Int) (Either EmailEvent Int)
routedLeft = left' someEmail
case routedLeft of
SomeSymTransducer t ->
omega t (initial t) (initialRegs t) (Left sampleSendEmail)
`shouldBe` [(Left sampleEmailEvent)]
SomeSymIdentity ->
expectationFailure
"left' (someSymTransducer emailDelivery) unexpectedly returned \
\the identity sentinel"
it "Right input passes through unchanged on the identity arm" $ do
let routedLeft :: SomeSymTransducer (Either EmailCmd Int) (Either EmailEvent Int)
routedLeft = left' someEmail
case routedLeft of
SomeSymTransducer t ->
omega t (initial t) (initialRegs t) (Right (42 :: Int))
`shouldBe` [(Right 42)]
SomeSymIdentity ->
expectationFailure
"left' (someSymTransducer emailDelivery) unexpectedly returned \
\the identity sentinel"
it "preserves Cat.id on the sentinel: left' Cat.id == Cat.id" $ do
let lifted :: SomeSymTransducer (Either Int Bool) (Either Int Bool)
lifted = left' (Cat.id :: SomeSymTransducer Int Int)
case lifted of
SomeSymIdentity -> pure ()
SomeSymTransducer _ ->
expectationFailure "left' Cat.id should preserve the identity sentinel"
describe "right'" $ do
it "Right input routes through the wrapped transducer" $ do
let routedRight :: SomeSymTransducer (Either Int EmailCmd) (Either Int EmailEvent)
routedRight = right' someEmail
case routedRight of
SomeSymTransducer t ->
omega t (initial t) (initialRegs t) (Right sampleSendEmail)
`shouldBe` [(Right sampleEmailEvent)]
SomeSymIdentity ->
expectationFailure
"right' (someSymTransducer emailDelivery) unexpectedly returned \
\the identity sentinel"
it "Left input passes through unchanged on the identity arm" $ do
let routedRight :: SomeSymTransducer (Either Int EmailCmd) (Either Int EmailEvent)
routedRight = right' someEmail
case routedRight of
SomeSymTransducer t ->
omega t (initial t) (initialRegs t) (Left (7 :: Int))
`shouldBe` [(Left 7)]
SomeSymIdentity ->
expectationFailure
"right' (someSymTransducer emailDelivery) unexpectedly returned \
\the identity sentinel"
it "preserves Cat.id on the sentinel: right' Cat.id == Cat.id" $ do
let lifted :: SomeSymTransducer (Either Bool Int) (Either Bool Int)
lifted = right' (Cat.id :: SomeSymTransducer Int Int)
case lifted of
SomeSymIdentity -> pure ()
SomeSymTransducer _ ->
expectationFailure "right' Cat.id should preserve the identity sentinel"
describe "isSingleValuedSym survives left' / right'" $ do
it "single-valuedness is preserved across left'" $
case left' someEmail ::
SomeSymTransducer (Either EmailCmd Int) (Either EmailEvent Int) of
SomeSymTransducer t ->
isSingleValuedSym (withSymPred t) `shouldBe` True
SomeSymIdentity ->
expectationFailure
"left' on a non-identity wrapper returned the identity sentinel"
it "single-valuedness is preserved across right'" $
case right' someEmail ::
SomeSymTransducer (Either Int EmailCmd) (Either Int EmailEvent) of
SomeSymTransducer t ->
isSingleValuedSym (withSymPred t) `shouldBe` True
SomeSymIdentity ->
expectationFailure
"right' on a non-identity wrapper returned the identity sentinel"