keiro-dsl-0.18.0.0: test/conformance-checked-mapping-replay/Main.hs
module Main (main) where
import Conformance.CheckedMappingReplay.Domain qualified as Domain
import Control.Monad (forM_, unless)
import Data.Aeson (Value (..), object, (.=))
import Data.Aeson qualified as Aeson
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KeyMap
import Data.ByteString qualified as BS
import Data.Either (isLeft)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Time.Calendar (fromGregorian)
import Generated.CheckedMappingReplay.Nominals (RetainedId, parseRetainedId, retainedIdText)
import Generated.CheckedMappingReplay.ReplayEvents.Contract
import Generated.CheckedMappingReplay.ReplayJobs.Queue (ReplayJob (..), encodeReplayJob, parseReplayJob)
import Generated.CheckedMappingReplay.ReplayLedger.Codec
import Generated.CheckedMappingReplay.ReplayLedger.Domain
import Generated.CheckedMappingReplay.ReplayLedger.EventStream (replayLedgerSnapshotFixture)
import Generated.CheckedMappingReplay.ReplayLedger.Harness (harnessAssertions)
import Generated.CheckedMappingReplay.ReplayLedger.Transducer (replayLedgerTransducer)
import Generated.CheckedMappingReplay.ReplayLookup.QueryContract (ReplayLookupQueryInput, ReplayLookupQueryResult)
import Generated.CheckedMappingReplay.ReplayReaction.Input (ReplayReactionInput (ReplayRequested))
import Generated.CheckedMappingReplay.ReplayReaction.ProcessHarness (processHarnessValues)
import Generated.CheckedMappingReplay.ReplayTarget.Harness qualified as ReplayTarget
import Generated.CheckedMappingReplay.StructuralConformance (structuralConformanceAssertions)
import Keiki.Core (applyEventsEither, (!))
import Keiro.Codec (EventType (..))
import Keiro.Test.ReplayCompatibility
import System.Exit (exitFailure)
main :: IO ()
main = do
retained <- Aeson.eitherDecodeFileStrict' retainedFixturePath
let retainedReplay = either (const False) retainedHistoryReplays retained
retainedNormalization = either (const False) retainedHistoryNormalizes retained
assertions =
[("structural/" <> label, passed) | (label, passed) <- structuralConformanceAssertions]
<> harnessAssertions
<> [("target/" <> label, passed) | (label, passed) <- ReplayTarget.harnessAssertions]
<> [ ("retained JSON fixture crosses the generated event parser and transducer", retainedReplay),
("admitted historical spellings normalize to canonical bytes and identical state", retainedNormalization),
("replay-only history preserves UUIDv5 and all checked mappings", replayOnlyHistory),
("serialized UUIDv5 and UUIDv7 event history retains exact identities", identifierHistory),
("queue, query, and contract surfaces preserve the integrated envelope", generatedSurfaceAgreement),
("generated process input carries the integrated envelope", processReactionInputAgreement),
("generated process reaction decisions remain pinned", processReactionHarnessAgreement),
("application-owned workflow result codec preserves the integrated envelope", workflowCodecAgreement),
("snapshot discriminator remains explicit in the integrated evidence", replayLedgerSnapshotFixture == (1, "checked-mapping-replay-v1")),
("baseline and candidate replay evidence satisfy the release comparator", releaseReady integratedInventory baselineReport candidateReport),
("date semantic mutations are detected", mutationDetected "date" (mutateState "date" (String "2000-03-01"))),
("set semantic mutations are detected", mutationDetected "set" (mutateState "labels" (Aeson.toJSON (["a", "c"] :: [Text])))),
("base16 semantic mutations are detected", mutationDetected "base16" (mutateState "contentHash" (String "00bf"))),
("identifier mutations are detected", mutationDetected "identifier" (mutateIdentity "entity" retainedV7Text)),
("consumer binding mutations are detected", mutationDetected "binding" (mutateState "binding" (String "transposed"))),
("event-shape mutations are detected", mutationDetected "event-shape" (integratedObservation {continuations = [String "MappingReplaced"]})),
("process-manager identity mutations are detected", mutationDetected "process" (mutateIdentity "process" "replay-process-v2")),
("workflow journal-key mutations are detected", mutationDetected "workflow" (mutateIdentity "workflow" "workflow/replay/v2")),
("unverified consumer evidence prevents release readiness", not (releaseReady integratedInventory baselineReport unverifiedCandidateReport)),
("information-losing event changes fail before replay", informationLossRejected)
]
forM_ assertions $ \(label, passed) ->
putStrLn ((if passed then "PASS " else "FAIL ") <> label)
unless (all snd assertions) exitFailure
retainedFixturePath :: FilePath
retainedFixturePath = "test/conformance-checked-mapping-replay/fixtures/retained/mapping-recorded-v1.json"
retainedHistoryReplays :: Value -> Bool
retainedHistoryReplays wire =
case replayRawEnvelope wire of
Right (vertex, current, currentId) ->
vertex == ReplayLedgerRecorded
&& current == expectedEnvelope
&& retainedIdText currentId == retainedV5Text
Left _ -> False
retainedHistoryNormalizes :: Value -> Bool
retainedHistoryNormalizes historical =
decodeReplayEnvelopeMapped historical == Right expectedEnvelope
&& encodeReplayEnvelopeMapped expectedEnvelope == canonicalEnvelope
&& null
( checkNormalizationLaw
decodeReplayEnvelopeMapped
encodeReplayEnvelopeMapped
replayRawEnvelopeChain
[]
historical
canonicalEnvelope
[]
)
replayRawEnvelopeChain :: [Value] -> Either Text (ReplayLedgerVertex, Domain.ReplayEnvelope, RetainedId)
replayRawEnvelopeChain [wire] = replayRawEnvelope wire
replayRawEnvelopeChain _ = Left "the terminal integration aggregate expects exactly one retained transition"
replayRawEnvelope :: Value -> Either Text (ReplayLedgerVertex, Domain.ReplayEnvelope, RetainedId)
replayRawEnvelope wire = do
let recorded = MappingRecorded (MappingRecordedData retainedV5 expectedEnvelope)
audited = MappingAudited (MappingAuditedData retainedV5 expectedEnvelope)
recordedEvent <- parseReplayLedgerEvent (EventType "MappingRecorded") (replaceObjectField "envelope" wire (encodeReplayLedgerEvent recorded))
auditedEvent <- parseReplayLedgerEvent (EventType "MappingAudited") (replaceObjectField "envelope" wire (encodeReplayLedgerEvent audited))
(vertex, registers) <- firstText (applyEventsEither replayLedgerTransducer (ReplayLedgerEmpty, initialReplayLedgerRegs) [recordedEvent, auditedEvent])
pure (vertex, registers ! #current, registers ! #currentId)
replayOnlyHistory :: Bool
replayOnlyHistory =
case parseReplayLedgerEvent (EventType "LegacyMappingImported") rawEvent of
Left _ -> False
Right parsed -> case applyEventsEither replayLedgerTransducer (ReplayLedgerEmpty, initialReplayLedgerRegs) [parsed] of
Left _ -> False
Right (vertex, registers) ->
vertex == ReplayLedgerRecorded
&& registers ! #current == expectedEnvelope
&& registers ! #currentId == retainedV5
where
event = LegacyMappingImported (LegacyMappingImportedData retainedV5 expectedEnvelope)
rawEvent = replaceObjectField "envelope" nonCanonicalEnvelope (encodeReplayLedgerEvent event)
identifierHistory :: Bool
identifierHistory =
decodeReplayEnvelopeMapped nonCanonicalEnvelope == Right expectedEnvelope
&& Map.keys identityMap == [retainedV7, retainedV5]
&& retainedIdText retainedV5 == retainedV5Text
&& retainedIdText retainedV7 == retainedV7Text
generatedSurfaceAgreement :: Bool
generatedSurfaceAgreement =
parseReplayJob (encodeReplayJob queuePayload) == Right queuePayload
&& queryInputIdentity expectedEnvelope == expectedEnvelope
&& queryResultIdentity [expectedEnvelope] == [expectedEnvelope]
&& parseReplayEventsPayload (encodeReplayEventsPayload contractPayload) == Right contractPayload
where
queuePayload = ReplayJob retainedV5 expectedEnvelope
contractPayload = ReplayLinked (ReplayLinkedData retainedV5)
queryInputIdentity :: ReplayLookupQueryInput -> Domain.ReplayEnvelope
queryInputIdentity = id
queryResultIdentity :: ReplayLookupQueryResult -> [Domain.ReplayEnvelope]
queryResultIdentity = id
workflowCodecAgreement :: Bool
workflowCodecAgreement =
decodeWorkflowResult (encodeWorkflowResult expectedEnvelope) == Right expectedEnvelope
&& decodeWorkflowResult nonCanonicalEnvelope == Right expectedEnvelope
encodeWorkflowResult :: Domain.ReplayEnvelope -> Value
encodeWorkflowResult = encodeReplayEnvelopeMapped
decodeWorkflowResult :: Value -> Either Text Domain.ReplayEnvelope
decodeWorkflowResult = decodeReplayEnvelopeMapped
processReactionInputAgreement :: Bool
processReactionInputAgreement = processReactionInputDecode == Aeson.Success processReactionInput
processReactionInputDecode :: Aeson.Result ReplayReactionInput
processReactionInputDecode = Aeson.fromJSON (Aeson.toJSON processReactionInput)
processReactionInput :: ReplayReactionInput
processReactionInput = ReplayRequested retainedV5 processEnvelope
-- Process input decoding is application-owned. This value deliberately stays
-- inside the consumer codec's admitted Day range; retained extended-year bytes
-- are exercised through the generated event codec above.
processEnvelope :: Domain.ReplayEnvelope
processEnvelope =
Domain.ReplayEnvelope
(Domain.MaybeLabel (Just "process"))
(Domain.ImportantDays [fromGregorian 2000 2 29])
(Domain.TextLabels (Set.fromList ["a", "b"]))
(Domain.MaybeContentHash (Just (Domain.ContentHash (BS.pack [0, 175]))))
retainedV5
identityMap
(Just (Set.fromList ["process", "reaction"]))
processReactionHarnessAgreement :: Bool
processReactionHarnessAgreement =
processHarnessValues
== [ ("reactionOwnership", "generated-declarative"),
("reactionVersion", "1"),
("reactionFingerprint", "cc73d755be55b8a2d237ac02752ad8d425a50c5fb9f7c3bab8b5f5ada239b1c8"),
("reaction.ReplayRequested.0", "guard=always;advance=Record;followUps=dispatch:ReplayTarget.Store;accepted=")
]
informationLossRejected :: Bool
informationLossRejected =
isLeft (parseReplayLedgerEvent (EventType "MappingRecorded") (deleteNestedEnvelopeField "contentHash" encoded))
where
encoded = encodeReplayLedgerEvent (MappingRecorded (MappingRecordedData retainedV5 expectedEnvelope))
expectedEnvelope :: Domain.ReplayEnvelope
expectedEnvelope =
Domain.ReplayEnvelope
(Domain.MaybeLabel (Just "retained"))
(Domain.ImportantDays [fromGregorian 2000 2 29, fromGregorian 12345678901234567890 6 30])
(Domain.TextLabels (Set.fromList ["a", "b"]))
(Domain.MaybeContentHash (Just (Domain.ContentHash (BS.pack [0, 175]))))
retainedV5
identityMap
(Just (Set.fromList ["a", "z"]))
identityMap :: Map RetainedId Text
identityMap = Map.fromList [(retainedV5, "historical-v5"), (retainedV7, "current-v7")]
canonicalEnvelope :: Value
canonicalEnvelope = encodeReplayEnvelopeMapped expectedEnvelope
nonCanonicalEnvelope :: Value
nonCanonicalEnvelope =
object
[ "label" .= String "retained",
"days" .= (["+002000-02-29", "12345678901234567890-06-30"] :: [Text]),
"labels" .= (["b", "a", "a"] :: [Text]),
"contentHash" .= String "00AF",
"primary" .= retainedV5Text,
"identities" .= object [Key.fromText retainedV5Text .= String "historical-v5", Key.fromText retainedV7Text .= String "current-v7"],
"optionalLabels" .= (["z", "a", "a"] :: [Text])
]
retainedV5Text, retainedV7Text :: Text
retainedV5Text = "retained_58kj0y515rbwebzaxwzzknjqnk"
retainedV7Text = "retained_01h455vb4pex5vsknk084sn02q"
retainedV5, retainedV7 :: RetainedId
retainedV5 = committedId retainedV5Text
retainedV7 = committedId retainedV7Text
committedId :: Text -> RetainedId
committedId value = case parseRetainedId value of
Right parsed -> parsed
Left reason -> error ("invalid committed RetainedId fixture: " <> show reason)
replaceObjectField :: Key.Key -> Value -> Value -> Value
replaceObjectField key inserted (Object value) = Object (KeyMap.insert key inserted value)
replaceObjectField _ _ value = value
deleteNestedEnvelopeField :: Key.Key -> Value -> Value
deleteNestedEnvelopeField key (Object outer) =
case KeyMap.lookup "envelope" outer of
Just (Object envelope) -> Object (KeyMap.insert "envelope" (Object (KeyMap.delete key envelope)) outer)
_ -> Object outer
deleteNestedEnvelopeField _ value = value
firstText :: (Show problem) => Either problem value -> Either Text value
firstText = either (Left . Text.pack . show) Right
integratedInventory :: EvidenceInventory
integratedInventory =
EvidenceInventory
{ inventoryVersion = inventoryVersionV1,
inventoryId = "checked-mapping-replay/inventory/v1",
buildPair = integratedBuildPair,
contributions =
[ InventoryContribution source Applicable integratedCases
| source <- inventorySourcesV1
]
}
integratedCases :: [RequiredCase]
integratedCases =
[ RequiredCase "aggregate-history" HistoricalRead aggregateSurface,
RequiredCase "semantic-equivalence" SemanticEquivalence aggregateSurface,
RequiredCase "old-reader-new-writer" OldReaderNewWriter aggregateSurface,
RequiredCase "snapshot-replay" SnapshotReplay snapshotSurface,
RequiredCase "process-recovery" ProcessManagerReplay processSurface,
RequiredCase "workflow-journal" WorkflowReplay workflowSurface
]
aggregateSurface, snapshotSurface, processSurface, workflowSurface :: PersistedSurface
aggregateSurface = PersistedSurface "event-stream" "ReplayLedger" "replayLedger/<retained-id>"
snapshotSurface = PersistedSurface "snapshot" "ReplayLedger" "checked-mapping-replay-v1"
processSurface = PersistedSurface "process-manager" "ReplayProjection" "replay-process-v1"
workflowSurface = PersistedSurface "workflow-journal" "ReplayWorkflow" "workflow/replay/v1"
integratedBuildPair :: BuildPair
integratedBuildPair =
BuildPair
(BuildIdentity "retained-v1" "keiro-dsl-6" "keiro-runtime-v1" "integrated-plan-v1")
(BuildIdentity "candidate-v1" "keiro-dsl-6" "keiro-runtime-v1" "integrated-plan-v1")
baselineReport, candidateReport, unverifiedCandidateReport :: CaptureReport
baselineReport = capture BaselineCapture
candidateReport = capture CandidateCapture
unverifiedCandidateReport =
candidateReport
{ results = case candidateReport.results of
[] -> []
row : remaining -> row {verdict = Unverified "consumer history was not captured"} : remaining
}
capture :: CaptureRole -> CaptureReport
capture captureRole =
CaptureReport
{ reportVersion = reportVersionV1,
role = captureRole,
buildPair = integratedBuildPair,
inventoryId = "checked-mapping-replay/inventory/v1",
corpusHash = "checked-mapping-replay-retained-v1",
observationContractVersion = "checked-mapping-replay/observation/v1",
highWaterMarks = [HighWaterMark "replayLedger/retained-v5" 2],
selectedSurfaces = [aggregateSurface, snapshotSurface, processSurface, workflowSurface],
determinismInputs = DeterminismInputs "frozen" "none" "none" "partial-success-after-first-target",
results = [CaseResult required Passed (Just (observationFor required.caseKind)) | required <- integratedCases]
}
observationFor :: CaseKind -> Observation
observationFor kind = case kind of
ProcessManagerReplay -> processObservation
WorkflowReplay -> workflowObservation
_ -> integratedObservation
integratedObservation :: Observation
integratedObservation =
Observation
{ durableState =
Map.fromList
[ ("date", String "2000-02-29"),
("labels", Aeson.toJSON (["a", "b"] :: [Text])),
("contentHash", String "00af"),
("binding", canonicalEnvelope),
("state", String "ReplayLedgerRecorded")
],
continuations = [String "MappingRecorded", String "MappingAudited"],
durableIdentities = Map.fromList [("entity", retainedV5Text), ("snapshot", "checked-mapping-replay-v1")],
freshAllocations = []
}
processObservation :: Observation
processObservation =
integratedObservation
{ durableState = Map.insert "partialFanout" (String "resume-second-target") integratedObservation.durableState,
continuations = integratedObservation.continuations <> [String "timer/retry-v1"],
durableIdentities = Map.insert "process" "replay-process-v1" integratedObservation.durableIdentities
}
workflowObservation :: Observation
workflowObservation =
integratedObservation
{ durableState = Map.insert "workflowResult" canonicalEnvelope integratedObservation.durableState,
continuations = integratedObservation.continuations <> [String "resume/checked-mapping"],
durableIdentities = Map.insert "workflow" "workflow/replay/v1" integratedObservation.durableIdentities
}
mutationDetected :: Text -> Observation -> Bool
mutationDetected label mutated = not (null (compareObservation label integratedObservation mutated))
mutateState :: Text -> Value -> Observation
mutateState key value = integratedObservation {durableState = Map.insert key value integratedObservation.durableState}
mutateIdentity :: Text -> Text -> Observation
mutateIdentity key value = integratedObservation {durableIdentities = Map.insert key value integratedObservation.durableIdentities}