Architecture Decision Record
ADR-058: Runtime Conformance Suites Shipped as a Package
Status
Accepted (2026-07-28; revised 2026-08-14, 2026-08-18, 2026-08-23, and 2026-09-03).
Revised 2026-09-22 (the layer rules also ship as compile-time MSBuild targets inside MMCA.Common.Shared; see the Revision below).
Revised 2026-09-25 (the live-document OpenAPI guard is reconciled with the committed OpenAPI documents both consumers now gate at build time, and the MMCA.Common.Testing.UI page-test bases for the confirm-email and role-admin pages are recorded; see the Revision (2026-09-25) below).
Revised 2026-09-10 (the security-headers suite reaches both UI web hosts) and 2026-10-01 (the switchable gateway probe gate and the two full-coverage suites are stated consistently); see those Revisions below.
Revised 2026-10-06: the consumers' lockstep MMCA.Common.Shared pin is now 1.232.0 and this Status block lists every Revision section; see the Revision (2026-10-06) below.
Revised 2026-10-07: the consumers' lockstep MMCA.Common.Shared pin is now 1.233.0 and the ADC pin anchor moved to Directory.Packages.props:101; see the Revision (2026-10-07) below.
Revised 2026-10-21: the OpenAPI base gains an opt-in committed-baseline breaking-change diff that all seven OpenAPI subclasses adopt, the decorator pipeline is now sealed against late registration through the framework helpers, the lockstep pin is 1.250.0, and anchors are refreshed; see the Revision (2026-10-21) below.
Revised 2026-10-26: anchors refreshed after the v1.261.0 consumer sweep (the lockstep MMCA.Common.Shared pin is now 1.261.0 and the ADC pin anchor moved to Directory.Packages.props:108), with the MiddlewarePipelineOrderTestsBase and ADR-015 / ADR-046 cross-reference anchors re-pointed; see the Revision (2026-10-26) below.
Revised 2026-10-28: the lockstep MMCA.Common.Shared pin is now 1.265.1, and the inventory of MMCA.Common's own conformance self-tests names the tenancy-base and breaking-change-detector tests; see the Revision (2026-10-28) below.
Context
ADR-015 turned the architecture invariants into build-gating tests, and drew its own boundary
explicitly: the fitness suite asserts "structure / registration, not runtime behavior"
(015-architecture-fitness-functions.md:118). NetArchTest can prove a controller lives in the right
assembly and that an outbound client wires resilience. It cannot prove that a booted host answers a
bad page number with an RFC 9457 problem document, emits HSTS on a liveness probe, drains within a
bounded stop, serves the OpenAPI document it claims to serve, answers two API versions, or nests the
ADR-014 decorators in the documented order. Those are runtime contracts of the framework, and every
one of them is only true if the consuming host wired it correctly: the framework ships the middleware,
the controller base, and the decorator registrations, but the host composes them.
The gap is visible in the ADR set. ADR-046 names exactly one of these checks (a "shared
fitness contract" for versioning, one Decision bullet at 046-http-api-versioning.md:100), and ADR-013 defines the RFC 9457
edge contract that another one guards, but neither decides the layer itself. The runtime-conformance
tier has been referenced repeatedly and never recorded on its own.
Decision
Ship the runtime conformance suites in the MMCA.Common.Testing package as abstract behavioral
bases that each consuming host subclasses, and run every one of them against a host that was actually
booted.
- Seven contract bases, one per runtime contract, all sitting under
Source/Hosting/MMCA.Common.Testing/Conformance/in namespaceMMCA.Common.Testing.Conformance, so every bareTestsBase.csname below resolves inside that folder; the fixtures they boot through sit beside it inFixtures/.ProblemDetailsContractTestsBase<TFixture>(Source/Hosting/MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:22) asserts the two error-shaping paths, ASP.NET Core model validation (400,application/problem+jsonwithtype/traceId/errors,ProblemDetailsContractTestsBase.cs:30) and the framework'sHandleFailureResult mapping (404,ProblemDetailsContractTestsBase.cs:42), against the shared shape checkAssertProblemDetailsShapeAsync(ProblemDetailsContractTestsBase.cs:68).OpenApiContractTestsBase<TFixture>(OpenApiContractTestsBase.cs:32) asserts the live/openapi/v1.jsondocument is OpenAPI 3.x and still describes the pinned public resources (OpenApiContractTestsBase.cs:83,OpenApiContractTestsBase.cs:98).ServiceInfoVersioningContractTestsBase<TFixture>(ServiceInfoVersioningContractTestsBase.cs:20) drives/ServiceInfoatapi-version: 1.0and2.0and checks the deprecated/supported reporting headers (ServiceInfoVersioningContractTestsBase.cs:39,:55).SecurityHeadersTestsBase(SecurityHeadersTestsBase.cs:16) probes/aliveand pins six response headers, includingContent-Security-Policy: frame-ancestors 'none'and an HSTSmax-age(SecurityHeadersTestsBase.cs:29-35).GracefulShutdownTestsBase<TEntryPoint>(GracefulShutdownTestsBase.cs:25) calls a realIHost.StopAsyncunder a bounded token and assertsApplicationStoppingthenApplicationStoppedfired (GracefulShutdownTestsBase.cs:57,:59,:61).MmcaGatewayHardeningTestsBase<TEntryPoint>(MmcaGatewayHardeningTestsBase.cs:39) drives a booted gateway through eight edge gates: the per-client-IP rate limiter and its bypass list, the tighter named policy on the credential route, a correlation id generated when the caller supplies none and echoed when it does, one readiness check per downstream service, the active health-probe state of every cluster, and partitioning by the forwarded client IP rather than the proxy IP (MmcaGatewayHardeningTestsBase.cs:136,:158,:194,:221,:239,:256,:284,:327). The probe gate is switchable: by default every cluster must carry an enabled active probe on the liveness path, and a host that overridesActiveHealthChecksExpectedtofalse(MmcaGatewayHardeningTestsBase.cs:111) gets the inverse assertion, that no cluster carries an enabled active probe (MmcaGatewayHardeningTestsBase.cs:308-313).DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult>(DecoratorPipelineOrderTestsBase.cs:38) asserts the ADR-014 nesting. - The host is really booted; nothing is inferred from registrations. Three of the six booted-host
suites (problem details, OpenAPI, versioning) reach the host through
IIntegrationTestFixture(Source/Hosting/MMCA.Common.Testing/Fixtures/IIntegrationTestFixture.cs:8) andIntegrationTestBase<TFixture>(Fixtures/IntegrationTestBase.cs:13), whoseClientcomes from the fixture'sWebApplicationFactory. The other three take a factory directly: security headers through its own abstractCreateClient()(SecurityHeadersTestsBase.cs:42), graceful shutdown and gateway hardening through a factory the subclass supplies (GracefulShutdownTestsBase.cs:32,MmcaGatewayHardeningTestsBase.cs:61). Two boot paths ship. Service hosts useSqlServerIntegrationTestFixtureBase<TEntryPoint>(Fixtures/SqlServerIntegrationTestFixtureBase.cs:27): it creates a GUID-named throwaway SQL Server database, pushes the connection string andASPNETCORE_ENVIRONMENT=Testingas process environment variables before the host is built (SqlServerIntegrationTestFixtureBase.cs:75,:76), lets the host's ownDatabaseInitStrategy=Migrateapply the schema when the first client forces the build (SqlServerIntegrationTestFixtureBase.cs:89), resets data between tests with Respawn (SqlServerIntegrationTestFixtureBase.cs:95,:115), and drops the database on disposal (SqlServerIntegrationTestFixtureBase.cs:132). Database-free hosts useProductionHostApplicationFactory<TEntryPoint>(Fixtures/ProductionHostApplicationFactory.cs:23), which pinsUseEnvironment("Production")(ProductionHostApplicationFactory.cs:37) so the production-only branches (restrictive CORS, HSTS emission) are the ones under test, and captures the startedIHost(ProductionHostApplicationFactory.cs:30) becauseStopAsyncis not reachable through theWebApplicationFactorysurface. - The consumer supplies its host and its host-specific facts, nothing else. The abstract surface
is deliberately small: two probe requests for problem details
(
ProblemDetailsContractTestsBase.cs:55,:61), a route-count floor and a pinned resource list for OpenAPI (OpenApiContractTestsBase.cs:58,:71), a client factory for security headers (SecurityHeadersTestsBase.cs:42), a service-collection configurator for the decorator pipeline (DecoratorPipelineOrderTestsBase.cs:46), the booted factory plus the host's own route-table facts (the permit limit, a limited path, the downstream service names) for gateway hardening (MmcaGatewayHardeningTestsBase.cs:61,:68,:71,:77), plus the optionalActiveHealthChecksExpectedswitch (MmcaGatewayHardeningTestsBase.cs:111), and for versioning and graceful shutdown nothing at all beyond the fixture or entry point. The two shutdown subclasses are one-line declarations with no body (MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GracefulShutdownTests.cs:9,MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GracefulShutdownTests.cs:9), and so is the ADC versioning subclass (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/ApiVersioningTests.cs:14). - The decorator suite is the one non-HTTP conformance check, and it inspects the object graph, not
the registration list. It builds a
ServiceCollection, runs the repo's own real registration sequence through the subclass, resolves the closed handler interface from the built provider, then walks outermost to innermost by reading each decorator's private inner-handler field (DecoratorPipelineOrderTestsBase.cs:105) and compares the names against the ADR-014 order (DecoratorPipelineOrderTestsBase.cs:49,:61). That is what makes it a runtime check: ScrutorTryDecorateapplies decorators in reverse registration order, so a regression in the decorator registration order changes the constructed pipeline while every registration still exists. A module scan that runs after the decorators no longer does so silently:AddApplicationDecorators()seals the collection, and every framework registration helper called after it throws (MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:310-319, checked byAddApplicationDecoratorsandAddMmcaApplicationPipelineat:116and:209,ScanModuleApplicationServicesatDependencyInjection.ModuleScanning.cs:49, andAddEntityCrud,AddEntityUpdateVerbandAddEntityUpdateatDependencyInjection.Crud.cs:80,:142,:193). A raw handler registration made outside those helpers is not checked at registration; theVerifyDecoratorPipeline()fitness check (DependencyInjection.cs:244) reports it instead. All three consumer subclasses compose the sequence through the sealingAddMmcaApplicationPipeline(...)(DependencyInjection.cs:207). - The live document is the oracle; a committed baseline is an opt-in breaking-change diff. By
default the OpenAPI guard reads no checked-in file (
OpenApiContractTestsBase.cs:15-17), so a new controller can never leave a stale snapshot behind, and those assertions are deliberately coarse: a path-count floor plus presence (not exact casing) of the pinned resources (OpenApiContractTestsBase.cs:106-114). A subclass that overridesBaselineDocumentPath(OpenApiContractTestsBase.cs:80, defaultnull) also runsOpenApiDocument_HasNoBreakingChangesAgainstTheBaseline(OpenApiContractTestsBase.cs:118), which diffs the live document against that committed baseline withOpenApiBreakingChangeDetector(OpenApiContractTestsBase.cs:149,OpenApiBreakingChangeDetector.cs:38) and fails only on a breaking change. Refreshing it is deliberate: settingMMCA_UPDATE_OPENAPI_BASELINE=1(OpenApiContractTestsBase.cs:41) rewrites the file and fails the run (:129-140), so a regenerating run never passes. All seven OpenAPI subclasses opt in to anopenapi-baseline.jsonbeside the test file (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/OpenApiContractTests.cs:29-32,MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Contract/OpenApiContractTests.cs:30). The committedopenapi/<host>.jsondocuments that MMCA.ADC and MMCA.Store diff in CI (ADR-046) are a separate, build-time check owned by each consumer, not an input to this suite. - Hosts extend the base where they have more to prove. ADC Conference adds a 412
stale-precondition test on top of the inherited 400/404 facts, driving two editors through the
same
If-Matchtag and reusing the inherited shape assertion (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/ProblemDetailsContractTests.cs:42,:70).
Adoption today is real but partial, and uneven per suite. The OpenAPI and problem-details guards are
the two with full coverage of the extracted REST hosts. OpenAPI covers all four ADC services
(MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/OpenApiContractTests.cs:15,
MMCA.ADC.Engagement.IntegrationTests/Contract/OpenApiContractTests.cs:15,
MMCA.ADC.Identity.IntegrationTests/Contract/OpenApiContractTests.cs:16,
MMCA.ADC.Notification.IntegrationTests/Contract/OpenApiContractTests.cs:17) and all three Store
services (MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Contract/OpenApiContractTests.cs:15,
MMCA.Store.Identity.IntegrationTests/Contract/OpenApiContractTests.cs:15,
MMCA.Store.Sales.IntegrationTests/Contract/OpenApiContractTests.cs:15). The problem-details guard
covers all three Store services
(MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Contract/ProblemDetailsContractTests.cs:21,
MMCA.Store.Identity.IntegrationTests/Contract/ProblemDetailsContractTests.cs:16,
MMCA.Store.Sales.IntegrationTests/Contract/ProblemDetailsContractTests.cs:16) and, since
2026-08-13, all four
ADC services (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/ProblemDetailsContractTests.cs:21,
MMCA.ADC.Engagement.IntegrationTests/Contract/ProblemDetailsContractTests.cs:17,
MMCA.ADC.Identity.IntegrationTests/Contract/ProblemDetailsContractTests.cs:17,
MMCA.ADC.Notification.IntegrationTests/Contract/ProblemDetailsContractTests.cs:16): ADC
Notification gained the missing subclass, merged to main on 2026-08-13, so every REST host in
both consumers is now guarded for this one contract. The versioning contract is subclassed once per
repo, on ADC Conference (ApiVersioningTests.cs:14) and Store Catalog
(MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Contract/ApiVersioningTests.cs:15),
which is enough to keep the machinery exercised but leaves the other five REST hosts unguarded. The
security-headers suite is subclassed on all four browser-facing hosts: the two Gateway hosts
(MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/SecurityHeadersTests.cs:12,
MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/SecurityHeadersTests.cs:12) and, since 2026-09-10, both
UI web hosts (MMCA.Store/Tests/Hosts/MMCA.Store.UI.Web.Tests/SecurityHeadersTests.cs:17-18,
MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/SecurityHeadersTests.cs:17-18, each over a
Production-pinned factory: StorefrontHostApplicationFactory and
MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/ConferenceUiHostApplicationFactory.cs:17). Graceful
shutdown remains Gateway-only (the two GracefulShutdownTests above); no service host asserts it
today. The gateway-hardening suite is
subclassed on both Gateway hosts and nowhere else, which is its whole addressable surface: it asserts
the shared gateway kit's edge behavior, and only a gateway adopts that kit
(MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GatewayHardeningTests.cs:29,
MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GatewayHardeningTests.cs:34). Each subclass states
its own route-table facts, turns the active-probe expectation off
(MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GatewayHardeningTests.cs:89,
MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GatewayHardeningTests.cs:93), so on both gateways
the probe gate asserts that no cluster carries an enabled active probe, and supplies a
Production-pinned factory with a recording forwarder standing in for IHttpForwarder
(MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GatewayHardeningTests.cs:103,
MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GatewayHardeningTests.cs:128). The decorator suite is
subclassed once per consumer repo and is the one base all three of them run: ADC and Store both
against the Identity module's ChangePreferencesCommand / GetUserPreferencesQuery pair
(MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:29,
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:29),
and MMCA.Helpdesk against a real Tickets pair, described below.
MMCA.Common ships no host of its own, so the decorator suite is the one of the seven it runs as a
collected subclass: a synthetic PingCommand/PingQuery pair driven through the framework's own
registration sequence
(Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/DecoratorPipelineOrderTests.cs:23). The same
folder holds two more collected subclasses, both of bases outside the seven: the ADR-079 edge-pipeline
check (Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/MiddlewarePipelineOrderTests.cs:12) and
the tenancy fitness check (TenantResolutionConformanceTests.cs:11). For the seven themselves, its
other conformance tests are self-tests of the bases, not adoption: the OpenAPI baseline gate runs
through a private stub-fixture subclass
(Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/OpenApiContractTestsBaseBaselineTests.cs:155-156),
and the gateway-hardening base is checked for shape over an abstract sample
(Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/MmcaGatewayHardeningTestsBaseTests.cs:21, :75).
The folder's remaining files are self-tests of the pieces outside the seven: the tenancy fitness base
is exercised through its one private Probe subclass, instantiated with different settings
(Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/TenantResolutionTestsBaseTests.cs:15, :163),
and the breaking-change detector behind the OpenAPI baseline gate has its own unit tests
(OpenApiBreakingChangeDetectorTests.cs:14, OpenApiBreakingChangeDetectorParameterTests.cs:15).
MMCA.Helpdesk adopts exactly one of them, the decorator suite, having adopted none of them when
this record was written. Its architecture-test project now references MMCA.Common.Testing alongside the
structural package
(MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/MMCA.Helpdesk.Architecture.Tests.csproj:26)
and subclasses DecoratorPipelineOrderTestsBase against a real Tickets pair: the framework's generic
UpdateEntityCommand<Ticket, TicketUpdateRequest, TicketIdentifierType>, closed over the Ticket
aggregate by the seed's own AddEntityCrud wiring, and the module's hand-written GetTicketByIdQuery
(MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/DecoratorPipelineOrderTests.cs:38,
:40-43), whose ConfigureServices runs the seed's own registration sequence through the composed
AddMmcaApplicationPipeline(...): AddApplication(), the Tickets module registration, then
AddApplicationDecorators() last, with the collection sealed afterwards (:67-68). Two files in that
project import the MMCA.Common.Testing.Conformance namespace, and only one of them belongs to this
record: that decorator subclass (:13). The other, MiddlewarePipelineOrderTests.cs (:1), is a
one-line subclass of MiddlewarePipelineOrderTestsBase
(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:31,
MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/MiddlewarePipelineOrderTests.cs:15)
guarding the ADR-079 shared HTTP edge-pipeline order, not one of the seven contract bases above.
The package reference the domain-test project
carries (MMCA.Helpdesk/Tests/Modules/Tickets/MMCA.Helpdesk.Tickets.Domain.Tests/MMCA.Helpdesk.Tickets.Domain.Tests.csproj:8)
is still unused. The six booted-host bases have no Helpdesk subclass, so the seed's three test
projects remain the Tickets domain and application suites plus the architecture suite, which now
carries both tiers: the ADR-015 structural bases from the separate MMCA.Common.Testing.Architecture
package (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/GlobalUsings.cs:3), and
from MMCA.Common.Testing one base from this record's set plus the ADR-079 edge-pipeline base.
Rationale
- Runtime conformance is the half ADR-015 excluded. Structural rules answer "is the code shaped correctly"; these suites answer "does the composed host behave correctly". A host that registers everything the framework asks for can still drop a security header in a middleware reorder, wedge a rolling deploy with a hosted service that will not drain, or silently invert the decorator pipeline. None of that is reachable by reflection over assemblies.
- The failure this catches does not announce itself in production. A non-draining host looks healthy until a deploy waits out its termination grace period; a missing HSTS header looks healthy until an audit; a reordered decorator pipeline looks healthy until a validation runs inside a transaction that should never have opened.
- Write the body once, subclass thin. Same lever as ADR-015: the assertions live in the package, so a new invariant reaches every consumer that already subclassed the base. The gateway shutdown subclasses are single lines with no body, which is the intended cost of adoption.
- Booting the host is what makes it honest. Pinning
Productionexercises the branches a defaultDevelopmentboot skips, and a throwaway migrated database means the schema under test is the one the host's own init strategy produced, not a fixture's guess. - A live document beats a snapshot as this suite's oracle. Asserting against the served OpenAPI
document removes the class of failure where the guard passes because the snapshot was regenerated
along with the regression. The opt-in baseline narrows that failure to a reviewed commit rather than
removing it: the baseline is only the comparison point for a breaking-change diff of the live
document, and a refresh run always fails (
OpenApiContractTestsBase.cs:129-140), so a CI run can never regenerate it and pass. A PR that commits a regeneratedopenapi-baseline.jsontogether with a breaking change does pass the gate on its next run; the baseline diff in that PR is what review has to catch. The consumers' committed documents (ADR-046) do not reopen that failure either: the build regenerates them and CI fails on any uncommitted difference, so a regenerated contract arrives as a reviewable diff in the PR that changed it rather than as a silent pass.
Trade-offs
- Opt-in per host, exactly like ADR-015. The framework ships the suites; a host gets the gate only once someone writes the subclass. That is the same audit-the-inventory caveat, and the adoption inventory above is the current answer to it, not a claim of completeness.
- Coverage is uneven by suite. Graceful shutdown is Gateway-only, versioning is one host per repo, and Helpdesk has only the decorator suite. Problem details and OpenAPI are the two suites subclassed on every REST host in both consumers (ADC Notification closed the last problem-details gap on 2026-08-13). Security headers is complete over the hosts it addresses rather than over all of them: both Gateway hosts and both UI web hosts subclass it, which is every host that serves a browser, and the seven extracted REST services (four ADC, three Store) are reached through a gateway that stamps the headers for them. Gateway hardening is Gateway-only by construction rather than by omission: it asserts the shared gateway kit, so a service host has nothing for it to check. Every remaining hole is an unguarded host for that contract, not a decision that the contract does not apply.
- Some of the suites need a real SQL Server. The three fixture-driven suites (problem details, OpenAPI, versioning) live in the per-service integration tier and cannot run in a database-free test pass; the three Gateway suites (security headers, graceful shutdown, gateway hardening: no DbContext, no broker) and the decorator suite run in the fast tier. That splits the runtime gate across two CI jobs with different prerequisites.
- The default assertions are coarse by construction.
MinimumPathCountis a floor,CorePublicResourceschecks presence rather than exact casing or schema (OpenApiContractTestsBase.cs:106-114), and the problem-details base checks shape (status,title, a diagnostic extension) rather than message content. On their own these catch wholesale regressions, not subtle ones. The opt-in baseline diff is what reaches the subtle OpenAPI changes (a newly required request property, a removed response property, a type change, a new required parameter:OpenApiBreakingChangeDetector.cs:6-13,:32-35), and all seven OpenAPI subclasses opt in; a host that does not overrideBaselineDocumentPathgets only the coarse checks, and the problem-details shape check has no such diff. - The decorator check reads private fields. Unwrapping the chain depends on each decorator holding
its inner handler in a field that implements the same closed interface
(
DecoratorPipelineOrderTestsBase.cs:112-115); a decorator that stored it differently would silently end the walk early rather than fail loudly. - A new contract base only reaches consumers at the next lockstep bump. The suites ship inside the package set, so adding one is a framework release plus a consumer sweep (ADR-016), not a local edit in the repo that needs the guard.
Revision (2026-09-10)
The security-headers suite reaches both UI web hosts, so the adoption inventory above is no longer
Gateway-only for it. ADC added MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests, whose
SecurityHeadersTests.cs:17-18 is the one-line subclass and whose
ConferenceUiHostApplicationFactory.cs:17 derives the framework's
ProductionHostApplicationFactory<Program> so the host boots on the environment that emits HSTS. The
project is listed in MMCA.ADC/MMCA.ADC.CI.slnf:61, the no-database tier, matching where Store's
equivalent sits (MMCA.Store/MMCA.Store.CI.slnf:55): adoption here means running on every pull
request, not merely existing.
That project also carries two ADC-local hardening tests over the same factory, which are not
framework-suite subclasses and are counted as neither: UiRateLimitingTests.cs:16 (burst shedding at
the configured window, exempt paths, per-IP partitioning, and the shipped values) and
BoundedCircuitHandlerTests.cs:16 (the active-circuit ceiling, permit release, and the retention
limits). They belong to ADR-088's own-host hardening rather than to this record, and they are noted
here only so the inventory above is not read as the whole content of that test project.
Revision (2026-09-22): the layer rules also ship as compile-time targets
The runtime suites above judge compiled assemblies, which places the first failure in the architecture
test tier. MMCA.Common.Shared now also packs buildTransitive/MMCA.Common.Shared.targets
(source MMCA.Common/Source/Build/MMCA.Common.Shared.targets, packed from
MMCA.Common/Source/Core/MMCA.Common.Shared/MMCA.Common.Shared.csproj), which NuGet imports into every
project that takes any MMCA.Common package, directly or through a project reference, with no opt-in.
It keys on the {App}.{Module}.{Layer} suffix (.Shared, .Domain, .Application,
.Infrastructure, .API, .UI) and judges only the references a project declares itself: a
forbidden ProjectReference fails with MMCA0001 and a forbidden PackageReference with MMCA0002
at ResolveProjectReferences, before compilation, on the developer's machine.
The rule set is the reference-level mirror of ArchitectureRules.Purity, .Transport and .Modules
and deliberately never exceeds it. The reference inventory of MMCA.ADC, MMCA.Store and MMCA.Helpdesk
was checked against it before it shipped, and all three pass. A project opts out with
<MmcaLayerEnforcement>false</MmcaLayerEnforcement> beside a comment saying why; the opt-out is per
project on purpose, so a whole repository cannot switch the rule off silently. The package-consumption
job in MMCA.Common/.github/workflows/ci.yml builds one valid probe and two violating probes against
the packed feed and asserts both error codes fire, so a file silently missing from the package cannot
pass. Consumers receive the targets on the next lockstep version bump (ADR-016); nothing in a consumer
changes until then.
Revision (2026-09-25): committed OpenAPI documents, and page-test bases in Testing.UI
The live-document guard now sits beside a committed document in both consumers. MMCA.ADC and
MMCA.Store each write Source/Services/<host>/openapi/<host>.json at build time, commit it, and fail
CI on an uncommitted difference (ADR-046, Revision 2026-09-25). This suite is unchanged:
OpenApiContractTestsBase still reads no committed file (OpenApiContractTestsBase.cs:16) and still
asserts the served document against a floor and a pinned resource list (:38, :51). The two checks
answer different questions: the runtime base asks whether a booted host serves a usable contract, and
the consumer's build-time gate asks whether the contract changed without review.
Shared page-test bases for the framework's identity pages ship in MMCA.Common.Testing.UI. They
are bUnit component bases, not runtime conformance suites, and are not counted among the seven above,
but they follow the same write-once, subclass-thin lever (ADR-015):
ConfirmEmailPageTestsBase
(MMCA.Common/Source/Hosting/MMCA.Common.Testing.UI/Pages/ConfirmEmailPageTestsBase.cs:26) pins the
anonymous /confirm-email page (a complete link redeemed exactly once, an incomplete one landing on
manual entry, a refusal keeping the form, a resend reporting the same thing either way);
RoleAdminListPageTestsBase<TPage>
(MMCA.Common/Source/Hosting/MMCA.Common.Testing.UI/Pages/RoleAdminListPageTestsBase.cs:21) and
RoleAdminEditPageTestsBase<TPage>
(MMCA.Common/Source/Hosting/MMCA.Common.Testing.UI/Pages/RoleAdminEditPageTestsBase.cs:22) pin the
consumer's own half of the ADR-116 role roster and role editor pages. Both consumers run all three
through one-line sealed subclasses in their Identity UI test projects: MMCA.ADC
(MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Pages/Users/ConfirmEmailTests.cs:11,
Pages/Roles/RoleListTests.cs:14, Pages/Roles/RoleEditTests.cs:16) and MMCA.Store
(MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.UI.Tests/Pages/Users/ConfirmEmail/ConfirmEmailTests.cs:14,
Pages/Roles/RoleListTests.cs:13, Pages/Roles/RoleEditTests.cs:13). Base and test counts for the
package are owned by MMCA.Common/FACTS.md.
Revision (2026-10-01)
No decision or rationale changed; the seven bases still ship in MMCA.Common.Testing and are
subclassed as inventoried above. Three statements are corrected. The gateway probe gate is
switchable. MmcaGatewayHardeningTestsBase exposes ActiveHealthChecksExpected
(MmcaGatewayHardeningTestsBase.cs:111, default true); when a host sets it to false the gate
asserts that no cluster carries an enabled active probe (MmcaGatewayHardeningTestsBase.cs:308-313).
Both gateway subclasses set it to false
(MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GatewayHardeningTests.cs:89,
MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GatewayHardeningTests.cs:93), so the Decision bullet
now describes the gate as the probe state of every cluster rather than an active probe on every
cluster. Two suites, not one, cover every REST host: OpenAPI and problem details are both
subclassed on all seven extracted REST services (four ADC, three Store), which the Context adoption
paragraph and the Trade-offs now say consistently. The page-test subclasses recorded in the
2026-09-25 Revision are not all one-line: only Store's ConfirmEmailTests.cs:14 has no body; ADC's
ConfirmEmailTests.cs:11 and RoleListTests.cs:14 carry a constructor plus overrides, and Store's
RoleListTests.cs:13 and RoleEditTests.cs:13 carry the overrides the role-admin bases require.
The layer targets recorded in the 2026-09-22 Revision are already in every consumer: MMCA.ADC,
MMCA.Store and MMCA.Helpdesk all pin MMCA.Common.Shared 1.216.0
(MMCA.ADC/Directory.Packages.props:105, MMCA.Store/Directory.Packages.props:8,
MMCA.Helpdesk/Directory.Packages.props:84). Citations refreshed: the gateway-hardening test methods,
the ADC Conference 412 test, the seven problem-details subclasses, the gateway subclasses and the
Store recording forwarder, the decorator subclasses (now under Cqrs/), and the ADR-015 and ADR-046
cross-references.
Revision (2026-10-06)
- The lockstep pin has moved; the claim it supports has not. MMCA.ADC, MMCA.Store and
MMCA.Helpdesk all pin
MMCA.Common.Shared1.232.0 now, not the 1.216.0 the 2026-10-01 Revision recorded, at the same lines (MMCA.ADC/Directory.Packages.props:105,MMCA.Store/Directory.Packages.props:8,MMCA.Helpdesk/Directory.Packages.props:84), so every consumer still carries the compile-time layer targets. - The Status block now names the 2026-09-10 and 2026-10-01 Revisions, which existed as body sections but were missing from it.
- ADC's local rate-limiting test moved: the class recorded at
UiRateLimitingTests.cs:16in the 2026-09-10 Revision is now declared atMMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/UiRateLimitingTests.cs:18;BoundedCircuitHandlerTests.cs:16is unchanged. - Anchors re-verified against current source. The
SqlServerIntegrationTestFixtureBasecitations in the Decision now point at the first client that forces the build andMigrate(:89), the Respawner creation and per-test reset (:95,:115), and the drop insideDisposeAsync(:132); the class (:27) and the two environment variables (:75,:76) were already current.
Revision (2026-10-07)
Re-verified against current source. No decision, rationale or adoption changed; the seven bases and the subclass inventory above stand. Only the lockstep pin moved.
- The lockstep pin is now 1.233.0. MMCA.ADC, MMCA.Store and MMCA.Helpdesk all pin
MMCA.Common.Shared1.233.0 (MMCA.ADC/Directory.Packages.props:101,MMCA.Store/Directory.Packages.props:8,MMCA.Helpdesk/Directory.Packages.props:84), not the 1.232.0 the 2026-10-06 Revision recorded. The pin carries the compile-time layer targets only into a package-mode build, because they arrive through the package'sbuildTransitivefolder (MMCA.Common/Source/Core/MMCA.Common.Shared/MMCA.Common.Shared.csproj:18). ADC and Store build in package mode. Helpdesk does not by default: its committedlocal.propssetsUseLocalMMCA=true(MMCA.Helpdesk/local.props:6) and its CI builds in local-source mode (MMCA.Helpdesk/.github/workflows/ci.yml:10). That mode swaps inMMCA.Common.LocalSource.targets(MMCA.Helpdesk/Directory.Build.targets:6), which does not importMMCA.Common.Shared.targets. For Helpdesk the pin therefore covers only a package-mode build (a generated app, or a clone withlocal.propsdeleted). - Anchors re-verified against current source: the ADC
MMCA.Common.Sharedpin is atMMCA.ADC/Directory.Packages.props:101(the 2026-10-01 and 2026-10-06 Revisions cite:105, which now opens theMMCA.Common.AIblock); the Store (:8) and Helpdesk (:84) pin lines are unchanged.
Revision (2026-10-21)
Re-verified against current source. The decision stands: the seven contract bases still ship in
MMCA.Common.Testing and are subclassed as inventoried above. What moved is the OpenAPI guard's
reach, the decorator registration it protects, the pin, and the anchors.
- The OpenAPI base carries an opt-in baseline diff, and every OpenAPI subclass opts in.
Overriding
BaselineDocumentPath(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:80) enablesOpenApiDocument_HasNoBreakingChangesAgainstTheBaseline(:118), which compares the live document with the committed baseline throughOpenApiBreakingChangeDetector.Detect(:149,OpenApiBreakingChangeDetector.cs:44) and fails only on a breaking change; settingMMCA_UPDATE_OPENAPI_BASELINE(:41) to1rewrites the baseline and fails the run (:129-140). All four ADC and all three Store subclasses return anopenapi-baseline.jsonbeside the test file (for exampleMMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/OpenApiContractTests.cs:29-32,MMCA.Store/Tests/Integration/MMCA.Store.Sales.IntegrationTests/Contract/OpenApiContractTests.cs:31). The Decision bullet formerly headed "No committed snapshot in the runtime guard" and the matching Rationale bullet now say so, the Rationale stating that the baseline narrows the regenerated-snapshot failure to a reviewed commit rather than removing it, and the Trade-offs bullet on coarse assertions now scopes "coarse" to the default checks, since the opted-in diff catches a newly required request property, a removed response property or a type change (OpenApiBreakingChangeDetector.cs:32-35); the 2026-09-25 statement that the base "still reads no committed file" holds only for its default path. - The decorator pipeline is sealed.
AddApplicationDecorators()marks the collection, and a laterScanModuleApplicationServices,AddEntityCrud,AddEntityUpdateVerb,AddEntityUpdateor a second pipeline call throws (MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:310-319,:116,:209,DependencyInjection.ModuleScanning.cs:49,DependencyInjection.Crud.cs:80,:142,:193); a raw handler registration outside those helpers is caught byVerifyDecoratorPipeline()(DependencyInjection.cs:244) rather than at registration. The ADC, Store and Helpdesk subclasses register throughAddMmcaApplicationPipeline(...)(MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:50,MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:52,MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/DecoratorPipelineOrderTests.cs:67-68), so the runtime walk now guards a regression in the decorator registration order rather than a late module scan, which fails at registration instead. The Decision bullet and the Helpdesk paragraph are updated. - The
Conformancefolder holds more than the seven bases and the ADR-079 base. It also shipsTenantResolutionTestsBase(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/TenantResolutionTestsBase.cs:27), a fitness base that builds no host and sends no request (:24), subclassed only by MMCA.Common's ownTests/Hosting/MMCA.Common.Testing.Tests/Conformance/TenantResolutionConformanceTests.cs:11and by no ADC, Store or Helpdesk test, plus the breaking-change detector and its two result types (OpenApiBreakingChangeDetector.cs:38,OpenApiBreakingChange.cs,OpenApiBreakingChangeKind.cs). None of them is counted among the seven; the Related entry names them. - MMCA.Common's own conformance tests are stated precisely. The decorator suite remains the one
base of the seven it runs as a collected subclass; it also runs two collected subclasses of bases
outside the seven,
MiddlewarePipelineOrderTests(Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/MiddlewarePipelineOrderTests.cs:12) andTenantResolutionConformanceTests(TenantResolutionConformanceTests.cs:11). The OpenAPI baseline gate and the gateway-hardening base are exercised as self-tests through a private stub subclass and an abstract sample (Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/OpenApiContractTestsBaseBaselineTests.cs:155-156,MmcaGatewayHardeningTestsBaseTests.cs:75). - The lockstep pin is now 1.250.0. MMCA.ADC, MMCA.Store and MMCA.Helpdesk all pin
MMCA.Common.Shared1.250.0 (MMCA.ADC/Directory.Packages.props:100,MMCA.Store/Directory.Packages.props:8,MMCA.Helpdesk/Directory.Packages.props:83), not the 1.233.0 the 2026-10-07 Revision recorded. - Anchors re-verified against current source:
OpenApiContractTestsBaseclass:32, its two facts:83and:98,MinimumPathCount:58,CorePublicResources:71, the presence check:106-114; the OpenAPI subclasses at ADC Conference:15, Engagement:15, Identity:16, Notification:17and Store Catalog, Identity and Sales:15; the Helpdesk decorator subclass:38with its generic arguments:40-43(the:13namespace import and the ADC and Store:29classes are unchanged); and the cross-references015-architecture-fitness-functions.md:95and046-http-api-versioning.md:99.
Revision (2026-10-26)
Re-verified against current source. No decision, rationale or adoption changed; the seven bases and the subclass inventory above stand. The lockstep pin moved, and four anchors were re-pointed.
- The lockstep pin is now 1.261.0. On merged
main, MMCA.ADC, MMCA.Store and MMCA.Helpdesk all pinMMCA.Common.Shared1.261.0 (MMCA.ADC/Directory.Packages.props:108,MMCA.Store/Directory.Packages.props:8,MMCA.Helpdesk/Directory.Packages.props:83), not the 1.250.0 the 2026-10-21 Revision recorded, so every consumer still carries the compile-time layer targets in a package-mode build. - Anchors re-verified against current source: the ADC
MMCA.Common.Sharedpin is atMMCA.ADC/Directory.Packages.props:108(the 2026-10-21 Revision cites:100); the Store (:8) and Helpdesk (:83) pin lines are unchanged; theMiddlewarePipelineOrderTestsBaseclass declaration in the Decision is atMMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:31(was:29; the pinned 1.261.0 contains the two added lines); and the Context cross-references are015-architecture-fitness-functions.md:118(was:95) and046-http-api-versioning.md:100(was:99).
Revision (2026-10-28)
Re-verified against current source. No decision, rationale or adoption changed; the seven bases and the subclass inventory above stand. The lockstep pin moved, and the inventory of MMCA.Common's own conformance tests now names every file in that folder.
- The lockstep pin is now 1.265.1. MMCA.ADC, MMCA.Store and MMCA.Helpdesk all pin
MMCA.Common.Shared1.265.1 (MMCA.ADC/Directory.Packages.props:108,MMCA.Store/Directory.Packages.props:8,MMCA.Helpdesk/Directory.Packages.props:83), not the 1.261.0 the 2026-10-26 Revision recorded, so every consumer still carries the compile-time layer targets in a package-mode build. - MMCA.Common's conformance self-tests are listed in full. Beyond the OpenAPI baseline stub and
the gateway-hardening sample, the
Tests/Hosting/MMCA.Common.Testing.Tests/Conformancefolder holdsTenantResolutionTestsBaseTests(TenantResolutionTestsBaseTests.cs:15, with its privateProbesubclass at:163), a self-test of the no-host tenancy base, and two unit-test classes for the breaking-change detector (OpenApiBreakingChangeDetectorTests.cs:14,OpenApiBreakingChangeDetectorParameterTests.cs:15). None of them is adoption, and none tests one of the seven bases directly; the adoption inventory in the Decision section now names them. - Anchors re-verified against current source: the three pin lines (
:108,:8,:83) are unchanged from the 2026-10-26 Revision; the Decision'sMinimumPathCount(:58) andCorePublicResources(:71) anchors inOpenApiContractTestsBase.csare current, which supersedes the:38and:51the 2026-09-25 Revision cites (:51now declaresOpenApiDocumentPath).
Related
ADR-015 (the structural / registration fitness layer this complements; its stated non-goal, "not
runtime behavior", is exactly this ADR's scope, and the two tiers ship as two separate packages),
ADR-014 (the decorator execution order DecoratorPipelineOrderTestsBase proves at runtime), ADR-013
(the RFC 9457 ProblemDetails edge contract ProblemDetailsContractTestsBase guards), ADR-046 (HTTP
API versioning, whose "shared fitness contract" is one of these bases), ADR-079 (the shared HTTP edge
pipeline, whose own order-checking base ships beside these in the same Conformance folder but is not
one of these seven; the folder also holds the no-host tenancy fitness base TenantResolutionTestsBase,
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/TenantResolutionTestsBase.cs:27, which only
MMCA.Common's own tests subclass, and the OpenApiBreakingChangeDetector the OpenAPI baseline gate uses,
with its OpenApiBreakingChange and OpenApiBreakingChangeKind result types),
ADR-016 (lockstep versioning:
a new conformance base reaches consumers only through a release and a full sweep). Package inventory
for the framework lives in MMCA.Common/FACTS.md.