↑↓ to navigate Enter to open "…" all these words ANDOR to combine

Onboarding guide

29. Testing & Quality Infrastructure

What this group covers. Everything the codebase uses to prove itself: the six reusable test-support packages that ship out of MMCA.Common/Source/Hosting (MMCA.Common.Testing, MMCA.Common.Testing.Architecture, MMCA.Common.Testing.Aspire, MMCA.Common.Testing.Azure, MMCA.Common.Testing.E2E, MMCA.Common.Testing.UI, six of the twenty-six packages published from MMCA.Common and listed in MMCA.Common/FACTS.md:19-47; the seventh test-support package, MMCA.Common.AI.Testing, belongs to group-27), the architecture-fitness rule library that gates the build, the runtime-conformance bases that gate a booted host, the backend-less component Gallery harness, the BenchmarkDotNet performance suite, the weekly load tier, and the many per-repo test projects that consume all of it. The distinction to hold onto while reading: most of the types in this group are reusable bases, fixtures, harnesses, and helpers compiled into and shipped by MMCA.Common, while the concrete [Fact]-bearing test classes that subclass them live in each consumer repo (MMCA.Common.*.Tests, MMCA.ADC.*.Tests, MMCA.Store.*.Tests). Those individual test classes are cataloged by project in the companion rollup section; this chapter teaches the machinery they stand on. For how the tiers map onto CI jobs and solution filters, see Testing Architecture & Solution Composition.

There are seven moving parts. Five map onto the test pyramid, one is a governance layer, and one evaluates a dependency that is not deterministic and so cannot be asserted exactly:

  1. Integration-test scaffolding (IIntegrationTestFixture, IntegrationTestBase<TFixture>, SqlServerIntegrationTestFixtureBase<TEntryPoint>, CrossServiceFixtureBase, CrossServiceHostIsolation, ProductionHostApplicationFactory<TEntryPoint>, JwtTokenGenerator, FeatureManagementTestExtensions, TestPolling, EntityBuilderBase<TBuilder, TEntity>) boots a real service host in-process against a throwaway SQL Server database and drives it over HTTP.
  2. Architecture fitness functions (IArchitectureMap, ArchitectureMapBase, Layer, LayerRef, ArchitectureAssert, RuleHelpers, CrossEntityNavigationFinder, the forty-four ArchitectureRules partial files, and the sixty-four abstract *TestsBase classes including RouteAuthorizationTestsBase, ModuleConformanceTestsBase<TModule>, SubstitutabilityTestsBase, PiiInventoryTestsBase and BrandColorTokenTestsBase) turn architectural rules into build-gating assertions that run identically across every repo.
  3. Component (bUnit) testing (BunitComponentTestBase, TestPrincipal, BunitInteractionExtensions, CapturingHttpMessageHandler, UiHttpServiceHarness, HttpTestDoubles, StubTokenStorageService, MarkupSnapshot, FieldErrorAccessibilityExtensions) render Blazor components in isolation with real MudBlazor services and faked HTTP/auth edges; the last of them asserts that a field's validation error reaches assistive technology, not just sighted users (a role="alert" element, aria-invalid="true" on the field, and an aria-describedby target holding the message, MMCA.Common.Testing.UI/Infrastructure/FieldErrorAccessibilityExtensions.cs:7-26).
  4. End-to-end (Playwright) testing (PlaywrightFixture, E2ETestBase, E2ETestConfiguration, AuthOutcomeRules, PageExtensions, AxeOptions, AccessibilityViolationException, WebVitalsCollector, the reusable page objects LoginPage / RegisterPage / ProfilePage / ForgotPasswordPage / ResetPasswordPage / RoleAdminPage, the E2EPolling wait helper, and the shipped workflow suites such as AuthorizationTestsBase and KeyboardOperabilityTestsBase, whose four keyboard-only checks cover the skip link, the dialog focus trap, Escape returning focus to the opener, and a visible focus indicator on the leading Tab stops, MMCA.Common.Testing.E2E/Workflows/Accessibility/KeyboardOperabilityTestsBase.cs:11-29,41) drive a real browser against a running app, asserting accessibility and performance alongside behavior.
  5. Contract and pipeline bases (SecurityHeadersTestsBase, OpenApiContractTestsBase<TFixture>, OpenApiBreakingChangeDetector, TenantResolutionTestsBase, ProblemDetailsContractTestsBase<TFixture>, ServiceInfoVersioningContractTestsBase<TFixture>, GracefulShutdownTestsBase<TEntryPoint>, DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult>, MiddlewarePipelineOrderTestsBase, DependencyInjectionAssert, HandlerTestBase<THandler>) pin cross-cutting HTTP and pipeline guarantees so a refactor cannot silently drop them.
  6. Model-evaluation tests (GoldenCase, GoldenCorpus, GoldenReplayTests, LiveJudgeTests, PromptContractTests, SessionScoringPromptContractPinTests) hold ADC's AI session-scoring judge to a committed corpus of recorded cases, one half replayed offline on every CI leg and one half scored by the real model behind a trait gate.
  7. Orchestration smoke tests (AppHostFixtureBase, AppHostTestBase<TFixture>, AppHostEnvironmentRequirement, AppHostEnvironmentGate, DockerAvailability, DeveloperCertificateAvailability, EphemeralRsaKeyPair, AppHostReadinessBudget, AppHostProbePaths, H2cProbe) boot a real Aspire AppHost once per collection and assert the orchestration every other tier boots past.

The cloud adapter packages of chapter 28 add a vendor dimension to that pyramid, and the group carries it in three layers. Each adapter package has a plain unit-test project (MMCA.Common.Aws.Tests, MMCA.Common.Azure.Tests, MMCA.Common.Gcp.Tests) that drives the adapter against in-process fakes such as FakeSecretsStore, FakeFcmMessagingClient and InMemoryGcsStorageClient, and each carries a CloudVendorRulesTests class of its own. Behind them sit the emulator tiers, each starting one pinned container per test assembly through an xUnit assembly fixture: LocalStackFixture for S3, SQS, SNS, Secrets Manager, Systems Manager and DynamoDB (MMCA.Common/Tests/Hosting/MMCA.Common.Aws.LocalStack.Tests/LocalStackFixture.cs:9,19), and FakeGcsServerFixture plus FirestoreEmulatorFixture for Cloud Storage and the FCM installation registry (MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Emulator.Tests/FakeGcsServerFixture.cs:7,18, FirestoreEmulatorFixture.cs:5,14). Each image is pinned so an upstream emulator change cannot turn into a red CI run nobody caused (LocalStackFixture.cs:14-17). The Azure Service Bus tier is the one that ships as a package: ServiceBusEmulatorFixtureBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Azure/Fixtures/ServiceBusEmulatorFixtureBase.cs:54) starts one warm emulator for the whole broker-parity tier to respect the emulator's connection quota and admin-plane throttle (:12-18), pins a 2.x image because the management plane MassTransit provisions its topology through first shipped in 2.0.0 (:20-27), keeps the bus on the fixture rather than on the per-test class so the topology is provisioned once per run (:29-38), and bounds both startup phases so a hang fails with a named timeout instead of being killed silently at the job timeout (:40-46). The third layer is the fitness rule that keeps the vendor SDKs out of the core: CloudVendorIsolationTestsBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/CloudVendorIsolationTestsBase.cs:15) fails the build when an assembly a repo declares cloud-neutral names an Azure, AWS or Google Cloud SDK type, references a vendor SDK or broker-transport assembly, or references a vendor adapter package (:4-7, two facts at :27-33). It is the compiled-assembly half of the EnforceCloudVendorBoundary build guard, which judges only the declared package references, so a vendor type arriving through a transitive path is caught too (:9-12, ADR-134). MMCA.Common activates it in CloudVendorIsolationTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/CloudVendorIsolationTests.cs:17).

This whole group is the [Rubric §14, Testability] story made concrete: the framework does not merely permit testing, it ships the reusable substrate so every consumer tests the same way. The front-end tiers additionally carry [Rubric §21, Accessibility], [Rubric §22, Responsive/Cross-Browser], [Rubric §23, Front-End Performance], and [Rubric §28, Front-End Testing]; the fitness library carries [Rubric §34, Architecture Governance & Documentation]. Two ADRs govern the two governance tiers and are worth reading before touching anything here: ADR-015 for the structural rules, and ADR-058 for the runtime conformance suites that cover exactly what ADR-015 declared out of scope: "the tests assert structure / registration, not runtime behavior" (Website/docs-src/adr/015-architecture-fitness-functions.md:64-65).

Integration tests: a real host, a throwaway database, a per-test reset

The integration tier boots the actual application, not a mock of it. The abstraction at its center is IIntegrationTestFixture (MMCA.Common.Testing/Fixtures/IIntegrationTestFixture.cs:8): a two-method contract, CreateClient() (IIntegrationTestFixture.cs:11) and ResetDatabaseAsync() (IIntegrationTestFixture.cs:19), that hides how the host and its database are provisioned. Its remarks are load-bearing: a host running multiple physical data sources (database per service, see primer and ADR-006) must reset every relational source, and a fixture can resolve IEntityDataSourceRegistry / IDataSourceResolver from the booted host to enumerate them (IIntegrationTestFixture.cs:13-18).

IntegrationTestBase<TFixture> (MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:13) is the per-test base every integration test class inherits. It implements xUnit's IAsyncLifetime, so InitializeAsync resets the database before each test (IntegrationTestBase.cs:31) and DisposeAsync disposes the HTTP client after (IntegrationTestBase.cs:34-39). It exposes typed HTTP helpers (GetAsync<T>, PostAsync<T>, PutAsync<T>, PutAsync, DeleteAsync, IntegrationTestBase.cs:51-72), bearer-token management (SetBearerToken / ClearAuthentication, IntegrationTestBase.cs:42-48), and a thread-safe NextId() counter seeded at 1000 (IntegrationTestBase.cs:16,75) so parallel tests never collide on generated identifiers. Downstream projects subclass it to add domain-specific auth and entity helpers.

SqlServerIntegrationTestFixtureBase<TEntryPoint> (MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:27) is the concrete fixture scaffolding. InitializeAsync (SqlServerIntegrationTestFixtureBase.cs:67) mints a GUID-suffixed database name (:71-72), sets ASPNETCORE_ENVIRONMENT=Testing and the top-level connection string as process environment variables (so the host reads them at configure-time, :75-77), builds the subclass-supplied WebApplicationFactory (:79), and forces database creation by requesting the first client, which runs the host's Migrate init strategy (:87-89). The "may need dropping" flag is set before that first client (:81-85), and the drop is guarded by IF DB_ID(...) (:186-188), so a host that fails after its migration created the database still has it dropped rather than leaked on the shared server. It then builds a Respawn checkpoint that ignores __EFMigrationsHistory (:95-99); ResetDatabaseAsync (:104) replays that checkpoint between tests, and DisposeAsync (:120) drops the throwaway database (:130-133, DropDatabaseAsync at :172) and restores every pushed environment variable (:135, restore loop at :162-170, first-value-wins bookkeeping at :151-160 so a re-pushed key cannot clobber its own restore point). The Testing environment is chosen deliberately so appsettings.Development.json (which points a module's DataSources entry at localhost) does not load, leaving the resolver to collapse onto the overridden top-level connection string, a single-database monolith shape (:16-24). Server selection defaults to LocalDB but is overridable through SqlBaseEnvironmentVariable (:58, read at :69-70) so CI can target a SQL service container. Subclasses push their own host-specific settings (test JWT key material, throttle lifts, faked gRPC edges) through the ConfigureTestEnvironment hook (:147, invoked at :77), which routes them through the same restore bookkeeping. The fixture also exposes ConnectionString (:45) so SQL-fidelity tests can read the raw tables, and Services (:52) so a cross-service test can resolve a consumer-side handler out of the booted host. Because these fixtures need a reachable SQL Server, the per-module *.Integration.slnf suites build in a headless sandbox but only run in CI.

One tier up sits CrossServiceFixtureBase (MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:47), which boots several hosts in one process against a real Testcontainers SQL Server and a real Testcontainers RabbitMQ (CrossServiceFixtureBase.cs:2-3,18-25), so the genuine outbox to broker to consumer round-trip is exercised rather than faked. Each logical source it routes is a CrossServiceDataSource record pairing the config key with its physical database (CrossServiceFixtureBase.cs:15, list supplied by the subclass at :60). The design note worth internalizing is why the hosts must boot strictly sequentially: every host reads its connection string, MessageBus settings and JWT settings from configuration at configure-time, before builder.Build(), so process environment variables are the only override channel that lands in time, and the one genuinely per-host key is the SQL connection string (CrossServiceFixtureBase.cs:26-39). A booted host has already snapshotted its connection, so mutating the environment for the next one is safe. Two smaller decisions in the same file are worth knowing because they are non-obvious: the databases are pre-created before any host boots, since EF's CREATE DATABASE runs before its migration lock is taken and a double-booted host would race itself (CrossServiceFixtureBase.cs:107-111), and each module is routed to a named data source whose connection string differs only by Application Name, so EF's process-global model cache keys each host's model separately instead of letting the first booted host win (CrossServiceFixtureBase.cs:231-246,260-276). Booting several hosts in one process has a second trap that the model-cache keying does not close: the default entity-configuration assembly provider scans AppDomain.GetAssemblies, so each host would see its peers' module assemblies, claim their data sources, and let its outbox and internal-command processors drain the peer databases, dead-lettering a row as handler_missing when only the peer holds its handler (MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:14-21). CrossServiceHostIsolation (CrossServiceHostIsolation.cs:30) closes it with one extension member, IsolateCrossServiceHost<TEntryPoint>() (:42), which every host factory of a CrossServiceFixtureBase subclass calls from ConfigureTestServices: it replaces the registered provider with a private decorator, HostScopedAssemblyProvider (:115-118), that narrows the inner result to the assemblies the host's own .deps.json names (the set it would load in its own process) and keeps explicitly registered AdditionalAssemblies unfiltered (:23-28,47-59), and it throws when that .deps.json is missing rather than silently scanning everything (:86-91). ADC's four cross-service factories each make that call (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/ConferenceCrossServiceFactory.cs:46, and the Identity, Engagement and Notification siblings at IdentityCrossServiceFactory.cs:40, EngagementCrossServiceFactory.cs:55, NotificationCrossServiceFactory.cs:56). A host that needs no database at all gets the much smaller boot path: ProductionHostApplicationFactory<TEntryPoint> (MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:23) pins UseEnvironment("Production") (ProductionHostApplicationFactory.cs:36) so the production-only branches (restrictive CORS, HSTS emission) are the ones under test, and captures the started IHost (ProductionHostApplicationFactory.cs:29) because StopAsync is not reachable through the WebApplicationFactory surface at all.

Four helpers round out the tier. JwtTokenGenerator (MMCA.Common.Testing/Support/JwtTokenGenerator.cs:30) issues RS256-signed tokens (GenerateToken, JwtTokenGenerator.cs:112, signing credentials built at :130-131) using an embedded development RSA keypair (DefaultPublicKeyPem at :49, DefaultPrivateKeyPem at :68) under a fixed kid of mmca-test-key (:41), so integration tests exercise the exact JWKS/RS256 validation code path production runs (ADR-004); its ConfigureInProcessTokenValidation (:170) is what a test factory calls to re-point a host's JwtBearerOptions at that committed key instead of a network authority (it nulls both Authority and ConfigurationManager, :174-175). The class remarks flag, correctly, that the committed keypair is insecure by design and must never be used in a real deployment (:22-28). FeatureManagementTestExtensions (MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:10) adds a ConfigureTestFeatureFlags extension member (:35) that layers an in-memory FeatureManagement:* collection on top of the configuration the host already registered and re-registers the merged root in its place (:40-59); building a flags-only root instead would silently hand every component constructed afterwards a configuration with nothing but feature flags in it, because .NET DI resolves a non-collection dependency to the last registration (:18-25). TestPolling (MMCA.Common.Testing/Support/TestPolling.cs:9) replaces the pre-assert sleep that eventually-consistent paths tempt you into: PollUntilAsync (TestPolling.cs:22) probes until the condition holds or a 60-second budget expires at a 500 ms interval (TestPolling.cs:31-32) and returns the last probed value either way, so a timeout still fails on the real assertion message. EntityBuilderBase<TBuilder, TEntity> (MMCA.Common.Testing/Builders/EntityBuilderBase.cs:9) is a minimal fluent-builder base whose single abstract Build() (:17) returns the entity through its domain factory, so test setup specifies only what a test cares about. Together these embody [Rubric §11, Security] (real token validation rather than bypassed auth middleware) and [Rubric §14, Testability].

Orchestration smoke tests: booting a real AppHost

Every tier above boots hosts directly, which means none of them ever runs the file that states how a whole stack fits together. The AppHost declares the project resources, the per-service databases, the broker, the JWKS and gRPC references and the WaitFor graph, and nothing else compiles a claim about any of it: a build type-checks its C# but never executes it, and the integration tier boots each host through WebApplicationFactory, bypassing the orchestration entirely (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Aspire/Fixtures/AppHostFixtureBase.cs:16-20, Website/docs-src/adr/117-apphost-integration-test-base.md:13-21). MMCA.Common.Testing.Aspire is the tier that closes that gap, shipped as one of the five test-support packages rather than copied per repo (ADR-117, accepted 2026-09-09 and revised 2026-09-11 once both consumer tiers subclassed it, Website/docs-src/adr/117-apphost-integration-test-base.md:3-10), and it adds the tier ADR-098 deliberately left out.

AppHostFixtureBase (MMCA.Common.Testing.Aspire/Fixtures/AppHostFixtureBase.cs:41) is the xUnit collection fixture: it boots a real AppHost once and hands the running DistributedApplication to every test in the collection. Its central distinction is readiness rather than liveness, per resource. "The application started" only means the orchestrator launched the processes, so resources carrying a health check are awaited healthy while resources carrying none can only be awaited Running, and the fixture says which of the two happened instead of pretending they are the same (AppHostFixtureBase.cs:22-28). Like the SQL fixtures it records only the first original value of every environment variable it pushes, so a re-pushed key cannot clobber its own restore point and a collection cannot leak key material into the ones that run after it (AppHostFixtureBase.cs:43-48). An unmet precondition is a skip, not a failure: the gate sets SkipReason and nothing is started (AppHostFixtureBase.cs:56, IsAvailable at :59), and the consuming project owns the skip call, because this package deliberately takes no dependency on the xUnit assertion library (AppHostFixtureBase.cs:29-34). The call is written as a branch, if (!Fixture.IsAvailable) { Assert.Skip(Fixture.SkipReason!); }, never as Assert.SkipWhen(!Fixture.IsAvailable, Fixture.SkipReason!): SkipWhen validates its reason before the condition, and the reason is null exactly when the stack did start, so that form throws on every runner able to boot the AppHost (AppHostFixtureBase.cs:34-38).

What counts as a precondition is declared, not guessed. AppHostEnvironmentRequirement (MMCA.Common.Testing.Aspire/Preconditions/AppHostEnvironmentRequirement.cs:10) is a [Flags] enum of None, OptIn, Docker and DeveloperCertificate (:12-30), and AppHostEnvironmentGate (MMCA.Common.Testing.Aspire/Preconditions/AppHostEnvironmentGate.cs:9) turns the missing ones into a sentence a human can act on, or null to run (AppHostEnvironmentGate.cs:23-27, with a testable core that takes every machine fact as a delegate at :38-42). The opt-in is the MMCA_APPHOST_TESTS variable (AppHostEnvironmentGate.cs:16), absent by default so an ordinary unit loop never starts an orchestrator (:12-14). The other two probes encode failures that cost real debugging time. DockerAvailability (MMCA.Common.Testing.Aspire/Preconditions/DockerAvailability.cs:13) checks DOCKER_HOST, the /var/run/docker.sock socket and the docker_engine named pipe (DockerAvailability.cs:16,19,22) rather than shelling out to docker info, because a process launch that can block for seconds on a misconfigured daemon is the wrong thing to put in front of a skip decision (DockerAvailability.cs:6-11). DeveloperCertificateAvailability (MMCA.Common.Testing.Aspire/Preconditions/DeveloperCertificateAvailability.cs:22) detects the ASP.NET Core HTTPS development certificate by the SDK's own extension OID 1.3.6.1.4.1.311.84.1.1 (:28, validity judged at :44-52) and detects only, never running dotnet dev-certs (:15-20): without that certificate an https resource answers its probe with UntrustedRoot, never turns healthy, and every WaitFor edge into it waits out the whole budget, so the symptom is a timeout on an unrelated resource (:7-14). EphemeralRsaKeyPair (MMCA.Common.Testing.Aspire/Preconditions/EphemeralRsaKeyPair.cs:20) closes the matching auth-side failure: it mints a throwaway RSA-2048 keypair (:38,42-46) into E2E_JWT_PRIVATE_KEY_PEM and E2E_JWT_PUBLIC_KEY_PEM (:26,32), the variables the framework's WithE2eRsaKeys() AppHost extension forwards to an Identity resource, because otherwise the appsettings.json placeholder reaches the JwtBearer options factory, which throws on the first request, so every request including the liveness probe answers 500 and the resource never turns healthy (:8-16).

Timing and probing are the remaining pieces. AppHostReadinessBudget (MMCA.Common.Testing.Aspire/Fixtures/AppHostReadinessBudget.cs:16) splits the wait into a startup budget and a readiness budget, five minutes each by default (:23-24), because the two fail for different reasons (a cold agent pulling container images versus a wedged health check) and one number makes those indistinguishable in the failure message (:6-11); Remaining(elapsed) (:37-38) keeps readiness a single shared deadline rather than a per-resource allowance, so a stack that wedges on the last of five resources still says so once (:28-33), and Validate (:45-49) rejects a non-positive budget that would cancel its own first wait. AppHostProbePaths (MMCA.Common.Testing.Aspire/Probes/AppHostProbePaths.cs:15) pins the paths a probe uses, /health (:18), /alive (:26), /health/ready (:32), the key set at /.well-known/jwks.json (:35) and the cleartext endpoint name http (:41); it mirrors the framework constants instead of referencing them, with a unit test in MMCA.Common.Testing.Aspire.Tests cross-asserting each pair so a rename cannot silently orphan a probe (:5-13). Its /alive remark carries the operational lesson: gate a startup wait on liveness, never on readiness, because a readiness endpoint aggregates downstream and warm-up checks and a gateway waiting for a service whose own readiness warms up through that gateway deadlocks (:20-25). H2cProbe (MMCA.Common.Testing.Aspire/Probes/H2cProbe.cs:25) asserts that a service's cleartext listener really speaks the protocol its gRPC callers need: with no TLS there is no ALPN to negotiate with, so the request goes out as HTTP/2 with prior knowledge pinned by RequestVersionExact and an HTTP/1.1-only listener answers with a connection error rather than a response (:9-16), under a deliberately short ten-second budget (:28); its keep-alive ping trio (one-second delay, one-second timeout, Always, :30-36) is load-bearing rather than tuning, because HTTP/2 pools one connection per origin and a connection accepted but never answered would otherwise queue every later probe behind it forever (:17-23). On top of all of it, AppHostTestBase<TFixture> (MMCA.Common.Testing.Aspire/Fixtures/AppHostTestBase.cs:29) gives a test the two things only a running application can supply, an HttpClient bound to a named resource endpoint (:44-45) and a resolved connection string (:54-55), plus an RS256 bearer token the booted stack accepts (:57-58). The package tests itself both ways: unit tests over the gates, budgets and probes in MMCA.Common.Testing.Aspire.Tests, and a real boot in MMCA.Common.Testing.Aspire.AppHostTests, where SampleAppHostFixture (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Aspire.AppHostTests/SampleAppHostFixture.cs:13), SampleAppHostCollection (SampleAppHostCollection.cs:9) and SampleAppHostTests (SampleAppHostTests.cs:21) exercise the fixture against a minimal AppHost. This tier is [Rubric §14, Testability] pointed at the one file every other tier looks past.

Architecture fitness functions: rules that gate the build

The layering and DDD conventions this codebase commits to are not left to code review, they are executed as tests. The reusable rule library lives in MMCA.Common.Testing.Architecture and is the subject of ADR-015. Its keystone is IArchitectureMap (MMCA.Common.Testing.Architecture/IArchitectureMap.cs:39): the single per-repo boundary every fitness function keys off. Each repo supplies one implementation (for example StoreArchitectureMap) declaring its layer and module assemblies as LayerRef records (IArchitectureMap.cs:31) tagged by the Layer enum (IArchitectureMap.cs:9), and exposes them through query members such as OfLayer, ModuleDomain, ModuleApplication, For, ModuleOf, and OtherModuleNamespaces (IArchitectureMap.cs:51-81). Most of that surface is derived rather than hand-written: ArchitectureMapBase (MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:11) computes the projections from a single DefineLayers() declaration (ArchitectureMapBase.cs:22, lazily materialized at :13,15-16,25), which also centralizes every assembly and namespace string in one file so Ubuntu CI's case sensitivity is handled in one place (ArchitectureMapBase.cs:8-9); it additionally ships a FindRepoRoot(solutionFileName) walker (ArchitectureMapBase.cs:79) so doc- and config-consistency rules can read committed files no matter what the runner's working directory is. The shared rules consume only the interface, which is why one rule body runs identically across MMCA.Common, MMCA.Store, MMCA.ADC, and Helpdesk: the map is the only thing that varies. Layer deliberately includes optional layers (Ui, Grpc, Contracts, ServiceHost, IArchitectureMap.cs:16-19) that a repo simply omits, so a rule iterating them is vacuously satisfied with no compile dependency on an absent assembly (IArchitectureMap.cs:3-7).

The rule bodies are split across forty-four ArchitectureRules partial files (AI model boundary, anonymization, cancellation tokens, cascade soft-delete, clock reads, cloud vendors, command validators, contracts, controllers, cycles, delete behavior, domain-event handler saves, domain throws, entities, error catalog, events, feature flags, folder width, forwarded JWT audiences, governance, handlers, handler results, idempotency, immutability, inline styles, integration-event purity, layers, lifetime tokens, localization, localized text, markup, modules, naming, PII inventory, protos, purity, query-handler repositories, slices, soft delete, specifications, strongly typed ids, substitutability, transport, and upcasters; the partial type is declared in the first of them at MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CancellationTokens.cs:5). The aggregate-convention rules live inside ArchitectureRules.Entities.cs (for example DomainExposesAggregateRoots at MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Entities.cs:8, AggregateRootsHaveResultFactory at :19, and the generalized DomainFactoriesReturnResult at :53 that extends the "factories always return Result<T>" convention from aggregate roots to value objects) rather than in a dedicated partial, and are surfaced through AggregateConventionTestsBase (MMCA.Common.Testing.Architecture/Bases/Domain/AggregateConventionTestsBase.cs:10). Above the rules sit the abstract *TestsBase classes under Bases/, sixty-four today (MMCA.Common/FACTS.md:89: LayerDependencyTestsBase, DomainPurityTestsBase, MicroserviceExtractionTestsBase, ModuleIsolationTestsBase, PiiConventionTestsBase, DependencyVersionTestsBase, IntegrationEventContractTestsBase, DataResidencyTestsBase, RawQueryableConventionTestsBase, and more), each exposing its rules as [Fact]s that a sealed per-repo subclass activates by supplying its map. Five of the newest carry the pattern into areas that used to be review questions: AiDependencyIsolationTestsBase (MMCA.Common.Testing.Architecture/Bases/Layering/AiDependencyIsolationTestsBase.cs:19) confines a language-model dependency to Infrastructure and the governed MMCA.Common.AI package in both directions, so a vendor SDK reference and a PromptContract used as a domain type both fail (AiDependencyIsolationTestsBase.cs:3-13, [Rubric §16, AI-Native]); DeleteBehaviorConventionTestsBase (Bases/Domain/DeleteBehaviorConventionTestsBase.cs:21) reads the MMCA:DeleteBehaviorSource stamp the framework's RestrictDeleteByDefaultConvention leaves on every foreign key back off a finalized EF Core model, so nothing cascades unless a configuration said so, asserted against an in-memory relational model with no server needed (DeleteBehaviorConventionTestsBase.cs:3-19); FeatureFlagLifecycleTestsBase (Bases/Governance/FeatureFlagLifecycleTestsBase.cs:10) makes every flag constant declare whether it is a permanent capability switch or a temporary rollout toggle and fails the build once a temporary one is past its RemoveBy date, with the judging date overridable so a release, not the build clock, decides (FeatureFlagLifecycleTestsBase.cs:3-9,14-18, ADR-031); IntegrationEventPayloadPurityTestsBase (Bases/Contracts/IntegrationEventPayloadPurityTestsBase.cs:33) requires an integration event to ship from a *.Shared assembly and no property on its wire shape to reach a *.Domain type, nested payload records included (IntegrationEventPayloadPurityTestsBase.cs:3-26, ADR-010); and RawSqlConventionTestsBase (Bases/Cqrs/RawSqlConventionTestsBase.cs:31) bans FromSqlRaw, SqlQueryRaw, ExecuteSqlRaw and ExecuteSqlRawAsync in module code while leaving the interpolated siblings and the framework's IRawSqlQueryExecutor allowed, because the raw four take a plain string, so a concatenated value compiles and every distinct value produces its own statement text (RawSqlConventionTestsBase.cs:5-26). A sixth, StronglyTypedIdTestsBase (Bases/Domain/StronglyTypedIdTestsBase.cs:9), is vacuously satisfied in every repo today: the primitive identifier aliases remain the default identifier model, and the base exists so the first wrapper a repo declares is held to the shape the framework's converters, comparer and route binding are written against (StronglyTypedIdTestsBase.cs:3-8, ADR-115). Two more pin a testability property and a read/write boundary: ClockReadTestsBase (Bases/Domain/ClockReadTestsBase.cs:15) fails when Domain or Application code reads DateTime.UtcNow, DateTime.Now or their DateTimeOffset siblings, so a handler injects TimeProvider and passes the instant in, with BaseDomainEvent's occurrence stamp always exempt and a reviewed AllowedClockReaders list for the deliberate cases (ClockReadTestsBase.cs:3-12,23, the rule at Rules/Domain/ArchitectureRules.ClockReads.cs:55); and QueryHandlerReadRepositoryTestsBase (Bases/Cqrs/QueryHandlerReadRepositoryTestsBase.cs:15) reads the IL of every IQueryHandler<,>, async and lambda bodies included, and fails any that calls IUnitOfWork.GetRepository instead of GetReadRepository, because the write repository hands a query a mutation surface it must not use (QueryHandlerReadRepositoryTestsBase.cs:3-13, the rule at Rules/Cqrs/ArchitectureRules.QueryHandlerRepositories.cs:37). ADC activates both with one-line sealed subclasses (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ClockReadTests.cs:8, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/QueryHandlerReadRepositoryTests.cs:9). AggregateConventionTestsBase shows the shape in miniature: one abstract Map property and one [Fact] per rule (MMCA.Common.Testing.Architecture/Bases/Domain/AggregateConventionTestsBase.cs:12-24). How many fitness methods the package ships, and how many of them MMCA.Common's own build executes, is a generated and CI-gated number in MMCA.Common/FACTS.md:80-85: read it there rather than restating it anywhere else.

Failures report through ArchitectureAssert (MMCA.Common.Testing.Architecture/ArchitectureAssert.cs:8), which has two overloads: one lists the failing types from a NetArchTest TestResult (ArchitectureAssert.cs:11-23), the other lists a reflection-derived violation set (ArchitectureAssert.cs:26-32). Rules NetArchTest cannot express (method return types, generic constraints, property accessors, attribute usage) reflect over loaded types via the internal RuleHelpers (MMCA.Common.Testing.Architecture/RuleHelpers.cs:14), whose LoadableTypes extension property tolerates a partially resolvable assembly by falling back to the ReflectionTypeLoadException's resolved types (RuleHelpers.cs:19-33), and whose HasPublicMutableSetter treats an init-only setter as immutable by checking for the IsExternalInit required modifier (RuleHelpers.cs:121-137). One such walk, CrossEntityNavigationFinder (MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:97), is an ExpressionVisitor that collects the entity types a specification's criteria navigates to beyond its own (ArchitectureRules.Specifications.cs:101-119), because in a polyglot setup that navigation may cross a physical data source where the join is not translatable (ArchitectureRules.Specifications.cs:9-15). These runtime rules are the second of two enforcement layers, the first being the compile-time MSBuild layer guard (MMCA.Common/Source/Build/MMCA.Common.LayerEnforcement.targets, see group 14); ADR-015 describes both, and this is the clearest [Rubric §34, Architecture Governance] expression in the codebase.

The fitness library reaches beyond pure layering into cross-cutting product guarantees. RouteAuthorizationTestsBase (MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:22, tagged rubric §25 in its own remarks, [Rubric §25, Navigation & IA]) reflects over routable Blazor pages and asserts every governed page keeps its [Authorize(Roles = "...")] gate (RouteAuthorizationTestsBase.cs:49-61), so an admin route cannot regress to a bare [Authorize] any authenticated user can reach. It detects RouteAttribute and AuthorizeAttribute by full-name reflection (RouteAuthorizationTestsBase.cs:24-25) so the package stays free of ASP.NET references, and a MinimumGovernedPages floor (:47, asserted at :63-74) guards against a moved namespace silently emptying the scan. ModuleConformanceTestsBase<TModule> (MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:21) pins the three-member contract ModuleLoader actually registers on (Name, Dependencies, RequiresDependencies), because drift in any of them does not throw, it silently reorders registration or swaps a real service for a disabled stub (ModuleConformanceTestsBase.cs:3-10); it reads those members through the IModule full name by reflection (:24) so the package stays free of the framework's transitive graph and still dispatches to the interface's default implementations (:12-17). BrandColorTokenTestsBase (MMCA.Common.Testing.Architecture/Bases/Ui/BrandColorTokenTestsBase.cs:13, [Rubric §20, Design System & Theming]) reads landing-page stylesheets embedded as manifest resources and fails the build if a host re-hardcodes the brand hex #1565C0 instead of sourcing var(--mmca-primary) from the shared token (BrandColorTokenTestsBase.cs:15-16,41-49), with a non-empty check on the embedded list so the guard cannot pass vacuously (:27-28). DependencyVersionTestsBase (MMCA.Common.Testing.Architecture/Bases/Governance/DependencyVersionTestsBase.cs:16, [Rubric §32, Dependency & Supply-Chain]) checks the repo's pinned package majors and fails the build on the MassTransit commercial-license trap a blanket package bump would otherwise walk into unnoticed: any of the five MassTransit packages it lists (DependencyVersionTestsBase.cs:18-25) at major 9 or above (:27-40, ADR-016), a failure CI would never see on its own because no CI leg starts a broker (:4-6). A consumer that pins only some of those packages overrides the list rather than inheriting it (:7-14). ConstructorDependencyCountTestsBase (Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:20) turns the SRP judgement call into three numeric ceilings, one each for Application *Service facades, API controllers and CQRS handlers (MaxConstructorDependencies, MaxControllerConstructorDependencies, MaxHandlerConstructorDependencies at :34,41,49), each a ratchet a consumer sets at its current high-water mark rather than a budget (:14-16, [Rubric §1, SOLID]), with its own non-vacuity guard so a scan that finds no services fails rather than passes (Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:60-61). ConstructorDependencyCountTestsBaseTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:14) proves the two newer populations actually fail on drift and name the offender, driving private subclasses over fixture controllers and handlers such as FatFixtureController so xUnit does not collect their deliberately failing facts (ConstructorDependencyCountTestsBaseTests.cs:6-13), and ObservabilityConventionTestsBase (Bases/Governance/ObservabilityConventionTestsBase.cs:30) pairs every SLO alert a consumer's infra/main.bicep provisions with a same-severity triage section in its infra/OPERATIONS.md, in both directions, with a minimum-spec floor so a drifted parse anchor fails loudly instead of passing with zero discovered alerts (Bases/Governance/ObservabilityConventionTestsBase.cs:6-13,39, [Rubric §13, Observability & Operability]). MMCA.Common runs that base against the artifact it ships rather than against a fixture: SampleDeploymentObservabilityTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/SampleDeploymentObservabilityTests.cs:12) points it at samples/deployment, the worked example consumers copy, pairing that sample's main.bicep with its OPERATIONS.md under a floor of four alert specs (SampleDeploymentObservabilityTests.cs:14-20), so a runbook section that goes missing there fails here instead of propagating into every repo that lifted it (SampleDeploymentObservabilityTests.cs:5-11).

Five of the newer bases show how far the pattern has been pushed. CancellationTokenConventionTestsBase (Bases/Cqrs/CancellationTokenConventionTestsBase.cs:16) requires every public async method on a public Application- or Infrastructure-layer type to declare a trailing CancellationToken cancellationToken, because cancellation is only end-to-end if it is uniform: one method that swallows the token turns a cancelled request, an expired CQRS timeout budget or a stopping host into work that keeps running against the database, and a token in a non-trailing position or under a different name defeats the mechanical forwarding the decorator pipeline and the repositories rely on (Bases/Cqrs/CancellationTokenConventionTestsBase.cs:3-9), with signatures the repo does not own exempted automatically (:11-14). NamespaceCycleTestsBase (Bases/Layering/NamespaceCycleTestsBase.cs:15) requires the top-level namespaces inside each layer assembly to form a directed acyclic graph, catching the folder pair that has grown into one tangled unit at a granularity the two coarse layer rules cannot see inside; its own remarks are careful to say the rule is signature-level reflection blind to method bodies, so green means "no structural cycle", not "no coupling" (Bases/Layering/NamespaceCycleTestsBase.cs:3-13). IdempotencyConventionTestsBase (Bases/Cqrs/IdempotencyConventionTestsBase.cs:10) requires every POST action in a repo's API layer to state in code whether a retried request replays the original response ([Idempotent]) or deliberately does not ([NonIdempotent("why")]), so the answer is a compile-time artifact rather than tribal knowledge (Bases/Cqrs/IdempotencyConventionTestsBase.cs:3-8). ProtoContractTestsBase (Bases/Contracts/ProtoContractTestsBase.cs:19) is the synchronous counterpart to IntegrationEventContractTestsBase: it rebuilds a repo's live gRPC .proto contract and diffs it against a committed snapshot, and it is explicitly consumer-facing, because MMCA.Common ships the gRPC plumbing rather than any contracts of its own (Bases/Contracts/ProtoContractTestsBase.cs:3-11). FolderWidthTestsBase (MMCA.Common.Testing.Architecture/Bases/Governance/FolderWidthTestsBase.cs:14, [Rubric §5, Vertical Slice]) walks a repo root and fails when any folder under Source/ or Tests/ holds more than MaxDirectFiles direct code files, twelve by default (FolderWidthTestsBase.cs:23), so a folder keeps naming a feature instead of drifting into a technical bucket (FolderWidthTestsBase.cs:3-13); a .razor component and its code-behind count as one unit, resource and generated files do not count at all, and build output plus tool-owned trees (bin, obj, Migrations, Platforms, Resources, wwwroot) are skipped outright (:8-12). A repo activates it with a thin subclass supplying its RepoRoot (:20) and, where a flat layout is documented, its ExemptFolderSuffixes (:29); the single [Fact] delegates to the ArchitectureRules.FoldersStayNarrow rule body (:31-33), which is how the feature-by-folder convention of ADR-109 became a build gate rather than a review note. Sibling bases pin integration-event contracts (ADR-010), the one-upcaster-per-source-contract rule that keeps the upcast chain a function (so which contract a handler receives cannot depend on DI registration order, MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:7-14, ADR-090), service-contract purity so an extracted service's wire surface carries only Shared and contract types and never the producer's internals (MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:13-18, ADR-007), data residency, forms conventions, localization resources, concurrency, controller shape, state management, UI architecture, and framework-version consistency, so the governance-as-tests pattern spans much of the 34-category rubric.

The newest gate in the library is also the one that tests itself. SliceCohesionTestsBase (MMCA.Common.Testing.Architecture/Bases/Cqrs/SliceCohesionTestsBase.cs:10, tagged to the vertical-slice rubric category in its own summary, :3-9) asks a repo only for its map and exposes two facts, one for handlers and one for validators (SliceCohesionTestsBase.cs:14-20), each delegating to a rule body in MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Slices.cs: a use-case slice keeps its command or query, the handler that serves it and its validator in one namespace, the .../UseCases/{Action}/ folder, instead of stranding the handler in a horizontal Handlers/ folder away from its contract (ArchitectureRules.Slices.cs:6-9). Two carve-outs keep it honest. Only same-assembly contracts are checked, because a module reusing a framework-generic command such as UpdateEntityCommand<,,> is legitimately not co-located with it, while a command the module derives from that generic is declared in its own assembly and is checked like any other (ArchitectureRules.Slices.cs:9-15); and the scope stops at the contract, its validator and its handler, because DTOs/, Specifications/ and a shared Validation/ rule set are aggregate-scoped by design, so forcing them into one slice's folder would either duplicate them per slice or make one slice the arbitrary owner the others reach into (ArchitectureRules.Slices.cs:17-25). Since the rule scans abstract handler bases too, which is the shape a consumer actually derives from (ArchitectureRules.Slices.cs:28-30), it carries its own self-test: SliceCohesionFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:17) points a fixture map at the test assembly itself (FixtureApplicationMap, SliceCohesionFitnessTests.cs:70-78) and pins the three shapes that matter: a stranded abstract base is flagged (StrandedFixtureHandlerBase, MMCA.Common.Architecture.Tests/SliceFixtures/Stranded/StrandedFixtureHandler.cs:11), a base declared beside its contract is not (GetFixturePreferencesHandlerBase, MMCA.Common.Architecture.Tests/SliceFixtures/SliceFixtures.cs:19, next to the GetFixturePreferencesQuery it serves at SliceFixtures.cs:13), and a base parameterized over its contract is skipped, because a type parameter is not a type any folder can hold (GetFixtureEntityHandlerBase<TQuery>, SliceFixtures.cs:33, all three asserted at SliceCohesionFitnessTests.cs:21-53). A fitness rule is production code for the build, and a rule whose own edge cases are compiled as fixtures is what holding it to that standard looks like.

Component tests: real MudBlazor, faked edges

The bUnit tier renders a single Blazor component in-process with its real dependencies but stubbed network and auth. BunitComponentTestBase (MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:38) registers MudBlazor services and the vendor-neutral toast/confirm facades every migrated page injects (BunitComponentTestBase.cs:46,53), puts JSInterop in loose mode so MudBlazor components that probe JS during render do not throw (BunitComponentTestBase.cs:55), then wires a mutable AuthenticationStateProvider (BunitComponentTestBase.cs:63, the provider class at :167) plus the real DefaultAuthorizationService (BunitComponentTestBase.cs:62) so both <AuthorizeView> cascades and pages that inject the provider directly behave. The real service is registered explicitly because bUnit pre-registers a placeholder that throws and AddAuthorizationCore only TryAdds, and because it evaluates the requirements a view actually builds (roles, the default deny-anonymous policy) where a permissive double would authorize every authenticated principal whatever the view asks for (BunitComponentTestBase.cs:58-61). Tests render anonymously by default via RenderUnderTest<TComponent> (BunitComponentTestBase.cs:129) or as a supplied ClaimsPrincipal via RenderAs<TComponent> (BunitComponentTestBase.cs:135, which sets the provider and the cascading AuthenticationState together at :140-143), with a SetUser hook (:126) for mid-test auth changes and TestPrincipal (MMCA.Common.Testing.UI/Infrastructure/TestPrincipal.cs:7) minting the authenticated principal: a name claim, the user identifier written twice (under sub and under NameIdentifier, because a real principal reaches a page under either name), the requested roles, and an authentication type so IsAuthenticated is true (TestPrincipal.cs:22-32, rationale at :14-21). RenderMudProviders (BunitComponentTestBase.cs:153) mounts the popover, dialog, and snackbar providers and returns them as a MudProviderHandles record (BunitComponentTestBase.cs:162) so components that open a dialog or raise a toast have somewhere to render. One helper is worth knowing before you write your first list-page test: ConfigureDataGridListPageHost (BunitComponentTestBase.cs:105) wires the list-page state services, an inert viewport double so IsMobile is deterministic (:108-111), persistent component state (:114), and SetRendererInfo last (:116-117), because SetRendererInfo freezes the bUnit service provider and every registration made after it is silently ignored (:77-83). The class is pinned to bUnit v2 (the line compatible with xUnit v3 and Microsoft Testing Platform) and isolates every version-specific symbol here so a bUnit change touches only this file (BunitComponentTestBase.cs:29-34). Localization is pre-registered (AddLocalization, BunitComponentTestBase.cs:63-64) so components injecting IStringLocalizer<T> (ADR-027) render without per-test setup, a [Rubric §27, i18n] touch. Test bodies then read as user actions rather than DOM queries through BunitInteractionExtensions (MMCA.Common.Testing.UI/Infrastructure/BunitInteractionExtensions.cs:12), a generic extension<TComponent>(IRenderedComponent<TComponent>) block (:14) offering FindButtonByText (:18, which throws listing every button present when nothing matches), ClickButtonByText (:28), and HasText (:32), all keyed on accessible text rather than brittle CSS paths.

HTTP-backed UI services are exercised without a server through CapturingHttpMessageHandler (MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:20), a canned-response, request-recording HttpMessageHandler supporting both a responder delegate (CapturingHttpMessageHandler.cs:39) and route registration (SetResponse, CapturingHttpMessageHandler.cs:49), with registered routes consulted first and unmatched requests returning 404 to mirror the WebAPI's not-found behavior (CapturingHttpMessageHandler.cs:7-17,119-137); it rebuilds each response fresh so a Polly retry never reuses a consumed HttpContent (CapturingHttpMessageHandler.cs:141-150), and records every request as a CapturedRequest (CapturingHttpMessageHandler.cs:158) against a registered Route (CapturingHttpMessageHandler.cs:139), flattening request and content headers into one case-insensitive lookup so a caller asserting If-Match or Content-Type does not have to know which half carries it (:94-117). UiHttpServiceHarness (MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:12) wraps that handler with a FreshApiClientFactory (UiHttpServiceHarness.cs:73) returning a fresh client per call, which is load-bearing because the UI services dispose the client after each request, so a caching factory would hand later calls a disposed one (UiHttpServiceHarness.cs:66-72), plus a fixed-token StubTokenStorageService (MMCA.Common.Testing.UI/Infrastructure/StubTokenStorageService.cs:13, constructed and exposed at UiHttpServiceHarness.cs:48,61), all on a https://gateway.test/ base address (UiHttpServiceHarness.cs:15). Tests that would rather wire the pieces individually reach for HttpTestDoubles (MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:12), which exposes the same factory and token stub standalone (:23,28) alongside the canned JsonResponse/EmptyResponse/ProblemResponse builders (:33,37,47) that reproduce the shapes the WebAPI actually emits. MarkupSnapshot (MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:22) adds dependency-free golden-markup regression testing: Match (MarkupSnapshot.cs:31) normalizes the per-render GUIDs MudBlazor injects (MarkupSnapshot.cs:64-70), compares against a committed baseline under Snapshots/ next to the calling test (located through a [CallerFilePath] argument, MarkupSnapshot.cs:31,37), and returns a MarkupSnapshotResult (MarkupSnapshot.cs:104) for the caller to assert on, which keeps the shipped package free of an assertion-library dependency (MarkupSnapshot.cs:10-12). UPDATE_SNAPSHOTS=1 rewrites baselines (MarkupSnapshot.cs:41-46) and a missing baseline is written but reported as a non-match so a regression cannot slip through on an absent snapshot (MarkupSnapshot.cs:48-54). Whole page suites ship on top of that base for the framework-owned pages of ADR-116: ConfirmEmailPageTestsBase (MMCA.Common.Testing.UI/Pages/ConfirmEmailPageTestsBase.cs:26) pins the anonymous /confirm-email page (a complete link redeemed once on arrival, an incomplete one landing on manual entry, a resend that answers the same whether or not the address has an account, ConfirmEmailPageTestsBase.cs:13-19), while RoleAdminListPageTestsBase<TPage> (Pages/RoleAdminListPageTestsBase.cs:21) and RoleAdminEditPageTestsBase<TPage> (Pages/RoleAdminEditPageTestsBase.cs:22) pin the consumer's own half of the routed shells over the shared role roster and permission editor: route parameters, links back and forth, the compiled permission locked, and a save that submits the stored set (RoleAdminEditPageTestsBase.cs:13-19). Each is activated by a one-line sealed subclass in the consuming repo, the ADR-015 pattern carried into the component tier. This tier is [Rubric §28, Front-End Testing] and [Rubric §18, UI Architecture].

End-to-end tests: a real browser, accessibility and performance as gates

The E2E tier drives a real browser through Playwright. PlaywrightFixture (MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:6) is an xUnit collection fixture (its E2ETestCollection definition sits beside it at PlaywrightFixture.cs:47-51, the class itself at :48) that launches the engine selected from configuration, chromium, firefox, or webkit, with unknown values falling back to Chromium (PlaywrightFixture.cs:17-22). That environment-selected engine is what lets CI run the same suite as a cross-browser matrix, [Rubric §22, Responsive/Cross-Browser]: MMCA.Common's ui-e2e job is a three-engine matrix (MMCA.Common/.github/workflows/ci.yml:236-237) with E2E_BROWSER handed to each leg (:298). Headless mode, slow motion, base URL, timeouts, trace capture, and the seeded admin/user credentials all come from E2ETestConfiguration (MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:15), whose nested AdminCredentials (E2ETestConfiguration.cs:80) and UserCredentials (E2ETestConfiguration.cs:92) let a downstream project set app-specific defaults through a [ModuleInitializer] while environment variables always win. Two of its knobs exist purely to de-flake CI: AuthTimeout (E2ETestConfiguration.cs:34) tunes the slowest step, the post-auth round-trip, independently of the 30-second general default (:25-26), and AuthGraceTimeout (E2ETestConfiguration.cs:45, default 15 seconds at :46) gives the success signal a window to appear after a transient error alert flashes during the success-path reload.

E2ETestBase (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:9) is the per-test base on top of that fixture. It opens a fresh browser context per test with IgnoreHTTPSErrors and the configured base URL (E2ETestBase.cs:20-37), optionally records a Playwright trace (:32-35) and, when E2E_TRACE names a directory, keeps only the traces of tests that failed so a full-suite run yields exactly the inspectable failures (:68-90, the failed-only branch at :79-85). Its LoginAsync (E2ETestBase.cs:98) clears both token stores before signing in (localStorage for the WASM host and the HttpOnly session cookie for the Server host, :100-114), and WaitForAuthResultAsync (:214) races three signals so success detection does not depend on the logout button having hydrated (:219-228, rationale at :206-213). The verdict itself is not inline: it is a pure function, AuthOutcomeRules.Classify (MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:38, invoked at E2ETestBase.cs:236-239), which returns an AuthOutcome (AuthOutcomeRules.cs:4) of Succeeded, ErrorShown, or Silent, so the classification is testable without a browser and the silent case (no navigation and no rendered error) can no longer be mistaken for success (AuthOutcomeRules.cs:19-26); anything other than success gets the grace window once (E2ETestBase.cs:248, AuthSucceededWithinGraceAsync at :282). ScanAsync (:334) and ScanGridAsync (:324) wrap the accessibility gate for settled pages and for MudDataGrid list pages respectively: the grid variant waits for a data row and for the loading bar to disappear before scanning and applies the pager carve-out (:326-329), while the plain scan stays fully strict on WCAG 2.1 AA (:336-337).

The hard part of Blazor E2E is timing, and PageExtensions (MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:23) is where that knowledge is centralized, as C# extension(IPage) and extension(ILocator) blocks (PageExtensions.cs:62,335, see primer §4). The app uses InteractiveAuto with prerendering, so a page appears as static HTML before the runtime wires its event handlers, and the runtime flag alone is a false green. WaitForBlazorAsync (PageExtensions.cs:85) therefore waits in two phases: first for window.Blazor._internal (:39), which only says the WASM CLR or the SignalR circuit is ready, then for the data-mmca-interactive attribute that MmcaThemeProviders stamps on the document element from its first interactive render (:47-48), which is the honest gate, and finally flushes one animation frame (:60,96). GotoAndWaitForBlazorAsync (:103) pairs navigation with that wait and deliberately settles on Load rather than NetworkIdle, because the persistent SignalR WebSocket means network idle never arrives (:106-108); BlazorNavigateAsync (:118) routes client-side so a protected page is not re-prerendered without its token, polling window.location instead of WaitForURLAsync because a same-document navigation fires no load event (:132-138); and GotoProtectedAsync (:160) probes the same runtime predicate and loads a public page first when the runtime is not yet up (:167-188). FillAndVerifyAsync (:347) fills a field then auto-waits until the value sticks, retyping character by character if hydration wiped it (:353-365), and ClickAndVerifyAsync (:380) and ClickAndWaitForUrlAsync (:423) retry a click until its visible effect appears so a click that beats hydration is not silently swallowed (:389-410). List-page helpers built on the same primitives (SearchAndWaitForRowAsync :233, ConfirmDeleteAsync :266, WaitForGridToSettleAsync :293) keep grid interactions out of every page object. These helpers encode hard-won lessons about the prerender and hydration race and are shared by every page object.

Accessibility and performance are asserted here, not deferred to a separate audit. AssertNoAccessibilityViolationsAsync (PageExtensions.cs:307) runs an axe-core scan and throws AccessibilityViolationException (MMCA.Common.Testing.E2E/Infrastructure/AccessibilityViolationException.cs:7) with a compact per-node summary of every violation (PageExtensions.cs:320-331), so an inaccessible page fails the build, [Rubric §21, Accessibility]. The scan scope itself is shipped as AxeOptions (MMCA.Common.Testing.E2E/Infrastructure/AxeOptions.cs:9): Wcag21Aa (AxeOptions.cs:17) pins the documented target of WCAG 2.1 AA tags (AxeOptions.cs:19-23) and deliberately excludes axe's advisory best-practice rules, and Wcag21AaExceptMudPagerCombobox (AxeOptions.cs:35) is the one documented carve-out, disabling only aria-input-field-name for MudBlazor 9.6.0's unlabeled pager select (AxeOptions.cs:26-33,42-45). WebVitalsCollector (MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:20) installs PerformanceObserver-based Core Web Vitals capture (LCP, CLS, FCP, TTFB, INP) as an init script before first paint (InstallAsync, WebVitalsCollector.cs:40, script at :26-35), reads the accumulated values back as a WebVitalsSample (CollectAsync, WebVitalsCollector.cs:47,76), and writes a citable JSON artifact under WEB_VITALS_OUTPUT_DIR (WriteArtifactAsync, WebVitalsCollector.cs:63, artifact record WebVitalsArtifact at :90) for CI, with WebVitalsBudget (:103) as the shared assert mechanics defaulting to the Core Web Vitals "good" band, LCP 2500 ms / FCP 1800 ms / TTFB 800 ms / CLS 0.1 / INP 500 ms (WebVitalsCollector.cs:104-108), and skipping the INP assertion when no interaction cleared the 16 ms threshold (:148-151), [Rubric §23, Front-End Performance] (the source tags it rubric §12). LCP and CLS are Chromium-only, so on Firefox and WebKit those fields stay 0 and the observers fail silently rather than throwing (WebVitalsCollector.cs:14-16,22-25).

Five reusable identity page objects ship with the package: LoginPage (MMCA.Common.Testing.E2E/PageObjects/LoginPage.cs:6), RegisterPage (MMCA.Common.Testing.E2E/PageObjects/RegisterPage.cs:6), ProfilePage (MMCA.Common.Testing.E2E/PageObjects/ProfilePage.cs:6), ForgotPasswordPage (MMCA.Common.Testing.E2E/PageObjects/ForgotPasswordPage.cs:6), and ResetPasswordPage (MMCA.Common.Testing.E2E/PageObjects/ResetPasswordPage.cs:6). They wrap the framework's real auth surfaces with role- and label-based locators (LoginPage.cs:12-18) and route their own fills through the anti-race helper (LoginPage.cs:31-32, invoked at :25-26). A sixth, RoleAdminPage (MMCA.Common.Testing.E2E/PageObjects/RoleAdminPage.cs:18), drives the ADR-116 role roster at /roles and editor at /roles/{role} purely through the data-testid and data-permission attributes the shared components render, so a MudBlazor upgrade cannot break it (RoleAdminPage.cs:5-17). For eventually-consistent state inside a browser test, E2EPolling (MMCA.Common.Testing.E2E/Infrastructure/E2EPolling.cs:18) is the package's counterpart of the integration tier's TestPolling (the E2E package does not reference MMCA.Common.Testing): PollUntilAsync (E2EPolling.cs:38,70) returns as soon as the condition holds, bounded by a 30-second deadline at a 500 ms interval by default (E2EPolling.cs:21,24), and serves only "until present" waits (E2EPolling.cs:13-16). Downstream apps add their own family, for example the 45 page-object classes under MMCA.ADC.E2E.Tests/PageObjects/ covering events, sessions, speakers, rooms, questions, feedback, sponsors, and the QR check-in and points surfaces. Whole workflows ship too, not just page objects: eight abstract suites under MMCA.Common.Testing.E2E/Workflows/ (six identity flows, AuthorizationTestsBase, UserLoginTestsBase, UserRegistrationTestsBase, LogoutTestsBase, ProfileManagementTestsBase and PasswordResetTestsBase, plus UserPreferencesTestsBase (Workflows/Preferences/UserPreferencesTestsBase.cs:21) and PseudoLocalizationTestsBase (Workflows/Globalization/PseudoLocalizationTestsBase.cs:52)) are authored once and re-run per consumer. Their shape is the same supply-only-your-facts contract as the fitness bases: AuthorizationTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/AuthorizationTestsBase.cs:18) asks the subclass only for its route lists (ProtectedPaths :26, PublicPaths :29, optional AuthenticatedUserPath :35 and AdminPaths :44) and owns the assertions, including the non-empty guard that keeps the anonymous-redirect check from passing vacuously (:49-50). PasswordResetTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:17) shows where the tier draws its own boundary: it asserts the recovery flow is reachable from the login page (:24-41) and that an unknown address produces the identical anti-enumeration confirmation (:43-58), but deliberately does not consume a real reset token, because that token only reaches the user by email and so belongs to an app-side integration test (:10-16).

Component and E2E coverage of MMCA.Common's own UI needs a page to render, but the framework is not a runnable app. MMCA.Common.UI.Gallery is a deliberately backend-less Blazor host that renders the real MMCA.Common.UI auth pages (/login, /register), the shared notification pages, and a primitives showcase (/components), so a real-browser axe scan can run inside MMCA.Common's own CI (GalleryHost, MMCA.Common.UI.Gallery/GalleryHost.cs:24, host built by BuildApp at :31). It is kept outside MMCA.Common.slnx (together with MMCA.Common.UI.E2E.Tests) so the unit-test run stays fast; the CI ui-e2e job builds that out-of-slnx graph directly by csproj path and runs the axe plus render smoke against the gallery (MMCA.Common/.github/workflows/ci.yml:223-227,260-264,293-306). Because the E2E suite self-hosts it in-process, where the entry assembly is the test host and the environment is Production, the host also has to point the static-web-assets loader at the gallery's own runtime manifest and force it on, otherwise the auth pages render unstyled, never become interactive, and axe's contrast checks are meaningless (GalleryHost.cs:36-50).

The host runs without a backend by registering stubs before AddUIShared so its TryAdd* registrations defer to them (GalleryHost.cs:57-65, AddUIShared at :102): NoOpAuthUIService (MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:14), NullTokenStorageService (MMCA.Common.UI.Gallery/Stubs/NullTokenStorageService.cs:11), NullTokenRefresher (MMCA.Common.UI.Gallery/Stubs/NullTokenRefresher.cs:9), and GalleryAuthenticationStateProvider (MMCA.Common.UI.Gallery/Stubs/GalleryAuthenticationStateProvider.cs:16), plus canned notification services (StubNotificationInboxUIService (Stubs/StubNotificationInboxUIService.cs:11), StubPushNotificationUIService (Stubs/StubPushNotificationUIService.cs:11)) so the bell and the inbox render populated markup (GalleryHost.cs:77-82), the framework's NullNotificationScopeProvider for the unscoped send page (GalleryHost.cs:87), and one empty GalleryUIModule (Stubs/GalleryUIModule.cs:14, registered at GalleryHost.cs:97) so the shared Router discovers the gallery's own page alongside the real ones. Because the notification pages carry a real [Authorize] that MapRazorComponents surfaces as endpoint metadata, the gallery also needs a genuine authentication scheme: GalleryFakeAuthenticationHandler (MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:21, registered at GalleryHost.cs:67-75) authenticates only requests carrying the gallery_auth=1 cookie (GalleryFakeAuthenticationHandler.cs:26,30-39), so the guarded pages are scanned signed in while /login, /register, and /components are scanned in their deliberate anonymous state (GalleryFakeAuthenticationHandler.cs:8-18). The gallery is also where the i18n evidence is produced: it enables the qps-Ploc pseudo-locale unconditionally (GalleryHost.cs:104-112), which real hosts keep Development-only, because the pseudo-localization pass is a required CI gate for [Rubric §27, i18n] and this host is unpackaged test infrastructure that is never deployed.

Evaluating a nondeterministic dependency: the AI-scoring golden corpus

Every tier above assumes a deterministic system under test. ADC's session-scoring judge is not one: it asks a hosted model for a structured score, and the same prompt can return a different number on a different day. MMCA.ADC.Conference.Scoring.Evaluation.Tests is the tier built for that case, and it is the subject of ADR-111. The corpus is a folder of committed JSON files copied next to the test assembly. GoldenCase (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/GoldenCase.cs:18) is one case: a session proposal, a stable id, a one-line note on what it pins, and the band its overall score has to land in. The recorded answer is no longer part of the record: it lives beside the case as Golden/<id>.response.json, written by the framework's RecordedResponses in the ChatResponse shape rather than a provider's wire format, so a provider swap cannot invalidate the corpus (GoldenCase.cs:7-17). GoldenCorpus (GoldenCase.cs:106) loads every case-*.json from the copied Golden/ folder ordered by id so a run is deterministic (GoldenCase.cs:118-129), and throws when it finds none, so a csproj that stopped copying the corpus fails loudly instead of passing on zero cases (GoldenCase.cs:131-134); it also exposes the Golden/prompt-versions.json path that maps a prompt version to its recorded hash (GoldenCase.cs:114-116).

Three suites read that corpus, and they fail for three different reasons. GoldenReplayTests (GoldenReplayTests.cs:35) is the regression half, a subclass of the framework's GoldenReplayTestsBase: every case runs through the real SessionScoringService against a fresh ReplayChatClient that returns the response recorded for it, so no API key and no network are involved, it runs on every CI leg, and the base collects every broken case instead of stopping at the first (GoldenReplayTests.cs:15-25). The replay client is the innermost provider under the host's own governed pipeline, composed through the same AddConferenceAiGuardrails call the Conference service makes, and the test asserts a GuardrailChatClient really is in front of it, so a guardrail that would change a production score fails here instead of shipping (GoldenReplayTests.cs:49-61,165-183). It pins both directions at once, what goes out (the delimited envelope and the untrusted-input brief on the request, including for the cases with no speakers and with an injection attempt, plus proof that request-side redaction stripped the contact details from the contact-details-in-bio case before they reached the provider) and what comes back (a recorded response still parses, still succeeds, and still produces the same weighted overall inside the case's band), so a change to prompt assembly or to the weighting math fails here with the case that noticed it named (GoldenReplayTests.cs:26-32,67-80,143,160). LiveJudgeTests (LiveJudgeTests.cs:39) is the live half, scoring the same proposals through the real provider to catch what a replay cannot: a model deprecation, a structured-output contract change, a prompt edit that reads fine but scores everything a point lower. It composes the client exactly as the host does, from the host's deployed Ai section and the provider adapter named by Ai:Provider, so it names no vendor type and keeps no guardrail list of its own (LiveJudgeTests.cs:23-30). It costs money and needs a key, so it is trait-gated Category=AiEval.Live and skips itself when AI_API_KEY is absent (LiveJudgeTests.cs:17-21,41,59). Its bands are deliberately generous, roughly plus or minus 1.5 around the recorded value and clamped to the 1.0 to 10.0 range, because a judge model is not deterministic even at the same prompt and a tight band would produce a flaky gate that gets ignored, which is worse than no gate: what the bands catch is a shift, not a wobble (LiveJudgeTests.cs:32-36).

PromptContractTests (MMCA.ADC.Conference.Scoring.Evaluation.Tests/PromptContractTests.cs:29) closes the loop that makes a persisted score meaningful. Scores are stored with the prompt version that produced them, which is worth something only if the version actually moves when the prompt does, so this suite renders the system brief plus the user message for one canonical proposal, hashes it, and compares against Golden/prompt-versions.json: a prompt edit without a version bump fails because the recorded hash for the current version no longer matches, and a version bump with no recorded hash fails too (MMCA.ADC.Conference.Scoring.Evaluation.Tests/PromptContractTests.cs:16-22). The canonical proposal is fixed in the test file rather than read from the corpus on purpose (MMCA.ADC.Conference.Scoring.Evaluation.Tests/PromptContractTests.cs:37-45), because editing a golden case must not be able to change what the contract hash covers (MMCA.ADC.Conference.Scoring.Evaluation.Tests/PromptContractTests.cs:25-26); it is chosen to exercise every branch of the prompt assembly, with a description and two speakers, one of them with no tagline and no bio (MMCA.ADC.Conference.Scoring.Evaluation.Tests/PromptContractTests.cs:34-35). Beside it in the same file, SessionScoringPromptContractPinTests (PromptContractTests.cs:180) runs the framework's own prompt-change protocol, PromptContractPinTestsBase, over the scorer's one contract: it pins the normalized contract hash (name, version, model and system brief) in the same Golden/prompt-versions.json under a session-scoring@<version> key, while the bare <version> keys stay the rendered-prompt hashes PromptContractTests reads, and each base ignores the keys it does not own (PromptContractTests.cs:166-187). Together these suites are [Rubric §14, Testability] applied to a dependency that cannot be asserted exactly, and [Rubric §29, Resilience & Business Continuity] on the operational side: the offline gate is free and always on, the paid gate is opt-in, and neither one can pass vacuously.

The framework side of the same dependency is tested differently, because what MMCA.Common ships is pipeline mechanics rather than a model. MMCA.Common.AI.Tests pins when the pieces run and what they do, against stub edges rather than a hosted model: GuardrailChatClientTests (MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/GuardrailChatClientTests.cs:12) holds the guardrail layer to being an extension point and not a policy (ADR-120), asserting when an application's guardrail runs and what a block does, never what a guardrail should consider unsafe (GuardrailChatClientTests.cs:7-11), driven by a StubGuardrail whose request, response and per-update verdicts the test supplies and which counts its request, response and streamed-update inspections (MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/Fixtures/StubGuardrail.cs:10-13,16,19,22), so the pipeline tests need no real policy. The shipped policies are asserted from the innermost client's point of view, because what the provider was actually sent is the only place they count: ContentPolicyGuardrailTests pins both what a marker costs an attacker and which benign sentences must still pass (MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/Guardrails/ContentPolicyGuardrailTests.cs:7-12), RequestRedactionTests that redaction reaches the provider without rewriting the caller's own messages (Guardrails/RequestRedactionTests.cs:8-14), ToolPolicyTests that a tool the policy removed is absent from what the model was offered (Guardrails/ToolPolicyTests.cs:10-15), and StreamedGuardrailTests that every streamed update is inspected before it is yielded (Guardrails/StreamedGuardrailTests.cs:7-12). The evaluation harness ADC's corpus runs on is held to the same standard before anything it gates is trusted: ReplayChatClientTests pins the replay client and the golden base so neither can answer with the wrong recording or pass on an empty corpus (Evaluation/ReplayChatClientTests.cs:6-13), RecordedResponsesTests pins the two on-disk formats and the exact failure messages of the prompt pin (Evaluation/RecordedResponsesTests.cs:6-12), and ReferenceGoldenReplayTests drives a tiny reference summarizer through GoldenReplayTestsBase against three recorded answers so the shipped base runs on every Common pull request, not only in the repos that consume it (Evaluation/ReferenceGoldenReplayTests.cs:6-14). On the ADC side, ConferenceAiGuardrailsRegistrationTests guards the one guardrail list the Conference host and both evaluation tiers share (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/ConferenceAiGuardrailsRegistrationTests.cs:12-17), SessionScoreResponseGuardrailTests asserts the answer rules at the pipeline extension point where they now live, a blocked answer returning as an ordinary failed score (Services/SessionScoreResponseGuardrailTests.cs:11-18), and SessionScoringServiceTests talks to a recording stub IChatClient rather than a fake provider HTTP handler, leaving token-cost counters to the framework's own meter (Services/SessionScoringServiceTests.cs:13-22); UsageRecordingChatClientTests does the same for the token-usage recorder, collecting what was emitted through a DurationMeasurement record declared in the test file itself (MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/UsageRecordingChatClientTests.cs:212). One fact in that family sits deliberately outside the AI package: AiTelemetryExportTests (MMCA.Common/Tests/Hosting/MMCA.Common.Aspire.Tests/Telemetry/AiTelemetryExportTests.cs:20) asserts that the meter and the activity source named MMCA.Common.AI really are exported by the Aspire defaults, which subscribe that name as a literal string because AiDependencyIsolationTestsBase bars a project reference from the Aspire package to the AI one, so a rename on either side goes red here instead of quietly taking model-spend telemetry off every dashboard (AiTelemetryExportTests.cs:12-19,22).

Contract, pipeline, and benchmark bases

The last family pins guarantees that live in the composition of the stack rather than in any one type, and it is the subject of ADR-058: these suites ship in MMCA.Common.Testing as abstract behavioral bases and every one of them runs against a host that was actually booted. SecurityHeadersTestsBase (MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:16, [Rubric §11, Security] and [Rubric §26, Front-End Security]) probes an always-responding endpoint (ProbePath, default /alive, SecurityHeadersTestsBase.cs:19) and asserts the hardened header set: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, a Permissions-Policy containing geolocation=(), a Content-Security-Policy containing frame-ancestors 'none', and, in the Production environment, HSTS (SecurityHeadersTestsBase.cs:29-35). Its siblings OpenApiContractTestsBase<TFixture> (MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:32, with a MinimumPathCount floor at :37, a pinned CorePublicResources list at :50, and deliberately no committed snapshot file so a new controller can never leave a stale baseline behind, :14-16), ProblemDetailsContractTestsBase<TFixture> (MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:22, asserting the RFC 9457 shape across both error-shaping paths, model validation at :30 and the framework's HandleFailure mapping at :42, through one shared AssertProblemDetailsShapeAsync at :67), and ServiceInfoVersioningContractTestsBase<TFixture> (MMCA.Common.Testing/Conformance/ServiceInfoVersioningContractTestsBase.cs:20, driving /ServiceInfo at both api-version: 1.0 and 2.0 and checking the deprecated/supported reporting headers at :38,:54) all subclass IntegrationTestBase<TFixture> and pin the corresponding API contracts, [Rubric §9, API & Contract Design]. The non-HTTP member of the family is GracefulShutdownTestsBase<TEntryPoint> (MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:25, [Rubric §29, Resilience & Business Continuity]): it boots a host through ProductionHostApplicationFactory, calls a real IHost.StopAsync under a bounded token defaulting to 20 seconds (:28,:55-56), and asserts ApplicationStopping then ApplicationStopped fired (:58-61). The failure it catches, a hosted service that refuses to drain, is invisible in production until it wedges a rolling deploy.

Four bases guard the composition of the pipelines themselves. DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult> (MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:38) is the opt-in fitness function for ADR-014: it builds a real ServiceCollection through the repo's own registration sequence (ConfigureServices, :46), resolves the decorated handlers, unwraps each decorator's private inner-handler field by reflection so it verifies the constructed object graph rather than the registration list (:105-125), and asserts the runtime nesting is exactly FeatureGate, Authorization, Logging, Caching, Validating, Timeout, Transactional, handler for commands (DecoratorPipelineOrderTestsBase.cs:49-58) and FeatureGate, Authorization, Logging, Caching, Validating, Timeout, handler for queries (:61-69), through the two [Fact]s at :71-77, with a final check that the innermost element is not itself a decorator (:96-97). Authorization and Timeout are the newest links in both chains, inserted by ADR-014's 2026-08-18 revision (Website/docs-src/adr/014-cqrs-decorator-pipeline.md:96-107); note that the revision's query line (:107) omits Validating, while the shipped registration and this base both include it (MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:144), so the code is the authority here. Because Scrutor's TryDecorate applies decorators in reverse registration order, an innocent-looking reorder of the AddApplicationDecorators() lines silently changes runtime behavior, and this base turns that into a test failure (see group 5). MiddlewarePipelineOrderTestsBase (MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:31) is its counterpart on the HTTP edge: it seeds MiddlewarePipelineBuilder.CreateDefault, applies the host's own Configure customization when it has one (:35), and asserts the eighteen-step order from the exception handler down to the controller endpoints (:38-58). Several adjacencies there are load-bearing (the pre-forwarded capture immediately before UseForwardedHeaders, authentication immediately before tenant resolution, authentication before the rate limiter per ADR-019, forwarded headers before the HTTPS redirect), and a reorder that breaks one of them fails at runtime looking like a configuration bug: an unreachable jwks_uri, a tenant that never resolves, a per-user rate cap that never engages (:13-18). Unlike the contract bases it needs no host at all, because the steps are pure data until they are applied, so it runs in the fast unit tier (:24-27). DependencyInjectionAssert (MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:13) guards the other half of that composition: ReturnsSameCollection (:21) asserts a registration extension hands back the very IServiceCollection it was given (:29-31), because an extension that returns a new collection silently drops every registration chained after it and nothing else catches services that are simply absent (:6-11). HandlerTestBase<THandler> (MMCA.Common.Testing/Support/HandlerTestBase.cs:38) is the fast unit-tier counterpart for exercising a single handler without a host: it owns a Mock<IUnitOfWork> whose SaveChangesAsync is pre-configured to succeed (HandlerTestBase.cs:41-42,45), a NullLogger<THandler> (:48), and RegisterRepository<TEntity, TIdentifierType>() (:56) / RegisterReadRepository<TEntity, TIdentifierType>() (:72) helpers that wire a repository mock into the read and write accessors (the read-only variant exists for child entities, which expose no read-write repository).

A smaller final tier measures rather than asserts behavior. MMCA.Common.Benchmarks (BenchmarkDotNet) covers the per-request query pipeline, where the dynamic-LINQ predicate is re-parsed per call and the shaper reflects over DTO properties (QueryPipelineBenchmarks, MMCA.Common.Benchmarks/QueryPipelineBenchmarks.cs:9-17), and the specification hot path (SpecificationBenchmarks, MMCA.Common.Benchmarks/SpecificationBenchmarks.cs:8-14), both with [MemoryDiagnoser] allocation tracking. Its results are compared in CI by build/perfgate against the committed MMCA.Common/Tests/Performance/perf-baseline.json (MMCA.Common/.github/workflows/ci.yml:397), so moving a number has to be a deliberate, reviewed change, [Rubric §12, Performance & Scalability]. The same job family carries one more quiet gate worth knowing: the unit run is invoked with --minimum-expected-tests 2000 (MMCA.Common/.github/workflows/ci.yml:161), so a discovery regression that silently drops thousands of tests fails the build instead of reporting a green, empty run.

Benchmarks time one operation in isolation; MMCA.Common.LoadTests measures the framework at volume. LoadStack (MMCA.Common/Tests/Performance/MMCA.Common.LoadTests/Support/LoadStack.cs:25) wires the real persistence stack exactly as a host does, AddApplication() plus AddInfrastructure(configuration), over one temp-file SQLite database, substituting nothing below the service registrations (LoadStack.cs:13-19); it is a file rather than :memory: because each scenario opens one connection per scope, as a deployed host does (LoadStack.cs:20-24). Two suites run on it. OutboxThroughputLoadTests (MMCA.Common.LoadTests/OutboxThroughputLoadTests.cs:20) enqueues 10,000 integration events as the framework writes them and lets the real OutboxProcessor, resolved as a hosted service with its default settings, drain them to a CountingMessageBus: every message must be delivered exactly once and stamped processed, above a floor of 500 messages per second (OutboxThroughputLoadTests.cs:12-19,22,30). PagedQueryLoadTests (MMCA.Common.LoadTests/PagedQueryLoadTests.cs:13) drives EntityQueryService.GetAllAsync over 100,000 rows seeded by PagedQueryFixture, asserting exact rows rather than a count (so a fast wrong answer still fails) under per-shape median latency ceilings and a p95 ceiling across 50 concurrent paged reads (PagedQueryLoadTests.cs:7-12,23-31). Each constant carries the local measurement it was derived from, with about 3x headroom over the worst of 25 runs. The tier is deliberately outside ci.yml and not a required check: the project sits outside MMCA.Common.slnx, so the solution-wide test run never discovers it, and a timing tier on shared runners is a trend signal rather than a merge gate, so load-tests.yml runs it weekly on a schedule plus manual dispatch (MMCA.Common/.github/workflows/load-tests.yml:3-17).

The takeaway for a new engineer: pick the tier that matches what you are proving (a fast unit test for domain logic, bUnit for a component, an integration fixture for a full request path, a cross-service fixture for a real broker round-trip, an AppHost collection for the orchestration itself, an E2E page object for a browser flow, a *TestsBase subclass for an architectural invariant, a contract base for a runtime guarantee of the composed host, a golden case for a change in what the scoring model returns, a benchmark for an allocation budget), and, for every tier but the ADC-local model-evaluation suite, the reusable base you need is already in one of the five MMCA.Common.Testing.* packages. Adoption is opt-in per host in both governance tiers, which is the standing caveat in ADR-015 and ADR-058 alike: the framework ships the gate, a host gets it only once someone writes the subclass (Website/docs-src/adr/015-architecture-fitness-functions.md:68-70, Website/docs-src/adr/058-runtime-conformance-suites-as-a-package.md:173-175). Every remaining concrete test class is cataloged by project in the companion per-project test rollup for this chapter.

ContractRow

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavigationContractTests.cs:330 · Level 0 · record (private, sealed, nested)

  • What it is - one parsed data row of the ## Route Contract table in adc-NavigationFlow.md: the backticked route template, the page cell and the access word, as three strings (NavigationContractTests.cs:330).
  • Depends on - nothing but the BCL; it is a positional record nested inside NavigationContractTests.
  • Concept introduced, a row as a value. Parsing the markdown table once into a value type lets every fact compare rows by Route, Page and Access instead of re-splitting text. ReadContractTable builds it from exactly three trimmed cells (:243, constructed at :268-:278 of the table reader).
  • Where it's used - only inside the defining file: ContractTable.Rows carries it, and EveryContractAccess_MatchesThePageGuard and RouteContractTable_IsWellFormed read it (:79, :107).

PageRoute

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavigationContractTests.cs:328 · Level 0 · record (internal, sealed, nested)

  • What it is - one routable ADC page as discovered from the source tree: Route (the @page template), RelativePath (the .razor file), PageName (file name without extension), Access (Anonymous, Authenticated or Organizer, null when unclassifiable) and Violation (why classification failed, else null) (NavigationContractTests.cs:328).
  • Depends on - nothing but the BCL; nested in NavigationContractTests.
  • Concept introduced, carry the failure with the fact. Classify returns (Access, Violation) and DiscoverPageRoutes stores both on the record (:209, :139), so the guard-presence fact can report a page with no or several access attributes without a second scan.
  • Where it's used - the four facts and the table-hygiene check in the defining file consume DiscoverPageRoutes() results (:38-:107). The record is internal rather than private because DiscoverPageRoutes is now internal static (:332) and NavItemGuardContractTests.DiscoverGuardedPages reuses it to map each page to a GuardedPage (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavItemGuardContractTests.cs:318-:324).

PageGuard

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavItemGuardContractTests.cs:464 · Level 0 · record (private, sealed, nested)

  • What it is - the normalized access requirement of one page, as five fields: RequiresAuthentication, AnyOfRoles (the roles of which a caller needs one), ClaimType, Permission and a human-readable Description such as [Authorize(Roles = RoleNames.Organizer)] (NavItemGuardContractTests.cs:464).
  • Depends on - nothing but the BCL; nested in NavItemGuardContractTests.
  • Concept introduced, a guard as a comparable value. Every page guard form ([AllowAnonymous], [Authorize], roles, a policy resolved to the claim or permission it enforces) reduces to this one shape (:321-:323, :354, :403), so Compare (:108) can test a nav item against it with four independent checks: signed-in, role, claim, permission (:117-:135).
  • Where it's used - only inside the defining file, held by GuardedPage.

GuardedPage

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavItemGuardContractTests.cs:462 · Level 1 · record (private, sealed, nested)

  • What it is - one routable page paired with its guard: Route, Source (the .razor path for ADC pages, the component's full type name for framework pages), Guard (a PageGuard, null when unreadable) and Problem (why it was unreadable) (NavItemGuardContractTests.cs:462).
  • Depends on - PageGuard and, for ADC pages, PageRoute from NavigationContractTests.DiscoverPageRoutes().
  • Concept introduced, carry the failure with the fact. Like PageRoute, an unreadable guard is data, not an exception: DiscoverGuardedPages (:316) builds a null Guard plus the Violation for an unclassified ADC page (:324), and Compare turns that into a violation (:110-:113) rather than skipping the page, so the gate cannot pass over a page it failed to read. ADC pages come from source text (:318-:324); the framework's routable components in MMCA.Common.UI are added by reflection (:333).
  • Where it's used - only inside the defining file: EveryNavItem_OpensARoutablePage (:75) matches nav hrefs against its Route, and EveryNavItem_IsNoLooserThanTheGuardOfThePageItOpens (:89) compares item against Guard.

ContractTable

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavigationContractTests.cs:332 · Level 1 · record (private, sealed, nested)

  • What it is - the parse result of the route-contract table: Rows (a List<ContractRow>) plus Problems (a List<string> of structural defects found while reading) (NavigationContractTests.cs:332).
  • Depends on - ContractRow, nested in NavigationContractTests.
  • Concept introduced, parse problems are data. ReadContractTable (:243) never throws on a malformed table. A missing ## Route Contract heading, a missing | Route | Page | Access | header, a row that is not three cells with a backticked route, or an empty table each append to Problems and return early with an empty Rows. RouteContractTable_IsWellFormed then surfaces them (:107).
  • Where it's used - only inside the defining file; ReadContractTable().Rows feeds three facts and .Problems feeds the hygiene fact.

CostTagConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/CostTagConventionTests.cs:10 · Level 1 · class (public, sealed)

  • What it is - the cost-attribution gate over ADC's infra/main.bicep: every container app must carry a service tag, and every per-service database must carry the matching one, so per-service spend is readable straight off the Azure cost report. It is a thin subclass of CostTagConventionTestsBase that supplies two values, MinimumContainerApps => 6 (MMCA.ADC.Architecture.Tests/Governance/CostTagConventionTests.cs:12) and DatabaseNamePrefix => "ADC_" (:14).
  • Depends on - CostTagConventionTestsBase (see the per-project rollup; MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs:29), and the infra.main.bicep manifest resource the base reads from the derived type's assembly (CostTagConventionTestsBase.cs:45, :51), the same resource ObservabilityConventionTests uses.
  • Concept introduced, cost as a governed dimension, authored once. Nothing in Bicep enforces a tagging convention; an untagged container app or database still deploys, it just becomes an unattributed line in the bill. The base owns both facts. EveryContainerApp_CarriesAServiceTag (CostTagConventionTestsBase.cs:59) requires at least MinimumContainerApps apps to be discovered (so the check cannot pass off a parse that matched nothing; ADC's six are four services, the Gateway and the UI host) and fails any app whose captured tags line lacks service: (:68). ServiceDatabases_CarryTheServiceTagOfTheOwningApp (:82) builds the expected literal tags: union(commonTags, { service: toLower(replace(dbName, '<prefix>', '')) }) from DatabaseNamePrefix (:88) and asserts the Bicep contains it, so a database's tag is derived from its own name. [Rubric §34 - Architecture Governance & Documentation] is the base's stated category (:6); the FinOps half of [Rubric §29 - Resilience & Business Continuity] applies too.
  • Walkthrough - two overrides and no test method. The prefix is why the base takes a parameter: ADC strips ADC_ to get conference, and an empty prefix fails the second fact (:84).
  • Why it's built this way - the rule lives in the shared package so Store and ADC cannot drift on it; ADC keeps only its identity (the same thin-subclass shape as BrandColorTokenTests). The trade-off is the hand-maintained floor: a new deployable raises the count and the override must follow.
  • Where it's used - runs with the rest of the architecture suite in ADC's build-and-test job.
  • Caveats / not-in-source - the tag test is a substring match on the captured tags: line, so a service: text elsewhere on that line would satisfy it.

SqlAuditConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/SqlAuditConventionTests.cs:12 · Level 0 · class

  • What it is - the guard that every database carrying dbo.AuditTrailEntries also provisions a database-scoped SQL auditing policy that records an UPDATE or DELETE against that table by ANY principal, the admin login included (MMCA.ADC.Architecture.Tests/Governance/SqlAuditConventionTests.cs:97-:110, :112-:129). See ADR-075.
  • Depends on - the same embedded infra.main.bicep resource CostTagConventionTests reads, loaded independently by its own ReadBicep() (:131-:138).
  • Concept introduced, the audit trail must itself be tamper-evident. AuditSaveChangesInterceptorTests (elsewhere in this unit) proves the application writes an audit row for every change; this type proves the database layer under that write cannot be bypassed by a principal with direct SQL access, the admin login included, which is exactly the actor the application-level interceptor cannot see. DatabaseAuditingPolicy_RecordsTrailUpdatesAndDeletes (:96) asserts three literal substrings: the resource type must be Microsoft.Sql/servers/databases/auditingSettings@... (database-scoped, never the server-level policy, because object-scoped actions like UPDATE ON dbo.AuditTrailEntries are only valid there), and the two action groups 'UPDATE ON dbo.AuditTrailEntries BY public' and 'DELETE ON dbo.AuditTrailEntries BY public'. AuditTrailDatabases_IncludeEveryDatabaseThatCarriesTheTrail (:112) is a [Theory] over the three databases that carry the trail table, ADC_Identity, ADC_Conference, ADC_Engagement (:113-:115), and asserts each name appears inside the auditTrailDatabaseNames array the DML audit loop iterates, so a fourth module that starts writing audit rows but is never added to that array fails the build instead of shipping an unaudited trail silently. [Rubric §11 - Security] and [Rubric §30 - Compliance/Privacy/Data Governance] are the direct targets: an audit trail that can be quietly altered or deleted by a privileged account is not evidence.
  • Walkthrough - AuditTrailDatabases_IncludeEveryDatabaseThatCarriesTheTrail locates the var auditTrailDatabaseNames = [ declaration by substring search (:120), asserts it was found at all (:121), then slices to the matching ] and checks membership of the quoted database name inside that slice (:123-:128), so the assertion is scoped to the declared list and cannot be satisfied by the name appearing anywhere else in the file.
  • Why it's built this way - BY public is the SQL Server auditing wildcard for "every principal", written into the policy rather than into an allowlist of expected callers, because the whole point is that the admin login itself cannot make an audit-trail change disappear from the trail.
  • Where it's used - runs with the rest of the suite in the build-and-test job (MMCA.ADC/.github/workflows/deploy.yml:189, :284).
  • Caveats / not-in-source - the three literal substrings are exact-text assertions against the compiled Bicep, so a semantically equivalent but differently formatted policy declaration (reordered JSON keys, a different quoting style) would fail this check even though the deployed policy is correct.

ObservabilityConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7 · Level 1 · class (public, sealed)

  • What it is - the SLO alert-to-runbook pairing gate for ADC. It derives from ObservabilityConventionTestsBase and overrides one member, raising the non-vacuity floor from the base's default of 3 to 6 (MMCA.ADC.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7-:15).
  • Depends on - ObservabilityConventionTestsBase, plus two EmbeddedResource entries in the csproj that supply the files the base reads: infra/main.bicep under the logical name infra.main.bicep and infra/OPERATIONS.md under infra.OPERATIONS.md (MMCA.ADC.Architecture.Tests/MMCA.ADC.Architecture.Tests.csproj:17-:22).
  • Concept introduced, identity by inheritance plus a floor override. This is the thin-subclass pattern from BrandColorTokenTests, extended one step. The base defaults ResourceAssembly to GetType().Assembly (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:51), so the derived type is the configuration: deriving in this assembly is what points the rule at ADC's embedded bicep and runbook, the same identity-only wiring the file comment describes ("this repo supplies only its identity", ObservabilityConventionTests.cs:3-:6). On top of that wiring, this subclass raises MinimumAlertSpecs from the base's default of 3 to 6 (:14), because ADC declares six SLO alert specs, the five request and resilience SLOs plus the AI scoring token ceiling (moved into sloAlertSpecs so the pairing gate covers it), and leaving the base floor in place would let the gate pass vacuously if the parse anchors drifted and discovered only three. [Rubric section 13 - Observability & Operability] assesses whether the system can be operated under failure, which means alerts that lead somewhere; this pairs each provisioned alert with a runbook section at build time instead of at 3am.
  • Walkthrough - one member. MinimumAlertSpecs => 6 (:14) raises the base's default of 3 (ObservabilityConventionTestsBase.cs:39) to ADC's actual alert-spec count, now that the AI scoring token ceiling moved into sloAlertSpecs alongside the five request and resilience SLOs. Everything else runs from the base's three inherited facts: SloAlertSpecs_AreDiscovered_GateIsNotVacuous (ObservabilityConventionTestsBase.cs:82) enforces that floor; EveryProvisionedSloAlert_HasASeverityCorrectRunbookSection (:64) walks the alerts declared in the embedded bicep and requires a matching, severity-correct section in the embedded runbook; and EveryRunbookAlertSection_MapsToAProvisionedAlert (:92) closes the other direction, failing on an orphan runbook section for an alert that no longer exists. The resource names the base reads default to infra.main.bicep and infra.OPERATIONS.md (:42, :45), which is why the csproj logical names must match exactly.
  • Why it's built this way - alert definitions live in infrastructure-as-code and the response procedure lives in a Markdown runbook; nothing in either file references the other, so the pairing is exactly the kind of invariant that decays silently. Embedding both into the test assembly turns the pairing into a compile-and-run artifact.
  • Where it's used - runs with the rest of the suite in the build-and-test job (MMCA.ADC/.github/workflows/deploy.yml:189, :284).

BrandColorTokenTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/BrandColorTokenTests.cs:12 · Level 1 · class (public, sealed)

  • What it is - the ADC end of the brand-token drift guard. It is a six-line subclass of the shared BrandColorTokenTestsBase that names one embedded stylesheet, ADCHome.Shared.razor.css (MMCA.ADC.Architecture.Tests/Ui/BrandColorTokenTests.cs:14-:17); the rule body itself lives in MMCA.Common.
  • Depends on - BrandColorTokenTestsBase from the MMCA.Common.Testing.Architecture package (referenced at MMCA.ADC.Architecture.Tests/MMCA.ADC.Architecture.Tests.csproj:41), plus the EmbeddedResource item that maps the conference landing page's scoped stylesheet into this assembly under that logical name (MMCA.ADC.Architecture.Tests/MMCA.ADC.Architecture.Tests.csproj:11-:13). Externals: xUnit v3, AwesomeAssertions, and NetArchTest (MMCA.ADC.Architecture.Tests.csproj:25-:27), the last two reachable everywhere in the assembly through the global usings (MMCA.ADC.Architecture.Tests/GlobalUsings.cs:1-:5).
  • Concept introduced, the thin-subclass fitness function. Every type in this unit follows one shape, so learn it once here. A fitness function is an executable test that asserts an architectural property instead of a behavior. MMCA keeps the property's logic in exactly one place, an abstract *TestsBase in the shared MMCA.Common.Testing.Architecture package, and each repo derives a sealed subclass that supplies only its own identity: which assemblies to scan, which floors and allowlists apply, which files to read. xUnit discovers [Fact]s on inherited members, so the subclass needs no test method of its own; deriving the class is what makes the rule run in this repo (ADR-015). [Rubric §34 - Architecture Governance & Documentation] assesses whether architectural decisions are recorded and enforced rather than trusted to reviewers; here the decision is enforced by a build that goes red. [Rubric §20 - Design System & Theming] assesses whether a design system has one source of truth for its tokens; this rule is what stops a host copy of the landing page from re-hardcoding the brand hex.
  • Walkthrough - one member. EmbeddedCssLogicalNames (BrandColorTokenTests.cs:14-:17) is a collection expression with a single entry, "ADCHome.Shared.razor.css". That string is not a file path: it is the LogicalName the csproj assigns when it embeds Source/Modules/Conference/MMCA.ADC.Conference.UI/Pages/Home/ADCHome.razor.css as a manifest resource (MMCA.ADC.Architecture.Tests.csproj:11-:13), which is how a test assembly reads a file from a project it does not reference. The inherited fact LandingPageCss_SourcesBrandColorFromToken_NotHardcodedHex (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/BrandColorTokenTestsBase.cs:25) then loads each named resource and asserts the token rule over it; the abstract hook this class implements is declared at :22.
  • Why it's built this way - the class comment records the split (BrandColorTokenTests.cs:3-:11): MMCA.Common's own BrandColorTokenTests guards the C#-to-CSS token definition, and this one guards the ADC consumer of it. Embedding the stylesheet rather than reading it off disk means the guard travels with the compiled test assembly and cannot be defeated by a runner whose working directory differs.
  • Where it's used - the whole project is inside ADC's CI solution filter (MMCA.ADC/MMCA.ADC.CI.slnf:58), which the build-and-test job restores, builds, and tests on every PR and every push to main (MMCA.ADC/.github/workflows/deploy.yml:4-:7, :189, :264, :270, :284).
  • Caveats / not-in-source - the guard only covers stylesheets that are both embedded and listed. ADC lists exactly one, so a second landing-page stylesheet added later is invisible to the rule until someone adds it to both places.

MessageBusBackpressureTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/MessageBusBackpressureTests.cs:10 · Level 1 · class (public, sealed)

  • What it is - the broker-backpressure gate for ADC's four service hosts. It overrides one member, Services, as ["Conference", "Identity", "Engagement", "Notification"] (MMCA.ADC.Architecture.Tests/Governance/MessageBusBackpressureTests.cs:12).
  • Depends on - MessageBusBackpressureTestsBase (see the per-project rollup; MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/MessageBusBackpressureTestsBase.cs:31), plus one embedded appsettings.json per listed service under the logical name services.{Service}.appsettings.json (MessageBusBackpressureTestsBase.cs:63).
  • Concept introduced, an unset knob is a silent default. MessageBusSettings leaves PrefetchCount and ConcurrentMessageLimit null and ignores values of zero or less, so a service that omits them runs on the transport's own defaults with nothing failing (MessageBusBackpressureTestsBase.cs:7-:12). The inherited fact EveryServiceAppSettings_BoundsBrokerBackpressure (:70) requires a non-empty service list, then for each service both keys set and positive and the prefetch window at least the concurrency it feeds; the public static Check (:93) holds the parse so it can be tested over a fixture document. The settings are read with System.Text.Json, not bound to MessageBusSettings, because the package takes no framework reference (:24-:28). [Rubric §12] is the base's stated category (:7).
  • Walkthrough - one member. The list names all four services, Notification included.
  • Why it's built this way - the key names are pinned against MessageBusSettings by MMCA.Common's own tests (:27-:28), so a rename there fails in the framework before it can desynchronize this gate.
  • Where it's used - runs with the architecture suite in ADC's build-and-test job.
  • Caveats / not-in-source - the gate reads the checked-in appsettings.json; a value overridden by an environment variable in Azure is not seen.

ImageSizingTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/ImageSizingTests.cs:13 · Level 4 · class (public, sealed, partial)

  • What it is - a markup-to-stylesheet drift guard for two images that can overflow a phone: the public sponsor-list logo and the badge QR code. Each fact finds the element in its .razor file, reads its Class attribute, and asserts some stylesheet rule under Source/ that names one of those classes declares the sizing the page needs (MMCA.ADC.Architecture.Tests/Ui/ImageSizingTests.cs:19, :33).
  • Depends on - ArchitectureMapBase.FindRepoRoot("MMCA.ADC.slnx") for the source root (:15), System.Text.RegularExpressions through GeneratedRegex properties with a 2000 ms timeout (:85-:109), and xUnit and AwesomeAssertions.
  • Concept introduced, assert the cascade without a browser. PublicSponsorListLogo_IsCappedToItsCardWidth (:19) requires a rule with max-width: 100%, height: auto and a max-height cap on the MudImage bound to sponsor.LogoUrl in PublicSponsorList.razor. MyBadgeQrCode_ShrinksToThePhoneWidthAndStaysCrisp (:33) requires max-width: 100%, height: auto and image-rendering: pixelated on the QrCodeImage in MyBadge.razor, so a scaled QR stays scannable. [Rubric §34 - Architecture Governance & Documentation] covers the enforced-not-trusted half: a responsive-layout regression fails the build instead of reaching a phone.
  • Walkthrough - two facts and two helpers. ClassesOf (:46) locates the element with a regex, fails if the markup moved, and splits its Class attribute. RulesFor (:60) walks every *.css under Source/ outside bin and obj (global sheets and scoped .razor.css alike), strips comments, and returns the declaration body of each rule whose selector names one of the classes, matched with a trailing (?![\w-]) so logo does not match logo-large.
  • Why it's built this way - reading CSS as text lets a plain architecture assembly check a Blazor UI it does not render. The because: text lists the classes found, so a failure names what to fix.
  • Where it's used - only inside the defining file.
  • Caveats / not-in-source - the rule regex excludes inner braces (:97), so a rule nested in a media query is not understood.

LegalDocumentConsistencyTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/LegalDocumentConsistencyTests.cs:22 · Level 4 · class (public, sealed, partial)

  • What it is - the guard that ADC's legal surface cannot drift apart: the terms version the Identity service enforces, the two copies of the privacy policy, and the production legal links the UI hosts ship (MMCA.ADC.Architecture.Tests/Governance/LegalDocumentConsistencyTests.cs:41, :62, :74).
  • Depends on - ArchitectureMapBase.FindRepoRoot("MMCA.ADC.slnx") (:30), System.Text.Json for the settings files (JsonOptions skips comments and allows trailing commas, :24-:28), a source-generated regex for the effective date (:132), and xUnit and AwesomeAssertions.
  • Concept introduced, documents as executable contracts. CurrentTermsVersion_IsPrintedVerbatimInTermsHtml (:41) reads Legal:CurrentTermsVersion from the Identity service's appsettings.json, requires it non-blank (an empty value silently turns terms acceptance off), and asserts the Gateway's wwwroot/terms.html contains it verbatim. PrivacyMarkdownAndPrivacyHtml_StateTheSameEffectiveDate (:62) parses the Effective date: line of PRIVACY.md and of privacy.html with ReadEffectiveDate (:114, format MMMM d, yyyy) and requires them equal. The [Theory] ProductionLegalUrls_AreAbsoluteHttpsOnAPublicHost_AndPointAtTheGatewayPages (:74) runs over ProductionLegalSettingsFiles (:34, the Web host's appsettings.Production.json and the MAUI host's appsettings.json) and calls AssertPublicHttpsUrl (:90) for TermsUrl (suffix /terms), PrivacyUrl (/privacy) and DeleteAccountUrl (/delete-account), plus CodeOfConductUrl when present: absolute, https, and not a local host (IsLocalHost, :109). [Rubric §30 - Compliance/Privacy/Data Governance] assesses whether legal commitments stay consistent with what ships.
  • Walkthrough - three guards, one per drift path: version against the page the user reads, markdown against html copy, and configuration against the Gateway's pages.
  • Why it's built this way - the assertion messages state the maintenance rule ("when the terms change, update terms.html and bump the version together", :55-:58), so the failure is the instruction.
  • Where it's used - only inside the defining file.
  • Caveats / not-in-source - the version check is a substring test, so 1.1 would also pass against a page that prints 1.10.

MauiHostShellTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/MauiHostShellTests.cs:18 · Level 4 · class (public, sealed, partial)

  • What it is - four text-level guards over the MAUI head's native shell, which the architecture assembly does not reference: the Android camera intent query, the pre-theme host page background, a language-neutral loading placeholder, and the Android App Links path restrictions (MMCA.ADC.Architecture.Tests/Ui/MauiHostShellTests.cs:26, :42, :61, :77).
  • Depends on - ArchitectureMapBase.FindRepoRoot("MMCA.ADC.slnx") to locate Source/Hosts/UI/MMCA.ADC.UI (:22), System.Xml.Linq for the manifest (the Android namespace, :20), and GeneratedRegex properties (:126-:160). It is the sibling of MobileHostParityTests, which guards the hostnames.
  • Concept introduced, platform requirements as tests. AndroidManifest_DeclaresTheImageCaptureIntentQuery (:26) requires a <queries><intent><action> for android.media.action.IMAGE_CAPTURE, because on Android 11+ the camera app is invisible to MediaPicker.CapturePhotoAsync without it and Take photo silently does nothing. HostPage_DoesNotPaintALightBackgroundBeforeTheThemeApplies (:42) joins the inline styles of wwwroot/index.html with app.css and requires the html/body rule to be transparent, or a prefers-color-scheme: dark block plus a background. HostPage_LoadingPlaceholderIsLanguageNeutral (:61) takes the text between the #app root and the first <script, strips tags, and requires no letter (\p{L}), since it renders before localization exists. AndroidAppLinks_ClaimOnlyTheDeepLinkablePaths (:77) finds each [IntentFilter] mentioning PublicWebHost in MainActivity.cs, resolves its DataPathPrefix values through PathPrefixValues (:105, literals and const string names), requires them non-empty, requires /conference/, /happening-now, /feedback/ and /auth/oauth-complete to be claimed, and forbids /, empty and /login*. [Rubric §34 - Architecture Governance & Documentation] is the enforced-not-trusted half.
  • Walkthrough - four facts; the fourth is the interesting one, because an App Links filter with no path restriction claims the whole host, so /login would open the app instead of the browser.
  • Why it's built this way - each assertion's because: records the platform failure it prevents, the part a reader cannot infer from the attribute or CSS alone.
  • Where it's used - only inside the defining file.
  • Caveats / not-in-source - these are regex reads of source text, not a build of the MAUI head; a differently shaped IntentFilter declaration would need the regexes revisited.

UIModuleNavItems

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavItemGuardContractTests.cs:460 · Level 2 · record (private, sealed, nested)

  • What it is - the nav items read from one ADC UI module: Name, RelativePath (the module's source file), Items (IReadOnlyList<NavItem>) and Problems (strings for anything that could not be read) (NavItemGuardContractTests.cs:460).
  • Depends on - NavItem and IUIModule from MMCA.Common.UI; nested in NavItemGuardContractTests.
  • Concept introduced, two read paths, one result shape. ReadModule (:156) instantiates the module and reads NavItems when its assembly is next to the test binaries (:159-:165); otherwise ReadModuleFromSource (:185) parses the NavItems { get; } = [ ... ]; initializer with regexes, which is how the MAUI head's DeviceUIModule is covered although a net10.0 test project cannot reference it. Both return this record, and Problems carries unreadable entries (:190, :204) so NavItems_AreReadFromEveryUIModule_GateIsNotVacuous (:55) fails instead of skipping them.
  • Where it's used - only inside the defining file: DiscoverModules (:139) builds the list that the three facts iterate.

MobileHostParityTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/MobileHostParityTests.cs:16 · Level 4 · class (public, sealed, partial)

  • What it is - the drift guard over the production hostnames the MAUI head hardcodes in four places no compiler connects: Api:ApiEndpoint and PublicSite:BaseUrl in the embedded appsettings.json, the Android App Links host MainActivity.PublicWebHost, and the iOS applinks: associated-domains entitlement (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/MobileHostParityTests.cs:6-:15). Unlike the other UI rules in this unit it derives from no shared base: it is an ADC-only test with one fact.
  • Depends on - ArchitectureMapBase.FindRepoRoot("MMCA.ADC.slnx") to locate Source/Hosts/UI/MMCA.ADC.UI from the compiled test assembly (MobileHostParityTests.cs:36-:37), and the BCL System.Text.Json and System.Text.RegularExpressions (:1-:2). Externals: xUnit v3 and AwesomeAssertions, global-used across the assembly (MMCA.ADC.Architecture.Tests/GlobalUsings.cs:1-:5).
  • Concept introduced, a cross-file configuration parity check. The four values live in a JSON file, a C# constant and a plist, so a type system cannot tie them together; only a test that reads all three files can. The rule states two different relationships. The web UI serves the app-link association, so PublicSite:BaseUrl, the Android host and the iOS applinks: host must be the identical host. The API endpoint is the gateway, a different app, so it is only required to share the Container Apps environment domain with the web host (:10-:13). [Rubric §34 - Architecture Governance & Documentation] covers the enforced-not-trusted half: an environment move or a custom-domain cutover that misses one spot fails the build instead of shipping a mobile build whose deep links or API calls point at the old environment (:13-:14).
  • Walkthrough - one fact and four helpers.
    • PublicSiteAndAppLinkHosts_AreIdentical_AndTheApiSharesTheirEnvironment (:33-:59) parses appsettings.json with JsonOptions (comments skipped, trailing commas allowed, :27-:31) and takes the Host of Api:ApiEndpoint and PublicSite:BaseUrl (:39-:43); today those are the adc-prod-gateway and adc-prod-ui hosts (MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/appsettings.json:20, :23). It reads Platforms/Android/MainActivity.cs and Platforms/iOS/Entitlements.plist as text (:45-:52), whose declarations sit at MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/Platforms/Android/MainActivity.cs:44 and MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/Platforms/iOS/Entitlements.plist:11. It then asserts Android equals the public-site host, iOS equals the public-site host, and the environment domains of the API and public-site hosts are equal, each with a because: that names the platform requirement (:54-:58).
    • SingleCapture (:61-:66) requires exactly one match before returning the host group, so a duplicated or missing declaration fails with "exactly one declaration of {what} is expected" rather than silently comparing the first hit.
    • EnvironmentDomain (:68-:69) strips everything up to the first dot, turning adc-prod-ui.<env>.eastus2.azurecontainerapps.io into <env>.eastus2.azurecontainerapps.io.
    • AndroidPublicWebHost and IosAppLinksHost (:71-:75) are GeneratedRegex properties with ExplicitCapture and a 2000 ms match timeout, matching PublicWebHost = "..." and <string>applinks:...</string> respectively.
  • Why it's built this way - reading the platform files as text rather than compiling them is what lets an architecture test assembly check a MAUI head it does not reference (the MAUI head is not in ADC's CI solution filter, which lists this test project at MMCA.ADC/MMCA.ADC.CI.slnf:59). The MainActivity constant carries its own comment pointing back at PublicSite:BaseUrl (MainActivity.cs:37), and this test is what makes that comment binding.
  • Where it's used - no usage outside the defining file; it runs with the rest of the architecture suite in the build-and-test job (MMCA.ADC/.github/workflows/deploy.yml:220), whose test step runs dotnet test --solution MMCA.ADC.CI.slnf (:346).
  • Caveats / not-in-source - the environment check compares everything after the first label, so it assumes both hosts are single-label app names under the same environment suffix; a custom domain that is not shaped <app>.<environment domain> would need the rule revisited. Not determinable from source: whether an Android AndroidManifest intent filter or other platform files carry the host as well (the test reads only the three files named above).

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavigationContractTests.cs:20 · Level 4 · class (public, sealed, partial)

  • What it is - ADC's navigation drift gate (rubric §25): the ## Route Contract table in adc-NavigationFlow.md must list exactly the routable pages ADC ships, each row's Access word must match the page's guard, and every page must state its guard as exactly one recognized attribute. It is the ADC counterpart of the framework's own same-named gate in MMCA.Common.Architecture.Tests.Ui, a different class.
  • Depends on - ArchitectureMapBase.FindRepoRoot("MMCA.ADC.slnx") (NavigationContractTests.cs:28), the nested records PageRoute, ContractRow and ContractTable (:328-:332), GeneratedRegex properties (:296-:325), and xUnit and AwesomeAssertions. Unlike the framework gate it reflects over nothing: it scans *.razor source text.
  • Concept introduced, documentation as an executable contract, read from source text. The doc lives in the Website repo, not next to the code, so ResolveDocPath (:288) reads it from the ADC_NAVIGATION_DOC environment variable (DocVariable, :23) and otherwise from ../Website/docs-src/guides/adc-NavigationFlow.md (DefaultDocPath, :30-:31); the existence assertion in ReadContractTable (:243) says CI downloads the published copy and sets the variable. [Rubric §25 - Navigation & IA] is the target; [Rubric §11 - Security] and [Rubric §26 - Front-End Security] apply because the guard column is verified against [AllowAnonymous], [Authorize] and [Authorize(Roles = RoleNames.Organizer)].
  • Walkthrough - a floor MinimumRoutes = 55 (:22) and five facts.
    • RoutablePages_AreDiscovered_GateIsNotVacuous (:38) requires at least 55 discovered routes (59 when written), so a moved tree cannot let the gate pass over nothing.
    • EveryRoutablePage_DeclaresARecognizedAccessAttribute (:44) reports pages whose Classify produced a violation (none, several, or an unrecognized attribute, :207) and any route declared by more than one page.
    • EveryRoutablePage_IsInTheRouteContract_AndEveryContractRoute_Exists (:63) compares route sets both ways, undocumented and phantom, verbatim including constraints such as {Id:int}.
    • EveryContractAccess_MatchesThePageGuard (:79) compares each row's Access with the page's classified guard, skipping rows the other facts already report.
    • RouteContractTable_IsWellFormed (:107) surfaces Problems, duplicate route rows, Access words outside Anonymous, Authenticated and Organizer, and Page cells that are neither - nor the declaring file's name.
    • Discovery is DiscoverPageRoutes (:139, internal static so NavItemGuardContractTests reuses it to read ADC's pages, :332): every *.razor under Source/ outside bin and obj (IsSourceFile, :170) with an @page line; access comes from @attribute lines, else from attribute lines above the matching partial class in the .razor.cs (ReadCodeBehindAccessAttributes, :180).
  • Why it's built this way - the failure messages name the remedy and its order ("landing that change before or with the code change"), so a missing row is a documentation task, not a mystery.
  • Where it's used - runs in ADC's architecture suite; a checkout without the Website repo and without ADC_NAVIGATION_DOC fails at ReadContractTable's existence assertion (:243).
  • Caveats / not-in-source - the code-behind lookup scans attribute lines directly above the first partial class matching the page name; an attribute placed elsewhere reads as no guard.

SpanishAccentTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/SpanishAccentTests.cs:9 · Level 4 · class (public, sealed)

  • What it is - the Spanish-localization gate for ADC: no *.es.resx string may ship a common word with its accent or n-tilde missing. It overrides one member, ResourceRoot, as Source/ under the repo root found by ArchitectureMapBase.FindRepoRoot("MMCA.ADC.slnx") (MMCA.ADC.Architecture.Tests/Governance/SpanishAccentTests.cs:11-:12).
  • Depends on - SpanishAccentTestsBase (see the per-project rollup; MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SpanishAccentTestsBase.cs:27) and ArchitectureMapBase.
  • Concept introduced, a localization defect that every functional test misses. An unaccented word compiles, renders and passes behavior tests, so a Spanish-speaking user finds it. The base's single fact Spanish_resources_keep_their_accents (SpanishAccentTestsBase.cs:68) enumerates *.es.resx under the root outside build output, requires at least MinimumResourceFileCount (default 1, :64) so a wrong root cannot pass vacuously, builds one whole-word case-insensitive regex from UnaccentedWords (default list :32, extendable per consumer, :51), and checks only string <value> text, not keys or comments. AllowedEntries (:58) holds deliberate exceptions as {path}:{key}. ADC overrides neither, so it takes the framework list and no exceptions. [Rubric §34 - Architecture Governance & Documentation] covers the enforced-not-trusted half.
  • Walkthrough - one override; the rule is inherited.
  • Why it's built this way - the word list leaves out words correct both ways (esta, solo) and plurals that drop the accent by rule (SpanishAccentTestsBase.cs:20-:23), trading recall for a gate with no false positives.
  • Where it's used - runs with the architecture suite in ADC's build-and-test job; the framework's own SpanishAccentTests subclasses the same base.
  • Caveats / not-in-source - only the listed words are caught; a missing accent on any other word passes.

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC.Architecture.Tests/Api/NavItemGuardContractTests.cs:41 · Level 5 · class (public, sealed, partial)

  • What it is - the menu-to-page guard contract: every sidebar entry an ADC UI module contributes must be no looser than the guard of the page it opens, so the menu never offers a link the page will refuse. Three facts enforce it: the nav items were actually read, every item opens a routable page, and every item requires at least what that page's guard enforces (MMCA.ADC.Architecture.Tests/Api/NavItemGuardContractTests.cs:54-:106). The contract is written out in the class summary (:16-:40).
  • Depends on - IUIModule and NavItem (with NavSection) for the menu side, NavigationContractTests for ADC's own page routes and access classification (NavigationContractTests.DiscoverPageRoutes, called at :318), RoleNames for the Organizer role, and ArchitectureMapBase for FindRepoRoot("MMCA.ADC.slnx") (:45). It takes no architecture map.
  • Concept introduced, comparing two guards that are written in different places. A navigation entry and the page behind it each carry an access decision, declared independently, so nothing in a single file notices when the menu shows a signed-out visitor a link that ends in a sign-in challenge. The shared NavMenu shows an item to a caller who satisfies its RequiredRole, RequiredClaim and RequiredPermission (each only when set), and shows the User and Admin sections to signed-in callers only, so an item counts as anonymous exactly when it sets none of the three and sits in NavSection.General (:116). Compare (:108-:136) then yields a problem per rule: an authenticated page needs a non-anonymous item, a page naming roles needs an item requiring one of them (no permission or claim implies a role statically, because the role-to-permission map is editable at runtime), and a page requiring a claim or a permission needs an item carrying the same one. [Rubric §25] is the section the assertion messages cite (:71, :85, :105); [Rubric §11 - Security] and [Rubric §26 - Front-End Security] apply because the pages are the authorization boundary and the menu must not contradict it.
  • Walkthrough - three facts, then the readers behind them.
    • NavItems_AreReadFromEveryUIModule_GateIsNotVacuous (:54-:72) fails when no IUIModule implementation is found under Source/, when a module reports a read problem, or when zero items were read in total, so the gate cannot pass by reading nothing.
    • EveryNavItem_OpensARoutablePage (:74-:86) fails on a dead link: an item whose Href matches no @page route, which would also make the guard comparison pass without checking anything.
    • EveryNavItem_IsNoLooserThanTheGuardOfThePageItOpens (:88-:106) runs Compare for each item against every page its Href resolves to. The failure message says to fix the nav item (add the role, claim or permission, or move it to the User or Admin section), never to loosen the page guard.
    • DiscoverModules (:139-:154) finds module classes in source with ModuleDeclarationRegex (:442), skipping bin and obj (:435-:440). ReadModule (:156-:169) uses the real instance (Activator.CreateInstance, .NavItems) when the owning project's assembly sits beside the test binary (FindOwningAssemblyName, :171-:183), and otherwise ReadModuleFromSource (:185-:209). That fallback exists for the MAUI head's DeviceUIModule, which a net10.0 test project cannot reference (:34-:39). It parses the NavItems { get; } = [ ... ]; initializer with generated regexes (:442-:458), splits arguments depth-aware (SplitArguments, :283-:313) and resolves only string literals, null, RoleNames.* and NavSection.* (TryResolveValue, :250-:281). An entry it cannot resolve is reported as a problem, so it fails the first fact instead of being skipped (ParseEntry, :211-:245).
    • DiscoverGuardedPages (:316-:337) builds the page side two ways. ADC's own routable components come from NavigationContractTests, whose access string (Anonymous, Authenticated, Organizer) is mapped to a PageGuard (:318-:326). The framework's routable components in MMCA.Common.UI are read by reflection: ReadComponentGuard (:350-:404) collects [Authorize] roles and, for each named policy, walks the policy requirements. It understands deny-anonymous, roles, a single-value permission claim, any other claim type, and NotificationPageRequirement by type name (:376-:399).
    • RouteMatches (:407-:433) compares segment by segment, case-insensitively, treating {param} as a wildcard, {*catchall} as matching the rest, and trailing {x?} as optional; the query string and fragment are stripped first (:409).
  • Why it's built this way - policies are read from the real registration rather than restated: ReadFrameworkAuthorizationOptions builds a ServiceCollection, calls AddUIShared, and pulls AuthorizationOptions from the provider (:339-:348), so the gate follows the framework's actual policy. An unrecognized requirement type returns a problem naming itself ("teach NavItemGuardContractTests what it enforces", :397) instead of silently passing. The unresolved cases always fail loudly: that is the design rule that keeps a contract test from going vacuous.
  • Where it's used - runs with the architecture suite; the nav items it reads come from the Identity, Conference, Engagement and device UI modules, for example MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.UI/IdentityUIModule.cs.
  • Caveats / not-in-source - the source reader understands only literal forms; a nav item built from a constant or a helper must be rewritten in literal form or its module referenced by the test project. The comparison is static on roles, so it cannot prove a permission-gated item reaches a role-gated page.

AnonymousEndpointTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC.Architecture.Tests/Api/AnonymousEndpointTests.cs:23 · Level 6 · class (public, sealed)

  • What it is - the security gate that no endpoint loses its authorization unnoticed: every [AllowAnonymous] reachable in ADC's three module API assemblies and three module UI assemblies must appear as a reviewed line in this file (MMCA.ADC.Architecture.Tests/Api/AnonymousEndpointTests.cs:25-:33, :35-:131). It is the largest subclass in the unit: 60 allowlist entries covering controllers and routable pages alike.
  • Depends on - AnonymousEndpointTestsBase, System.Reflection.Assembly (global-used at MMCA.ADC.Architecture.Tests/GlobalUsings.cs:1), and, as type references pinning the three API assemblies, IdentityModule, ConferenceModule, and EngagementModule (:27-:29). The three UI assemblies are loaded by name (:30-:32). Note it does not take the map: it names its own assembly set.
  • Concept introduced, the reviewed-allowlist gate. The rules elsewhere in this unit assert a structural property. This one asserts a review property: the set of anonymous endpoints is not wrong, it is simply not allowed to change silently. The base makes that stick in three directions at once: AnonymousEndpoints_AreAllowListed fails on an unlisted [AllowAnonymous] (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:80), ScannedEndpointSet_IsNotEmpty fails when fewer than MinimumScannedTypes endpoint types were discovered at all (:92), and AllowList_HasNoStaleEntries fails on a listed entry that no longer matches anything (:105), which is what stops the list from silently accumulating names for endpoints that were renamed or re-gated. [Rubric §11 - Security] assesses whether the authorization posture is deliberate and verifiable; an allowlist that must be edited, in a file a reviewer reads, is the mechanism. [Rubric §26 - Front-End Security] applies too, because routable Blazor components are scanned alongside controllers.
  • Walkthrough - four members.
    • TargetAssemblies (:25-:33) names six assemblies: the Identity, Conference, and Engagement API assemblies by anchor type, and the matching three UI assemblies via Assembly.Load. The base scans each for the two shapes it understands, MVC controllers (any type whose base chain reaches ControllerBase, AnonymousEndpointTestsBase.cs:156-:170) and routable Blazor components (any type carrying a RouteAttribute, :172-:176), both matched by attribute full name so the rule library keeps no ASP.NET reference (:34-:35).
    • AllowedAnonymousEndpoints (:35-:131) is the reviewed list, grouped by justification rather than alphabetically. Two Identity credential-exchange actions on AuthController, LoginAsync and RegisterAsync, because requiring a token to mint one would be circular; they are throttled by the auth-ip rate-limit policy instead (:37-:41). Then the Conference public-browse reads: the GetAllAsync / GetAllForLookupAsync / GetByIdAsync triple on fourteen agenda controllers (ActivitiesController, CategoryItemsController, EventsController, PartnersController, QuestionsController, RoomsController, SessionsController, SpeakersController, SponsorsController, and their category and join siblings, :47-:88), because the conference website is readable without an account and is output-cached per ADR-040; the comment is careful to note that every create/update/delete on those same controllers stays behind the class-level [HasPermission] (:43-:46). Then the session-materials read on SessionAssetsController.GetBySessionAsync (:95), part of the public session page and output-cached like the agenda reads above, scoped by the same public-session projection so a session the public cannot see answers an EMPTY list rather than leaking one; every write on that controller stays behind [Authorize] and is re-authorized in the handler against the session's own speaker list (:90-:94). Then three smaller families with their own reasons: the Now/Next wayfinding reads (:97-:100), the ICS calendar exports a calendar client fetches without a bearer token (:102-:105), and the aggregate bookmark counts behind the popularity badge, counts only and never a per-user list (:107-:110). Then the type-level entry for ServiceInfoController, which must answer before a caller has a token to negotiate with (:114). Last, the eight public conference-website pages (PublicActivityList, PublicEventDetail, PublicEventList, PublicSessionDetail, PublicSessionList, PublicSpeakerDetail, PublicSpeakerList, PublicSponsorList, :123-:130) that render the agenda, speakers, sponsors and activities a visitor browses before creating an account. Type-level and method-level attributes use different identifier shapes, a bare FullName versus FullName.MethodName, which is why the ServiceInfoController entry and the page entries carry no method suffix.
    • RequireExplicitAuthorizationDecision => true (:139) is SEC-ADC-03: an endpoint carrying neither [Authorize] nor [AllowAnonymous] is invisible to the allowlist check above, exactly the shape a forgotten [Authorize] produces. The base now defaults the same way (AnonymousEndpointTestsBase.cs:69, backed by Endpoints_DeclareAnAuthorizationDecision at :119), so the ADC override restates the default explicitly. On a controller the framework's fallback authorization policy also closes the gap at run time; on a routable page nothing does, because AuthorizeRouteView reads attributes and ignores the fallback entirely, so for pages this flag is the only control (AnonymousEndpointTests.cs:133-:138).
    • MinimumScannedTypes => 80 (:144) raises the base floor of 1 (AnonymousEndpointTestsBase.cs:52) to the exact count of controller and routable-component types across the six assemblies today, SessionAssetsController included, so a renamed assembly or a dropped surface is a failure rather than a quietly smaller scan (:141-:143).
  • Why it's built this way - the class comment explains what the absences mean, which is the part a reader cannot infer. The two password-recovery actions on PasswordResetController and the two email-confirmation actions on EmailConfirmationController (ADR-116) are anonymous for the same circularity reason as login, but ADC does not override them, so the framework bases own their allowlist entries and none appear here (:11-:16); the same is true of refresh (:39). The base reads attributes with DeclaredOnly and inherit: false precisely so an inherited framework action is reported once at its declaration site rather than once per derived controller in every consumer (AnonymousEndpointTestsBase.cs:262-:264). Notification is absent because that module ships no controller and no routable component, hosting only the SignalR hub, so it contributes nothing to the scan (:18-:21).
  • Caveats / not-in-source - the base states its own blind spot: minimal-API endpoints opt out through the .AllowAnonymous() builder call, which produces endpoint metadata at map time and is invisible to static reflection, so the framework's own small anonymous minimal-API surface (JWKS, OIDC discovery, app-association, session-cookie refresh, health) is not covered here (AnonymousEndpointTestsBase.cs:18-:24). Nothing recomputes the 80 floor, so it is a lower bound a human maintains.

InlineStyleTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/InlineStyleTests.cs:10 · Level 6 · class (public, sealed)

  • What it is - the gate that no .razor file under Source/ carries an inline style=/Style=/...StyleFunc= attribute, so every visual rule lives in a stylesheet (adc-ui.css or a component's scoped .razor.css) instead of scattered inline across markup. ADC's class is now a thin subclass of the framework's shared base (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/InlineStyleTests.cs:10-:16).
  • Depends on - InlineStyleTestsBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/InlineStyleTestsBase.cs:12) and ArchitectureMapBase.FindRepoRoot to locate the repo root from the compiled test assembly (InlineStyleTests.cs:12).
  • Concept introduced, a text scan authored once and re-run per repo. The other UI fitness functions in this unit (BrandColorTokenTests, SortableColumnConventionTests) read a compiled artifact; this one reads source .razor files, because inline styles are a markup-authoring concern that never survives into a compiled component in a form worth inspecting. The scan itself lives in ArchitectureRules.RazorMarkupCarriesNoInlineStyles (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.InlineStyles.cs:37), which the base's single fact RazorMarkup_CarriesNoInlineStyles calls (InlineStyleTestsBase.cs:30-:31) with the subclass's MinimumRazorFiles floor. The floor is 80 (InlineStyleTests.cs:15, "enough to prove the scan actually walked the UI tree, three module UI projects and both heads"), so a moved or renamed Source/ root fails loudly rather than passing on zero files scanned. [Rubric §20 - Design System & Theming] is the direct target: a single source of truth for styling is what an inline style= attribute bypasses outright, and [Rubric §34 - Architecture Governance & Documentation] covers the enforced-not-trusted half.
  • Walkthrough - two overrides and one inherited fact. RepoRoot (InlineStyleTests.cs:12) is ArchitectureMapBase.FindRepoRoot("MMCA.ADC.slnx"), implementing the base's abstract root (InlineStyleTestsBase.cs:14-:15), and MinimumRazorFiles => 80 (:15) implements the abstract floor (InlineStyleTestsBase.cs:18). The inherited RazorMarkup_CarriesNoInlineStyles (InlineStyleTestsBase.cs:30) passes MarkupRoots and AllowedInlineStyles to the rule; ADC adds no allow-list, as the class summary states ("No allow-list: a genuinely dynamic value belongs in a class toggled from code or a CSS custom property", InlineStyleTests.cs:3-:8).
  • Why it's built this way - a component-parameter convention (Style="..." on a MudBlazor component, say) is textually indistinguishable from the native HTML style= attribute, so the rule catches both the same way a reviewer scanning markup would, rather than special-casing every UI library's parameter names. Moving the regex and file walk into the shared base means MMCA.Common and ADC run one implementation and differ only in root and floor.
  • Where it's used - runs with the rest of the suite in the build-and-test job (MMCA.ADC/.github/workflows/deploy.yml:189, :284).
  • Caveats / not-in-source - the exact regex and its lookbehind now live in the shared rule, not in this class; the rule file was not re-read for this section beyond its signature, so its false-positive heuristics are Not determinable from this unit's source.

LifetimeTokenConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/LifetimeTokenConventionTests.cs:10 · Level 6 · class (public, sealed)

  • What it is - the component-lifetime gate for ADC's module UI code: no component under Source/Modules reads _cts.Token directly (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/LifetimeTokenConventionTests.cs:10-:17).
  • Depends on - LifetimeTokenConventionTestsBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LifetimeTokenConventionTestsBase.cs:13) and ArchitectureMapBase.FindRepoRoot (LifetimeTokenConventionTests.cs:13).
  • Concept introduced, reading a cancellation token off a disposed source. Reading Token off a disposed CancellationTokenSource throws ObjectDisposedException and crashes the Blazor circuit after a navigation away, so components must read it as _cts.LifetimeToken() (MMCA.Common.UI) or through a LifetimeToken property (LifetimeTokenConventionTestsBase.cs:6-:9). The rule ArchitectureRules.ComponentsReadTheirTokenThroughLifetimeToken (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.LifetimeTokens.cs:28) does the scan; the base's single fact Components_ReadTheirTokenOnlyThroughLifetimeToken (LifetimeTokenConventionTestsBase.cs:28-:29) calls it with the subclass's scan root and file floor. [Rubric §34 - Architecture Governance & Documentation] covers the enforced-not-trusted half.
  • Walkthrough - two overrides and one inherited fact. ScanRoot (LifetimeTokenConventionTests.cs:12-:13) is Source/Modules under the repo root found via FindRepoRoot("MMCA.ADC.slnx"), and MinimumCodeBehindFiles => 60 (:16, "enough to prove the scan actually walked the module UI code-behind, three module UI projects") is the vacuity floor, so a moved root cannot pass on zero files (LifetimeTokenConventionTestsBase.cs:18-:19). The base's optional ScanAllCodeFiles (:25) stays at its default false, so only .razor and .razor.cs files are scanned (:21-:25).
  • Why it's built this way - authored once in the framework and re-run as a thin subclass in each repo, which supplies only the root and the floor (LifetimeTokenConventionTestsBase.cs:9-:11); MMCA.Common has its own subclass of the same base.
  • Caveats / not-in-source - the rule's matching details are not re-read here, so how it treats a token read through a helper other than LifetimeToken is Not determinable from this unit's source.

ProtoContractTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Contracts · MMCA.ADC.Architecture.Tests/Contracts/ProtoContractTests.cs:3 · Level 6 · class (public, sealed)

  • What it is - the frozen wire contract for ADC's synchronous cross-service API. It names the eight .proto files the four *.Contracts projects compile and commits a 99-line snapshot of everything they declare, so a renumbered field or a renamed rpc fails the build (MMCA.ADC.Architecture.Tests/Contracts/ProtoContractTests.cs:9-:19, :21-:122).
  • Depends on - ProtoContractTestsBase. Nothing else: the rule reads .proto files off disk from the repo root, so this class takes no map and the csproj needs no reference to the *.Contracts projects.
  • Concept introduced, pinning a contract that no compiler checks. A .proto file is a published contract between processes that are built separately, so nothing in a single repo's build notices when it changes incompatibly. The rule library rebuilds the live contract by parsing the files and diffs it against the committed list, reporting each side separately as "present in the .proto files but NOT frozen" and "frozen but NOT present in the .proto files" (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:68-:77). What gets pinned is exactly the wire surface: the package, every rpc with its request/response types and streaming flags, every message field with its declared type, label, and field number, and every enum value with its number (ArchitectureRules.Protos.cs:22-:24). What is deliberately not pinned is syntax, import, and option lines including csharp_namespace, because none of them changes a byte on the wire and failing on them is the fastest way to teach a team to update a snapshot without reading it (:27-:31). [Rubric §9 - API & Contract Design] assesses contract governance across the whole surface, not just REST. [Rubric §7 - Microservices Readiness]: this list is the synchronous coupling between ADC's four services, in the same way IntegrationEventContractTests is the asynchronous one (ADR-007).
  • Walkthrough - three members, all implementing abstract hooks.
    • SolutionFileName => "MMCA.ADC.slnx" (:5) implements ProtoContractTestsBase.SolutionFileName (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ProtoContractTestsBase.cs:22). The rule resolves the repo root from it via ArchitectureMapBase.FindRepoRoot (ArchitectureRules.Protos.cs:46), so the files are read from the working tree regardless of the runner's working directory.
    • ProtoFiles (:9-:19) lists eight repo-root-relative paths, and the comment states the scope rule: every .proto compiled by the four *.Contracts projects (:7-:8). Three from Conference (event_live_validation, session_bookmark_validation, user_conference_export), two from Engagement (bookmark_count, user_engagement_export), one from Identity (attendee_query), and two from Notification (live_channel, user_notification_export).
    • FrozenProtoContracts (:21-:122) is the snapshot: 85 message ... lines (:23-:107), one per field, each ending in = <number> : <type>, and 14 service ... lines (:108-:121), one per rpc. The entries are sorted, which is what makes a regenerated snapshot diff line by line, and the base's <remarks> explains how to regenerate it (print ArchitectureRules.BuildProtoContract(...) and paste, ProtoContractTestsBase.cs:12-:17). Reading the list is the fastest way to see what ADC's services actually say to each other: live-window validation, current-room lookup, sponsor live info and the overlapping-session lookup from Conference (GetOverlappingSessionIds, :110), plus its per-user event and session answer export (UserConferenceExportService, :23-:30, :58-:60, :115), bookmark counts and the engagement export from Engagement (the export response now also carries poll_votes = 7 and question_upvotes = 8, with their own EngagementPollVoteExportItem and EngagementQuestionUpvoteExportItem messages, ProtoContractTests.cs:91-:92, :73-:77), the attendee user-id list from Identity, and channel push plus the notification export from Notification.
    • The single inherited fact is ProtoContracts_ShouldMatch_TheFrozenSnapshot (ProtoContractTestsBase.cs:33).
  • Why it's built this way - the base is explicit that this is consumer-facing only: MMCA.Common ships the gRPC plumbing but no .proto of its own, so the framework does not subclass it, and a repo with a *.Contracts project does (ProtoContractTestsBase.cs:9-:11). The four *.Contracts projects are not registered in AdcArchitectureMap at all: this rule works from file paths rather than mapped assemblies, which is why it reaches them anyway.
  • Caveats / not-in-source - the file list is hand-maintained, so a brand-new .proto added to a *.Contracts project is not pinned until someone lists it here. Nothing asserts that ProtoFiles covers every .proto in the tree.

SortableColumnConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/SortableColumnConventionTests.cs:11 · Level 6 · class (public, sealed)

  • What it is - the MudDataGrid sorting guard: a column marked sortable must be a PropertyColumn, not a TemplateColumn. The subclass supplies one scan root, the whole ADC Source tree (MMCA.ADC.Architecture.Tests/Ui/SortableColumnConventionTests.cs:14-:17).
  • Depends on - SortableColumnConventionTestsBase, and ArchitectureMapBase called statically for the repo root (:16). Like ProtoContractTests and TranslationCompletenessTests, it takes no map: the rule is a markup text scan, so a directory is all it needs.
  • Concept introduced, guarding a defect that renders correctly. Server-side sort reads the bound property off the column, and a TemplateColumn has none, so Sortable="true" on one produces a header that toggles a sort arrow without ordering the data (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/SortableColumnConventionTestsBase.cs:4-:8). The defect compiles, renders, and stays invisible until somebody checks the order, which is the exact profile of a bug a fitness function should own rather than a code review. [Rubric §24 - Forms/Validation/UX Safety] is the base's own stated target, since an affordance that lies to the user is a UX-safety failure; [Rubric §18 - UI Architecture] and [Rubric §12 - Performance & Scalability] are the neighbours, because the whole point of server-side sort is to keep the grid from pulling the full set into the browser.
  • Walkthrough - one member. MarkupRoots (:14-:17) is a single-element collection expression: Path.Combine(ArchitectureMapBase.FindRepoRoot("MMCA.ADC.slnx"), "Source"). Building the path from FindRepoRoot rather than a relative path is what makes the scan independent of the runner's working directory (SortableColumnConventionTestsBase.cs:19-:23), and pointing it at the whole Source tree rather than one project is what makes it cover the three module UI projects and both web heads, which the class comment states explicitly (SortableColumnConventionTests.cs:7-:9). The inherited fact is SortableColumns_ShouldNotBe_TemplateColumns (SortableColumnConventionTestsBase.cs:27), delegating to ArchitectureRules.SortableGridColumnsUsePropertyColumn(MarkupRoots). The base notes that @* *@ comments are ignored and that a root which does not exist fails the test rather than silently passing (:12-:13).
  • Why it's built this way - the class comment records that ADC already used PropertyColumn for every sortable column when the rule was adopted, so this subclass freezes an already-clean state rather than pinning debt (SortableColumnConventionTests.cs:7-:8). Compare RawQueryableConventionTests, which had to be adopted the other way, over an existing set of violations.
  • Caveats / not-in-source - detection is textual over .razor markup, so a grid composed at runtime, or a column whose Sortable value comes from a variable rather than a literal, is outside what a text scan can judge.

TranslationCompletenessTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/TranslationCompletenessTests.cs:12 · Level 6 · class (public, sealed)

  • What it is - the internationalization completeness gate: every base *.resx under Source/ must have a complete, non-empty Spanish .es.resx sibling, so adding an English key without its translation fails CI instead of shipping a half-translated UI (MMCA.ADC.Architecture.Tests/Ui/TranslationCompletenessTests.cs:3-:11).
  • Depends on - LocalizationResourceTestsBase. Note the deliberate name divergence: the ADC subclass is named for what it guarantees (translation completeness), not for the base it derives from.
  • Concept introduced, the non-vacuity floor. A convention scan that discovers nothing passes trivially, which is the failure mode that makes fitness functions untrustworthy over time. The MMCA bases answer it with a minimum-count floor that the subclass raises to the repo's real magnitude, so a broken scan root (a moved directory, a renamed convention, a case-sensitivity slip on the Ubuntu runner) fails loudly instead of going green while checking zero files. You have already seen the floor in AnonymousEndpointTests, and you will see it again in CommandValidatorCoverageTests, ErrorCatalogTests, FormsConventionTests, and LocalizedTextConventionTests. [Rubric §27 - i18n] assesses whether localization is enforced rather than aspirational; the gate is the enforcement, and ADR-027 (which supersedes the single-locale ADR-011) is the decision it executes.
  • Walkthrough - two members. RequiredCultures => ["es"] (TranslationCompletenessTests.cs:14) implements the base's abstract culture list (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizationResourceTestsBase.cs:13), so Spanish is the one culture ADC contractually completes. MinimumBaseResources => 40 (TranslationCompletenessTests.cs:16) raises the base's default of 0 (LocalizationResourceTestsBase.cs:21), which would otherwise let an empty scan pass. The inherited fact is Translations_AreComplete_ForEveryRequiredCulture (LocalizationResourceTestsBase.cs:24).
  • Why it's built this way - the class comment justifies the floor from the repo's real shape: ADC has 40 or more localized resource sets across the three module UIs, the UI hosts' landing page, the nav-item module descriptors, and the API error-resource sets, so a near-zero discovery count means the scan path is wrong (TranslationCompletenessTests.cs:8-:10).
  • Caveats / not-in-source - the floor is a lower bound stated in the subclass, not a count computed from the tree, so it stays correct only as long as someone raises it when the resource set grows materially.

FolderWidthTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/FolderWidthTests.cs:10 · Level 6 · class (public, sealed)

  • What it is - the layout gate: no folder under ADC's Source/ or Tests/ tree may hold more than 12 direct code files, so a folder keeps naming a feature instead of turning into a technical bucket. It is a four-line subclass whose only override supplies the repository root (MMCA.ADC.Architecture.Tests/Governance/FolderWidthTests.cs:10-:13).
  • Depends on - FolderWidthTestsBase and ArchitectureMapBase, whose static FindRepoRoot("MMCA.ADC.slnx") walks up from the test assembly until it finds the solution file, which is how the rule locates the working tree regardless of the runner's working directory (FolderWidthTests.cs:12).
  • Concept introduced, a fitness function that reads the filesystem rather than IL. Every other rule in this unit reflects over compiled assemblies. This one cannot: the defect is a layout defect, so the rule walks directories from disk (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FolderWidth.cs:9-:11, :42-:63). That is also why the subclass supplies a path instead of a map. [Rubric §5 - Modular Monolith & Vertical Slices] assesses whether the code is organized by feature rather than by technical layer; this makes "module by project, feature by folder, use case by leaf" an assertion instead of a review habit (ADR-109).
  • Walkthrough - one member. RepoRoot (:12) is the single override. Everything else is inherited: the cap MaxDirectFiles => 12 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FolderWidthTestsBase.cs:23), an empty ExemptFolderSuffixes default that ADC does not override, so ADC carries no documented exemption (:29), and the single fact Folders_stay_narrow, which hands both to the rule (:31-:33). The counting rules are the interesting part: only files directly in the directory count, a .razor component and its co-located X.razor.cs code-behind count as one unit, .resx satellites never count, and generated files (*.g.cs, *.generated.cs, *.Designer.cs) never count because nobody chose to put them there (ArchitectureRules.FolderWidth.cs:13-:19). Directories with a path segment named bin, obj, Migrations, Platforms, Resources, node_modules, wwwroot or .git are skipped outright, being build output or tool-owned trees whose shape is not the author's decision (:22-:24). Violations are collected, sorted ordinally, and reported together through ArchitectureAssert.NoViolations with the remedy in the message, "split it by feature or aggregate" (:66-:70).
  • Why it's built this way - the class comment names the layouts the rule protects, Services/{Feature}/ beside Pages/{Feature}/, Controllers/{Aggregate}/, EntityConfiguration/{Aggregate}/, and states the failure mode it prevents: drifting back into flat technical buckets (FolderWidthTests.cs:3-:8). A cap enforced per repo rather than per project is deliberate, because the drift shows up wherever a folder is convenient, not only in the module the change belongs to.
  • Where it's used - the project is inside ADC's CI solution filter (MMCA.ADC/MMCA.ADC.CI.slnf:59), which the build-and-test job builds and tests on every PR (MMCA.ADC/.github/workflows/deploy.yml:205, :284, :300).
  • Caveats / not-in-source - the rule sees files, not namespaces, so a folder that stays under 12 while mixing unrelated features passes; the cap is a ceiling on width, not a check that the folder names one thing.

DecoratorPipelineOrderTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:29 · Level 9 · class (public, sealed)

  • What it is - the one type in this unit that builds a real DI container instead of reading metadata. It asserts that ADC's genuine registration sequence produces the ADR-014 decorator nesting at runtime, exercised against a real Identity command/query pair (MMCA.ADC.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:19-:28).
  • Depends on - DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult> from the MMCA.Common.Testing package (MMCA.ADC.Architecture.Tests.csproj:43), closed over ChangePreferencesCommand / Result and GetUserPreferencesQuery / Result<UserPreferencesResponse> (DecoratorPipelineOrderTests.cs:30). Externals: Microsoft.Extensions.DependencyInjection, Microsoft.FeatureManagement, NullLogger<>, and Moq (:1-:13).
  • Concept introduced, an object-graph assertion. Scrutor's TryDecorate applies decorators in reverse registration order, so the last decorator registered becomes the outermost wrapper. That makes an innocent-looking reorder of the AddApplicationDecorators() lines, or a module handler scan that runs after it instead of before, a silent change in runtime behavior: the code still compiles, the container still resolves, and the pipeline quietly runs validation after the transaction opens. The base turns that into a test failure by resolving the handler and walking the constructed graph via reflection over each decorator's private inner-handler field, so it verifies the objects that actually exist rather than the registration list (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:29-:32, :100-:125). [Rubric §2 - Design Patterns] assesses whether patterns are applied deliberately and correctly; the decorator chain is the framework's central pattern and this is the only test that proves its composition. [Rubric §14 - Testability]: the fact that a production registration sequence can be replayed in a bare ServiceCollection with seven mocked dependencies is itself the evidence that the composition root is not entangled with hosting.
  • Walkthrough - two members.
    • ConfigureServices(IServiceCollection) (DecoratorPipelineOrderTests.cs:32) implements the base's single abstract hook (DecoratorPipelineOrderTestsBase.cs:46) and reads in two halves. Test doubles for the decorator constructor dependencies (:33-:39): Mock.Of<IFeatureManager>(), Mock.Of<ICurrentUserService>(), Mock.Of<IPermissionRegistry>(), Mock.Of<ICorrelationContext>(), and Mock.Of<ICacheService>() as singletons, a scoped IUnitOfWork factory, and the open generic ILogger<> mapped to NullLogger<>. These exist only so the decorators can be constructed; the test never invokes a handler. The base names the same seven dependencies as the contract for a subclass (DecoratorPipelineOrderTestsBase.cs:22-:25). The real registration sequence (:41-:42) is now one composed call, services.AddMmcaApplicationPipeline(pipeline => pipeline.ScanModule<MMCA.ADC.Identity.Application.ClassReference>()), written this way rather than the three hand-sequenced calls (AddApplication(), the module scan, AddApplicationDecorators()) so this fitness function exercises the same helper the four service hosts compose with: a regression in the helper's own ordering surfaces here as a wrong nesting rather than only as a startup failure nobody runs. AddMmcaApplicationPipeline still runs AddApplication() first, then the module's handler scan, then AddApplicationDecorators() last (Scrutor's TryDecorate can only wrap handlers already registered), and seals the collection.
    • ComposedPipeline_LeavesNoHandlerUndecorated (:45-:56) is a [Fact] of its own, not inherited: it builds a fresh ServiceCollection, calls ConfigureServices on it, then asserts services.VerifyDecoratorPipeline(). The base's nesting facts (below) prove ONE command and ONE query are wrapped correctly; this proves no handler in the whole Identity module escaped the pipeline, the failure mode that ships silently because an undecorated handler runs with no feature gate, authorization, validation or transaction and nothing throws at startup to say so.
    • The two inherited facts then assert the chains. CommandPipeline_NestsDecorators_InAdr014Order (DecoratorPipelineOrderTestsBase.cs:72) expects FeatureGate, Authorization, Logging, Caching, Validating, Timeout, Transactional, then the concrete handler (:49-:58); QueryPipeline_NestsDecorators_InAdr014Order (:76) expects FeatureGate, Authorization, Logging, Caching, Validating, Timeout, then the handler (:61-:69). The query chain carries a ValidatingQueryDecorator too (:67), so the two lists differ only by the transactional stage. Both are asserted by AssertPipeline (:79-:98), which compares every element except the last against the expected list (:93-:94) and then requires the innermost element not to end in "Decorator" (:96-:97), so a truncated chain cannot pass. ADC overrides neither expected list, so it accepts the framework default order as its contract.
  • Why it's built this way - the pair was chosen for realism rather than convenience. ChangePreferencesCommand and its handler are shipped ADC Identity use cases, while GetUserPreferencesQuery is declared in MMCA.Common.Application and its concrete GetUserPreferencesHandler lives in ADC on top of Common's GetUserPreferencesHandlerBase<TUser>. So the scan-then-decorate ordering is exercised across the framework and app boundary rather than against a fixture, which is exactly what the base asks a subclass to supply (DecoratorPipelineOrderTestsBase.cs:26-:27).
  • Where it's used - not an independent class any more: ComposedPipeline_LeavesNoHandlerUndecorated's own base (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/DecoratorPipelineOrderTests.cs) exercises the same helper, and MiddlewarePipelineOrderTests is a sibling gate over the same registration sequence at the middleware layer rather than a consumer of this type.
  • Caveats / not-in-source - the nesting chain is still unwrapped by reading compiler-generated private fields (DecoratorPipelineOrderTestsBase.cs:105-:125), so a future decorator that stores its inner handler somewhere other than a field (a property-only or captured-closure design) would be invisible to that walk. VerifyDecoratorPipeline() proves every handler is wrapped by something, but does not itself replay the exact wrapper order for each one; that is still the base's job. The base flags the reflection strategy explicitly (:29-:32).

AdcArchitectureMap

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests/AdcArchitectureMap.cs:14 · Level 12 · class (internal, sealed)

  • What it is - the single declaration of what "the ADC architecture" is, in assembly terms: five MMCA.Common framework layers, the Identity, Conference, and Engagement modules at six layers each, and the thin Notification module at three, 26 entries in all (MMCA.ADC.Architecture.Tests/AdcArchitectureMap.cs:18-:55). Every map-driven rule in this unit scans exactly the assemblies listed here.
  • Depends on - ArchitectureMapBase (which implements IArchitectureMap), the Layer enum and the LayerRef record, and System.Reflection.Assembly (global-used at MMCA.ADC.Architecture.Tests/GlobalUsings.cs:1). Through its anchor types it also depends on Result, BaseEntity<TIdentifierType>, EntityQueryService<TEntity, TEntityDTO, TIdentifierType>, ApplicationDbContext, and ApiControllerBase on the framework side, and on User, Event, UserSessionBookmark, UserNotificationExportItemDTO, their sibling DTOs, and the four IdentityModule / ConferenceModule / EngagementModule / NotificationModule entry points on the app side.
  • Concept introduced, the architecture map as data. NetArchTest works on Assembly objects, so a fitness function needs some way to know which assembly plays which role. Rather than hard-coding assembly names inside each rule, MMCA reifies the answer in one object: a flat list of (module, layer, assembly) triples. ArchitectureMapBase derives everything else from that list: the module-name set, per-layer projections, namespace derivation, and the module-isolation target sets (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:28-:72). The consequence is that adding a module to ADC is a three-line change here, after which roughly three dozen rules start covering it. [Rubric §3 - Clean Architecture] assesses whether layer boundaries are real rather than aspirational, and this is the machine-readable statement of those boundaries. [Rubric §7 - Microservices Readiness]: the map's Module grouping is what lets the isolation and extraction rules ask "could this module leave the process", which for ADC is not hypothetical, since every module already runs as its own service host.
  • Walkthrough
    • RepoToken => "MMCA.ADC" (AdcArchitectureMap.cs:16) is more load-bearing than it looks. The base composes namespaces from it ({RepoToken}.{Module}.{Segment}, ArchitectureMapBase.cs:63-:66, :98-:99), and the doc/config rules resolve the repository root by walking up from the test binary until they find {RepoToken}.slnx (ArchitectureMapBase.cs:79-:91), which is how DataResidencyTests, FormsConventionTests, and RawQueryableConventionTests read committed files regardless of the runner's working directory.
    • Framework layers (AdcArchitectureMap.cs:21-:25) are declared with the Framework(layer, assembly) helper, which records an empty module name (ArchitectureMapBase.cs:94-:95), so they are excluded from the per-module projections. Each is pinned by an anchor type rather than a string: Result for Shared, BaseEntity<> for Domain, EntityQueryService<,,> for Application, ApplicationDbContext for Infrastructure, and ApiControllerBase for API. A rename or a package move breaks the compile instead of silently producing an empty scan.
    • Module layers (:28-:54) use the instance Module(name, layer, assembly) helper. Identity, Conference, and Engagement each declare six layers; Domain, Shared, and API are pinned by anchor type (Identity.Domain.Users.User, Conference.Shared.Events.EventDTO, Engagement.API.EngagementModule, and their siblings) while Application, Infrastructure, and UI are loaded by name through Assembly.Load (for example :29, :30, :33), because those assemblies have no convenient public anchor type (:5-:6). Assembly.Load succeeds here only because the csproj takes a ProjectReference on all 21 module projects (MMCA.ADC.Architecture.Tests.csproj:46-:71), which is what puts the DLLs beside the test binary.
    • Notification (:52-:54) declares three layers, not six: Application (loaded by name), Shared (pinned by Notification.Shared.UserNotifications.UserNotificationExportItemDTO), and Api (pinned by Notification.API.NotificationModule). The class comment states why (:7-:12): Notification is a deliberately thin module that owns no aggregate and no persistence, so it ships no Domain, Infrastructure, or UI assembly, and LayerDependencyTests records that shape as a per-module required-layer override rather than letting the whole repo drop the requirement.
    • Laziness is inherited: DefineLayers() is materialized once through a Lazy<IReadOnlyList<LayerRef>> built in the base constructor (ArchitectureMapBase.cs:13-:16), so the three dozen subclasses that each construct their own map instance still pay the Assembly.Load cost only on first use.
  • Why it's built this way - centralizing every namespace and assembly string in one file also fixes Ubuntu CI case sensitivity in one place, which the base states as an explicit goal (ArchitectureMapBase.cs:8-:9). Compare CommonArchitectureMap, the same abstraction for a repo with no business modules.
  • Where it's used - instantiated as a field initializer by every map-driven subclass in this unit (34 of the 43 types here, all of them at Level 13), for example ConcurrencyConventionTests.cs:5. It is internal, so it never leaves this assembly. The nine non-map types are the two embedded-resource guards, AnonymousEndpointTests, ProtoContractTests, SortableColumnConventionTests, TranslationCompletenessTests, the two pipeline-order tests, and the map itself.
  • Caveats / not-in-source - the four *.Contracts service projects under Source/Services/ are not registered, so no map-driven rule scans the gRPC adapters directly; ProtoContractTests covers that surface by file path instead. Nothing in this repository asserts that the map lists every module that exists, so a fifth module would have to be added here by a human.

MiddlewarePipelineOrderTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC.Architecture.Tests/Api/MiddlewarePipelineOrderTests.cs:16 · Level 12 · class (public, sealed)

  • What it is - the HTTP-edge counterpart of DecoratorPipelineOrderTests, and, like ObservabilityConventionTests, a bodyless declaration: public sealed class MiddlewarePipelineOrderTests : MiddlewarePipelineOrderTestsBase; (MMCA.ADC.Architecture.Tests/Api/MiddlewarePipelineOrderTests.cs:16). Deriving it is the whole assertion.
  • Depends on - MiddlewarePipelineOrderTestsBase from the MMCA.Common.Testing package (using MMCA.Common.Testing;, :1), and transitively on MiddlewarePipelineBuilder and MiddlewarePipelineStepNames.
  • Concept introduced, an empty subclass as a conformance claim. The class comment states what the emptiness means (:5-:14): every ADC REST and gRPC service host calls the zero-argument UseCommonMiddlewarePipeline(), so the framework's default step order is ADC's contract and the base needs no overrides. The two hooks that exist are the escape hatch a host would use if it customized the pipeline: Configure, which defaults to null (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:37), and ExpectedStepNames (:38-:58). Leaving both alone is a positive statement, not an omission. [Rubric §12 - Performance & Scalability] assesses whether cross-cutting behavior is applied uniformly rather than per host; [Rubric §11 - Security] is the sharp edge, because the ordering invariants below are authentication and rate-limiting invariants.
  • Walkthrough - no members. Two inherited facts run against a builder seeded from MiddlewarePipelineBuilder.CreateDefault() (MiddlewarePipelineOrderTestsBase.cs:81-:84).
    • EdgePipeline_OrdersSteps_InDocumentedOrder (:61) compares builder.StepNames against the 18-step default sequence, outermost first: exception handler, correlation id, request localization, pre-forwarded capture, forwarded headers, HTTPS redirection, response compression, routing, CORS, authentication, tenant resolution, rate limiting, soft-deleted-user filter, authorization, output cache, JWKS endpoint, OIDC discovery endpoint, controllers (:40-:57).
    • EdgePipeline_SatisfiesLoadBearingInvariants (:70) calls Build() and requires it not to throw, because Build() re-checks the load-bearing adjacencies at startup, so a pipeline that failed here would have thrown while the host was starting (:73-:76).
    • Four adjacencies are named as load-bearing in both the base and the ADC comment: the pre-forwarded capture immediately before the forwarded-headers rewrite (or jwks_uri stops being reachable), authentication immediately before tenant resolution (so the claim strategy sees HttpContext.User), authentication before the rate limiter per ADR-019 (so the per-user cap engages), and forwarded headers before the HTTPS redirect (MiddlewarePipelineOrderTests.cs:10-:13, MiddlewarePipelineOrderTestsBase.cs:68).
  • Why it's built this way - a reorder here fails at runtime in ways that look like configuration bugs: an unreachable jwks_uri, a tenant that never resolves, a per-user rate cap that never engages (MiddlewarePipelineOrderTestsBase.cs:16-:18). Making it a red test in the consumer repo is what turns a framework-side reorder into a build failure rather than a silent production behavior change (ADR-079). No WebApplication is built: the steps are pure data until they are applied, so this runs in the fast unit tier with no database and no host (:24-:27).
  • Caveats / not-in-source - the test asserts the framework default, seeded from CreateDefault(). It does not read any ADC Program.cs, so the claim that every ADC host calls the zero-argument overload is asserted by the comment, not by this test. A host that started passing a customization would silently fall outside this gate unless someone also overrode Configure here.

CommandValidatorCoverageTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/CommandValidatorCoverageTests.cs:18 · Level 13 · class (public, sealed)

  • What it is - the gate that every data-carrying command has something for the Validating decorator to run. It freezes the 26 entries that are exempt today, and fails on any new one (MMCA.ADC.Architecture.Tests/Cqrs/CommandValidatorCoverageTests.cs:23-:86).
  • Depends on - CommandValidatorCoverageTestsBase and AdcArchitectureMap.
  • Concept introduced, the allowlist grouped by reason rather than by name. The list is not alphabetical: it is split into three commented buckets, because the entries are not equally benign and a reviewer needs to see which kind an entry is (:25-:30). That is the difference between an inventory and a mute button. [Rubric §6 - CQRS & Event-Driven] assesses whether the pipeline stages actually do work; the Validating decorator is present for every command whether or not a validator resolves, so a missing validator is a stage that runs and asserts nothing. [Rubric §11 - Security] and [Rubric §24 - Forms/Validation/UX Safety]: the base's framing is that a command with no validator carries whatever the caller sent straight into the handler, and the gap is invisible until bad input reaches the domain (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CommandValidatorCoverageTestsBase.cs:4-:7).
  • Walkthrough - three members.
    • Map (:20).
    • AllowedUnvalidatedCommands (:23-:85) overrides the base's empty default (CommandValidatorCoverageTestsBase.cs:31) with 26 entries in three groups. (1) Identifier-only commands (:25-:61), twenty-two entries whose whole payload is a server-supplied or route-supplied id plus, in a few cases, a RowVersion or an enum the compiler already constrains: seventeen remove/publish/link/delete commands across Conference and Engagement, the live-poll Close, Delete and Open among them (:31-:47), ModerateQuestionCommand with its own note that every field is an id or type-constrained and the authorization decision is the handler's (:49-:51), SetLeaderboardParticipationRequest because "there is no invalid value of a bool" (:53-:54), two Identity deletes (:56-:57), and the framework's generic MMCA.Common.Application.UseCases.Crud.DeleteEntityCommand closed over ADC aggregates, which ADC does not author (:59-:61). (2) False positives of the scan (:63-:77): ForgotPasswordCommand and ResetPasswordCommand (:70-:71), plus the email-confirmation pair (ADR-116), SendEmailConfirmationCommand and ConfirmEmailCommand (:76-:77), all four carrying an ICommandWithRequest<T> whose IValidator<T> is real but lives in MMCA.Common.Application, and the rule reads only the repo's per-module Application assemblies, so it cannot see the framework half of the bridge. The comment ends with an instruction to a future maintainer: these are validated at runtime, do not "fix" them by writing a duplicate. (3) Frozen debt (:79-:85): empty, and the comment records what left it (the two question-answer updates, ChangePreferences, and SetUserAvatar each now carry a validator in their own slice).
    • MinimumCommands => 55 (:93) raises the base floor of 1 (CommandValidatorCoverageTestsBase.cs:38). The comment states the measurement it sits under: the scan found 62 handled, data-carrying commands at adoption, so the floor leaves room for normal churn while still failing loudly if a module drops out of the map (:88-:92).
    • Two facts are inherited: Commands_ShouldHave_ValidationCoverage (CommandValidatorCoverageTestsBase.cs:41) and the non-vacuity check CommandInventory_ShouldNotBe_Empty (:45).
  • Why it's built this way - the base explains why both halves of the request bridge are required for a command to count as covered: the framework registers the bridge validator for every ICommandWithRequest<TRequest> whether or not a request validator resolves, and one that resolves none adds no rules (CommandValidatorCoverageTestsBase.cs:9-:15). Counting the bridge without the request validator would have made the gate pass on exactly the commands it exists to catch.
  • Caveats / not-in-source - group (2) is a stated limitation of the scan rather than a property of ADC, so an ADC command that legitimately binds a framework request validator in future would land in the same bucket and would need the same hand-written note.

ClockReadTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/ClockReadTests.cs:8 · Level 13 · class (public, sealed)

  • What it is - the map-only ADC subclass of the framework's clock-read fitness function: the whole body is one line supplying AdcArchitectureMap (MMCA.ADC.Architecture.Tests/Domain/ClockReadTests.cs:8-:11).
  • Depends on - ClockReadTestsBase from MMCA.Common.Testing.Architecture and AdcArchitectureMap.
  • Concept introduced - the same thin-subclass shape as BrandColorTokenTests and AiDependencyIsolationTests: the rule body lives once in the shared base, and deriving a sealed subclass over ADC's map is what turns it on for this repo (ADR-015). The base's own concern (proved once in MMCA.Common and exercised here by name only) is that direct reads of DateTime.Now/DateTime.UtcNow/DateTimeOffset.Now inside domain and application code are untestable and non-deterministic; production code is expected to take an injected clock abstraction instead. [Rubric §14 - Testability] is the direct target.
  • Walkthrough - one member, Map (:10), implementing the base's abstract property over ADC's assemblies.
  • Why it's built this way - identical rationale to every other map-only subclass in this unit: the fixture proof lives once in MMCA.Common (see MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs and MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs), so each consumer repo need only wire its own map to gain the coverage.
  • Where it's used - runs with the rest of the suite in the build-and-test job; not consumed by any other type in this unit.

CascadeSoftDeleteConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:12 · Level 13 · class (public, sealed)

  • What it is - the guard that an aggregate root which owns auditable children deletes them in its own Delete() override, carrying exactly one reviewed exemption (MMCA.ADC.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:17-:26).
  • Depends on - CascadeSoftDeleteConventionTestsBase and AdcArchitectureMap.
  • Concept introduced, the consequence of soft delete not being a delete. MMCA never removes a row: an entity sets IsDeleted = true and the EF global query filter hides it. That means nothing cascades for free, because a soft delete is an ordinary UPDATE. Without an explicit cascade the root vanishes behind the filter while its child rows stay active, orphaned and unreachable through the root, yet still present for exports, reports, and erasure requests (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/CascadeSoftDeleteConventionTestsBase.cs:4-:9). The framework's DeleteChildren<TChild, TChildId>(...) helper is the one-line fix, and a hand-rolled loop over the children counts too. [Rubric §8 - Data Architecture] assesses whether the persistence model's invariants hold end to end; [Rubric §30 - Compliance/Privacy/Data Governance] is the sharper edge, since an orphaned child row is data a subject-access or erasure request has to account for (ADR-005).
  • Walkthrough - two members. Map (:14), and AllowedCascadeExemptTypes (:17-:26), which overrides the base's empty default (CascadeSoftDeleteConventionTestsBase.cs:27) with a single entry, MMCA.ADC.Conference.Domain.Speakers.Speaker. The inherited fact is AggregatesWithChildCollections_MustCascadeSoftDelete_InDelete (:30).
  • Why it's built this way - the exemption is justified in place rather than in a wiki (CascadeSoftDeleteConventionTests.cs:19-:24): Speaker's junction rows (SpeakerCategoryItems, SpeakerQuestionAnswers) deliberately outlive the soft-deleted root per BR-70/BR-71, because the Sessionize import reactivates them in place when the speaker returns (BR-135) and no cascade-restore counterpart exists, so cascading here would silently drop a returning speaker's categories and answers. The safety argument is the second half: junction reads follow the parent's visibility (BR-132), so the surviving children are not observable while the speaker is deleted. The base frames the whole exemption list this way, as the point of the rule rather than a concession: it turns "delete cascades, mostly" into a reviewed inventory of every aggregate that leaves children behind on purpose (CascadeSoftDeleteConventionTestsBase.cs:11-:15).
  • Caveats / not-in-source - the entry is a type full name, so the exemption is matched by string; the reactivation behavior it depends on (BR-135) is asserted by the Sessionize import tests, not here.

AiDependencyIsolationTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Layering · MMCA.ADC.Architecture.Tests/Layering/AiDependencyIsolationTests.cs:14 · Level 13 · class (public, sealed)

  • What it is - the model-boundary fitness function for ADC: a language-model dependency must stay confined to Infrastructure and to the governed MMCA.Common.AI package, in both directions. Map-only shape (ConcurrencyConventionTests): the whole body is one line supplying the map (MMCA.ADC.Architecture.Tests/Layering/AiDependencyIsolationTests.cs:16).
  • Depends on - AiDependencyIsolationTestsBase and AdcArchitectureMap.
  • Walkthrough - one member, Map (:16), implementing the base's abstract property (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/AiDependencyIsolationTestsBase.cs:21). Two inherited facts: LanguageModelSdks_ShouldStayAt_TheModelBoundary (:24) fails on a module naming the vendor SDK directly (Anthropic, OpenAI, Azure.AI, or Microsoft.Extensions.AI itself) outside Infrastructure, and GovernedAiPackage_ShouldStayBehind_Infrastructure (:28) fails on a module taking the framework's governed MMCA.Common.AI package as a convenience and passing a PromptContract around as a domain type.
  • Why it's built this way - the base's summary states the reason both directions are checked: either leak turns "the app calls a model at one place" into "the model is spread through the app", and a dependency that is everywhere cannot be versioned, evaluated, capped or swapped (AiDependencyIsolationTestsBase.cs:7-:12). [Rubric §16 - AI-Native Development] is the direct target.

ConcurrencyConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the guard that no *UpdateRequest carries a concurrency token in its body. It is also the plainest example of the Level 13 shape in this unit: a sealed class whose entire body is one line supplying the map (MMCA.ADC.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:5).
  • Depends on - ConcurrencyConventionTestsBase and AdcArchitectureMap.
  • Concept introduced, the map-only subclass. Eighteen of the 34 map-driven types in this unit are exactly this: protected override IArchitectureMap Map { get; } = new AdcArchitectureMap(); and nothing else. Note the property is an auto-property with an initializer, not an expression body, so each class constructs its map once per test-class instance rather than per fact. Everything else (the rule bodies, the [Fact] attributes, the failure messages) is inherited, which is precisely the point: MMCA.Common, MMCA.Store, and MMCA.ADC run byte-identical rule logic and differ only in what they point it at. The sections below for the other map-only subclasses do not repeat this explanation; they name the base and list what it asserts. [Rubric §15 - Best Practices & Code Quality] assesses duplication and change cost: a new rule ships to all three repos by adding a base and one derived line per repo.
  • Walkthrough - one member, Map (:5), implementing the base's abstract property (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ConcurrencyConventionTestsBase.cs:11). The inherited fact is UpdateRequests_ShouldNotImplement_IConcurrencyAware (:14), which delegates to ArchitectureRules.UpdateRequestsAreNotConcurrencyAware(Map). [Rubric §8 - Data Architecture] and [Rubric §9 - API & Contract Design]: the rule enforces a single source for the token. The version a conditional update is checked against is read from the If-Match request header, so a token in the request body would give the same check a second, competing source (ConcurrencyConventionTestsBase.cs:4-:7). That is the inverse of what the same-named rule asserted before the header became the only path, and it is why IConcurrencyAware must now be absent from request bodies rather than present. The base notes that modules with no mutable aggregate are legitimately vacuous here (:7).
  • Where it's used - runs with the whole suite in the build-and-test job (MMCA.ADC/.github/workflows/deploy.yml:189, :284). The same is true of every remaining type in this unit and is not repeated below.

ConstructorDependencyCountTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/ConstructorDependencyCountTests.cs:20 · Level 13 · class (public, sealed)

  • What it is - a single-responsibility ceiling applied to three separate populations: no *Service in a mapped module Application assembly may exceed eight constructor dependencies, no API controller may exceed ten, and no CQRS handler may exceed seven (MMCA.ADC.Architecture.Tests/Cqrs/ConstructorDependencyCountTests.cs:22-:31, :59, :68).
  • Depends on - ConstructorDependencyCountTestsBase and AdcArchitectureMap.
  • Concept introduced, the ratchet set to the real high-water mark, and its history kept in the file. A ceiling is only a ceiling if it sits at the current maximum, so the class comment keeps a ledger of every move, on the stated principle that "a ceiling only means something if every move of it was deliberate". It was 8 while the comment claimed AuthenticationService had 8 dependencies; it had 7, so the gate carried a phantom slot of headroom and was tightened to the real mark on 2026-07-28. It moved to 9 when refresh tokens became multi-device sessions: the framework's AuthenticationServiceBase constructor took on the session store and its options, and ADC's subclass passed both through on top of the IExternalLoginEmailVerifier its OAuth auto-link gate needs. It moved again, 9 to 10, on 2026-09-23: the AuthenticationServiceSettings holder that had bundled IOptions<RefreshSessionSettings> and IOptions<EmailConfirmationSettings> into one slot was deleted, and both options now reach the framework base directly (ConstructorDependencyCountTests.cs:24-:27). It then moved down, 10 to 8, on 2026-10-06: Common 1.218.1 folded the session store into IAuthSessionIssuer, so AuthenticationService takes 8, and the base's ApplicationServices_ConstructorDependencyCeilingIsTight now fails whenever the ceiling is left above the observed mark (:28-:31). The comment then makes the judgement explicit each time: the additions are framework-imposed collaborators of the same facade rather than a second responsibility, so the honest response is a raised ceiling and not an artificial bundle. [Rubric section 1 - SOLID] assesses single responsibility among other things; constructor arity is the cheapest mechanical proxy for a class that has accumulated too many jobs, and the comment is careful to distinguish a cohesive facade from a bundle.
  • Walkthrough - four members. Map (:22), and MaxConstructorDependencies => 8 (:31), which implements the base's abstract service ceiling (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:43) and backs ApplicationServices_DoNotExceedConstructorDependencyCeiling (:52), which scans every non-abstract class whose name ends in "Service" in the map's module Application assemblies (:54-:58), asserts the set is non-empty so the guard cannot pass vacuously (:60-:61), and reports each offender with its own arity (:63-:80). The subclass comment names AuthenticationService, now at the mark of 8, and CreateSessionHandler next at 7 (ConstructorDependencyCountTests.cs:6-:8). The other two members arrived with the framework's 2026-09 wave, which added a controller ceiling and a handler ceiling to the base alongside the existing service one. MaxControllerConstructorDependencies => 10 (:59) implements the base's Controllers_DoNotExceedConstructorDependencyCeiling (ConstructorDependencyCountTestsBase.cs:119), which scans every concrete ControllerBase-derived class across the map's API assemblies. Its own doc comment keeps the same kind of ledger: the fact first reported 14 (SpeakersController), 12 (EventsController), 11 (SessionsController) and 10 (LivePollsController), each serving several sub-resources from one class; each was split by sub-resource into sibling controllers on the same route prefix, action templates and authorization attributes, so no URL, verb or authorization posture moved (SpeakerLinksController/SpeakerSessionsController 14 to 9, EventLifecycleController 12 to 9, SessionCalendarController 11 to 10, LivePollVotingController 10 to 7). SessionsController stops at 10 rather than 9 deliberately: its remaining dependency is IEntityQueryService<Event, ...>, held only for the BR-86 advisory X-Warning header on create, which the update path gets from UpdateSessionResult.HasDateRangeWarning but the create path cannot, because the framework base fixes its create handler at ICommandHandler<SessionCreateRequest, Result<SessionDTO>> and the Application layer has no way to set a response header; the honest mark is therefore 10, to be lowered the moment a framework change makes 9 true (ConstructorDependencyCountTests.cs:36-:58). MaxHandlerConstructorDependencies => 7 (:68) implements Handlers_DoNotExceedConstructorDependencyCeiling (ConstructorDependencyCountTestsBase.cs:143), scanning every concrete ICommandHandler<,> / IQueryHandler<,> implementation in the map's module Application assemblies, set at the mark the fact first reported: ResetPasswordHandler and SubmitQuestionHandler jointly at 7, with CreateSessionHandler, RefreshFromSessionizeHandler, UploadSessionAssetHandler, CreateBookmarkHandler and RecordRoomCheckInHandler next at 6 (:62-:67).
  • Why it's built this way - each ceiling is meant to be raised consciously rather than drifted past, which is what the comments ask for and what their own history demonstrates. The controller split is the sharpest example: four controllers that had accreted several sub-resources were split along existing route and authorization boundaries rather than pinned at their old, inflated marks.
  • Caveats / not-in-source - the sibling rule HandlerConventionTests raises the base's separate arity check to 10 for a different population (HandlerConventionTests.cs:18), and its <remarks> states the coupling directly: the two guards must move together, or the stricter one turns the other into a comment (:11-:12). Note the service scan is name-based (*Service), so a handler or a builder with the same arity is judged only by its own ceiling, not this one.

ContractImplementationTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Contracts · MMCA.ADC.Architecture.Tests/Contracts/ContractImplementationTests.cs:20 · Level 13 · class (public, sealed)

  • What it is - the encapsulation half of the [ServiceContract] boundary: the concrete class serving a published contract interface must not be public. Map-only shape (ConcurrencyConventionTests): the body is one line (MMCA.ADC.Architecture.Tests/Contracts/ContractImplementationTests.cs:22).
  • Depends on - ContractImplementationTestsBase and AdcArchitectureMap.
  • Concept introduced, guarding a boundary from both sides. This rule is the twin of ServiceContractPurityTests, and the base says so in as many words: purity keeps the producer's internals out of the contract, this keeps the contract's implementation out of the consumer's reach (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ContractImplementationTestsBase.cs:11-:13). A public implementation lets a consumer reference, construct, or subclass the type, and each of those references is a coupling the extraction has to sever later, because an interface can be answered over a wire and a class cannot (:5-:8). [Rubric §7 - Microservices Readiness] assesses whether extraction is reversible in practice; [Rubric §1 - SOLID]: this is dependency inversion enforced at the assembly boundary rather than only at the call site (ADR-007).
  • Walkthrough - one member, Map (:21), implementing the base's abstract property (ContractImplementationTestsBase.cs:22). One inherited fact, ServiceContractImplementations_ShouldNotBe_Public (:33), and the AllowedPublicImplementations hook (:30) is left at its empty default. The subclass documentation is where the real teaching is (ContractImplementationTests.cs:3-:18): ADC's six contracts (IAttendeeQueryService, IBookmarkCountService, IEventLiveValidationService, ISessionBookmarkValidationService, IUserEngagementExportService, IUserNotificationExportService) each have three implementations, the in-process one in *.Application, the Disabled* stub in *.Shared, and the gRPC adapter in Services/*.Contracts, and all of them are internal sealed. The <remarks> explains why the allowlist stays empty: nothing in ADC ships a contract implementation a consumer is meant to construct, and the assemblies that register or exercise these classes reach them through InternalsVisibleTo on the producing projects rather than through a public type (:14-:18).
  • Caveats / not-in-source - the rule is scoped to the assemblies AdcArchitectureMap registers, and the Services/*.Contracts projects are not among them, so the gRPC adapters' internal visibility is asserted by the class comment rather than by this test.

ControllerConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC.Architecture.Tests/Api/ControllerConventionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the API-layer convention guard, with a two-entry exemption list for the controllers that legitimately do not route through the framework's base controller (MMCA.ADC.Architecture.Tests/Api/ControllerConventionTests.cs:11-:15).
  • Depends on - ControllerConventionTestsBase, AdcArchitectureMap, and, by name only (they are strings, not type references), OAuthController and ServiceInfoController.
  • Concept introduced, the documented exemption. A rule with no escape hatch gets deleted the first time reality disagrees with it; a rule with an undocumented escape hatch rots. The middle path here is an allowlist of fully qualified names, each justified in the comment above it (:7-:10): the OAuth controller drives a redirect, challenge, and cookie flow with an out-of-band token exchange, and the service-info controller is an anonymous version-discovery diagnostic, so neither returns a domain Result and neither has anything to gain from the Result-to-HTTP mapping in ApiControllerBase. [Rubric §9 - API & Contract Design] assesses consistency of the HTTP surface; the rule keeps the default consistent while naming the two deliberate outliers.
  • Walkthrough - two members. Map (:5) and ControllersExemptFromApiControllerBase (:11-:15), which overrides the base's empty default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/ControllerConventionTestsBase.cs:12). Four facts are inherited: controllers do not depend on Infrastructure (:15), do not depend on EF Core (:18), are sealed (:21), and inherit ApiControllerBase except for the exempt names (:24).
  • Caveats / not-in-source - the exemptions are matched as strings, so renaming or moving either controller silently drops it from the list and turns the rule red rather than passing wrongly, which is the safe direction.

DataResidencyTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/DataResidencyTests.cs:12 · Level 13 · class (public, sealed)

  • What it is - a compliance drift guard: the data-residency statement published in ADC's PRIVACY.md must match the Azure region where personal data is actually provisioned, parsed out of the deployment workflow (MMCA.ADC.Architecture.Tests/Governance/DataResidencyTests.cs:3-:11).
  • Depends on - DataResidencyTestsBase, AdcArchitectureMap, System.IO.File/Path, and AwesomeAssertions (used directly inside the override, :26).
  • Concept introduced, a test as the join between a document and an infrastructure fact. Most of the rules in this unit compare code to code. This one compares prose to infrastructure: it reads the deployed region out of the source of truth (SQL_LOCATION="${SQL_LOCATION_OVERRIDE:-westus2}", MMCA.ADC/.github/workflows/deploy.yml:1158) and then requires PRIVACY.md to say the same thing, comparing whitespace-insensitively and case-insensitively so "West US 2" matches the westus2 region token. [Rubric §30 - Compliance/Privacy/Data Governance] assesses whether privacy claims are true and stay true; a policy that names a region the data never lived in is a compliance defect that no code review would catch, and this is the mechanism that closes it.
  • Walkthrough - three members.
    • Map (:14) exists only so the base can resolve the repo root through ArchitectureMapBase.FindRepoRoot($"{Map.RepoToken}.slnx"); no assembly scanning happens in this rule.
    • ForbiddenResidencyClaims => ["central United States"] (:16) overrides the base's empty default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DataResidencyTestsBase.cs:24) and blocks a specific stale statement from returning, one that once contradicted the deployed region (DataResidencyTests.cs:9-:10).
    • ExtractDeployedRegion(string repoRoot) (:20-:31) implements the base's abstract hook (DataResidencyTestsBase.cs:87). It reads .github/workflows/deploy.yml (:22), locates the literal marker SQL_LOCATION_OVERRIDE:- with an ordinal IndexOf (:24-:25), asserts the marker exists with a because explaining what the workflow must declare (:26-:27), then takes the alphanumeric run that follows as the region (:29-:30). Assert-then-parse rather than return-empty is what the base asks implementations to do.
    • The inherited fact PrivacyPolicy_DataStorageRegion_MatchesDeployedRegion (DataResidencyTestsBase.cs:27) then asserts the normalized policy contains the normalized region and contains none of the forbidden claims.
  • Why it's built this way - the account data and session bookmarks live in the Azure SQL database, and the QiMata Sponsorship subscription forces that SQL server into a different region from the Container Apps (DataResidencyTests.cs:5-:9), so "where the app runs" is genuinely not "where the personal data sits". Parsing the SQL region default rather than the app region encodes that distinction.
  • Caveats / not-in-source - the parse is positional: it takes the first occurrence of the marker in deploy.yml. The workflow contains both a job-level SQL_LOCATION_OVERRIDE env binding (deploy.yml:1111) and the shell default (:1158), so the rule depends on the marker string SQL_LOCATION_OVERRIDE:- appearing only in the latter form.

DomainEventHandlerSaveTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/DomainEventHandlerSaveTests.cs:11 · Level 13 · class (public, sealed)

  • What it is - the rule that a domain-event handler mutates state and lets the owning unit of work flush it, rather than persisting on its own. It carries two allowlisted entries (MMCA.ADC.Architecture.Tests/Cqrs/DomainEventHandlerSaveTests.cs:16-:28).
  • Depends on - DomainEventHandlerSaveTestsBase and AdcArchitectureMap.
  • Concept introduced, the transitive call-graph rule. Every other assembly-scanning rule in this unit judges a type by its own declarations. This one walks the call graph out of every handler method, so the common real shape (handler to a domain service to SaveChangesAsync) is caught rather than only a save typed directly into the handler (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/DomainEventHandlerSaveTestsBase.cs:10-:14). The walk depth is bounded by MaxCallDepth, defaulted to 6, which the base describes as covering the realistic handler-to-service-to-helper-to-repository chain while keeping the scan fast (:35-:40); ADC does not override it. [Rubric §6 - CQRS & Event-Driven] assesses the discipline around dispatch; [Rubric §8 - Data Architecture]: dispatch runs after SaveChangesAsync, and after commit inside an ITransactional command, so a handler that saves opens a second write in the middle of the first one, re-entering the change tracker, potentially raising a fresh event cascade, and persisting work the outer transaction may still roll back (:4-:9).
  • Walkthrough - two members. Map (:13), and AllowedSavingTypes (:16-:28), which overrides the base's default (DomainEventHandlerSaveTestsBase.cs:33) with two entries. The first, "MMCA.Common" (:20), is the base's own default carried forward: the framework's outbox event bus persists by design, and a handler publishing an integration event is not the defect the rule hunts (:18-:19). The second is PointsAwarder (:27), labelled an accepted deliberate trade-off: the gamification points ledger is written by the awarder that the point-scoring domain-event handlers call, so awarding points is a second write that follows the first, and the comment states plainly that the flow is intentional and is not to be refactored onto the outbox, because a point award that lost its triggering write would be worse than one that trails it (:22-:26). The inherited fact is DomainEventHandlers_ShouldNotReach_SaveChanges (DomainEventHandlerSaveTestsBase.cs:43).
  • Why it's built this way - an allowlist entry both silences the type and stops the walk from descending into it (DomainEventHandlerSaveTestsBase.cs:16-:19), which is why the class comment insists the two accepted ADC cascades are named explicitly rather than silenced module-wide (DomainEventHandlerSaveTests.cs:8-:9): a namespace-wide entry would have hidden whatever lies beyond it too.
  • Caveats / not-in-source - the depth bound is 6, so a save reached through a deeper chain is outside the walk. The base names that as a tuning knob rather than a guarantee.

DomainPurityTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Layering · MMCA.ADC.Architecture.Tests/Layering/DomainPurityTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the Clean Architecture purity guard, plus one repo-specific addition: RabbitMQ is added to the forbidden-dependency list for Domain and Shared (MMCA.ADC.Architecture.Tests/Layering/DomainPurityTests.cs:9).
  • Depends on - DomainPurityTestsBase and AdcArchitectureMap.
  • Concept introduced - the map-only shape from ConcurrencyConventionTests, extended by a one-line hook. The extra entry is not decoration: ADC runs on a broker (RabbitMQ locally, Azure Service Bus in production, :7-:8), and a broker client reference inside Domain would tie the model to a transport. [Rubric §3 - Clean Architecture] assesses inward-only dependencies; [Rubric §7 - Microservices Readiness]: keeping the transport out of the core is what makes IMessageBus substitutable between in-process and broker delivery.
  • Walkthrough - two members: Map (:5) and ExtraForbiddenDomainDependencies => ["RabbitMQ"] (:9), overriding the base's empty default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/DomainPurityTestsBase.cs:12). Four facts are inherited: Domain is framework-free (:15), Shared is framework-free (:18), Application does not depend on EF Core (:21), and Application does not depend on ASP.NET Core (:24). The extra token is passed to the first two only.

DomainThrowTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/DomainThrowTests.cs:9 · Level 13 · class (public, sealed)

  • What it is - the Result-pattern purity guard: no throw in ADC's Domain assemblies except an argument guard. The allowlist is present and empty (MMCA.ADC.Architecture.Tests/Domain/DomainThrowTests.cs:14-:17).
  • Depends on - DomainThrowTestsBase and AdcArchitectureMap.
  • Concept introduced, the cost of a thrown business failure. The framework's whole error model is that a business outcome comes back as a Result (ADR-013), and the base names the three concrete consequences of throwing one instead: it skips Result.Combine invariant composition, it arrives at the API as a 500 rather than an outcome the caller can act on, and it pays an exception unwind on a path that is not exceptional (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/DomainThrowTestsBase.cs:4-:9). Argument guards stay allowed because they report a caller bug rather than a business outcome, and the modern guard style always passes anyway: a bare throw; rethrow is ignored, and the ArgumentNullException.ThrowIfNull family emits no throw in the caller at all (:11-:12). [Rubric §15 - Best Practices & Code Quality] assesses whether a stated convention actually holds; [Rubric §9 - API & Contract Design]: which failures a caller can handle is part of the contract.
  • Walkthrough - two members. Map (:11), and AllowedThrowingTypes (:14-:17), which overrides the base's empty default (DomainThrowTestsBase.cs:31) with a collection expression containing only a comment, TRIAGE PLACEHOLDER: filled from the first run (DomainThrowTests.cs:16). Since the list is empty and the rule is in the CI suite, that comment is now a historical note rather than an open item: ADC's Domain assemblies throw nothing beyond the argument guards, or the gate would be red. The inherited fact is Domain_ShouldNotThrow_ExceptArgumentGuards (DomainThrowTestsBase.cs:34).
  • Why it's built this way - the base spells out the adoption path the placeholder anticipated (:14-:19): subclass, run once, then either convert each reported site to a Result.Failure or move it into the list with a comment. ADC took the first branch everywhere, which is why the list stayed empty.
  • Caveats / not-in-source - the exception filter is by type (ArgumentException, ArgumentNullException, ArgumentOutOfRangeException), so a custom guard exception outside that family would be reported.

EntityConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/EntityConventionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the DDD entity-shape guard for ADC's three module domains: aggregate roots exist, each has a Result-returning static factory and no public constructor, domain entities are sealed and live in the Domain layer, entity properties carry no public setters, and DTOs or requests do not live in Domain or Infrastructure.
  • Depends on - EntityConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). Eight inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/EntityConventionTestsBase.cs:15, :18, :21, :24, :27, :30, :33, :36) cover aggregate-root exposure, the Result-returning Create factory, constructor visibility, factory return types, sealing, layer placement, non-public property setters, and the DTO/request exclusion. [Rubric §4 - DDD] assesses whether the tactical patterns are actually applied; this is the rule that keeps the factory-plus-Result construction contract from being optional in a new module, and the setter check is what keeps an entity's state from being reachable without going through a behavior method.

ErrorCatalogTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Contracts · MMCA.ADC.Architecture.Tests/Contracts/ErrorCatalogTests.cs:20 · Level 13 · class (public, sealed)

  • What it is - the guard over ADC's error-code vocabulary: one literal code means one thing, and a code carries the prefix of the vocabulary that owns it. It is the second-largest subclass in the unit, at 108 lines, and almost all of it is justification (MMCA.ADC.Architecture.Tests/Contracts/ErrorCatalogTests.cs:28-:59, :65-:81).
  • Depends on - ErrorCatalogTestsBase and AdcArchitectureMap.
  • Concept introduced, treating error codes as a public contract. The class comment states the constraint that shapes every decision in this file (:12-:18): error codes reach clients in problem-details payloads and get quoted in support tickets, so nothing already shipped is renamed to satisfy the rule. Today's catalog is locked exactly as it stands and only new drift is gated. [Rubric §9 - API & Contract Design] assesses whether the error surface is governed like the rest of the API; [Rubric §15 - Best Practices & Code Quality]: a vocabulary in which one code means two things is a vocabulary a client cannot switch on.
  • Walkthrough - three members plus three inherited facts.
    • Map (:22).
    • AllowedCodePrefixes (:28-:59) overrides the base's default of the map's module names (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ErrorCatalogTestsBase.cs:29) because ADC namespaces its codes by aggregate (Session.Room.DoubleBooked) rather than by module. It spreads Map.ModuleNames (:30) and then adds 22 literal aggregate prefixes in three commented groups: ten Conference aggregates and children (:33-:42, SessionAsset newly added), nine Engagement ones (:45-:53), and three Identity ones (:56-:58). The comment frames the list as an inventory: a code under a new prefix fails until the prefix is reviewed and added, which is the drift the gate exists to catch (:8-:10).
    • AllowedSharedCodes (:65-:81) spreads the base's three generic statics (ErrorCatalogTestsBase.cs:35-:36) and freezes seven ADC codes that two types both construct today (:74-:80). Each carries an inline note naming the pair, for example CheckIns.EventNotPublished reached from CheckInProcessor, RecordRoomCheckIn and RecordSponsorVisit, and SessionQuestion.InvalidTransition reached from ModerateQuestionHandler and the SessionQuestion aggregate. The reasoning is stated once above them (:69-:73): each pair is the same failure reported at two entry points, so the sharing is the correct behavior, a client switching on the code wants one answer rather than three near-identical ones, and renaming them would break that client. The rule is therefore told the sharing is deliberate rather than told to look away.
    • MinimumErrorCodes => 57 (:107) raises the base floor of 1 (ErrorCatalogTestsBase.cs:43), and its documentation is the most instructive passage in the file (ErrorCatalogTests.cs:83-:106). The catalog held 73 distinct non-generic codes at adoption; the floor has since been lowered twice, and both times for the same non-alarming cause: a code stopped being a literal the IL scan can see, because it moved from an Error factory call in an ADC module into an argument of a framework member that builds the Error inside MMCA.Common. The comment records each move (70 to 66 when five X.NotDeleted codes moved into RestoreChild(notDeletedErrorCode: ...) arguments; 66 to 57 when eight more moved into ICurrentUserService.RequireUserId(code, ...) and CommonInvariants members), notes that the codes themselves ship unchanged, and states that the scanned count after the change is 60, measured from a run rather than modelled.
    • The three inherited facts are ErrorCodes_ShouldBe_Unique (ErrorCatalogTestsBase.cs:46), ErrorCodes_ShouldCarry_TheOwningModulePrefix (:50), and the non-vacuity check ErrorCodeCatalog_ShouldNotBe_Empty (:54). Prefix matching is prefix + ".", ordinal (:62-:67).
  • Why it's built this way - codes are read out of IL at the Error factory call sites, so a code that is not a literal cannot be judged statically; the base lists such codes as UNVERIFIABLE in the failure message rather than passing or failing them (ErrorCatalogTestsBase.cs:9-:12). That design is exactly what makes the floor drift downward as code construction moves into framework arguments, and why the subclass documents each drop instead of quietly editing the number.
  • Caveats / not-in-source - a scanner follow-up that would also read framework-member code arguments is described in the comment as tracked in the Wave 6 plan (ErrorCatalogTests.cs:90-:90); nothing in this repository implements it today. Also note the floor of 57 now sits below a measured 60, so it is a margin rather than an exact count.

EventConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Contracts · MMCA.ADC.Architecture.Tests/Contracts/EventConventionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the integration-event shape guard: every integration event declares a schema version, inherits the framework's base integration event, and lives in an *.IntegrationEvents namespace under Shared; and every event upcaster is unique and moves the version forward.
  • Depends on - EventConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). Five inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/EventConventionTestsBase.cs:15, :17, :20, :23, :26). The first three enforce SchemaVersion, base-type inheritance, and namespace placement (ADR-010). The last two guard the upcaster machinery that ADR-010 depends on: EventUpcasters_ShouldHave_UniqueSourceTypes (:23), so two upcasters cannot claim the same source shape, and EventUpcasters_ShouldIncrease_SchemaVersion (:26), so an upcaster cannot map an event onto the same or an earlier version. [Rubric §6 - CQRS & Event-Driven] assesses the discipline around asynchronous contracts; this rule handles the shape, while IntegrationEventContractTests freezes the content.

FeatureFlagLifecycleTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:9 · Level 13 · class (public, sealed)

  • What it is - the feature-flag lifecycle guard (ADR-031): every flag constant declares whether it is a permanent capability switch or a temporary rollout toggle, and a temporary one fails the build once its RemoveBy date has passed. Map-only shape (ConcurrencyConventionTests): the body is one line (MMCA.ADC.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:11).
  • Depends on - FeatureFlagLifecycleTestsBase and AdcArchitectureMap.
  • Concept introduced, the dead-toggle detector. A flag that gates a finished rollout stops being a choice and becomes a branch nobody takes, and nothing else in a codebase notices; the base's summary names this the open trade-off the two rules close (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FeatureFlagLifecycleTestsBase.cs:4-:8). [Rubric §31 - Observability] and [Rubric §15 - Best Practices & Code Quality]: an undeclared flag or one still gating code long after its rollout finished is exactly the kind of silent drift a fitness function, not a code review, is positioned to catch on every build.
  • Walkthrough - one member, Map (:11). Two inherited facts: FeatureFlags_ShouldDeclare_Lifetime (FeatureFlagLifecycleTestsBase.cs:21), delegating to ArchitectureRules.FeatureFlagsDeclareLifetime(Map), and TemporaryFeatureFlags_ShouldNotBe_PastRemoveBy (:24-:25), delegating to ArchitectureRules.TemporaryFeatureFlagsAreNotPastRemoveBy(Map, Today). Today is the base's virtual DateOnly Today => DateOnly.FromDateTime(DateTime.UtcNow) (:18), which ADC does not override, so the expiry check judges against the build's own clock rather than a frozen date.
  • Why it's built this way - the [FeatureFlag(FeatureFlagLifetime.Permanent|Temporary, RemoveBy, Owner)] attribute and the FeatureFlagRegistry runtime inventory it feeds are the mechanism (MMCA.Common/CLAUDE.md, ADR-031's 2026-09-11 revision); this class is ADC's adoption of the pair of fitness rules built to enforce that attribute against ADC's own flag constants, mirroring the identical subclass shape MMCA.Common carries for its own flags.
  • Caveats / not-in-source - adoption is per repo (ADR-031): the rule only ever sees flags declared in assemblies the map scans, so a flag constant outside those assemblies is invisible to it.

FormsConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/FormsConventionTests.cs:13 · Level 13 · class (public, sealed)

  • What it is - the UX-safety guard over ADC's admin forms. It is now a Map-plus-floor subclass of the shared rule: the whole body is two overrides, and the requiredness and Profile facts that ADC once hand-wrote here live in the base (MMCA.ADC.Architecture.Tests/Ui/FormsConventionTests.cs:13-:18). The <summary> still names what the nine Conference create forms must keep (:3-:12).
  • Depends on - FormsConventionTestsBase and AdcArchitectureMap.
  • Concept introduced, a default that became the convention. ADC used to replace the base's marker list and carry two facts of its own, because the shared rule could not yet reach the model-bridge markers, the shared *FormFields.razor component, or the Profile page. The base now defaults to exactly that set, so the subclass shrank to configuration: the summary says the base's default markers are the model-bridge set ADC uses and that the Identity Profile page is checked at the base's conventional path (FormsConventionTests.cs:10-:11). Rubric tags: [Rubric §24 - Forms/Validation/UX Safety] (users are protected from losing work and from unclear validation) and [Rubric §18 - UI Architecture] (the field block is a shared component the scan follows).
  • Walkthrough
    • Map (:15) returns a new AdcArchitectureMap, and MinimumCreateForms => 9 (:17) lifts the base default of 1 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:53) to ADC's nine Conference create forms (Activity, ConferenceCategory, Event, Partner, Question, Room, Session, Speaker, Sponsor; FormsConventionTests.cs:5-:6).
    • Three inherited facts run: AdminCreateForms_KeepUnsavedChangesGuardAndValidation (FormsConventionTestsBase.cs:148) checks the ten default RequiredMarkers (:63-:75): UnsavedChangesGuard, IsDirtyAccessor, _isDirty, <MudForm, Model="_model", Validation="@_validate", <ErrorSummary, Result="_saveResult", Messages="_form?.Errors" and Validation.CorrectFollowing. AdminCreateForms_ReadRequirednessOffTheirModel (:156) looks for ModelValidation.IsRequired across the create page plus its sibling *FormFields.razor. ProfileForm_KeepsErrorSummaryAndPasswordValidation (:188) checks the Profile page.
    • The Profile page path is the base's virtual ProfileFormPath (:82-:92), built as Source/Modules/Identity/{RepoToken}.Identity.UI/Pages/Users/Profile/Profile.razor; ADC does not override it.
  • Why it's built this way - the Profile form is a single-section password and delete form with no navigate-away step, so it carries no unsaved-changes guard and does not match the *Create.razor glob; the base checks the markers that do apply instead (FormsConventionTestsBase.cs:86-:80). Hoisting the facts into the base means Store and any later consumer get the same coverage by subclassing.
  • Caveats / not-in-source - ProfileFormPath is conventional, so moving ADC's Profile.razor fails the test until the property is overridden (the safe direction); the ADC AGENTS.md notes the same.

ForwardedJwtAudienceTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Api · MMCA.ADC.Architecture.Tests/Api/ForwardedJwtAudienceTests.cs:10 · Level 13 · class (public, sealed)

  • What it is - the fail-closed audience gate (ADR-004) for ADC's extracted service hosts: every host that calls AddForwardedJwtBearer must pass its audience as JwtAudience.RequireConfigured(configuration[JwtAudience.ConfigKey]) with no ?? fallback, so a host deployed without Jwt:Audience fails at startup rather than validating against a hard-coded default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/ForwardedJwtAudienceTestsBase.cs:3-:10). The subclass is two members (MMCA.ADC.Architecture.Tests/Api/ForwardedJwtAudienceTests.cs:10-:15).
  • Depends on - ForwardedJwtAudienceTestsBase, AdcArchitectureMap and ArchitectureRules.ForwardedJwtBearerAudienceIsFailClosed (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Api/ArchitectureRules.ForwardedJwt.cs:25).
  • Concept introduced, a security default enforced by scanning source. The rule is a source scan, not a reflection check: it walks the *.cs files under SourceRoots (default: the repo's Source folder, ForwardedJwtAudienceTestsBase.cs:24-:25) and fails any AddForwardedJwtBearer call whose audience is not wrapped in RequireConfigured.
  • Walkthrough - Map (:12) and MinimumForwardedJwtHosts => 3 (:14), the non-vacuity floor: the scan must find at least three calls, matching the base's description of "the repo's number of extracted service hosts that are not the token issuer" (ForwardedJwtAudienceTestsBase.cs:15-:19). ADC has four service modules and Identity issues the tokens, so three forwarding hosts is the expected count. One inherited fact, ForwardedJwtBearerHosts_ResolveTheirAudienceFailClosed (:27-:29).
  • Why it's built this way - authored once in Common and re-run as a thin subclass per repo, the same shape as the other Map-plus-number subclasses (FormsConventionTests); the floor keeps the gate from passing vacuously if a refactor moves the call sites.
  • Caveats / not-in-source - the count of three is a floor, not an equality: a fourth forwarding host does not fail the test, and a host that stops forwarding only fails once the count drops below three.

FrameworkVersionConsistencyTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/FrameworkVersionConsistencyTests.cs:9 · Level 13 · class (public, sealed)

  • What it is - the lockstep-versioning gate: every MMCA.Common.* package pinned in ADC's Directory.Packages.props must carry one and the same version, so a partial sweep fails CI instead of producing a subtly mismatched framework surface at runtime (MMCA.ADC.Architecture.Tests/Governance/FrameworkVersionConsistencyTests.cs:3-:8).
  • Depends on - FrameworkVersionConsistencyTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests); the map is used for its repo token, to find the props file from the repo root.
  • Concept introduced - a policy made executable. ADR-016 says consumers bump every MMCA.Common.* entry together, with no phased rollout. A policy that lives only in a document is enforced by memory; this rule enforces it by build. [Rubric §32 - Dependency & Supply-Chain] assesses how dependency versions are governed; [Rubric §15 - Best Practices & Code Quality]: a half-swept pin set is the kind of defect that surfaces as an unrelated runtime error weeks later.
  • Walkthrough - one member, Map (:11). The inherited fact is AllMmcaCommonPackages_ArePinnedToOneVersion (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FrameworkVersionConsistencyTestsBase.cs:25), and the base's non-vacuity floor MinimumCommonPackageCount stays at its default of 13 (:22). ADC currently pins 16 MMCA.Common.* packages in MMCA.ADC/Directory.Packages.props, so the floor is met with room to spare.

HandlerConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/HandlerConventionTests.cs:16 · Level 13 · class (public, sealed)

  • What it is - the CQRS handler placement and composition guard: handlers and validators live in the Application layer, handlers do not inject other handlers, application services do not inject handlers, domain event handlers are sealed and live in Application, and application services respect a constructor-arity limit that ADC raises to ten (MMCA.ADC.Architecture.Tests/Cqrs/HandlerConventionTests.cs:18).
  • Depends on - HandlerConventionTestsBase and AdcArchitectureMap.
  • Walkthrough - two members. Map (:16) and MaxServiceConstructorParameters => 10 (:18), raising the base default of 8 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerConventionTestsBase.cs:12). Six facts are inherited (:15, :18, :21, :24, :27, :30). [Rubric §6 - CQRS & Event-Driven] assesses whether the command/query split is structural rather than nominal; a handler that injects another handler is the classic way that split quietly becomes a call graph. [Rubric §1 - SOLID] covers the arity half.
  • Why it's built this way - the <remarks> (:6-:13) records both the reason for the raise and the coupling it creates. The shared AuthenticationServiceBase constructor took on the refresh-session store and its options, and ADC's subclass passed both through alongside the IExternalLoginEmailVerifier its OAuth auto-link gate needs; the rationale lives in ConstructorDependencyCountTests, which sets the same ceiling. Both moved 9 to 10 on 2026-09-23 for AuthenticationService, when the options holder that bundled its two settings objects into one slot was deleted (:11-:13). The last sentence of the <remarks> is the operative one: the two guards must move together, or the stricter one turns the other into a comment.
  • Caveats / not-in-source - ADC therefore has two overlapping arity limits with the same value (10) but different scans (this one over application services generally, the sibling over *Service types in module Application assemblies). Whether they cover an identical type set is decided inside ArchitectureRules and is not determinable from these subclasses.

IntegrationEventPayloadPurityTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Contracts · MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs:14 · Level 13 · class (public, sealed)

  • What it is - the guard on ADC's cross-service async contract (ADR-010): every integration event ships from a *.Shared assembly, and no property on its wire shape, including a nested payload record, reaches a type declared in a *.Domain assembly.
  • Depends on - IntegrationEventPayloadPurityTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (IntegrationEventPayloadPurityTests.cs:16). Two inherited facts, IntegrationEvents_ShouldShipFrom_SharedAssemblies and IntegrationEventPayloads_ShouldNotExpose_DomainTypes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventPayloadPurityTestsBase.cs:38, :41). [Rubric §9 - API & Contract Design] and [Rubric §7 - Microservices Readiness]: a domain event is an internal fact free to change with the model that raises it, an integration event is a public contract another service deserializes by shape and cannot be asked to redeploy because a producer renamed a field, and the two rules keep that distinction structural rather than cultural (IntegrationEventPayloadPurityTestsBase.cs:6-:10).

LayerDependencyTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Layering · MMCA.ADC.Architecture.Tests/Layering/LayerDependencyTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the Clean Architecture layer-flow guard, and the highest-fact-count rule in the unit: fifteen inherited facts covering which layer may reference which, plus one override that records Notification's deliberately thin shape (MMCA.ADC.Architecture.Tests/Layering/LayerDependencyTests.cs:15-:19).
  • Depends on - LayerDependencyTestsBase and AdcArchitectureMap.
  • Concept introduced, the per-module exception recorded where it is true. Two of the fifteen facts are map-completeness checks: LayerMap_DeclaresEveryExpectedLayer and LayerMap_ModulesDeclareEveryExpectedLayer (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/LayerDependencyTestsBase.cs:53, :56) require the five core layers Shared, Domain, Application, Infrastructure, and Api to be declared, both overall and by every mapped module (:16-:17, :27). ADC has one module that legitimately cannot satisfy the module half, so it uses the narrow hook rather than the broad one. The base is explicit about why that matters (:22-:25): trimming RequiredModuleLayers would apply to every module, so one thin module would stop the rule from catching a forgotten assembly anywhere in the repo. [Rubric §3 - Clean Architecture] is the whole point of the rule; [Rubric §34 - Architecture Governance & Documentation]: the exception is written down, in code, at the one place it is true.
  • Walkthrough - two members. Map (:5), and ModuleRequiredLayerOverrides (:15-:19), which overrides the base's empty dictionary (LayerDependencyTestsBase.cs:49-:50) with one entry mapping "Notification" to [Layer.Shared, Layer.Application, Layer.Api]. Its <summary> states the justification (LayerDependencyTests.cs:7-:14): Notification owns no aggregate and no persistence, so there is no Domain and no Infrastructure assembly to declare, and it has no pages of its own so there is no UI assembly either; its API project composes MMCA.Common's notification infrastructure (the SignalR hub and sender) directly rather than wrapping it in an ADC Infrastructure layer. The remaining thirteen facts (LayerDependencyTestsBase.cs:60 through :96) assert the directed rules: Domain depends on neither Application, Infrastructure, nor Api; Application on neither Infrastructure nor Api; Infrastructure not on Api; Shared on nothing above it; and Ui on none of Domain, Application, or Infrastructure. Note the two-gate model described in MMCA.Common/CLAUDE.md: the same boundaries are enforced at compile time in the framework by MMCA.Common.LayerEnforcement.targets, and here at test time against compiled assemblies.

LocalizedTextConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:15 · Level 13 · class (public, sealed)

  • What it is - the companion to TranslationCompletenessTests. Where that one asks "is every key translated", this one asks "does every user-visible string go through a key at all": no hard-coded literals in .razor or .razor.cs under Source/ (MMCA.ADC.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:3-:14).
  • Depends on - LocalizedTextConventionTestsBase and AdcArchitectureMap.
  • Concept introduced - the per-line escape marker. Some literals genuinely should not be translated (the conference brand name, content data), so the rule exempts them with an i18n: allow comment on the offending line rather than an allowlist file (:10-:11). Keeping the exemption physically next to the literal is what makes it reviewable. [Rubric §27 - i18n], executing ADR-027.
  • Walkthrough - the class is a thin subclass with no facts of its own: it declares only Map (:17) and MinimumScannedFiles => 60 (:19). The floor of 60 raises the base default of 1 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizedTextConventionTestsBase.cs:22); the comment sizes it against roughly 77 razor files across the three module UIs and the UI hosts (LocalizedTextConventionTests.cs:12-:13). The inherited fact is UserVisibleText_IsLocalized (LocalizedTextConventionTestsBase.cs:32), which resolves the repo root from Map.RepoToken and scans Source/ (:34-:38); the base's AllowedFiles hook (:29) is left empty, so ADC exempts nothing at file granularity. The scan covers snackbar messages, page Title properties, <PageTitle> markup, breadcrumb labels, NavItem titles (which must carry a TitleResource), and the shared delete confirmation's EntityType noun (LocalizedTextConventionTests.cs:6-:9).
    • The EntityType check lives in the shared rule, not in ADC. The delete confirmation renders EntityType in its dialog title and prompt, so a literal noun would show an English word inside a Spanish sentence. ArchitectureRules.UserVisibleTextIsLocalized tracks a DeleteConfirmation start tag across lines until it closes and reports any line matching the LiteralEntityType generated regex as file:line passes a literal EntityType to DeleteConfirmation (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.LocalizedText.cs:133-:146). That regex flags a quoted value not starting with @ or an unquoted bare word, so a razor expression such as EntityType="@EntityName" passes (:38-:41, with a 1000 ms match timeout). Omitting the attribute is also fine, because the component then falls back to its own localized generic noun.

MicroserviceExtractionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Layering · MMCA.ADC.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the single-fact guard that transport never leaks into the core layers, so a module behaves identically in-process or extracted.
  • Depends on - MicroserviceExtractionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). One inherited fact, CoreLayers_ShouldNotDependOn_Transport (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/MicroserviceExtractionTestsBase.cs:13). [Rubric §7 - Microservices Readiness] assesses whether extraction is a configuration change or a rewrite. ADC is the repo where this rule has already been cashed in: all four modules run as separate service hosts behind a YARP gateway (ADR-007, ADR-008), and the rule is what keeps the core layers clean enough for the next one.

ModuleIsolationTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Layering · MMCA.ADC.Architecture.Tests/Layering/ModuleIsolationTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the guard that Identity, Conference, Engagement, and Notification do not reach into each other: module Domains, Applications, Infrastructures, and APIs are each isolated from their siblings, neither Domain nor Application may reach another module's Infrastructure, and a seventh rule closes every remaining internal-layer pair.
  • Depends on - ModuleIsolationTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). Seven inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleIsolationTestsBase.cs:13, :16, :19, :22, :25, :28, :36). The last, ModuleInternalLayers_ShouldNotReach_OtherModuleInternalLayers (:36), exists to close the coverage the first six leave open: every remaining cross-module internal-layer pair (Domain to another Application or Api, Application to another Domain or Api, Infrastructure and Api to anything but their own module), with UI excluded on purpose (:30-:34). The targets are computed by the map: for each module and layer it derives the other modules' root namespaces (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:69-:72), which is why adding a module to the map immediately expands what every other module is forbidden to touch. [Rubric §7 - Microservices Readiness] and [Rubric §5 - Vertical Slice]: cross-module collaboration in ADC goes through interfaces satisfied by gRPC clients, and this rule is what stops a direct reference from quietly becoming the cheaper option.

NamingConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/NamingConventionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the ten-fact naming and sealing guard: handler, command, query, validator, DTO, specification, repository, and EF configuration suffixes, plus domain events sealed in a *.DomainEvents namespace and invariant classes static.
  • Depends on - NamingConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). Ten inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/NamingConventionTestsBase.cs:13 through :40). [Rubric §15 - Best Practices & Code Quality], and more practically [Rubric §15 - Best Practices & Code Quality]: several framework mechanisms (the Scrutor handler scan, the DTO/mapper registration, the decorator wrapping) find their targets by convention, so a naming slip is not cosmetic, it silently un-registers a type.

PiiConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Privacy · MMCA.ADC.Architecture.Tests/Privacy/PiiConventionTests.cs:18 · Level 13 · class (public, sealed)

  • What it is - the privacy structural guard, in both directions (ADR-005): every domain entity declaring a PiiAttribute-marked property must implement IAnonymizable, so an entity that holds personal data always has an erasure path; and whatever Anonymize() overwrites must itself carry [Pii], so the audit trail redacts it instead of keeping the pre-erasure value as an OldValue.
  • Depends on - PiiConventionTestsBase and AdcArchitectureMap.
  • Walkthrough - two members: Map (:44) and AnonymizableSamples (:46), which overrides the base's empty default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/PiiConventionTestsBase.cs:31) with a private Samples dictionary (:20-:42). Three inherited facts: EntitiesWithPiiProperties_ShouldImplement_IAnonymizable (PiiConventionTestsBase.cs:34), EveryAnonymizableDomainEntity_IsCoveredBySample (:37), and PropertiesOverwrittenByAnonymize_MustCarryPii (:43). The reverse rule is behavioral, so it only sees what a sample actually populates. The Identity.Domain.Users.User sample is an external account (CreateExternal, which sets LoginProvider and ProviderKey) with every device field and an avatar filled in, so each field the erasure nulls starts at a value it has to move (PiiConventionTests.cs:23-:30). The Conference.Domain.Speakers.Speaker sample fills every profile field and links a user (:31-:41), so an Anonymize() that moved anything beyond the email would fail the reverse rule on a public field that carries no [Pii]. [Rubric §30 - Compliance/Privacy/Data Governance] assesses whether privacy obligations are structural rather than procedural: soft delete is the default everywhere in MMCA, so erasure has to be an explicit, tested capability (ADR-005). Unlike its counterpart in MMCA.Common, this instance is non-vacuous: ADC's Identity domain holds real attendee data.

PiiInventoryTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Privacy · MMCA.ADC.Architecture.Tests/Privacy/PiiInventoryTests.cs:15 · Level 13 · class (public, sealed)

  • What it is - the two-way personal-data inventory gate for ADC: the [Pii] markers in the domain code and the inventory table in PRIVACY.md must describe the same set of members (ADR-005).
  • Depends on - PiiInventoryTestsBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Privacy/PiiInventoryTestsBase.cs:33), AdcArchitectureMap, and ArchitectureMapBase (statically, for the repo root). Companion to PiiConventionTests, which checks the erasure path rather than the documentation.
  • Walkthrough - two members, both supplying the base's abstract inputs. Assemblies (PiiInventoryTests.cs:17) is every Domain-layer assembly the map knows, new AdcArchitectureMap().OfLayer(Layer.Domain). PrivacyPolicyPath (:19-:20) is PRIVACY.md at the repo root, found via FindRepoRoot("MMCA.ADC.slnx"). Two inherited facts do the work. PiiMembers_ShouldHave_CompleteInventoryRows (PiiInventoryTestsBase.cs:42) requires every [Pii] property to have an inventory row with the Purpose, Lawful basis and Retention cells filled. InventoryRows_ShouldMapTo_LivePiiMembers (:46) is the reverse, so the policy cannot keep promising things about data the code no longer marks. The table is any Markdown table in PRIVACY.md whose header holds Member, Purpose, Lawful basis and Retention, with the member written Type.Property (PiiInventoryTestsBase.cs:13-:25). If the file is missing, the base fails with a message naming the path (:51-:52). [Rubric §30 - Compliance/Privacy/Data Governance].

QueryHandlerReadRepositoryTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/QueryHandlerReadRepositoryTests.cs:9 · Level 13 · class (public, sealed)

  • What it is - the read-repository guard: every IQueryHandler in ADC's module Application assemblies must take IUnitOfWork.GetReadRepository and never the write repository, since a query handler reads and the write repository hands it the mutation surface (add, remove, tracked reads) meant only for aggregate roots (MMCA.ADC.Architecture.Tests/Cqrs/QueryHandlerReadRepositoryTests.cs:3-:7).
  • Depends on - QueryHandlerReadRepositoryTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:11). One inherited fact, QueryHandlers_ShouldUseReadRepositories (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/QueryHandlerReadRepositoryTestsBase.cs:24-:26), delegating to ArchitectureRules.QueryHandlersUseReadRepositories(Map, AllowedHandlers), which reads the IL of every IQueryHandler<,> implementation in the map's Application assemblies, async and lambda bodies included (QueryHandlerReadRepositoryTestsBase.cs:6-:8). ADC adopts it with an empty allowlist: AllowedHandlers keeps the base's empty default, because every query handler already reads through the narrow interfaces (QueryHandlerReadRepositoryTests.cs:6-:7; QueryHandlerReadRepositoryTestsBase.cs:22). [Rubric §8 - Data Architecture] assesses this same read/write separation directly; ADC's read-only data access convention (handler locals take IEntityQuerier<,>/IEntityReader<,> from IUnitOfWork.GetReadRepository<>()) is what this rule verifies structurally rather than by review.
  • Why it's built this way - the base's own doc comment states the adoption path for a repo with existing offenders: subclass, run once, and either switch each reported handler to GetReadRepository or move it into AllowedHandlers with a comment saying why it needs the write surface (QueryHandlerReadRepositoryTestsBase.cs:9-:13). ADC needed neither: the empty allowlist means the rule already passed clean on adoption.
  • Where it's used - MMCA.Common carries the base's own self-test in MMCA.Common.Architecture.Tests, which runs the same rule over Common's own Application layer and also over compiled fixtures in ReadRepositoryFixtures, proving the IL-reading rule catches a write-repository call sitting after an await, inside the compiler's state machine (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/Repositories/QueryHandlerReadRepositoryTests.cs:8-:12, :23-:26).

RawQueryableConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:11 · Level 13 · class (public, sealed)

  • What it is - the rule that Application-layer code must not use the repository's raw IQueryable surfaces (Table / TableNoTracking*), carrying a seven-file allowlist that pins ADC's existing deliberate uses (MMCA.ADC.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:3-:10, :33-:49).
  • Depends on - RawQueryableConventionTestsBase, AdcArchitectureMap, ArchitectureMapBase (statically, for the repo root at :28), and System.IO.Path.
  • Concept introduced, the adoption ratchet. A convention introduced into a codebase that already violates it in seven places has two bad options: fail the build on day one, or exempt the whole layer. The ratchet is the third: enumerate the existing violations explicitly so that new code is what the rule blocks, then shrink the list over time. The comment states the discipline directly, "Shrink it over time; never grow it without the same scrutiny" (:8-:9). [Rubric §8 - Data Architecture] assesses how query access is layered; [Rubric §7 - Microservices Readiness] is the stated motivation: a raw-queryable handler is EF-coupled and cannot move behind a gRPC boundary (:5-:6).
  • Walkthrough - three members.
    • Map (:13).
    • ApplicationSourceDirectories() (:21-:30) overrides the base's virtual enumeration (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawQueryableConventionTestsBase.cs:45) by yielding everything the base produces (:23-:26) and then appending one more path: Source/Modules/Notification/MMCA.ADC.Notification.Application, resolved from FindRepoRoot("MMCA.ADC.slnx") (:28-:29).
    • AllowedFiles (:33-:49) overrides the base's empty default (RawQueryableConventionTestsBase.cs:38) with seven file names, each grouped under a comment saying why it is exempt: the Engagement live layer's conference-day hot-path aggregations, which need GROUP BY and COUNT shapes the focused repository surface cannot express (LivePollResultsBuilder, SessionQuestionViewBuilder, GetModerationQueueHandler, GetSessionQuestionsHandler, :35-:40); the Engagement bookmark count and page projection (BookmarkCountService, GetUserBookmarksHandler, :42-:45); and Notification's GDPR export joins (UserNotificationExportService, :47-:48). The two Identity entries (the user-list paging handler and the administration-roster service) are no longer allowlisted. Every entry is annotated as intra-module, which is the actual test for whether an exemption is safe: the queries never cross a module boundary, so they would travel with the module if it moved.
    • The inherited fact is ApplicationLayer_DoesNotUseRawQueryableSurfaces (RawQueryableConventionTestsBase.cs:61), a textual scan rather than an assembly scan, which is why it needs directories rather than the map's assemblies; it also asserts the directory set is non-empty first (:65-:66).
  • Caveats / not-in-source - AllowedFiles matches by file name, not by path, so two files with the same name in different modules would both be exempted. The <remarks> on the directory override still says the thin Notification module "is not a mapped module" (:16-:20), but AdcArchitectureMap now registers it (AdcArchitectureMap.cs:52-:54) and the base derives its directories from Map.ModuleNames (RawQueryableConventionTestsBase.cs:50-:57), so the appended path is a duplicate of one the base already yields and the comment no longer matches the map. Nothing here enforces the "shrink it over time" discipline the class comment asks for.

RawSqlConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/RawSqlConventionTests.cs:14 · Level 13 · class (public, sealed)

  • What it is - the companion rule to RawQueryableConventionTests: module code must reach SQL only through the parameterizing APIs, banning FromSqlRaw, SqlQueryRaw, ExecuteSqlRaw and ExecuteSqlRawAsync (each takes a plain string, so an interpolated value silently loses its parameters and the injection becomes a code-review question) while leaving the interpolated siblings and IRawSqlQueryExecutor allowed (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawSqlConventionTestsBase.cs:6-:19).
  • Depends on - RawSqlConventionTestsBase, AdcArchitectureMap, ArchitectureMapBase (statically, for the repo root at RawSqlConventionTests.cs:31), and System.IO.Path.
  • Walkthrough - two members. Map (:16), and ScannedSourceDirectories() (:24-:33), overriding the base's virtual enumeration (RawSqlConventionTestsBase.cs:48) exactly as RawQueryableConventionTests' ApplicationSourceDirectories() does: it yields everything the base produces (:26-:29) and appends Source/Modules/Notification/MMCA.ADC.Notification.Application, resolved from FindRepoRoot("MMCA.ADC.slnx") (:31-:32), for the same reason (the thin Notification module is not a mapped module). Unlike its sibling, ADC declares no AllowedFiles override, so the base's empty default (RawSqlConventionTestsBase.cs:39) stands: no raw-SQL exemption exists anywhere in ADC today. The inherited fact is ModuleCode_UsesParameterizedSqlOnly (RawSqlConventionTestsBase.cs:65-:66), the same textual scan, over .cs files under ScannedSourceDirectories, that its sibling uses and for the same reason: this package carries no IL or Roslyn dependency, so reflection cannot see member usage inside method bodies (RawSqlConventionTestsBase.cs:21-:28). [Rubric §11 - Security] is the direct read (a raw string reaching SQL is the injection surface); [Rubric §8 - Data Architecture] covers the same discipline as its sibling.

ServiceContractPurityTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Layering · MMCA.ADC.Architecture.Tests/Layering/ServiceContractPurityTests.cs:9 · Level 13 · class (public, sealed)

  • What it is - the purity rule for the published gRPC wire surface: a type marked ServiceContractAttribute must not depend on the producing service's Domain, Application, or Infrastructure (MMCA.ADC.Architecture.Tests/Layering/ServiceContractPurityTests.cs:3-:8). Map-only shape (ConcurrencyConventionTests).
  • Depends on - ServiceContractPurityTestsBase and AdcArchitectureMap.
  • Concept introduced, the attribute-driven ratchet. The other purity rules in this unit iterate layers. This one cannot: no repo registers Layer.Contracts in its map today, so a layer-iterating rule would pass vacuously forever without anyone noticing (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ServiceContractPurityTestsBase.cs:8-:11). Instead it scans every assembly the map registers for types carrying the marker, wherever they live, and enforces the invariant from the first marked type onward (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:21-:24). [Rubric §7 - Microservices Readiness] assesses whether a service can be consumed without its internals: a contract that leaks a domain entity or a persistence type forces every consumer to take the producer as a package dependency, which is what makes an extraction irreversible (ArchitectureRules.Contracts.cs:14-:18). [Rubric §9 - API & Contract Design] covers the same boundary from the contract side, and ContractImplementationTests is the twin guarding it from the other direction.
  • Walkthrough - one member, Map (:11), implementing the base's abstract property (ServiceContractPurityTestsBase.cs:22). One inherited fact, ServiceContracts_ShouldNotDependOn_ServiceInternals (:25), delegating to ArchitectureRules.ServiceContractsDoNotDependOnServiceInternals(Map) (ArchitectureRules.Contracts.cs:32-:54). The rule computes the forbidden internal namespaces from the map, returns immediately if that set is empty (:34-:38), and otherwise runs a NetArchTest query per mapped assembly against the types carrying the marker (:40-:53). The marker is matched by full name, MMCA.Common.Shared.Abstractions.ServiceContractAttribute (:10-:11), keeping the rule library free of a framework reference.
  • Why it's built this way - the base adds a second consequence that reads as a design statement (ArchitectureRules.Contracts.cs:26-:29): a marked type living inside a Domain, Application, or Infrastructure assembly fails by construction, and that is the intent, because a published contract belongs in a *.Contracts or Shared assembly rather than inside the service it describes.
  • Where it's used - the rule is live in ADC, not latent. Six [ServiceContract] interfaces are declared in mapped module Shared assemblies today: IAttendeeQueryService (MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Shared/Users/IAttendeeQueryService.cs:10), IEventLiveValidationService (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/Events/Live/IEventLiveValidationService.cs:12), ISessionBookmarkValidationService (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/Sessions/ISessionBookmarkValidationService.cs:10), IBookmarkCountService (MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/UserSessionBookmarks/IBookmarkCountService.cs:10), IUserEngagementExportService (MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/Exports/IUserEngagementExportService.cs:13), and IUserNotificationExportService (MMCA.ADC/Source/Modules/Notification/MMCA.ADC.Notification.Shared/UserNotifications/IUserNotificationExportService.cs:13). All six sit in Shared assemblies the map registers, so the rule inspects them on every run.
  • Caveats / not-in-source - the *.Contracts projects that hold the generated gRPC clients and adapters are not in AdcArchitectureMap, so any [ServiceContract] type declared there rather than in Shared would be outside this scan.

HandlerResultConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:8 · Level 13 · class (public, sealed)

  • What it is - the gate that turns a runtime constraint into a build-time one: every ADC command and query handler's TResult must be Result or Result<T> (MMCA.ADC.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:3-:7).
  • Depends on - HandlerResultConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Concept introduced - shifting a failure left. The decorator pipeline can short-circuit (a feature flag off, an authorization denial, a validation failure, a cache hit), and to do that it must manufacture a failed result of the handler's TResult; the comment names the mechanism, ResultFailureFactory (:5-:6). A handler returning a bare DTO therefore compiles and registers cleanly and only explodes the first time a short-circuit fires in production. [Rubric §6 - CQRS & Event-Driven] and [Rubric §14 - Testability]: an invariant the type system cannot express is exactly what a fitness function is for.
  • Walkthrough - one member, Map (:10). Three inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerResultConventionTestsBase.cs:21, :24, :27): the Application layers declare at least one handler (a non-vacuity check), command handlers return result types, and query handlers do too. Opt-in from v1.120.0 (HandlerResultConventionTests.cs:4).

IdempotencyConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the rule that every POST action in ADC's API layer states, in code, whether a retried request replays the original response or deliberately does not. Map-only shape (ConcurrencyConventionTests): the body is one line (MMCA.ADC.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:5).
  • Depends on - IdempotencyConventionTestsBase and AdcArchitectureMap, and, by attribute name, IdempotentAttribute and NonIdempotentAttribute.
  • Concept introduced, forcing a decision rather than a default. POST is the one verb HTTP does not define as idempotent, so a client retrying a timed-out POST cannot know whether the first attempt landed. The framework's answer is the Idempotency-Key filter, and it costs an existing client nothing, because the filter no-ops for a request that carries no key header. That makes the only failure mode worth gating an omission nobody noticed: an action that should replay but silently does not. The rule therefore does not demand [Idempotent]; it demands that the author write down which of the two applies, with [NonIdempotent("why")] carrying its own justification string (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:14-:18, :22-:28, :58-:60). [Rubric §9 - API & Contract Design] assesses retry semantics as part of the contract; [Rubric §29 - Resilience & Business Continuity]: a retry that double-writes is precisely the failure a resilience policy creates when the endpoint has not thought about it.
  • Walkthrough - one member, Map (:5), implementing the base's abstract property (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/IdempotencyConventionTestsBase.cs:12). One inherited fact, PostActions_ShouldDeclare_IdempotencyIntent (:15), delegating to ArchitectureRules.PostActionsDeclareIdempotencyIntent(Map) (ArchitectureRules.Idempotency.cs:44-:61), which walks the concrete controllers in every Layer.Api assembly the map declares (:48-:54). Attributes are read with inherit: true and abstract controller types are skipped, so an ADC controller that inherits a POST action from AuthControllerBase or AggregateRootEntityControllerBase already satisfies the rule through that base rather than needing its own attribute (:31-:35).
  • Why it's built this way - the base states the subclassing rule: derive it in a repo whose map declares an Api layer, and a repo with no API layer simply does not subclass (IdempotencyConventionTestsBase.cs:6-:8). ADC declares an Api layer for all four mapped modules, Notification included (AdcArchitectureMap.cs:54), so the gate is live across the whole REST surface.
  • Caveats / not-in-source - detection is by attribute type name, keeping the rule library free of an ASP.NET reference, and only [HttpPost] is recognised: an action routed through [AcceptVerbs("POST")] or a conventional route is out of scope (ArchitectureRules.Idempotency.cs:38-:41).

ImmutabilityTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/ImmutabilityTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the immutability guard across five categories of type: DTOs, commands and queries, domain events, integration events, and value objects (the last also required to be sealed and to live in Shared).
  • Depends on - ImmutabilityTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). Five inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ImmutabilityTestsBase.cs:13, :16, :19, :22, :25). [Rubric §15 - Best Practices & Code Quality] assesses whether the codebase holds its stated conventions; required/init immutability is a workspace-wide convention, and an event whose properties can be mutated after publication is a correctness hazard, not a style preference.

IntegrationEventContractTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Contracts · MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the frozen wire contract for ADC's cross-service asynchronous API. It commits a seven-line snapshot of every integration event's wire name and property shape, and the build fails if the live contract differs (MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:9-:20).
  • Depends on - IntegrationEventContractTestsBase and AdcArchitectureMap.
  • Concept introduced, the approval snapshot. The rules above check shape rules; this one checks identity. A consumer in another service deserializes by shape, so a renamed, removed, or retyped property (or a brand-new event shipped without a consumer) breaks the contract at runtime with no compile error anywhere. The base rebuilds the live contract from the map (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventContractTestsBase.cs:38) and reports every observable difference against the committed list (:63-:78): a missing event, a new event, and per event a missing, extra, or retyped member. So any change surfaces as a diff in this file that a reviewer must consciously accept. [Rubric §9 - API & Contract Design] assesses contract governance, and the asynchronous contract is as much an API as the REST surface; [Rubric §7 - Microservices Readiness]: with all four ADC modules already running as separate services, this list is the actual coupling between them. Its synchronous twin is ProtoContractTests.
  • Walkthrough - two members. Map (:5), and ExpectedContract (:9-:23), a collection expression of seven strings in Key { Prop:Type, ... } form, where the key is the event's [EventName] wire identity (Conference.EventFeedbackSubmitted.v1, Identity.UserRegistered.v1) and falls back to the full type name only for an event that declares none (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:98-:89): EventFeedbackSubmitted and SessionFeedbackSubmitted from Conference, SpeakerLinkedToUser and SpeakerUnlinkedFromUser from Conference (the pair Identity consumes to set and clear the linked-speaker reference on the user), AttendeeCheckedIn from Engagement, and UserDeleted plus UserRegistered from Identity. The builder lists properties sorted by name with generic arguments spelled out (SessionId:Nullable<Int32>, ArchitectureRules.Events.cs:80-:75, :98), but the base compares each event's members as a set, not a sequence, because JSON carries no member order and a reordered declaration changes nothing a consumer can observe (IntegrationEventContractTestsBase.cs:11-:15). The inherited fact is IntegrationEventContracts_ShouldMatch_TheFrozenSnapshot (IntegrationEventContractTestsBase.cs:36).
  • Why it's built this way - the comment states the rule of engagement (:7-:8): update the snapshot deliberately, and version the event or coordinate the consumer rollout in the same commit. Keying each line on the [EventName] value (for example [EventName("Engagement.AttendeeCheckedIn.v1")], MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/CheckIns/IntegrationEvents/AttendeeCheckedIn.cs:23) means a CLR rename or namespace move does not churn the snapshot, exactly as it does not churn the wire (ArchitectureRules.Events.cs:93-:81); the .v1 suffix is where a breaking change gets versioned. The AttendeeCheckedIn entry carries its own inline justification (:15-:16): SponsorId is additive, optional, defaults to null, and is declared last precisely so a payload written before the sponsor scope existed still deserializes (confirmed in the event itself, MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/CheckIns/IntegrationEvents/AttendeeCheckedIn.cs:22, :31). UserRegistered now carries the same pattern (:19-:21): EmailVerified is additive and optional, defaulting to false, so a payload written before the flag existed still deserializes, and it deserializes to the value that links nothing, since the Conference consumer auto-links a speaker only on a verified provider email.
  • Caveats / not-in-source - the snapshot proves that the shape has not changed, not that any consumer actually handles it. Consumer-side behavior is exercised by the cross-service Testcontainers tier, not here.

StronglyTypedIdTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/StronglyTypedIdTests.cs:9 · Level 13 · class (public, sealed)

  • What it is - the strongly typed identifier fitness function for ADC (ADR-115): any wrapper type that opts into IStronglyTypedId<TSelf, TValue> must be a readonly record struct, held to the shape the framework's converters, comparer, and route binding are written against.
  • Depends on - StronglyTypedIdTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:11). One inherited fact, StronglyTypedIds_ShouldBe_ReadonlyRecordStructs (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/StronglyTypedIdTestsBase.cs:14-:15). [Rubric §8 - Data Architecture].
  • Caveats / not-in-source - the base is vacuously satisfied in a repo that has not opted in, which is ADC today: the primitive identifier aliases (ADR-048/ADR-085) remain the default identifier model here, so this instance passes with nothing to check, and exists so the FIRST wrapper ADC declares is held to the shape (StronglyTypedIdTestsBase.cs:4-:7).

UIArchitectureConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:10 · Level 13 · class (public, sealed)

  • What it is - the container/presentational split enforced mechanically: every code-behind under Source/ (module UI and UI hosts alike) stays within the 400-line convention cap, and inline @code blocks stay small (MMCA.ADC.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:3-:9).
  • Depends on - UIArchitectureConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Concept introduced - a size cap as a proxy for a structural property. Nothing can test "this component separates orchestration from presentation", but a code-behind that has grown past 400 lines has almost certainly stopped doing so. The comment records the payoff: the gate subsumes tech-debt item TD-13, because the oversized Conference dashboards were split to conform when it landed (:7-:8). That is the ratchet working in the other direction from RawQueryableConventionTests: instead of pinning the violations, the violations were fixed. [Rubric §18 - UI Architecture] assesses front-end composition and is the rule's own stated target.
  • Walkthrough - one member, Map (:12). Two inherited facts, CodeBehinds_StayWithinTheLineCap and RazorFiles_KeepInlineCodeBlocksSmall (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/UIArchitectureConventionTestsBase.cs:44, :63). All four tuning hooks stay at their base defaults: MaxCodeBehindLines 400 (:22), MaxInlineCodeLines 120 (:29), MinimumCodeBehindFiles 1 (:35), and an empty ExcludedPathFragments (:41), so ADC excludes nothing from the scan.

DeleteBehaviorConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:29 · Level 15 · class (public, sealed)

  • What it is - the ADC-side delete-behavior fitness function: every foreign key that cascades across ADC's four finalized service models must carry an explicit MMCA:DeleteBehaviorSource = Explicit annotation, and every foreign key the framework's RestrictDeleteByDefaultConvention stamped Convention must restrict rather than cascade (:8-:11).
  • Depends on - DeleteBehaviorConventionTestsBase for the shared assertion logic and the Model extension point, ArchitectureRules for the two rule methods it invokes directly (CascadingForeignKeysAreExplicitlyOptedIn, ConventionForeignKeysRestrictDeletes), and the nested ServiceModels factory for every model it checks.
  • Concept introduced - cross-reference DeleteBehaviorConventionTestsBase, which is where the base assertion mechanism is taught; this type is the ADC-specific instantiation of that base over four databases instead of one. [Rubric §8 - Data Architecture] applies: an unannotated cascade is exactly the kind of silent-fan-out delete this gate exists to catch. [Rubric §14 - Testability] also applies: the whole check runs against an in-memory-built model, so it stays in the fast tier instead of needing a live database.
  • Walkthrough - Model (:31) overrides the base class's single-model extension point with ServiceModels.Conference(), so every [Fact] inherited from DeleteBehaviorConventionTestsBase runs against Conference by default (it "owns the aggregates with the most relationships", :24-:26). The other three databases get their own explicit fact pairs, one CascadingDeletes_AreExplicitlyOptedIn and one ConventionDeletes_AreRestricted per service, each wrapping the matching ArchitectureRules call in FluentActions.Invoking(...).Should().NotThrow(): Identity (:39-:47), Engagement (:49-:57), Notification (:59-:67). The nested ServiceModels class (:70-:129) supplies every model these facts and the inherited base facts check.
  • Why it's built this way - the class doc comment records the deliberate scope: the migrations projects themselves are NOT referenced here, because their generated migration and snapshot code would otherwise land in the unit-tier coverage denominator (:18-:19); instead ServiceModels rebuilds the same finalized model the migrations projects' own design-time factories build, so the two must be kept in step by hand. See ADR-119 (restrict-delete-by-default) for the policy this gate enforces.
  • Where it's used - a standalone architecture-tier test class; nothing else in the codebase references it.
  • Caveats / not-in-source - the doc comment flags that these framework-table flags must stay in step with each service's migrations design-time factory and its appsettings.json, or the gate and the scaffolded migrations silently disagree (:16-:17); nothing in source enforces that agreement mechanically.

SliceCohesionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Cqrs · MMCA.ADC.Architecture.Tests/Cqrs/SliceCohesionTests.cs:8 · Level 13 · class (public, sealed)

  • What it is - the vertical-slice cohesion rule: every module's Application/{Aggregate}/UseCases/{Operation}/ slice keeps its command or query, its handler, and its validator in one namespace, and the build fails if a handler is stranded from its contract (MMCA.ADC.Architecture.Tests/Cqrs/SliceCohesionTests.cs:3-:7).
  • Depends on - SliceCohesionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:10). Two inherited facts, Handlers_ShouldBeCoLocatedWith_TheirContracts and Validators_ShouldBeCoLocatedWith_TheirContracts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/SliceCohesionTestsBase.cs:15, :19). [Rubric §5 - Vertical Slice] assesses whether a feature is one navigable unit; the rule turns the folder convention into something the build can check, so the slice does not erode into a layer-per-type layout one refactoring at a time.

SoftDeleteEnforcementTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:15 · Level 13 · class (public, sealed)

  • What it is - the ADC soft-delete enforcement guard (ADR-005): ADC deletes by setting IsDeleted = true, so EF Core's row-erasing members are banned everywhere except a reviewed allowlist of purge types (MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:3-:7).
  • Depends on - SoftDeleteEnforcementTestsBase and AdcArchitectureMap. Unlike the map-only instances it also overrides the base's allowlist hook.
  • Concept introduced - an allowlist that names types, not a namespace. Every allowlisted type lives in MMCA.Common, and no ADC module, service or host erases a row of its own; the list still names each framework type individually rather than exempting MMCA.Common wholesale, so a framework type that starts erasing rows in a later release fails this gate and gets reviewed (:9-:12). [Rubric §8 - Data Architecture] applies to the delete policy itself, and [Rubric §11 - Security] to the retention entries, where erasing is the privacy requirement rather than the defect.
  • Walkthrough - two members. Map (:17) supplies AdcArchitectureMap. AllowedHardDeleteTypes (:20-:56) overrides the base's empty default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SoftDeleteEnforcementTestsBase.cs:27) with seven fully qualified type names, each with its reason in a comment (the base accepts full type names or namespace prefixes, SoftDeleteEnforcementTestsBase.cs:21-:26, and ADC uses full names only): EFRepository`2 as the framework's set-based delete escape hatch, whose one ADC caller (SessionScoringRunner) replaces derived AI scores before rescoring (:22-:26); OutboxCleanupService for outbox and inbox retention (:28-:31); AuditTrailCleanupJob for audit-trail retention (:33-:35); RefreshSessionCleanupService for refresh-session rows, which carry no IsDeleted flag and hold device data (:37-:41); InternalCommandCleanupService and InternalCommandAdministration for scheduled-command retention, on a timer and on demand (:43-:50); and EFPermissionGrantStore, because a revoked grant must erase its row (:52-:55). One inherited fact, HardDeletes_ShouldOnlyOccurIn_AllowedPurgeTypes, which hands the map and the list to ArchitectureRules .HardDeletesOnlyInAllowedTypes (SoftDeleteEnforcementTestsBase.cs:29-:31).
  • Caveats / not-in-source - the allowlist is matched by type name string, so a rename of any listed framework type turns this gate red until the entry is updated; that coupling is the intended review trigger, not a defect.

SpecificationConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC.Architecture.Tests/Domain/SpecificationConventionTests.cs:8 · Level 13 · class (public, sealed)

  • What it is - the cross-source specification guard: no specification may filter by navigating to another entity, because such a filter would not translate if that entity later moved to a different data source. The stated alternative is CrossSourceSpecification (MMCA.ADC.Architecture.Tests/Domain/SpecificationConventionTests.cs:3-:7).
  • Depends on - SpecificationConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Concept introduced - a forward safeguard for a capability that is not currently exercised. Every ADC entity routes to SQL Server today: the Conference Session-to-Cosmos and Room-to-SQLite polyglot trial was reverted, but the framework extension points were kept (ADR-018), so ADC opts into the rule anyway (:3-:5). This is worth noticing as a deliberate choice: the rule costs nothing while the repo is single-engine and prevents a class of query from being written that would have to be unwritten later.
  • Walkthrough - one member, Map (:10). One inherited fact, Specifications_ShouldNotNavigate_ToOtherEntities (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SpecificationConventionTestsBase.cs:16). [Rubric §8 - Data Architecture].

StateManagementConventionTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Ui · MMCA.ADC.Architecture.Tests/Ui/StateManagementConventionTests.cs:9 · Level 13 · class (public, sealed)

  • What it is - the Blazor state-ownership guard: the Identity, Conference, and Engagement UI assemblies carry no mutable static state, and stateful UI services stay scoped (MMCA.ADC.Architecture.Tests/Ui/StateManagementConventionTests.cs:3-:8).
  • Depends on - StateManagementConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Concept introduced - why this is a correctness rule and not a style rule. Under Blazor Server every user gets a circuit inside one process, so a mutable static member is shared across every connected attendee, and a singleton-registered stateful service is the same defect wearing a DI hat: one user's selection becomes everyone's. The comment says exactly this (:6-:7). [Rubric §19 - State Management] assesses how client state is scoped and owned; [Rubric §11 - Security] is the sharper edge, since cross-circuit leakage of user state is a data exposure, not just a bug.
  • Walkthrough - one member, Map (:11). Two inherited facts, UiAssemblies_CarryNoMutableStaticState and UiProjects_RegisterStatefulServicesScoped (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:29, :66). The base's AllowedStaticMembers hook (:25) is left at its empty default, so ADC's module UIs have zero static-state allowances. Notification declares no Layer.Ui assembly in the map, so the scan covers the three module UIs only.

SharedLayerTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Layering · MMCA.ADC.Architecture.Tests/Layering/SharedLayerTests.cs:3 · Level 13 · class (public, sealed)

  • What it is - the guard on the Shared layer, the one layer other modules are allowed to reference: a module's Shared project must not depend on that module's own internal layers, must not reach sibling modules, and must stay free of EF Core.
  • Depends on - SharedLayerTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:5). Three inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/SharedLayerTestsBase.cs:12, :15, :18). [Rubric §7 - Microservices Readiness] and [Rubric §9 - API & Contract Design]: Shared holds the DTOs, requests, integration events, and (in ADC) the six [ServiceContract] interfaces that cross a module boundary, so a dependency from Shared into Domain or Infrastructure would drag the module's internals along with its contract and make extraction impossible.

SubstitutabilityTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Governance · MMCA.ADC.Architecture.Tests/Governance/SubstitutabilityTests.cs:9 · Level 13 · class (public, sealed)

  • What it is - the Liskov substitutability guard: no override of a base virtual or abstract member, and no interface member implementation, in the ADC module assemblies throws NotSupportedException or NotImplementedException, so a subtype never refuses at run time an operation its base type or interface promises (MMCA.ADC.Architecture.Tests/Governance/SubstitutabilityTests.cs:3-:8).
  • Depends on - SubstitutabilityTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
  • Walkthrough - one member, Map (:11). One inherited fact, OverridesAndInterfaceImplementations_ShouldNotThrow_NotSupportedOrNotImplemented (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SubstitutabilityTestsBase.cs:34). The base reads IL, so only an exception constructed in place and thrown is judged, and a method that overrides and implements nothing may throw freely (SubstitutabilityTestsBase.cs:11-:13). The base's ExcludedMembers hook (:25-:30) is left at its empty default, so ADC carries no reviewed refusals. [Rubric §3 - SOLID Principles] (Liskov) assesses whether a subtype honors its contract; this turns that into a build failure.

SoftDeleteEnforcementTests

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:15 · Level 13 · class (public, sealed)

  • What it is - the rule that EF Core's row-erasing members are banned outside seven named framework types (MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:20-:56). It is the counterpart to CascadeSoftDeleteConventionTests: that one checks the cascade is a soft delete, this one checks nothing erases in the first place.
  • Depends on - SoftDeleteEnforcementTestsBase and AdcArchitectureMap.
  • Concept introduced, an allowlist that names types instead of a namespace. The base bans DbSet.Remove/RemoveRange, DbContext.Remove/RemoveRange, and ExecuteDelete/ExecuteDeleteAsync outside the purge and erasure types the repo lists (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SoftDeleteEnforcementTestsBase.cs:4-:9). ADC's <para> explains the shape of its own list (SoftDeleteEnforcementTests.cs:8-:13): every allowlisted type is in MMCA.Common, because no ADC module, service, or host erases a row of its own, and the list names those framework types individually rather than exempting the MMCA.Common namespace wholesale, so a future framework type that starts erasing rows still fails here and gets reviewed. [Rubric §30 - Compliance/Privacy/Data Governance] and [Rubric §8 - Data Architecture]: soft delete is what keeps a deleted row auditable, restorable, and accounted for under an erasure request (ADR-005).
  • Walkthrough - two members. Map (:17), and AllowedHardDeleteTypes (:20-:56), which overrides the base's empty default (SoftDeleteEnforcementTestsBase.cs:27) with seven entries, each carrying its own justification.
    • EFRepository<TEntity, TIdentifierType> (:26), the framework's set-based delete escape hatch. Erasing is the caller's explicit ask there, and the comment names the one ADC caller, SessionScoringRunner, which replaces a session's AI scores before rescoring, so it is deleting derived rows it is about to rewrite rather than user data (an AI score carries no audit or erasure obligation of its own) (:22-:25).
    • OutboxCleanupService (:31, listed by its full name MMCA.Common.Infrastructure.Persistence.Outbox.Administration.OutboxCleanupService), outbox and inbox retention: delivery plumbing with a bounded lifetime, where the dead-letter sweep is the retention policy and soft-deleting would grow the table the job exists to bound (:28-:30).
    • AuditTrailCleanupJob (:35), audit-trail retention, with the sharpest line in the file: erasing past the retention window is the requirement, because keeping an audit row forever is the privacy defect and not the safeguard (:33-:34).
    • RefreshSessionCleanupService (:41), refresh-session retention. The reasoning is worth reading in full (:37-:40): a session row is framework bookkeeping rather than an aggregate, it carries no IsDeleted flag and no audit stamps, and its content is a credential digest plus the IP and user-agent of a device, so flagging it instead of erasing it would keep a growing record of a data subject's devices past any use for it.
    • InternalCommandCleanupService (:46, MMCA.Common.Infrastructure.Persistence.InternalCommands.Administration.InternalCommandCleanupService), internal-command retention: a scheduled-command row is deferred-work plumbing on the outbox's design, and purging processed and dead-lettered rows past the retention window IS the policy, so soft-deleting them would grow the table the cleanup exists to bound (:43-:45).
    • InternalCommandAdministration (:50, MMCA.Common.Infrastructure.Persistence.InternalCommands.Administration.InternalCommandAdministration), the internal-command operator surface: the administration purge is the same retention action taken on demand rather than on a timer, over the same non-aggregate plumbing rows (:48-:49).
    • EFPermissionGrantStore (:55, MMCA.Common.Infrastructure.Persistence.Auth.EFPermissionGrantStore), revoked permission grants: revoking a stored grant must erase the row, since a flagged row would leave a capability record behind and the compiled registry, not the table, is the audit of what a role can do (ADR-005) (:52-:54).
    • The inherited fact is HardDeletes_ShouldOnlyOccurIn_AllowedPurgeTypes (SoftDeleteEnforcementTestsBase.cs:30).
  • Why it's built this way - the entries are a good worked example of how MMCA reasons about erasure: most of them erase because privacy requires it, not despite it. The base frames the list the same way as the other ratchets, as a reviewed inventory of every place that does not soft-delete rather than a blanket exemption (SoftDeleteEnforcementTestsBase.cs:11-:15).
  • Caveats / not-in-source - entries are full names including the generic-arity suffix (EFRepository`2), so an arity change would drop the exemption and turn the rule red, which is the safe direction.

ServiceModels

MMCA.ADC.Architecture.Tests · MMCA.ADC.Architecture.Tests.Domain · MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:70 · Level 14 · class (private, static, nested in DeleteBehaviorConventionTests)

  • What it is - a private static factory nested inside DeleteBehaviorConventionTests.cs that builds each of ADC's four service databases' finalized EF model offline, one method per service (Identity, Conference, Engagement, Notification, :76-:98), so the delete-behavior fitness functions can assert against the real scaffolded model without ever opening a connection.
  • Depends on - DesignTimeDbContextHelper (.CreateSqlServer, :107), DataSourceEntrySettings (:114-:118), and DefaultEntityConfigurationAssemblyProvider as Notification's configuration assembly (:96, since Notification's EF configurations ship inside MMCA.Common.Infrastructure rather than its own module assembly). Each of Identity/Conference/Engagement instead points Build at its own module's Infrastructure.AssemblyReference marker type (:79, :85, :88).
  • Concept introduced - building a design-time EF model in-process as a fitness-function fixture. This mirrors exactly what dotnet ef migrations add builds when a migration is scaffolded (same DesignTimeDbContextHelper entry point, same single-named-DataSource collapse onto Default), which is the point: if ServiceModels and a service's real migrations design-time factory ever disagree, the scaffolded migrations and this gate stop agreeing too. The class is deliberately private and file-scoped rather than shared, so each architecture-test file owns its own snapshot of "how do I build service X's model" without a shared dependency that could silently drift for one caller and not another.
  • Walkthrough - DesignTimeConnection (:74) is a placeholder connection string; it is never opened because the model is built purely from configuration. Identity() (:76-:82) is the one call site that passes enableRefreshSessions: true, because refresh sessions are Identity-only data. Conference() (:84-:85) and Engagement() (:87-:88) call Build with only a data-source name and configuration assembly, taking every flag default. Notification() (:93-:98) is the one caller that turns audit trail and scheduler off, matching the thin module's factory. Build (:100-:128) is the shared mechanism: it sets options.DataSourceName and a single DataSources entry (:113-:118), so DataSourceResolver seeds Default from that one named source and the model gets the host-scoped ScheduledJobs/RefreshSessions tables the real factories rely on; it then applies the three framework-table flags (:120-:122) and the configuration assembly (:124) before returning context.Model (:127).
  • Why it's built this way - the delete-behavior gate (ADR-119, restrict-delete-by-default) needs the finalized model, foreign keys and all, for every service database, and building it offline (rather than against a real SQL Server) keeps the check in the fast unit/architecture tier.
  • Where it's used - DeleteBehaviorConventionTests, the only consumer in the defining file.
  • Caveats / not-in-source - none outside the defining file; MMCA.Common's own DeleteBehaviorConventionTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs) builds its own model the same way but is a separate type in a separate assembly, not a caller of this one.

AbstractAnonymousFixtureControllerBase, AnonymousFixtureController, TypeLevelAnonymousFixtureController

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · (see per-type table) · Level 0 · class

Three throwaway MVC controllers nested inside AnonymousEndpointTestsBaseTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AnonymousEndpointTestsBaseTests.cs). Between them they cover every placement of [AllowAnonymous] the scan has to recognize: on an action of a concrete controller, on an action of an abstract base, and on the controller type itself. They are the input side of the anonymous-endpoint allow-list gate; the subclasses that consume them supply the expectations.

  • Depends on - Microsoft.AspNetCore.Mvc.ControllerBase and Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute (AnonymousEndpointTestsBaseTests.cs:1-:2). No state, no behavior: every action is => Ok().
  • Concept introduced - an allow-list is only as good as the identifier shapes it can be written in. AnonymousEndpointTestsBase turns each discovered [AllowAnonymous] into a string, and there are exactly two shapes: a type-level attribute becomes the type's FullName, and a method-level attribute becomes {declaring type FullName}.{method name} (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:255-:152). A repo's allow-list is hand-written against those strings, so if the emitter and the hand-written convention ever disagree the gate silently reports phantom offenders or, worse, accepts a stale entry. These three fixtures exist so both shapes are produced by a real scan and matched by a real allow-list in the framework's own CI. Two scoping decisions in the base are also on display here: ControllerBase, RouteAttribute and AllowAnonymousAttribute are all matched by full name through reflection (:32-:34) so the rule package carries no ASP.NET reference, and abstract controllers are deliberately included in the scan (IsController walks BaseType, :101-:112) because a framework base action is where the attribute is declared. [Rubric §11 - Security] assesses whether the authorization posture of the HTTP surface is deliberate; an ungated endpoint that nobody reviewed is the single cheapest way to lose an app. [Rubric §26 - Front-End Security] extends the same scan to routable Blazor components (IsRoutableComponent, :117-:118). [Rubric §14 - Testability] covers the fixtures themselves.
  • Walkthrough - the scan enumerates LoadableTypes of each target assembly, keeps controllers and routable components, and projects each survivor through AnonymousEndpointsOf, distinct and ordinal-ordered (AnonymousEndpointTestsBase.cs:183-:131). Methods are read with BindingFlags.DeclaredOnly and inherit: false (:142,:146), which is what decides where an inherited action gets reported (see InheritingFixtureController).
Type File:Line The identifier shape it produces
AnonymousFixtureController MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:137 The ordinary case: a sealed ControllerBase whose PeekAsync carries [HttpGet] and [AllowAnonymous] (:78-:80). Emits the method-level identifier ...AnonymousFixtureController.PeekAsync, and is the name the drifted subclass's failure message must contain (:22).
AbstractAnonymousFixtureControllerBase MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:147 The declaration site: an abstract controller whose virtual InheritedAnonymousAsync carries [HttpGet("inherited")] and [AllowAnonymous] (:88-:90). Emits one identifier at the base, mirroring how the framework's own AuthControllerBase actions are declared once for every consumer that derives from them.
TypeLevelAnonymousFixtureController MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:161 The other identifier shape: [AllowAnonymous] on the type (:97) with a body-less declaration and no actions at all. Emits the bare FullName, with no method suffix.
  • Why they're built this way - a gate whose fixtures only ever exercise one attribute placement would pass while being blind to the other, and the failure would land in a consumer repo rather than here. Nesting the fixtures inside the test class keeps them out of the assembly's public surface and, critically, out of the framework's real AnonymousEndpointTests scan, which targets the API and UI assemblies rather than this one. See ADR-015.
  • Where they're used - scanned by the four nested subclasses DriftedTests, StaleAllowListTests, EmptyScanTests and ConformantTests, and named in the assertions of AnonymousEndpointTestsBaseTests.

AbstractFitnessControllerBase, IdempotentFitnessController, NonIdempotentFitnessController, UndeclaredFitnessController

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · (see per-type table) · Level 0 · class

Four throwaway MVC controllers nested inside IdempotencyFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/IdempotencyFitnessTests.cs). Each is a ControllerBase subclass with exactly one [HttpPost] action, and the only thing that varies between them is which idempotency attribute the action carries. Together they form the truth table for the PostActionsDeclareIdempotencyIntent fitness function: one offender, one covered directly, one covered by inheritance, one opted out with a reason, and one abstract declaration site that must be skipped entirely.

  • Depends on - Microsoft.AspNetCore.Mvc.ControllerBase (IdempotencyFitnessTests.cs:1), plus IdempotentAttribute and NonIdempotentAttribute from MMCA.Common.API.Idempotency (:2). Nothing else: these types have no state and no behavior worth exercising.
  • Concept introduced - declared idempotency intent, and the truth table that proves a gate reads it. A POST that is retried by a client, a proxy, or a mobile app on a flaky connection either replays its original response or writes twice. IdempotentAttribute opts an action into the replay path served by IdempotencyFilter; NonIdempotentAttribute records in code why replaying would be wrong (issuing a token, revoking a credential, exchanging a single-use code). What the framework will not accept is silence, so ArchitectureRules.PostActionsDeclareIdempotencyIntent (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:44) fails the build on any POST carrying neither attribute. These four fixtures exist because a gate like that has four distinct behaviors to prove, not one. [Rubric §9 - API & Contract Design] assesses whether the HTTP surface states its own semantics; a declared retry posture is part of the contract, not an implementation detail. [Rubric §29 - Resilience & Business Continuity] is the reason the rule exists at all: retries are the default failure response of every modern client, so a POST with undeclared replay semantics is a duplicate-write incident waiting for a network blip. [Rubric §14 - Testability] covers the fixtures themselves, which turn each branch of the rule into an observable case.
  • Walkthrough - the rule reads attributes by simple type name ("IdempotentAttribute", "NonIdempotentAttribute", "HttpPostAttribute", ArchitectureRules.Idempotency.cs:6,:9,:12) so the rule package keeps no ASP.NET reference, and it reads them with inherit: true (:80-:81), which is what makes the inherited case work. Two filters decide what is even looked at: .SelectMany(a => a.ConcreteClasses) (:49), whose helper keeps only non-abstract classes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/RuleHelpers.cs:36-:37), and .Where(IsController) (:51).
Type File:Line The case it models
IdempotentFitnessController MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:61 The primary covered shape: CreateThingAsync carries [HttpPost] and [Idempotent] on the action itself (:65-:67). The rule must not report it.
AbstractFitnessControllerBase MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:71 The declaration site: an abstract controller whose virtual CreateInheritedAsync carries [HttpPost("inherited")] and [Idempotent] (:75-:77). Because it is abstract it never reaches ConcreteClasses, so it is skipped rather than judged; its concrete subclasses are what actually route.
NonIdempotentFitnessController MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:84 The justified opt-out: IssueTokenAsync carries [NonIdempotent("Issues a token; a replayed response would hand back credentials minted for an earlier call.")] (:88-:90). A declaration, not an omission, so the rule stays quiet.
UndeclaredFitnessController MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:94 The offender: a bare [HttpPost] CreateThingAsync with neither attribute (:98-:99). This is the one name the rule's exception message must contain.
  • Why they're built this way - a fitness function that only ever sees compliant code proves nothing, and one that flags everything is worse than none. Four controllers whose only difference is the attribute set turn the rule into something with a pass and a fail on either side of each branch (ADR-015). Keeping them nested inside the test class keeps them out of the assembly's public surface and out of any other rule's reach.
  • Where they're used - reflected over by IdempotencyFitnessTests through IdempotencyTestMap, which registers this test assembly as the map's single Api layer.
  • Caveats / not-in-source - these fixtures deliberately derive from raw ControllerBase rather than ApiControllerBase, which the framework's separate controller-inheritance rule would flag. They escape it only because that rule runs over CommonArchitectureMap, which does not include this test assembly.

ArchiveTicketCommand, CreateTicketCommand, PurgeTicketsCommand, RebuildTicketIndexCommand, ReopenTicketRequest, UpdateTicketRequest

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CommandValidatorFixtures · (see per-type table) · Level 0 · record

Six internal sealed record payload types in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs), each a positional record with at most one property. They are the input side of the command-validation coverage truth table: four commands that a handler will pick up, and two request records that a bridged command embeds.

  • Depends on - nothing beyond the BCL, except that the file as a whole references ICommandHandler<in TCommand, TResult> and ICommandWithRequest<out TRequest> from MMCA.Common.Application.UseCases (CommandValidatorFixtures.cs:2) and FluentValidation (:1). These six records implement neither: they are pure payload.
  • Concept introduced - what makes a command "data-carrying", and why that decides whether it is judged at all. The validation-coverage rule does not ask "does every command have a validator". It asks the narrower question "does every command that could carry bad input have something checking it", and the test for that is mechanical: at least one public instance property with a public setter, init included (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:135-:138). A positional record is exactly that shape, which is why the usual command is inspected and why a payload-free marker command is skipped as having nothing to validate (rule remarks, :19-:21). The rule then keeps only commands whose type appears as the TCommand of some concrete ICommandHandler in scope, and only closed ones: an open generic handler base contributes a type parameter, not a command a repo ships (:96-:102). [Rubric §6 - CQRS & Event-Driven] assesses whether the command surface is a governed contract rather than an ad-hoc bag of DTOs. [Rubric §24 - Forms/Validation/UX Safety] is the property being defended at its far end: the Validating decorator runs before the transaction opens, so a command with no validator carries whatever the caller sent straight into the handler and the pipeline stage exists with nothing to run (rule doc, :9-:13). [Rubric §14 - Testability] covers the fixtures.
  • Walkthrough - the file's header doc (:7-:12) enumerates the shapes deliberately: a command covered by its own validator, one covered only through the request bridge, one covered by neither, one carrying no payload at all, and a bridge command whose request has no validator.
Type File:Line The case it models
CreateTicketCommand MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:13 record CreateTicketCommand(string Title). Direct coverage: CreateTicketCommandValidator validates it explicitly, so it must be absent from the report.
UpdateTicketRequest MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:28 record UpdateTicketRequest(string Title), the request a bridged command embeds. Never a command itself (no handler takes it), so the rule never judges it; it exists to be the validated half of the bridge.
ReopenTicketRequest MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:45 record ReopenTicketRequest(string Reason), deliberately with no validator (doc, :44). It is what makes the half-bridge case possible.
ArchiveTicketCommand MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:60 record ArchiveTicketCommand(Guid TicketId). Carries data, has a handler, has no validation of any kind (doc, :59). The primary offender, and the name the failure message must contain.
PurgeTicketsCommand MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:69 record PurgeTicketsCommand(DateTime OlderThan). A second uncovered command, kept separate so one fact can allowlist it and prove an exemption silences exactly the command it names while the other offender still fails (doc, :68).
RebuildTicketIndexCommand MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:78 record RebuildTicketIndexCommand(), a parameterless marker with no settable property (doc, :77). Skipped by HasSettablePublicProperty, so it is neither covered nor reported, and it is the one command excluded from the inventory count.
  • Why they're built this way - two uncovered commands rather than one is the load-bearing choice: an allowlist test that had only a single offender could not distinguish "the exemption worked" from "the rule stopped firing". Keeping every record to one property means a change in the rule's classifier is diagnosed by which fixture moved rather than by reading a wall of assertion output. See ADR-015.
  • Where they're used - handled by the six fixture handlers (ArchiveTicketHandler and family), scanned through FixtureModuleMap, and named by nameof in the assertions of CommandValidatorCoverageFitnessTests.

ArgumentGuardFixture, IndirectThrowFixture, InvalidOperationThrowingFixture, NonThrowingFixture, RethrowingFixture, TicketDomainException

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainThrowFixtures · (see per-type table) · Level 0 · class

Six types in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs): five static classes holding compiled throw sites and one custom exception for them to throw. They are the truth table for the domain-throw rule, which reads IL, so the only honest way to test it is to compile the throws it must and must not flag into this assembly and point a map at it (file doc, :3-:8).

  • Depends on - the BCL only: ArgumentException, ArgumentNullException, ArgumentOutOfRangeException, InvalidOperationException, Exception, Action, ICollection<string>.
  • Concept introduced - the Result pattern's build-time boundary, and the three exceptions it still allows. ADR-013 says the domain reports failure by returning Result, never by throwing: a thrown business failure skips Result.Combine invariant composition, turns a 4xx outcome into a 500, and pays an exception unwind on a path that is not exceptional (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:26-:29). The rule is not "never throw", though. Argument guards report a caller bug rather than a business outcome, and a caller cannot recover from passing null, so there is nothing for a Result to carry; exactly three exception types are permitted (:16-:21). Everything else in a Domain assembly is a violation. The mechanism is deliberately narrow: for each throw opcode the rule looks at the immediately preceding instruction, skipping the nops a Debug build interleaves, and reads the exception type off a newobj (:143-:154). If the preceding instruction is anything else, the thrown value came from somewhere the instruction stream cannot name, and the site is reported as UNVERIFIABLE rather than guessed at (:127-:131). [Rubric §4 - DDD] assesses whether the domain expresses outcomes in its own vocabulary; an exception is control flow borrowed from the host. [Rubric §15 - Best Practices & Code Quality] and [Rubric §14 - Testability] cover the rest.
  • Walkthrough - a bare throw; inside a catch compiles to the distinct rethrow opcode, not throw, so it never even enters the scan (fixture doc, :72-:75). The rule also skips compiler-synthesized bodies: the skeleton members of a C# extension(T) block, and the explicit interface implementations on compiler-generated types such as the read-only list wrapper emitted for a collection expression (:156-:177).
Type File:Line What it models
ArgumentGuardFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:9 All three permitted guards in one type: ArgumentNullException via a null-coalescing throw (:11-:12), ArgumentOutOfRangeException from a range check (:14-:20), and ArgumentException after an ArgumentNullException.ThrowIfNull (:22-:30). The whole type name must be absent from the report.
InvalidOperationThrowingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:37 The canonical offender: a business outcome signalled by throwing, throw new InvalidOperationException("The ticket is already closed") (:43). Its doc names the fix, a Result.Failure (:33-:36).
TicketDomainException MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:49 The only public type in the file: a three-constructor Exception subclass (:51-:63). It carries no throw of its own; it exists so the rule can be shown to judge by the constructed type rather than by a name allowlist, since a custom exception is the same defect wearing a domain name (doc, :48).
RethrowingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:76 The negative control for opcode discrimination: a try/catch that logs and then rethrows bare (:83-:91). Preserving a caught exception must stay free, and the rethrow opcode is what makes that possible without an allowlist entry.
IndirectThrowFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:99 The blind spot, stated honestly: Fail(Exception prepared) => throw prepared (:101). The value was constructed elsewhere, so the preceding instruction is not a newobj and the rule reports UNVERIFIABLE instead of passing or failing it.
NonThrowingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:105 The shape the rule exists to protect: CanClose(bool alreadyClosed) => !alreadyClosed (:107). Returns instead of throwing.
  • Why they're built this way - the rule has five outcomes (allowed guard, business exception, custom exception, rethrow, unverifiable) plus a clean case, and each needs a subject that trips exactly one of them. Every fixture except TicketDomainException is internal static, so nothing here widens the assembly's public surface, and putting them in their own namespace lets one allowlist entry silence the whole set for the escape-hatch facts. See ADR-015.
  • Where they're used - scanned by DomainThrowFitnessTests through its FixtureAssemblyMap, which registers this test assembly as the map's single Domain layer.

CompliantFixtureService, MissingTokenFixtureService, MisplacedTokenFixtureService, MisnamedTokenFixtureService, ExemptableFixtureService, ExternalContractFixtureService

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CancellationFixtures · (see per-type table) · Level 0 · class

Six tiny services in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs), each a hand-shaped input for one branch of the AsyncMethodsDeclareTrailingCancellationToken fitness function. None of them does any work; the method signatures are the fixture.

  • Depends on - the BCL only: Task/ValueTask, CancellationToken, IAsyncEnumerator<T>, and one [SuppressMessage] from System.Diagnostics.CodeAnalysis (CancellationTokenFixtures.cs:1).
  • Concept introduced - the trailing-token convention, and why it is enforced mechanically. Work that cannot be cancelled keeps running against the database after its caller is gone. The framework's rule is not "accept a token somewhere" but the far stricter "every public awaitable method on an Application or Infrastructure type ends with a parameter of type CancellationToken named exactly cancellationToken" (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CancellationTokens.cs:35). The uniformity is the point: only a positionally and nominally fixed parameter lets the decorator pipeline and the repositories forward a linked token mechanically, without a per-method decision. The rule scans only the Application and Infrastructure layers (:45-:47), only publicly visible non-delegate types (:92-:94), and only public, non-special-name, non-Dispose methods whose return type is Task, Task<T>, ValueTask, or ValueTask<T> (:97-:103). TokenProblem (:115) then classifies each survivor into one of three messages: "no CancellationToken parameter" (:134), "the CancellationToken must be the LAST parameter" (:131), or "the trailing CancellationToken must be named 'cancellationToken'" (:130). [Rubric §12 - Performance & Scalability] assesses whether the system sheds abandoned work instead of paying for it; an uncancellable query holds a connection for the full duration of a request nobody is waiting for. [Rubric §6 - CQRS & Event-Driven Design] applies because cancellation is plumbing that has to be uniform to be automatable, and [Rubric §14 - Testability] because these six fixtures are what make the classifier provable rather than assumed.
  • Walkthrough - two escape hatches also need fixtures, and they work differently. The explicit one is the exemptMethods parameter (ArchitectureRules.CancellationTokens.cs:37), a caller-supplied list matched as "TypeName.MethodName" (:78); it exists for shipped public APIs where adding a parameter would be a breaking change. The automatic one is ExternallyFixedMethods (:141), which drops any method that overrides a base declared outside the map's assemblies or implicitly implements an interface declared outside them: a signature this repo does not own is not this repo's to fix.
Type File:Line What it models
CompliantFixtureService MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:6 The clean case, and two out-of-scope shapes in the same type: GetAsync(string, CancellationToken) and DoAsync(CancellationToken) both comply (:9-:17), Count(string) is not awaitable (:20), and HiddenAsync() is internal (:23), so neither is scanned. The whole type's name must be absent from the report.
MissingTokenFixtureService MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:27 No token at all, twice: RunAsync(string) (:30) and PingAsync() (:33). The parameterless one is deliberate: it pins the ruling that a method with no parameters is not excused, because the token would simply be its only one.
MisplacedTokenFixtureService MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:37 The token leads instead of trailing: RunAsync(CancellationToken, string) (:44). Writing it required suppressing CA1068, and the justification says exactly why (:40-:43), which is the honest way to keep an analyzer-hostile fixture in a warnings-as-errors repo.
MisnamedTokenFixtureService MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:49 The token trails but is named token (:52). Proves the rule checks the name, not just the position and type.
ExemptableFixtureService MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:57 A plain offender, LegacyAsync(string) (:60), used twice by the test: once with no exemptions (reported) and once listed in exemptMethods (silent). It is what makes the explicit escape hatch provable.
ExternalContractFixtureService MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:67 An IAsyncEnumerator<int> implementation whose MoveNextAsync() (:73) is awaitable, public, and token-less, yet is declared by the BCL. Proves the automatic exemption: an implicit implementation of an interface outside the map is never flagged.
  • Why they're built this way - the six shapes are the rule's entire decision surface. Putting them in their own CancellationFixtures namespace keeps them out of the way of the other fitness maps in this assembly, and keeping every one of them trivial means a future change to the rule's classifier is diagnosed by which fixture moved, not by a wall of assertion output. See ADR-015.
  • Where they're used - driven entirely by CancellationTokenFitnessTests through CancellationTestMap.

AsyncClockReadingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ClockReadFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:24 · Level 0 · class

  • What it is - a fixture whose clock read happens inside an async body, internal sealed class AsyncClockReadingFixture with public async Task<DateTimeOffset> StampAsync() that awaits Task.Yield() before returning DateTimeOffset.UtcNow (ClockReadFixtures.cs:24-:32).
  • Depends on - nothing beyond the BCL: Task, Task.Yield(), and DateTimeOffset.UtcNow (:29-:30).
  • Concept introduced - why the clock-read rule has to follow a compiler-generated state machine, not just the method body as written. An async method with an await is rewritten by the compiler into a state machine type, so the actual DateTimeOffset.UtcNow read lives on a nested MoveNext rather than on StampAsync itself; a rule that only enumerates a type's own IL would miss it. ClockReadTestsBase must resolve that nested read back to the member that wrote the source, the same requirement a lambda body creates for LambdaClockReadingFixture, so ClockReadTests proves both cases side by side.
  • Walkthrough - one method, StampAsync() (:27-:31): await Task.Yield() (:29) forces the compiler to build the state machine, then return DateTimeOffset.UtcNow (:30) is the flagged read.
  • Where it's used - asserted by ClockReadTests.ReadsInsideLambdaAndAsyncBodies_AreFlaggedUnderTheirSourceMember, which requires the report contain "AsyncClockReadingFixture.StampAsync reads DateTimeOffset.UtcNow" (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:46).

AsyncLambdaClockReadingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ClockReadFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:54 · Level 0 · class

  • What it is - a fixture whose clock read sits inside an async lambda, internal sealed class AsyncLambdaClockReadingFixture with public Func<Task<DateTime>> Clock() returning async () => { await Task.Yield(); return DateTime.UtcNow; } (ClockReadFixtures.cs:54-:61).
  • Depends on - nothing beyond the BCL: Func<>, Task, Task.Yield() and DateTime.UtcNow (:56-:60).
  • Concept introduced - why the clock-read rule has to handle a lambda and an async body at the same time. AsyncClockReadingFixture covers an async method and LambdaClockReadingFixture covers a plain lambda; this one combines them, so the DateTime.UtcNow read lives on a compiler-generated state machine nested inside a closure class, two levels away from the source member Clock. The rule must resolve it back to Clock for the report, and an allow-list entry for Clock must also cover it.
  • Walkthrough - Clock() (:56-:60) returns the lambda without running it; inside the lambda await Task.Yield() (:58) forces the state machine and return DateTime.UtcNow (:59) is the flagged read.
  • Where it's used - asserted by ClockReadTests.ReadsInsideAnAsyncLambda_AreAttributedToTheSourceMemberAndExemptedWithIt (rolled up under per-project test rollup): the report must contain "AsyncLambdaClockReadingFixture.Clock reads DateTime.UtcNow", and an allow-list entry for ...AsyncLambdaClockReadingFixture.Clock must remove it (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:54-:61).

Declaration

MMCA.Common.Architecture.Tests · (global namespace) · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/JwtAudienceFixtures/Clean/Program.cs:14 · Level 0 · class

  • What it is - a near-miss in the conforming JWT-audience fixture, public static class Declaration whose only member is public static IServiceCollection AddForwardedJwtBearer(IServiceCollection services) => services (Program.cs:14-:17). It is a method declaration with the same name as the audited call, not a call.
  • Depends on - IServiceCollection only (:16).
  • Concept introduced - why a text-scanning fitness rule needs fixtures that look like the target but are not. The file is uncompiled text (its header says it is excluded from compilation in the csproj, :1) read by the forwarded-JWT-audience gate. Next to the real call (:8-:12, with the audience from JwtAudience.RequireConfigured), it carries a comment mention (:7), a doc-comment mention (:4) and this declaration, so the rule is proved not to count them as calls.
  • Walkthrough - nothing executes; the class exists only so the scanner meets a line containing AddForwardedJwtBearer( that is a declaration.
  • Where it's used - ForwardedJwtAudienceFitnessTests points ArchitectureRules.ForwardedJwtBearerAudienceIsFailClosed at the Clean folder and expects it to pass with minimumCalls: 1 and to fail the over-count with minimumCalls: 2, which shows exactly one call was counted (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/ForwardedJwtAudienceFitnessTests.cs:24, :31-:32).

EngineHit

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DataSourceBranchingFitnessTests.cs:149 · Level 0 · record

  • What it is - the one-line scan result of the data-source branching gate, private sealed record EngineHit(string File, int Line, string Text) nested inside DataSourceBranchingFitnessTests (DataSourceBranchingFitnessTests.cs:149). Its doc names the three fields: the file path relative to Source/, the 1-based line number, and the line text (:148).
  • Depends on - nothing beyond the BCL; three positional properties, no members of its own.
  • Concept introduced - why a text-scan fitness gate keeps each hit as a value, not a formatted string. Engine-specific behavior is meant to live behind IDataSourceEngine (ADR-130), and nothing in the compiler stops a new DataSource.Sqlite comparison or is SqliteDbContext check from growing back outside the strategies, so the gate scans Source/**/*.cs as text against a file-granular allow-list (class doc, :7-:16). Keeping the hit structured lets two opposite assertions share one scan: ConcreteEngineBranching_StaysInsideTheEngineStrategies filters hits whose File is not allow-listed (:73-:77), and AllowList_HasNoStaleEntries projects the same hits to a set of File values and fails on any allow-listed file that no longer produces one (:85-:89), so the allow-list shrinks with the code. [Rubric §14 - Testability] covers the gate.
  • Walkthrough - built only in ScanSource() (:112-:140): for every .cs file under the repo's Source root, excluding bin and obj (:114-:120), each line matching the EngineReference regex (:142-:146) becomes new EngineHit(relative, i + 1, lines[i].Trim()) with the path normalized to forward slashes (:128, :134). Before scanning, the method asserts more than 500 files were found so a wrong scan root cannot pass vacuously (:122-:123). The violating hits are rendered as Source/{File}:{Line}: {Text} and handed to ArchitectureAssert (:77-:79).
  • Where it's used - private to DataSourceBranchingFitnessTests (rolled up under per-project test rollup); not referenced outside the defining file.

FatFixtureController

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs:17 · Level 0 · class

  • What it is - a top-level [ApiController] fixture with three injected string collaborators, internal sealed class FatFixtureController(string first, string second, string third) : ControllerBase (ConstructorDependencyFixtures.cs:17), built to sit one dependency over a tight ceiling.
  • Depends on - Microsoft.AspNetCore.Mvc.ApiControllerAttribute/ControllerBase (:1,:16-:17) only; each injected string is read back through Collaborators (:19) so none is an unread primary-constructor parameter (CS9113).
  • Concept introduced - why the constructor-dependency ceiling needed a controller and a handler population, not just the Application *Service classes it already covered. ConstructorDependencyCountTestsBase gained Controllers_DoNotExceedConstructorDependencyCeiling and Handlers_DoNotExceedConstructorDependencyCeiling against Map.Api() and Map.ModuleApplication(), and this fixture is the offender each assertion must name. It is deliberately top-level, never nested, because the gate skips nested types (file doc, :10-:11).
  • Walkthrough - ConstructorDependencyCountTestsBaseTests wires it through a private FixtureMap whose controller ceiling is fixed to 2 (ConstructorDependencyCountTestsBaseTests.cs:151), so FatFixtureController's three constructor dependencies exceed it and the failure message must contain FatFixtureController (3 ctor dependencies) (:23); the same map with the ceiling raised to 3 (:116) must not flag it.
  • Where it's used - referenced by ConstructorDependencyCountTestsBaseTests (:23,:81-:82), and shares its file with a thinner one-dependency sibling controller, a payload-free marker command and handler, and the query/handler pair below, each proving a different branch of the same ceiling.

FixturePiiSubject

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.PiiInventoryFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/PiiInventoryFixtures/PiiInventoryFixtures.cs:10 · Level 0 · class

  • What it is - the data subject the personal-data inventory fixtures describe: internal sealed class FixturePiiSubject with two [Pii] string members (Email, Phone) and one unmarked int Id (PiiInventoryFixtures.cs:10-:22). Its doc names the pii-inventory-*.md files beside it as the inventories it is checked against (:5-:9).
  • Depends on - MMCA.Common.Domain.Attributes for the [Pii] marker (:1) and nothing else.
  • Concept introduced - a personal-data inventory is a two-way contract between code and a document. The inventory rule demands a row, with a lawful-basis cell, for every [Pii] member and a live [Pii] member behind every row. The unmarked Id proves the rule does not demand rows for non-personal members (:12). [Rubric §34 - Architecture Governance & Documentation] covers keeping such policy documents and code from drifting; [Rubric §14 - Testability] is why the subject is compiled rather than described.
  • Walkthrough - PiiInventoryTestsBaseTests points the rule at this assembly (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/PiiInventoryTestsBaseTests.cs:16) and asserts four failures: FixturePiiSubject.Phone has no row for a missing row (:28), an empty Lawful basis cell reported for Phone while the complete Email row is not (:38-:39), and a row for FixturePiiSubject.Fax that names no live [Pii] member (:48).
  • Where it's used - only by PiiInventoryTestsBaseTests (:11,:16,:28-:48).

FixtureShape

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.SubstitutabilityFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:9 · Level 0 · class

  • What it is - the abstract base for the substitutability fixtures: internal abstract class FixtureShape with an abstract Corners() and a virtual Describe() returning "shape" (SubstitutabilityFixtures.cs:9-:14).
  • Depends on - nothing.
  • Concept introduced - Liskov substitutability read from IL. The rule behind ArchitectureRules.SubstitutableMembersDoNotRefuseTheContract (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/SubstitutabilityRuleTests.cs:25) flags an override that throws instead of honouring what its base promised. A base that offers both an abstract and a virtual member lets the fixtures refuse each one. [Rubric §34 - Architecture Governance & Documentation] covers the property; the fixture file's doc says an IL-reading rule can only be tested honestly by compiling the cases into the assembly (:3-:8).
  • Walkthrough - it has no behavior of its own. FixtureRefusingShape overrides Describe with a NotSupportedException (:21) and must be flagged; FixtureExcludedShape refuses Corners (:52) and is silenced only when the test lists it by name (SubstitutabilityRuleTests.cs:18,:62-:69).
  • Where it's used - derived by FixtureRefusingShape (:17) and FixtureExcludedShape (:50); not referenced outside the defining file.

FixtureStandaloneThrower

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.SubstitutabilityFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:40 · Level 0 · class

  • What it is - the innocent control in the substitutability fixtures: internal static class FixtureStandaloneThrower whose Load(bool supported) throws NotSupportedException when supported is false (SubstitutabilityFixtures.cs:40-:44).
  • Depends on - nothing beyond the BCL.
  • Concept introduced - the rule's negative space. A throw is only a broken promise when something promised the operation. This method overrides nothing and implements nothing, so no caller was promised it through a base type, and the rule must leave it alone (doc, :36-:39). Without this control, a rule that flagged every throwing method would pass the other fixtures. [Rubric §14 - Testability].
  • Walkthrough - NonOverrideMethodThatThrows_IsNotFlagged runs the rule over the fixture assembly, expects it to fail for the other fixtures, and asserts the report does not contain FixtureStandaloneThrower (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/SubstitutabilityRuleTests.cs:45-:53).
  • Where it's used - referenced once, by SubstitutabilityRuleTests (:51).

GetFixtureFarQuery

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.SliceFixtures.Stranded · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/Stranded/GetFixtureFarQuery.cs:7 · Level 0 · record

  • What it is - a payload-only query record, internal sealed record GetFixtureFarQuery(int Id) (GetFixtureFarQuery.cs:7), declared in the Stranded namespace one level below the handler that serves it. Its doc names that handler, FixtureTwoContractHandlerBase, and says it is handled beside a second stranded contract (:3-:6).
  • Depends on - nothing beyond the BCL. It is the first TQuery that FixtureTwoContractHandlerBase closes IQueryHandler<TQuery, TResult> over (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/FixtureTwoContractHandlerBase.cs:13).
  • Concept introduced - a handler is checked against every contract it serves, not only the first one reflection lists. Registration wires a handler class for every handler interface it implements, so a class implementing two query handlers serves two slices, and the slice-cohesion rule has to judge each contract on its own (FixtureTwoContractHandlerBase.cs:7-:11). This record is one of the two contracts that make that observable: both live one namespace away from the handler, so a rule that stopped at the first interface would report one and miss the other. The co-located-versus-stranded idea itself is introduced under GetFixturePreferencesQuery. [Rubric §5, Vertical Slice] is the property; [Rubric §14, Testability] covers the fixture.
  • Walkthrough - one property, int Id (:7). No behavior; FixtureTwoContractHandlerBase.HandleAsync(GetFixtureFarQuery, CancellationToken) returns Result.Success() (FixtureTwoContractHandlerBase.cs:15-:16) and is never called, only reflected over.
  • Where it's used - handled by FixtureTwoContractHandlerBase (FixtureTwoContractHandlerBase.cs:13,:15), and named in the failure message that AHandlerServingTwoStrandedContracts_IsFlaggedForBoth asserts in SliceCohesionFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:62), which runs ArchitectureRules.HandlersAreCoLocatedWithTheirContracts (:59).

GetFixturePreferencesQuery

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.SliceFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/SliceFixtures.cs:13 · Level 0 · record

  • What it is - a payload-only query record, internal sealed record GetFixturePreferencesQuery(int UserId) (SliceFixtures.cs:13), that exists purely as the contract type three abstract handler shapes close over in the slice-cohesion fitness truth table.
  • Depends on - nothing beyond the BCL. It implements no interface itself; it is the TQuery that GetFixturePreferencesHandlerBase and StrandedFixtureHandlerBase close IQueryHandler<TQuery, TResult> over.
  • Concept introduced - why the slice-cohesion rule needs an abstract handler in its truth table, not just a concrete one. SliceCohesionTestsBase's rule used to read only concrete classes, so an abstract handler whose contract lived one namespace away from its declaration was invisible to it. The file's own header doc says these types are exactly what the rule started seeing once it was widened (SliceFixtures.cs:7-:11). Every handler over this query is deliberately abstract, and the query is deliberately a plain record rather than a command, so the file's fixtures never register as commands and skew the separate command-validator coverage rule, which also points a map at this assembly (:9-:11). [Rubric §5, Vertical Slice] is the property: a handler and the contract it serves belong in the same namespace so a slice reads as one cohesive unit; [Rubric §14, Testability] covers the fixture itself.
  • Walkthrough - one property, int UserId (:13). No behavior; its only role is to be the closed generic argument three handler shapes are proven against.
  • Where it's used - closed over by GetFixturePreferencesHandlerBase (SliceFixtures.cs:19) as the co-located case the rule must pass, by StrandedFixtureHandlerBase (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/Stranded/StrandedFixtureHandler.cs:11) as the one-namespace-away case the rule must catch, and registered as this assembly's Layer.Application in SliceCohesionFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:76).

GetFixtureProjectionQuery

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs:45 · Level 0 · record

  • What it is - a payload-only query record, internal sealed record GetFixtureProjectionQuery(int ProjectionId) (:45), declared immediately beside the handler that serves it.
  • Depends on - nothing beyond the BCL; it is the TQuery that GetFixtureProjectionHandler closes IQueryHandler<TQuery, TResult> over (:48-:49).
  • Concept introduced - slice cohesion as a byproduct of a different fixture's design. The file groups the query beside its handler deliberately (doc, :9), which keeps the constructor-dependency ceiling fixtures out of the separate command-validator coverage rule: as a plain record with no settable property, it is not scanned as a command and does not skew the count of data-carrying commands the file's marker command already fixes (file doc, :12-:14).
  • Walkthrough - one property, int ProjectionId (:45). No behavior; its only role is to be the closed generic argument GetFixtureProjectionHandler's three injected collaborators are proven against.
  • Where it's used - closed over by GetFixtureProjectionHandler in the same file (:48-:49); not referenced outside the defining file.

GetFixtureSecondFarQuery

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.SliceFixtures.Stranded · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/Stranded/GetFixtureSecondFarQuery.cs:4 · Level 0 · record

  • What it is - the second stranded contract, internal sealed record GetFixtureSecondFarQuery(int Id) (GetFixtureSecondFarQuery.cs:4), which its doc describes as the second contract FixtureTwoContractHandlerBase handles (:3).
  • Depends on - nothing beyond the BCL. It is the second TQuery that FixtureTwoContractHandlerBase closes IQueryHandler<TQuery, TResult> over (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/FixtureTwoContractHandlerBase.cs:13).
  • Concept introduced - cross-reference: the "every contract, not just the first" case is introduced under GetFixtureFarQuery. This record is the half that a first-interface-only rule would miss, so its presence in the failure message is the actual proof.
  • Walkthrough - one property, int Id (:4). No behavior; its handler overload returns Result.Success() (FixtureTwoContractHandlerBase.cs:18-:19) and is only reflected over.
  • Where it's used - handled by FixtureTwoContractHandlerBase (FixtureTwoContractHandlerBase.cs:13,:18), and asserted in the failure message of AHandlerServingTwoStrandedContracts_IsFlaggedForBoth in SliceCohesionFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:63).

IBadgeGranter

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:64 · Level 0 · interface

  • What it is - a one-method fixture abstraction with a single implementation, existing only so that one of the domain-event handler fixtures reaches its save through an interface call rather than a direct one.
  • Depends on - nothing. internal interface IBadgeGranter with Task GrantAsync(CancellationToken cancellationToken) (DomainEventSaveFixtures.cs:64-:67).
  • Concept introduced - why an IL walk has to resolve interface dispatch, and why a fixture must force it to. Domain event handlers run inside the save that raised the event: dispatch happens after SaveChangesAsync, or after commit inside a transactional command. A handler that saves again therefore opens a second write in the middle of the first one, re-entering the change tracker, possibly raising a fresh event cascade, and persisting work the outer transaction may still roll back (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:23-:29). Catching that means following what a handler calls, and in IL a call through an abstraction records only the interface method: callvirt IBadgeGranter::GrantAsync, with no trace of the implementation that will actually run. A walker that stopped there would report nothing. The rule therefore expands each callee to the implementations in scope through CallGraphIndex, and this interface is the fixture that makes that expansion observable. The doc says it in one line: the collaborator is an abstraction, so IL records only the interface call (:63). [Rubric §6 - CQRS & Event-Driven] is the property; [Rubric §14 - Testability] is why the fixture exists. Full mechanism at DomainEventHandlerSaveFitnessTests.
  • Walkthrough - one member, GrantAsync(CancellationToken) (:66). It takes a token but returns nothing meaningful; the signature exists only to be callable.
  • Where it's used - implemented by BadgeGranter (:70) and injected into InterfaceDispatchSavingHandler (:83).

IFixtureExporter

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.SubstitutabilityFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:25 · Level 0 · interface

  • What it is - a one-method promise, internal interface IFixtureExporter { string Export(); } (SubstitutabilityFixtures.cs:25-:28), that the next fixture breaks.
  • Depends on - nothing.
  • Concept introduced - the interface half of the substitutability rule. It checks interface implementations as well as overrides: FixtureUnfinishedExporter.Export throws NotImplementedException (:33), a placeholder that was never written, and the rule must name it (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/SubstitutabilityRuleTests.cs:34-:42). Companion of FixtureShape, which covers the override half. [Rubric §34 - Architecture Governance & Documentation].
  • Walkthrough - one member, Export() (:27), callable and nothing more.
  • Where it's used - implemented by FixtureUnfinishedExporter (:31); not referenced outside the defining file.

InjectedClockFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ClockReadFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:34 · Level 0 · class

  • What it is - a compiled-in fixture behind ClockReadTestsBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ClockReadTestsBase.cs:15) that takes a constructor-injected TimeProvider timeProvider and calls timeProvider.GetUtcNow() (ClockReadFixtures.cs:34-:37): the sanctioned shape the clock-read rule asks for.
  • Depends on - System.TimeProvider (BCL) only.
  • Concept introduced - fitness rules that read IL instead of source. ArchitectureRules.DomainAndApplicationDoNotReadTheClock (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.ClockReads.cs:59) loads each Domain/Application assembly with Mono.Cecil and scans every method body, lambdas and async state machines included since the compiler turns both into nested types, for a call to one of four ambient-clock getters: DateTime.UtcNow, DateTime.Now, DateTimeOffset.UtcNow, DateTimeOffset.Now (:20-:26, :38-:41). A clock read buried in a business rule cannot be driven by a test, so every expiry or "is it overdue" branch it guards is either untested or tested by sleeping (:32-:35). This fixture proves the rule stays silent when time is taken as an input rather than read from the ambient clock, the complement to LambdaClockReadingFixture and OffsetNowReadingFixture, which prove the rule fires. [Rubric §34 - Architecture Governance & Documentation] covers exactly this kind of self-verifying fitness function: the rule's own test fixtures are compiled, not asserted against a description of the rule.
  • Walkthrough - one method, Stamp(), returning timeProvider.GetUtcNow() (:36). Never executed; only its IL is read.
  • Why it's built this way - the file's own class-level comment states the rule reads IL, so its behavior must be pinned against real method bodies of each shape rather than against a reading of the rule (ClockReadFixtures.cs:3-:4).
  • Where it's used - exercised by ClockReadTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs), which drives ClockReadTestsBase.DomainAndApplicationDoNotReadTheClock.

LambdaClockReadingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ClockReadFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:18 · Level 0 · class

  • What it is - a compiled-in offender fixture whose Clock() method returns a Func<DateTime> that reads DateTime.UtcNow from inside a lambda body (ClockReadFixtures.cs:18-:21); the compiler moves that lambda into a nested type, so the fixture proves the clock-read rule (see InjectedClockFixture) still attributes the violation back to the declaring member.
  • Depends on - nothing beyond System.DateTime (BCL).
  • Concept introduced - cross-reference: the IL-scanning approach is introduced under InjectedClockFixture. This fixture is the "lambda closure" edge case: ClockReadsIn (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.ClockReads.cs:99) resolves a compiler-generated nested type back to the source member that declared it, so a violation inside Clock()'s lambda is still reported and allowlisted against Clock, not against an anonymous internal type name.
  • Walkthrough - one method, Clock(), returning () => DateTime.UtcNow (:20).
  • Why it's built this way - proves the rule's owner-attribution works for the lambda case, not just a plain method body.
  • Where it's used - exercised by ClockReadTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs).

LeftModelBase

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CycleFixtures.Left · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Left/LeftFixtures.cs:13 · Level 0 · class

  • What it is - one arm of a deliberately-built namespace cycle: an abstract base living in the Left fixture namespace, which a type in the Right namespace derives from. That inheritance edge is what closes the loop back into Left.
  • Depends on - nothing. It is public abstract class LeftModelBase with a single int Id { get; set; } (LeftFixtures.cs:13-:17).
  • Concept introduced - namespace cycles, and how a structural rule sees them. ArchitectureRules.NamespacesHaveNoDependencyCycles (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:45) groups a layer's types by the namespace segment directly beneath the layer's RootNamespace, records a directed edge A -> B whenever a type in A structurally references a type in B in the same assembly, and reports every strongly connected component of that graph (:12-:16). "Structurally" is precise here: base types, implemented interfaces, field and property types, method return and parameter types (public and non-public), and attribute types, with generics and array elements expanded (:25-:27). What it deliberately cannot see is a reference that exists only inside a method body, because the package carries no IL or Roslyn dependency (:30-:36), so a clean report is a statement about the type surface, not a claim of zero coupling. A base class is therefore the cheapest possible cycle edge to build: inheritance is always visible to reflection. [Rubric §34 - Architecture Governance & Documentation] assesses whether the codebase stays reasonable to reason about, and a two-namespace cycle is the classic early warning that two folders can no longer be understood, tested, or moved independently. [Rubric §7 - Microservices Readiness] is the sharper consequence: neither namespace can be extracted without the other.
  • Walkthrough - one auto-property, int Id (:16), present only so the type is not empty. Being abstract is irrelevant to the cycle rule (which walks the type surface, not ConcreteClasses); what matters is that RightModel names it as a base.
  • Why it's built this way - the fixture pair models the smallest honest cycle: one edge through a property type and one through inheritance, so the rule is proven against both edge kinds it claims to detect rather than just the easy one.
  • Where it's used - the base of RightModel; the pair drives NamespaceCycleFitnessTests through CycleTestMap.

NotAnonymizableSubject

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.AnonymizationFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AnonymizationFixtures/AnonymizationFixtures.cs:58 · Level 0 · class

  • What it is - the control entity of the anonymization fixtures: internal sealed class NotAnonymizableSubject with one Nickname property and no IAnonymizable implementation (AnonymizationFixtures.cs:58-:61).
  • Depends on - nothing. Its siblings implement IAnonymizable (:16,:37,:46,:52); this one deliberately does not.
  • Concept introduced - reverse-PII rules only judge types that can erase themselves. A plain entity with no erasure path gives neither rule anything to say (doc, :57). The file header adds that no fixture carries [Pii], because the personal-data inventory self-test scans the whole assembly and a marked fixture would need a row in every inventory fixture (:6-:9; see FixturePiiSubject). [Rubric §14 - Testability].
  • Walkthrough - AnonymizationRulesTests expects the sample-key rule to report NotAnonymizableSubject: the sample key does not implement (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/AnonymizationRulesTests.cs:86-:89) and the erasure-path report to not name it (:124).
  • Where it's used - only by AnonymizationRulesTests (:86,:111,:124,:135).

OffsetNowReadingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ClockReadFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:12 · Level 0 · class

  • What it is - a compiled-in offender fixture whose Stamp() method reads DateTimeOffset.Now directly, the local-time variant of the ambient-clock read on the other clock type (ClockReadFixtures.cs:12-:15).
  • Depends on - nothing beyond System.DateTimeOffset (BCL).
  • Concept introduced - cross-reference: the IL-scanning approach is introduced under InjectedClockFixture. This fixture is the plainest offender shape: a direct getter call in an ordinary method body, with no lambda or async indirection.
  • Walkthrough - one method, Stamp(), returning DateTimeOffset.Now (:14).
  • Why it's built this way - the baseline offender the rule must catch before the harder lambda and async-state-machine variants are proven.
  • Where it's used - exercised by ClockReadTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs).

IFakeExportService

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:25 · Level 0 · interface

  • What it is - a marker-only cross-module contract that exists purely as test data: it stands in for the kind of service one module exports and another module resolves, so the module-conformance base can be proven to reach a real DI container.
  • Depends on - nothing. The whole declaration is public interface IFakeExportService; (ModuleConformanceTestsBaseTests.cs:25), a body-less interface using the semicolon form, with a one-line doc above it (:24).
  • Concept introduced - the cross-module export contract, in miniature. Under the module system (IModule, ModuleLoader), a module that is switched off in ModulesSettings still has to leave behind stub registrations, otherwise every other module that resolves its exported interface fails to construct. IFakeExportService is the smallest possible stand-in for such an exported interface. [Rubric §14 - Testability] assesses whether a rule can be proven with focused inputs; a marker interface is the least amount of surface that still lets the fitness base observe a real ServiceDescriptor.
  • Walkthrough - no members. Its only role is to be the ServiceType that FakeDependentModule registers a stub against (:44) and that FakeDependentModuleConformanceTests looks up in the built collection (:74).
  • Why it's built this way - a real cross-module contract would drag a module's whole application surface into the framework's architecture-test assembly. A local marker keeps the fixture self-contained, which is the same discipline the rule library itself follows (it matches framework types by full name rather than referencing them).
  • Where it's used - implemented by DisabledFakeExportService and registered by FakeDependentModule.

TwoMemberClockFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ClockReadFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:40 · Level 0 · class

  • What it is - internal sealed class TwoMemberClockFixture (:40-:45) with two clock-reading methods, Exempted (:42) and StillReported (:44), both returning DateTime.UtcNow.
  • Depends on - nothing beyond System.DateTime; a leaf fixture compiled purely so the clock-read rule has real IL to scan.
  • Concept - the member-level allowlist case for the clock-read rule: with two otherwise-identical members reading the same forbidden API, an allowlist entry naming only TwoMemberClockFixture.Exempted must silence that one member and leave StillReported reported, proving the allowlist is checked per member rather than per type.
  • Walkthrough - Exempted() and StillReported() are single-expression methods, => DateTime.UtcNow;, differing only in name.
  • Where it's used - MemberAllowlistEntry_ExemptsOnlyThatMember (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:71-:57) allowlists {FixtureNamespace}.{nameof(TwoMemberClockFixture)}.Exempted and asserts the report excludes Exempted (:55) while still containing StillReported (:56).

TodayReadingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ClockReadFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:48 · Level 0 · class

  • What it is - internal sealed class TodayReadingFixture (:48-:51) with one member, Stamp(), that returns DateOnly.FromDateTime(DateTime.Today) (:50). Its doc comment calls DateTime.Today "the fifth ambient-clock getter" (:47).
  • Depends on - nothing beyond System.DateTime and System.DateOnly.
  • Concept - widens the clock-read rule's coverage past UtcNow/Now: DateTime.Today reads the ambient local clock just as DateTime.Now does, so a member that only wants a date still has to be reported. Wrapping the read in DateOnly.FromDateTime also shows the rule keys on the getter call itself, not on the member's return type.
  • Walkthrough - public DateOnly Stamp() => DateOnly.FromDateTime(DateTime.Today); (:50), the whole type.
  • Where it's used - DateTimeToday_IsFlagged (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:32-:35) asserts the report contains {nameof(TodayReadingFixture)}.Stamp reads DateTime.Today (:34), in the ClockReadTests suite.

UndecoratedFixtureController

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:128 · Level 0 · class

  • What it is - a ControllerBase fixture with a single undecorated action, the forgotten-[Authorize] shape that the undecorated-endpoint check exists to catch: no [Authorize], no [AllowAnonymous], nothing but the route (:125-:132).
  • Depends on - nothing beyond ControllerBase; it is a leaf fixture with no members other than its one action.
  • Concept - the negative case for AnonymousEndpointTestsBaseTests's strict-mode undecorated-endpoint assertions: a controller carrying no authorization decision at all is invisible to the [AllowAnonymous] allow-list, which is the gap the check closes (Base_Fails_WhenAControllerCarriesNoAuthorizationDecisionAtAll, :75-:84).
  • Walkthrough - public sealed class UndecoratedFixtureController : ControllerBase (:126) with one member, [HttpGet("undecorated")] public IActionResult ListAsync() => Ok(); (:130-:131); its doc comment describes the action as gated only by the fallback policy (:128).
  • Where it's used - Base_Fails_WhenAControllerCarriesNoAuthorizationDecisionAtAll asserts the strict-mode failure message names it via nameof(UndecoratedFixtureController) (:81).

UtcNowReadingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ClockReadFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:6 · Level 0 · class

  • What it is - internal sealed class UtcNowReadingFixture (:6-:9) with one member, Stamp(), that returns DateTime.UtcNow directly.
  • Depends on - nothing beyond System.DateTime.
  • Concept - the simplest positive case for the clock-read rule: a direct, unconditional read of DateTime.UtcNow from an ordinary instance method, with no lambda, async state machine or injected TimeProvider in the way.
  • Walkthrough - public DateTime Stamp() => DateTime.UtcNow; (:8), the whole type.
  • Where it's used - DirectReadsOfEitherClockType_AreFlagged (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:22-:29) asserts the report contains UtcNowReadingFixture.Stamp reads DateTime.UtcNow (:27).

ThinFixtureController

MMCA.Common.Architecture.Tests - MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures - MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs:24 (span 24-27) - Level 0 - class

  • What it is - internal sealed class ThinFixtureController(string only) : ControllerBase, the "one injected collaborator" member of the API-controller population compiled for ConstructorDependencyCountTestsBaseTests (:22-:27).
  • Depends on - a single primary-constructor parameter, string only, read back through internal string Collaborators => only; (:26) so it is never an unread parameter (CS9113).
  • Concept introduced - the "thin" half of a fat/thin fixture pair. ConstructorDependencyCountTestsBaseTests enforces a ceiling on injected collaborators per top-level type; the fixture file supplies one member at or under that ceiling and one over it for each population the gate covers (:8-:9). FatFixtureController is the three-collaborator sibling in the same population (:17-:20); ThinFixtureController is the counterexample that must pass where FatFixtureController must fail, so the ceiling test has a known-good and a known-bad case to distinguish rather than a single sample.
  • Walkthrough - one constructor parameter, only, surfaced through Collaborators for the same read-back reason every fixture in the file follows.
  • Why it's built this way - [ApiController] (:23) and top-level placement match exactly what the gate scans; being sealed and internal keeps the fixture out of any public surface while still being a real, compilable controller type.
  • Where it's used - paired against FatFixtureController as the API-controller population read by ConstructorDependencyCountTestsBaseTests.

RebuildFixtureProjectionCommand

MMCA.Common.Architecture.Tests - MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures - MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs:30 - Level 0 - record

  • What it is - internal sealed record RebuildFixtureProjectionCommand; (ConstructorDependencyFixtures.cs:30), a payload-free marker command in the compiled injection-shape fixtures for ConstructorDependencyCountTestsBaseTests (:763 in the group's type map).
  • Depends on - nothing; the record declares no members.
  • Concept introduced - why a fitness fixture command can be deliberately empty. The class-level doc comment on the file explains the fixtures exist to give ConstructorDependencyCountTestsBaseTests one fat and one thin top-level member of each population the ceiling covers, API controllers, command handlers and query handlers (:8-:9). Types are TOP-LEVEL on purpose because the gate skips nested types, and every injected collaborator is read back so none is an unread primary-constructor parameter (:10-:12). This command is marked payload-free specifically so the command-validator coverage rule skips it: pointing a coverage map at this assembly still counts the same five data-carrying commands it counted before the fixture file existed (:12-:14, :29).
  • Walkthrough - no fields, no properties, nothing to read back; the type exists purely to give RebuildFixtureProjectionHandler a command generic argument to close over.
  • Why it's built this way - a command with a payload would add itself to the validator-coverage count and skew an unrelated fitness assertion; a marker record sidesteps that without weakening the constructor-dependency fixture it exists to support.
  • Where it's used - the command type of ICommandHandler<RebuildFixtureProjectionCommand, Result> implemented by RebuildFixtureProjectionHandler (:33-:42), the "three collaborators" (fat) member of the command-handler population.

ResourceEntry

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/PluralSentenceResourceTests.cs:70 · Level 0 · record

  • What it is - private sealed record ResourceEntry(string Key, string Value); (:70), nested inside PluralSentenceResourceTests and documented as "one resource entry: its key and its value" (:69).
  • Depends on - nothing; two positional string members.
  • Concept - the unit of input for the fixed-plural-sentence detector. PluralSentenceResourceTests fails on a .resx value that renders a count inside a fixed plural sentence ("{0} recipients", "{0} item(s)"), which reads wrongly for a count of one; count-sensitive messages are expected to declare .One / .Other siblings resolved through the plural localizer extension (ADR-027) (:6-:14). Flattening every resource to a (Key, Value) pair lets the same detector run over real resource files and over hand-written inline cases.
  • Walkthrough - ReadEntries projects each <data> element of a .resx file into new ResourceEntry(name, value), defaulting a missing attribute or element to string.Empty (:72-:77). FixedPluralSentences(IReadOnlyList<ResourceEntry>) collects every key, skips a .Other key and the base key of a pair that declares a .Other sibling (:87-:90), then reports each value where a format placeholder is followed by a plural-marked word (:91-:94).
  • Where it's used - only inside the defining file: built by ReadEntries for the repository scan in FrameworkResources_HaveNoCountInAFixedPluralSentence (:18-:39), and constructed inline (new(key, value)) by the detector theories Detector_Flags_ACountInAFixedPluralSentence (:46-:48), Detector_Accepts_AValueThatIsNotAFixedPluralSentence (:56-:58) and Detector_Accepts_TheBaseKeyOfAPluralPair_AsItsFallback (:61-:67). PluralSentenceResourceTests itself is rolled up per project.

SwitchExpressionFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainThrowFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:115 · Level 0 · class

  • What it is - internal static class SwitchExpressionFixture (:115-:120) with one method, Label(int n), whose body is a switch expression with arms for 1 and 2 and no discard arm (:118).
  • Depends on - nothing beyond the compiler: the #pragma warning disable CS8509, IDE0072 around the method (:117, :119) is what lets an incomplete switch expression compile at all.
  • Concept - a false-positive guard for the domain-throw rule. For a switch expression with no discard arm the compiler writes its own default-arm throw of SwitchExpressionException into the method; that throw is not the developer's, so the rule must stay silent about it (doc comment, :110-:114). Without this fixture an IL-level throw scan could pass every other case and still flag every non-exhaustive switch in a domain assembly.
  • Walkthrough - internal static string Label(int n) => n switch { 1 => "one", 2 => "two" }; (:118), the whole type.
  • Where it's used - CompilerSwitchExpressionThrow_IsNotFlagged (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:72-:79) runs ArchitectureRules.DomainThrowsOnlyArgumentGuards over the fixture assembly (:73) and asserts the failure message does not contain SwitchExpressionException (:76-:78), in the DomainThrowFitnessTests suite; the rule body lives in ArchitectureRules.

ReadRepositoryFixtureQuery

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ReadRepositoryFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ReadRepositoryFixtures/ReadRepositoryFixtures.cs:12 · Level 0 · record

  • What it is - internal sealed record ReadRepositoryFixtureQuery; (ReadRepositoryFixtures.cs:12), the payload-free marker query shared by both query-handler fixtures the read-repository rule is proven against.
  • Depends on - nothing; the record declares no members.
  • Concept introduced - the read-repository fitness rule. ArchitectureRules.QueryHandlersUseReadRepositories, run through QueryHandlerReadRepositoryTestsBase.QueryHandlers_ShouldUseReadRepositories (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/QueryHandlerReadRepositoryTestsBase.cs:15, :24-:26), reads the IL of every IQueryHandler<,> implementation in the map's Application assemblies and asserts each one asks IUnitOfWork for GetReadRepository, never GetRepository: the write repository hands a query the mutation surface (add, remove, tracked reads) it must not use (:4-:8). Two handlers close over this same query to prove both directions: WriteRepositoryQueryHandlerFixture calls unitOfWork.GetRepository<ReadRepositoryFixtureAggregate, int>() after an await Task.Yield(), so the offending call sits inside a state-machine body (ReadRepositoryFixtures.cs:17-:26), and ReadRepositoryQueryHandlerFixture calls unitOfWork.GetReadRepository<ReadRepositoryFixtureAggregate, int>() directly, the sanctioned shape (:29-:37). [Rubric §34 - Architecture Governance & Documentation] covers this fitness function; the async-state-machine offender in particular pins the rule against a violation that source review alone tends to miss.
  • Walkthrough - no fields, no properties; the type exists purely to close over a query generic argument for both handler fixtures.
  • Why it's built this way - one query shared by both handlers keeps the fixture pair a controlled minimal-difference comparison: the only variable between them is which repository accessor is called.
  • Where it's used - the query type of IQueryHandler<ReadRepositoryFixtureQuery, Result<int>> implemented by both WriteRepositoryQueryHandlerFixture and ReadRepositoryQueryHandlerFixture (ReadRepositoryFixtures.cs:16-:37), which close over ReadRepositoryFixtureAggregate (:10) as the repository's entity type argument.

PasswordProbe

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/PasswordRuleParityTests.cs:54 · Level 0 · record

  • What it is - private sealed record PasswordProbe(string Password); nested inside PasswordRuleParityTests (PasswordRuleParityTests.cs:54), documented as "the smallest model the server rule can be bound to" (:53). It gives both the client attribute and the server validator a real object to evaluate one password against.
  • Depends on - nothing; a positional record with one string property.
  • Concept introduced - client/server validation parity. The Register form validates a password with PasswordComplexityAttribute while the API validates it with StrongPasswordRules<T>; if the two disagree, the form either rejects a password the API would accept or lets one through that the API refuses after a round trip (PasswordRuleParityTests.cs:7-:12). The test lives in the architecture test project because it is the one project referencing both sides: the UI test project references MMCA.Common.UI only, and UI may depend on Shared alone (:14-:15). [Rubric §24 - Forms, Validation & UX Safety] assesses whether client-side validation matches the server's verdict; this probe is what lets a single theory compare the two verdicts directly rather than trusting they were written alike.
  • Walkthrough - the theory ClientPasswordComplexity_MatchesServerStrongPasswordRules builds one probe per row (:38), then computes the client verdict the way an EditForm does, calling GetValidationResult with a ValidationContext(probe) whose MemberName is nameof(PasswordProbe.Password) (:41-:43), and the server verdict by binding new StrongPasswordRules<PasswordProbe>(static p => p.Password) and calling Validate(probe).IsValid (:44-:46). The two must be equal (:48-:50). The twelve InlineData rows cover ASCII valid and invalid cases plus accented, umlaut, CJK and full-width-digit inputs (:24-:35), written as escapes so the source stays ASCII (:16-:18).
  • Why it's built this way - the server rule is a FluentValidation validator over a model, so it needs a type with a property selector to bind to; a private nested record is the least surface that satisfies that without borrowing a real request DTO.
  • Where it's used - only inside PasswordRuleParityTests (no usages outside the defining file).

ReadRepositoryFixtureCommand

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ReadRepositoryFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ReadRepositoryFixtures/ReadRepositoryFixtures.cs:14 · Level 0 · record

  • What it is - internal sealed record ReadRepositoryFixtureCommand; (ReadRepositoryFixtures.cs:14), a payload-free marker command that gives WriteRepositoryCommandHandlerFixture a generic argument; it exists so a command handler using the write repository sits in the fixture set as the out-of-scope control case.
  • Depends on - nothing; the record declares no members.
  • Concept introduced - cross-reference to the compiled-fixture pattern introduced under InjectedClockFixture: the class-level comment on this file states the read-repository rule reads IL, so its behavior is pinned against real handler bodies rather than executed (ReadRepositoryFixtures.cs:8-:9; none of the fixtures is ever run).
  • Walkthrough - no fields, no properties; the type exists purely to close over a command generic argument.
  • Why it's built this way - QueryHandlerReadRepositoryTestsBase.QueryHandlers_ShouldUseReadRepositories (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/QueryHandlerReadRepositoryTestsBase.cs:15, :24-:26) only walks IQueryHandler<,> implementations; a command handler on the write repository is deliberately out of scope, so this fixture proves the rule leaves it alone.
  • Where it's used - the command type of ICommandHandler<ReadRepositoryFixtureCommand, Result> implemented by WriteRepositoryCommandHandlerFixture (ReadRepositoryFixtures.cs:40-:49).

CustomExceptionThrowingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainThrowFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:67 · Level 1 · class

  • What it is - a one-method static fixture that throws the file's custom domain exception, so the domain-throw rule can be shown to judge by constructed type rather than by a list of framework exception names.
  • Depends on - TicketDomainException (DomainThrowFixtures.cs:69).
  • Concept - cross-references the domain-throw concept introduced by its sibling fixtures. What this one adds is the discrimination that matters most in practice: teams that adopt the Result pattern often keep a DomainException hierarchy as a compromise, and a rule built around a deny-list of BCL exception names would wave it through. The rule instead allows exactly three types and reports everything else (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:19-:21,:132-:135), so a custom exception is caught for what it is. The fixture doc puts it in one sentence: the same defect wearing a domain name (:48).
  • Walkthrough - the whole type is internal static class CustomExceptionThrowingFixture (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:67) with one member, internal static void Reject() => throw new TicketDomainException("The ticket was rejected"); (:69). The newobj immediately precedes the throw, so ConstructedExceptionType names it and the site is a hard violation rather than an UNVERIFIABLE.
  • Where it's used - the subject of CustomDomainException_IsAlsoFlagged (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:49-:57), and the residual offender the allowlist facts assert is still reported when only its sibling is exempted (:114-:126).

AnonymousPage

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:74 · Level 1 · class

  • What it is - public sealed class AnonymousPage; (RouteAuthorizationTestsBaseTests.cs:74), a body-less fixture page carrying [Route("/fixture/anonymous")] (:73) and no [Authorize] attribute at all.
  • Depends on - nothing; only the Route attribute from Microsoft.AspNetCore.Components (:2).
  • Concept - the "no authorize attribute" negative case for the role check. RouteAuthorizationTestsBase.RequiresRole answers whether any of a page's own or inherited AuthorizeAttributes names exactly the required role (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:86-:91); a page with no attribute has nothing to match, so the answer must be false.
  • Walkthrough - no members. It is the last [InlineData(typeof(AnonymousPage), false)] row of RequiresRole_ReadsEveryAuthorizeAttribute (:25-:27), which calls RoleProbe.Requires(page, AdminRole) and asserts the expected boolean.
  • Where it's used - only inside the defining file, as that theory row (:25). It is not a governed page of either StackedAttributeTests (:93-:96) or the missing-role subclass.

BareOnlyPage

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:66 · Level 1 · class

  • What it is - public sealed class BareOnlyPage; (RouteAuthorizationTestsBaseTests.cs:66), a body-less fixture page with [Route("/fixture/bare-only")] and a bare [Authorize] (:64-:65), so it requires a signed-in user but names no role.
  • Depends on - nothing; Authorize from Microsoft.AspNetCore.Authorization (:1).
  • Concept - a bare [Authorize] is not a role gate. RequiresRole compares each attribute's Roles property to the required role with ordinal equality (RouteAuthorizationTestsBase.cs:86-:91), so an attribute with Roles unset never satisfies it. This page pins that boundary, and its sibling BareOverRoleGatedPage (:60-:62) shows the opposite: a bare attribute on a derived page does not hide the inherited [Authorize(Roles = "Admin")] of RoleGatedPage.
  • Walkthrough - no members. It is the [InlineData(typeof(BareOnlyPage), false)] row of RequiresRole_ReadsEveryAuthorizeAttribute (:23, :26-:27), expected to report no role requirement.
  • Where it's used - only inside the defining file, as that theory row (:23).

CreateTicketCommandValidator, UpdateTicketRequestValidator

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CommandValidatorFixtures · (see per-type table) · Level 1 · class

Two one-line FluentValidation validators, identical in shape and different only in what they validate. Between them they supply both forms of coverage the command-validation rule accepts: a validator for the command itself, and a validator for the request a bridged command embeds.

  • Depends on - FluentValidation.AbstractValidator<T> (CommandValidatorFixtures.cs:1) and the record each one validates.
  • Concept introduced - direct coverage versus bridge coverage, and why the bridge needs both halves. A command is covered when a concrete IValidator<TCommand> exists for it, or when the command implements ICommandWithRequest<TRequest> and a concrete IValidator<TRequest> exists (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:112-:115). The second condition insists on both halves for a reason the rule doc spells out (:23-:29): CommandRequestValidator<TCommand, TRequest> is auto-registered for every bridged command, so a naive check for "is a validator registered" would always say yes, while the bridge adds no rule at all when no request validator resolves. Half a bridge is a validator that validates nothing. The rule also reads commands and validators from the same place the framework's AddValidatorsFromAssembly scan looks, the per-module Application assemblies (:32-:36), because a validator living anywhere else would not resolve at run time either. [Rubric §24 - Forms/Validation/UX Safety] and [Rubric §6 - CQRS & Event-Driven] are the properties; [Rubric §14 - Testability] covers the fixtures.
  • Walkthrough - both are internal sealed, both declare an internal constructor with a single expression-bodied rule, and both use NotEmpty() on the one string property. Nothing about the rule content matters to the fitness function; the existence of a closed IValidator<T> implementation is the whole signal.
Type File:Line What it covers
CreateTicketCommandValidator MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:16 AbstractValidator<CreateTicketCommand> with RuleFor(c => c.Title).NotEmpty() (:18). Direct coverage: the validator names the command itself.
UpdateTicketRequestValidator MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:33 AbstractValidator<UpdateTicketRequest> with RuleFor(r => r.Title).NotEmpty() (:35). Bridge coverage: it names the request, and UpdateTicketCommand is covered only through it.
  • Why they're built this way - having exactly one validator on each side of the bridge is what lets a single rule execution produce a positive result for both coverage forms while ReopenTicketCommand, whose request deliberately has none, still fails. Adding a second rule to either validator would prove nothing extra.
  • Caveats / not-in-source - the rule's own limits section records two behaviors these fixtures do not exercise: a validator declared for a base command type does not count for its derived commands (matching FluentValidation's closed-type resolution), and abstract validators are ignored because nothing registers them (ArchitectureRules.CommandValidators.cs:37-:41).
  • Where they're used - discovered by ValidatedTypes (ArchitectureRules.CommandValidators.cs:105-:109) during every fact of CommandValidatorCoverageFitnessTests.

UpdateTicketRequestValidator

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CommandValidatorFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:33 · Level 1 · class

  • What it is - the validator that makes ReopenTicketCommand, UpdateTicketCommand's bridge half a real hit: an AbstractValidator<UpdateTicketRequest> for the request UpdateTicketCommand wraps, not for the command itself.
  • Depends on - FluentValidation's AbstractValidator<T> (:1,:33) and UpdateTicketRequest (:28), the record it validates.
  • Concept introduced - the request bridge only counts if a validator actually resolves for the wrapped request type. CommandRequestValidator reaches the command's request through ICommandWithRequest<out TRequest> and asks whether ValidatedTypes contains it; this class is what makes that lookup non-empty for UpdateTicketRequest, so UpdateTicketCommand reads as covered even though it declares no validator of its own. ReopenTicketCommand is the same shape with no validator at all, and the pair only proves the rule because they differ in exactly this one respect.
  • Walkthrough - one rule, RuleFor(r => r.Title).NotEmpty() (:35), applied through the constructor.
  • Where it's used - resolved by name in CommandCoveredThroughTheRequestBridge_IsCovered (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:44-:53), which asserts ValidatedTypes contains UpdateTicketRequest and that UpdateTicketCommand is reported covered.

NoModuleAssemblies, NoOutboxSignal

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · (see per-type table) · Level 1 · class

Two private, minimal interface stand-ins that let DeleteBehaviorConventionTests build a real EF model without wiring the framework's full DI graph.

  • Depends on - IEntityConfigurationAssemblyProvider and IOutboxSignal respectively (DeleteBehaviorConventionTests.cs:102,:135).
  • Concept - cross-references the minimal-DI-graph pattern this fixture-only DbContext test shares with its siblings NoEntityDataSources and NoDomainEventDispatcher: a context built only to inspect its finalized model has no need for real module assemblies or a live outbox signal, so it satisfies the two interfaces with the smallest possible bodies rather than resolving the production implementations.
  • Walkthrough - NoModuleAssemblies implements GetConfigurationAssemblies() by returning an empty array (:102-:105), so the model built from it carries only the framework's own tables, not any module's. NoOutboxSignal implements both IOutboxSignal members as no-ops: Signal() does nothing (:135-:139, with a comment stating why: this context is built for its model and never saves) and WaitAsync returns Task.CompletedTask (:148).
  • Where it's used - NoModuleAssemblies is reused outside its defining file by RestrictDeleteByDefaultConventionTests and EFRawSqlQueryExecutorTests (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/Conventions/RestrictDeleteByDefaultConventionTests.cs, MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFRawSqlQueryExecutorTests.cs), each twice; NoOutboxSignal is used only within its own defining file, wired into DomainEventSaveChangesInterceptor's constructor (DeleteBehaviorConventionTests.cs:95).
Type File:Line Interface implemented
NoModuleAssemblies MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:102 IEntityConfigurationAssemblyProvider, empty assembly list.
NoOutboxSignal MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:135 IOutboxSignal, both members no-op.

NoServices

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance.Deployment · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/MessageBusBackpressureTestsBaseTests.cs:59 · Level 1 · class

  • What it is - a private, sealed negative-control subclass nested inside MessageBusBackpressureTestsBaseTests. It derives from MessageBusBackpressureTestsBase and overrides the one abstract member, Services, with an empty list (MessageBusBackpressureTestsBaseTests.cs:59-:62).
  • Depends on - MessageBusBackpressureTestsBase, whose abstract IReadOnlyList<string> Services (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/MessageBusBackpressureTestsBase.cs:52) names the service appsettings files the gate checks.
  • Concept - a gate that checks nothing must fail, not pass. The base's single fact opens by asserting Services is not empty (MessageBusBackpressureTestsBase.cs:70-:72) before it loops over the services. A consumer that forgot to list any service, or a refactor that emptied the list, would otherwise get a vacuously green broker-backpressure gate. NoServices is the minimal subclass that exercises that guard.
  • Walkthrough - the outer test EmptyServiceList_FailsRatherThanCheckingNothing instantiates NoServices and takes its inherited fact EveryServiceAppSettings_BoundsBrokerBackpressure as a delegate, then asserts it throws with the reason "a gate that lists no service checks nothing" (MessageBusBackpressureTestsBaseTests.cs:51-:57). Invoking the fact on a hand-built instance, rather than letting xUnit discover it, is what lets a failing fact be asserted on instead of failing the run.
  • Where it's used - only inside its defining file; it has no consumers outside MessageBusBackpressureTestsBaseTests.

FixtureGoodFeatures

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.FeatureFlagFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/FeatureFlagFixtures/FeatureFlagFixtures.cs:11 · Level 1 · class

  • What it is - the negative-control half of the feature-flag lifecycle fixture set: a static class holding two constants that are declared exactly right, one Permanent and one Temporary with a RemoveBy far enough in the future to stay green (:14-:19). Nothing points the framework's own FeatureFlagLifecycleTests at this assembly; it scans MMCA.Common.Shared instead (class doc, :6-:9).
  • Depends on - [FeatureFlag] (MMCA.Common.Shared.FeatureFlags, :1,:14,:18), the same attribute ArchitectureRules's lifecycle rules read.
  • Concept introduced - a rule that only ever fails proves nothing about the passing case. Its siblings FixtureBadFeatures and FixtureExpiredFeatures each carry one deliberate violation, but a rule tuned to fire on every input would also fire here, so FixtureGoodFeatures is what lets the adversarial tests assert absence: the declaration rule's failure message must NOT name either of its two constants, and the expiry rule's failure message must not name LiveTemporaryFlag even though it is Temporary, because its window has not closed. [Rubric §14 - Testability] is the property; ADR-031 is the lifecycle policy under test.
  • Walkthrough - PermanentFlag = "Fixture.Permanent" (:15), tagged [FeatureFlag(FeatureFlagLifetime.Permanent, Owner = "Fixtures")]; LiveTemporaryFlag = "Fixture.LiveTemporary" (:19), tagged [FeatureFlag(FeatureFlagLifetime.Temporary, RemoveBy = "2099-12-31", Owner = "Fixtures")]. Both are the shape the declaration rule requires, and the far-future date is what keeps the second one out of the expiry rule's reach.
  • Where it's used - asserted absent from DeclarationRule_LeavesCorrectlyDeclaredFlagsAlone's message (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:60-:67) and from ExpiryRule_FlagsATemporaryFlagPastItsRemoveBy_NamingTheDateAndTheOwner's message (:70-:82).

InheritingFitnessController

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:81 · Level 1 · class

  • What it is - a body-less concrete controller that inherits AbstractFitnessControllerBase and, with it, the base's declared POST action. It is the fixture for the "covered by inheritance" branch of the idempotency gate.
  • Depends on - AbstractFitnessControllerBase (public sealed class InheritingFitnessController : AbstractFitnessControllerBase;, IdempotencyFitnessTests.cs:81).
  • Concept introduced - inherited attributes are the reason a base-class action does not have to be redeclared. Reflection over a derived type reports the base's CreateInheritedAsync as one of its public methods, and the rule reads attributes with inherit: true (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:80-:81), so the [Idempotent] sitting on the base action covers every subclass. That is not a detail of the fixture: in the real framework, a concrete controller deriving from AuthControllerBase or AggregateRootEntityControllerBase satisfies the gate through those bases (ArchitectureRules.Idempotency.cs:31-:34), which is the only reason the rule is not a wall of duplicate annotations. This fixture is what pins that behavior. [Rubric §9 - API & Contract Design] and [Rubric §14 - Testability] apply, as for its sibling fixtures.
  • Walkthrough - no members at all; the declaration ends at its semicolon (:81). Being concrete is what puts it into the rule's ConcreteClasses scan while its abstract base stays out.
  • Where it's used - asserted absent from the rule's failure message by Rule_AcceptsDirectAndInheritedAndOptedOutDeclarations (IdempotencyFitnessTests.cs:34-:36).

InheritingFixtureController

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:157 · Level 1 · class

  • What it is - a body-less concrete controller deriving AbstractAnonymousFixtureControllerBase. It is the fixture for the ruling that an inherited [AllowAnonymous] is reported once, at the base that declares it, and never again on the derived type.
  • Depends on - AbstractAnonymousFixtureControllerBase (public sealed class InheritingFixtureController : AbstractAnonymousFixtureControllerBase;, AnonymousEndpointTestsBaseTests.cs:157).
  • Concept introduced - the same inheritance question, answered the opposite way from the idempotency gate. InheritingFitnessController exists because the idempotency rule reads attributes with inherit: true, so a base declaration covers every subclass. The anonymous-endpoint scan does the reverse: it reads methods with BindingFlags.DeclaredOnly and attributes with inherit: false (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:264,:146). Both choices are correct for their own rule, and the reason is who writes the allow-list. Idempotency intent is declared by the framework and must flow down to consumers, so inherited reads are what stop a wall of duplicate annotations. An anonymous endpoint must be reviewed, and if the scan reported the framework's base action once per derived controller, every consumer repo would have to re-approve the framework's six credential-exchange endpoints under its own type names, forever. The base's own comment states exactly that (:140-:141). [Rubric §11 - Security] is the property; [Rubric §33 - Developer Experience] is the reason for the shape, since a gate that demands the same approval in every downstream repo gets rubber-stamped rather than read.
  • Walkthrough - no members; the declaration ends at its semicolon (:94). Being concrete puts it in the scan (IsController walks the base chain, AnonymousEndpointTestsBase.cs:159-:112), and the DeclaredOnly filter is what keeps it silent: it declares no methods of its own, so AnonymousEndpointsOf yields nothing for it.
  • Where it's used - the subject of Base_DoesNotReport_AnInheritedAttributeOnTheDerivedController (AnonymousEndpointTestsBaseTests.cs:61-:71), which reads ConformantTests's scan output and asserts it does not contain {InheritingFixtureController.FullName}.InheritedAnonymousAsync (:69-:70). The inline comment there names the consequence being prevented (:64-:66).

HighFloorTemplate

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance.Deployment · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/CostTagConventionTestsBaseTests.cs:59 · Level 1 · class

  • What it is - a private sealed nested subclass of CostTagConventionTestsBase inside CostTagConventionTestsBaseTests (:59-:66). It supplies the three abstract members: MinimumContainerApps => 3 (:61), DatabaseNamePrefix => "Fixture_" (:63) and BicepResource => "fixtures.costtag-main.bicep" (:65).
  • Depends on - CostTagConventionTestsBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs:36, the abstract MinimumContainerApps) and the embedded template fixtures.costtag-main.bicep, the same one the outer class points at (CostTagConventionTestsBaseTests.cs:17).
  • Concept - the floor-guard negative case. The outer class declares a floor of 2 (:13) against that template and passes; this subclass raises the floor to 3 against the same template, so the base discovers fewer container apps than demanded. The test's own reason string says why that must fail: discovering fewer apps than the floor means the parse anchor drifted, so the gate must not pass vacuously (:37-:38).
  • Walkthrough - nothing but the three property overrides; the only difference from the passing configuration is the 3 against 2. Being private, it is invisible to xUnit's discovery, so only the test that instantiates it runs it.
  • Where it's used - TooFewContainerApps_FailsTheFloor (CostTagConventionTestsBaseTests.cs:33-:39) builds new HighFloorTemplate().EveryContainerApp_CarriesAServiceTag and asserts it throws. No usages outside the defining file.

FixtureAiConsumer

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.AiBoundaryFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AiBoundaryFixtures/FixtureAiConsumer.cs:9 · Level 1 · class

  • What it is - a public static fixture class whose one job is to name a governed MMCA.Common.AI type from inside the architecture test assembly, so a map that treats that assembly as a non-Infrastructure layer must fail GovernedAiPackageStaysBehindInfrastructure (class doc, FixtureAiConsumer.cs:5-:8).
  • Depends on - PromptContract (using MMCA.Common.AI;, FixtureAiConsumer.cs:1; parameter type at :14).
  • Concept introduced - a boundary rule needs a real violator to prove it matches anything. ArchitectureRules.GovernedAiPackageStaysBehindInfrastructure walks every layer of the map whose assembly is not at the model boundary and asserts its types have no dependency on GovernedAiPackageNamespace (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Ai.cs:75-:90), where that constant is the undotted "MMCA.Common.AI" (:25). An assembly counts as the model boundary when its name contains .Infrastructure, equals MMCA.Common.AI, or starts with the governed package prefix (:99-:106); MMCA.Common.Architecture.Tests is none of those, so a type in it that names PromptContract is a genuine violation. The test class doc records why the fixture exists: the rule once matched a dotted entry that NetArchTest never matches, because it compares whole namespace segments, so the gate passed vacuously in every subclass (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/GovernedAi/AiBoundaryRulesTests.cs:7-:12). [Rubric §14 - Testability] is the property: the fixture keeps AiDependencyIsolationTestsBase, which calls the same rule, from going green by scanning nothing.
  • Walkthrough - one member, NameOf(PromptContract contract) (FixtureAiConsumer.cs:14-:18): it guards the argument with ArgumentNullException.ThrowIfNull and returns contract.Name. The body is deliberately trivial; reading the contract's name "is all it takes to depend on the package" (:11), and a parameter typed as a governed type is the dependency the rule must see.
  • Where it's used - only by AiBoundaryRulesTests. ALayerNamingAGovernedAiType_FailsTheRule passes typeof(FixtureAiConsumer).Assembly into a private one-layer ArchitectureMapBase subclass, SingleLayerMap, that declares the assembly as the framework's Application layer (AiBoundaryRulesTests.cs:18-:19, :36-:41), then asserts the failure message contains "must not reference MMCA.Common.AI" and nameof(FixtureAiConsumer) (:21-:23). Its sibling fact runs the same map over the MMCA.Common.API assembly and asserts no throw, pinning that API is not mistaken for the AI segment (:26-:33).

FixtureBadFeatures, FixtureExpiredFeatures, FixtureGoodFeatures

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.FeatureFlagFixtures · (see per-type table) · Level 1 · class

Three static fixture classes carrying [FeatureFlag]-annotated constants for FeatureFlagLifecycleTests: one class of constants that must pass the lifecycle rule, one of constants that must each fail it for a distinct reason, and one that is expired.

  • Depends on - FeatureFlagAttribute and FeatureFlagLifetime (MMCA.Common.Shared.FeatureFlags, FeatureFlagFixtures.cs:11-:41).
  • Concept - cross-references the flag-lifecycle concept introduced by FeatureFlagLifecycleTestsBase (Bases/Governance/FeatureFlagLifecycleTestsBase.cs:10): every flag constant must declare [FeatureFlag(Permanent|Temporary, RemoveBy, Owner)], and a temporary flag past its RemoveBy date fails the build (ADR-031). These three classes are the fixture matrix FeatureFlagLifecycleTests runs the rule against: one constant per violation the rule is meant to catch, plus the shapes that must stay silent.
  • Walkthrough - FixtureGoodFeatures (:11-:20) has two constants that must pass: PermanentFlag, declared Permanent with no RemoveBy (:13-:14), and LiveTemporaryFlag, Temporary with a RemoveBy far enough out to stay green, 2099-12-31 (:16-:18). FixtureBadFeatures (:23-:41) has four constants that must each fail for a different reason: UnannotatedFlag carries no [FeatureFlag] at all (:28-:29); PermanentWithRemoveByFlag is Permanent yet still names a RemoveBy date, a contradiction (:31-:33); TemporaryWithoutRemoveByFlag is Temporary with no RemoveBy, which the rule treats as permanent in everything but name (:35-:37); TemporaryWithUnparseableRemoveByFlag is Temporary with a RemoveBy in an order the rule cannot parse, "31/12/2030" (:39-:41). FixtureExpiredFeatures (:42-:47) has one constant, ExpiredFlag, Temporary with RemoveBy = "2020-01-01", which is what the expiry check exists to catch (:44-:46).
  • Why it's built this way - splitting the matrix into good/bad/expired classes rather than one flat list lets each FeatureFlagLifecycleTests fact read only the constants relevant to it, so adding a new failure mode to FixtureBadFeatures cannot accidentally break a fact that only reads FixtureGoodFeatures.
Type File:Line Role
FixtureGoodFeatures MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/FeatureFlagFixtures/FeatureFlagFixtures.cs:11 Two constants that must pass the lifecycle rule.
FixtureBadFeatures MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/FeatureFlagFixtures/FeatureFlagFixtures.cs:23 Four constants, each failing for a distinct reason (unannotated, contradictory, dateless-temporary, unparseable date).
FixtureExpiredFeatures MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/FeatureFlagFixtures/FeatureFlagFixtures.cs:42 One constant past its RemoveBy date.
  • Where it's used - all three are read across multiple facts of FeatureFlagLifecycleTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs), which asserts each fixture's constants land in the rule's expected pass/fail buckets.

FixtureDomainEvent

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:13 · Level 1 · record

  • What it is - the single throwaway domain event every handler fixture in DomainEventSaveFixtures is generic over. It carries no meaning; it exists so the four fixture handlers can implement a real, closed IDomainEventHandler<T>.
  • Depends on - IDomainEvent (public sealed record FixtureDomainEvent(DateTime DateOccurred, Guid MessageId) : IDomainEvent;, DomainEventSaveFixtures.cs:13, with the interface import at :3).
  • Concept - cross-references the handler-save concept introduced by IBadgeGranter. Its own contribution is the closure: the rule finds handlers by asking CallGraphIndex whether a type implements the open interface MMCA.Common.Application.Interfaces.IDomainEventHandler1 matched by full name with Cecil's arity marker (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:8-:9,:96`), so any closed instantiation works and one event type is enough for the whole fixture set.
  • Walkthrough - two positional members, DateTime DateOccurred and Guid MessageId, which are the two members IDomainEvent requires. It is the only public type in the file; the handlers around it are all internal.
  • Where it's used - the type argument of DirectSavingHandler, TransitiveSavingHandler, InterfaceDispatchSavingHandler and InnocentHandler, and the assembly anchor of DomainEventHandlerSaveFitnessTests's FixtureAssemblyMap (DomainEventHandlerSaveFitnessTests.cs:116).

FixtureExcludedShape

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.SubstitutabilityFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:50 · Level 1 · class

  • What it is - the exclusion control among the compiled IL fixtures for the substitutability rule: an internal sealed subclass of FixtureShape whose Corners() override throws NotSupportedException("Excluded by name in the test.") (SubstitutabilityFixtures.cs:50-:53). It refuses an abstract member exactly like FixtureRefusingShape, but the test lists that member as reviewed.
  • Depends on - FixtureShape, the abstract base declared in the same file (:9-:14, abstract Corners() at :11), and ArchitectureRules.SubstitutableMembersDoNotRefuseTheContract, which takes a FrozenSet<string> of excluded members (SubstitutabilityRuleTests.cs:25).
  • Concept - an exclusion list silences exactly the member it names. The rule reads IL, so the refusal has to be real compiled code. ExcludedMember_IsNotFlagged builds the key {FullName}.Corners (SubstitutabilityRuleTests.cs:18), proves the rule reports it with an empty exclusion set (:58-:60), then proves it disappears when the set names it while FixtureRefusingShape's Describe is still reported (:62-:69).
  • Walkthrough - one override, Corners() => throw new NotSupportedException(...); no other members.
  • Where it's used - only SubstitutabilityRuleTests.cs:18 (the excluded-member key), through the fixture assembly map at :73-:81.

FixtureRefusingShape

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.SubstitutabilityFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:17 · Level 1 · class

  • What it is - the positive control for an override that refuses its base: an internal sealed subclass of FixtureShape whose Corners() returns 4 (honoured) and whose Describe() throws NotSupportedException("A refusing shape cannot describe itself.") (SubstitutabilityFixtures.cs:17-:22).
  • Depends on - FixtureShape (:9-:14), whose virtual Describe() returns "shape" (:13), so the override really does replace a working default with a refusal.
  • Concept - only the refusal is reported, not the whole type. Because Corners() is honoured and Describe() is not, the assertion message.Should().Contain("FixtureRefusingShape.Describe") (SubstitutabilityRuleTests.cs:29) pins that the rule flags the member and names the exception, System.NotSupportedException (:30).
  • Walkthrough - two overrides, one conforming (:19) and one throwing (:21).
  • Where it's used - SubstitutabilityRuleTests.cs:29 and :69 (it must still be reported after the exclusion) and :79, where its assembly anchors the fixture map's Domain layer.

FixtureUnfinishedExporter

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.SubstitutabilityFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:31 · Level 1 · class

  • What it is - the positive control for an interface member left as a placeholder: an internal sealed implementation of IFixtureExporter (:25-:28) whose Export() throws NotImplementedException (:31-:34).
  • Depends on - IFixtureExporter, the one-member interface declared in the same file (:25).
  • Concept - the rule covers interface implementations as well as overrides. InterfaceImplementationThrowingNotImplemented_IsFlagged asserts the report names FixtureUnfinishedExporter.Export and System.NotImplementedException (SubstitutabilityRuleTests.cs:40-:41). The negative counterpart is FixtureStandaloneThrower.Load (SubstitutabilityFixtures.cs:40-:44), which throws but overrides and implements nothing, so no caller was promised it and the rule must not flag it (SubstitutabilityRuleTests.cs:45-:53).
  • Walkthrough - a single expression-bodied member.
  • Where it's used - only SubstitutabilityRuleTests.cs:40 (the name assertion).

EditorRequiredParameterConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/EditorRequiredParameterConventionTests.cs:26 · Level 1 · class

  • What it is - a framework-only component-contract gate (rubric section 18, per the class doc at :5-:9): every public [Parameter] on a public MMCA.Common.UI component whose type is a non-nullable reference type must carry [EditorRequired], so the Razor compiler flags a call site that omits it (RZ2012) instead of the component meeting null at render time.
  • Depends on - UISharedAssemblyReference (the reflection anchor for the shared UI assembly, EditorRequiredParameterConventionTests.cs:104), plus externals: ASP.NET Core IComponent, ParameterAttribute, EditorRequiredAttribute, CascadingParameterAttribute, SupplyParameterFromQueryAttribute, RenderFragment and RenderFragment<T> (:1, :105-:109, :119-:120), System.Reflection.NullabilityInfoContext (:72, :125), xUnit [Fact] and AwesomeAssertions.
  • Concept introduced - nullability annotations as a component contract. A non-nullable reference parameter is a promise that the caller supplies a value; [EditorRequired] is what turns that promise into a compile-time diagnostic at the call site. Reflection reads the nullability the compiler emitted, so the gate can find every parameter whose type says "never null" but whose attributes do not ask the caller for it. What reflection cannot see is a property initializer, so a parameter that is genuinely optional because its initializer supplies a real default is listed by name with the default it relies on; an initializer that only guards against null (string.Empty, []) is not a default and is not listed (class doc, :17-:23). [Rubric §18 - UI Architecture] is the category the class doc names; [Rubric §14 - Testability] applies because the gate also proves its own scan and allow-list stay live.
  • Walkthrough - one private set and three [Fact]s.
    • ParametersWithInitializerDefaults (:31-:62) is an ordinal HashSet<string> of ten Component.Property keys, each preceded by a comment naming its default: ApiFileDownloadButton.ContentType ("application/octet-stream"), ApiFileDownloadButton.HttpClientName ("APIClient"), ApiFileDownloadButton.Icon, ClickableCard.Class, EmptyState.Icon, ErrorSummary.Class, MmcaThemeProviders.Theme (MMCATheme.Instance), the EmptyIcon of MobileCardList1andMobileInfiniteScrollList1, and UserAdminList1.AssignableRoles` (an empty list). Generic components appear under their reflection names with the arity marker.
    • Components_AreDiscovered_GateIsNotVacuous (:64-:67) asserts the scan finds at least one parameter, so a drifted anchor cannot let the gate pass having scanned nothing.
    • NonNullableReferenceParameters_ShouldBe_EditorRequired (:69-:86) keeps every discovered parameter that is a non-nullable reference type and lacks [EditorRequired] (read with inherit: false), keys it as DeclaringType.Name.PropertyName, drops the allow-listed keys, orders the rest ordinally and asserts the list is empty; the failure message names the three remedies (add [EditorRequired], make it nullable, or list it with its initializer default) and every offender.
    • InitializerDefaultAllowList_HasNoStaleEntries (:88-:97) asserts every allow-list key still names a live parameter, since an entry for a parameter that no longer exists hides nothing.
    • Two private helpers. ComponentParameters (:103-:109) takes the public classes in the shared UI assembly that implement IComponent, their public instance properties declared on the type itself (BindingFlags.DeclaredOnly), and keeps those marked [Parameter] but not [CascadingParameter] or [SupplyParameterFromQuery]. IsNonNullableReference (:111-:126) excludes value types, generic parameters and RenderFragment / RenderFragment<T>, then returns whether NullabilityInfoContext.Create(property).WriteState is NotNull.
  • Why it's built this way - the scope exclusions are each a ruling in the class doc (:10-:15): value types cannot be null; an absent RenderFragment or EventCallback legitimately means "nothing to render / nobody listening" (EventCallback is a struct, so the value-type test already drops it); an unconstrained generic parameter's nullability belongs to the caller's type argument; cascading and query-supplied members are supplied by the framework, not the caller. The doc also names two initializer guards it refuses to exempt, PageHeader.Title (an empty level-one heading) and MobileCardList.Items (the list's data), as gaps rather than exemptions (:22-:23). The stale-entry fact is what keeps the allow-list from outliving the components it excuses.
  • Where it's used - an independent class in the MMCA.Common.Architecture.Tests suite with no usages outside its defining file. It guards only the framework's own shared UI assembly, so it has no Store or ADC counterpart.

DisabledFakeExportService

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:28 · Level 1 · class

  • What it is - the stub implementation a disabled FakeDependentModule leaves behind, so the fixture models the real "module off, contract still resolvable" behavior rather than describing it.
  • Depends on - IFakeExportService (public sealed class DisabledFakeExportService : IFakeExportService;, ModuleConformanceTestsBaseTests.cs:28).
  • Concept - cross-references the disabled-stub concept introduced by IFakeExportService. The pairing matters: a stub type distinct from the real implementation is what lets an assertion prove which implementation the container holds, not merely that the service type is registered.
  • Walkthrough - no members; a body-less sealed class with its doc comment at :27. It is the exact type FakeDependentModuleConformanceTests asserts on via descriptor.ImplementationType.Should().Be<DisabledFakeExportService>() (:76).
  • Where it's used - registered as a singleton by FakeDependentModule.RegisterDisabledStubs (:43-:44).

ReopenTicketCommand, UpdateTicketCommand

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CommandValidatorFixtures · (see per-type table) · Level 1 · record

Two bridged commands: each is a one-property record that wraps a request record and implements ICommandWithRequest<out TRequest>. They differ in exactly one thing, whether the wrapped request has a validator, and that single difference is what proves the bridge is checked on both halves.

  • Depends on - ICommandWithRequest<out TRequest> (CommandValidatorFixtures.cs:2) and the request record each wraps.
  • Concept - cross-references the coverage concept introduced by CreateTicketCommandValidator and UpdateTicketRequestValidator. The pair is the negative-control design that makes the bridge check meaningful: both commands are structurally identical to the rule's discovery pass (both are closed types with a settable property, both are handled, both implement the bridge marker), so the only variable left is whether ValidatedTypes contains their request. One passes, one fails, and nothing else about them can explain the difference.
  • Walkthrough - the rule reaches the request type by projecting the command through the closed generic arguments of ICommandWithRequest1 matched by full name (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:5,:114), then asks whether any of those arguments is in the validated set. Matching the interface by name rather than by reference is what keeps the rule package free of a compile dependency on the framework's Application layer (:117-:120`).
Type File:Line The case it models
UpdateTicketCommand MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:31 record UpdateTicketCommand(UpdateTicketRequest Request) : ICommandWithRequest<UpdateTicketRequest>. No validator of its own, but its request has one, so the bridge covers it and the rule must stay silent.
ReopenTicketCommand MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:51 record ReopenTicketCommand(ReopenTicketRequest Request) : ICommandWithRequest<ReopenTicketRequest>. Implements the bridge marker, but its request has no validator, so CommandRequestValidator resolves nothing and adds no rule. Its doc states the ruling: half a bridge is not coverage (:47-:50).
  • Why they're built this way - the half-bridge is the failure mode a validation-coverage rule is most likely to miss, because the registration exists and only the resolution is empty. Compiling both halves side by side is what turns that from a code-review concern into an assertion.
  • Where they're used - named by nameof in CommandCoveredThroughTheRequestBridge_IsCovered and BridgeWithNoRequestValidator_IsNotCoverage (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:44-:64), and handled by ReopenTicketHandler and UpdateTicketHandler.

SampleDeploymentObservabilityTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/SampleDeploymentObservabilityTests.cs:12 · Level 1 · class

  • What it is - a subclass of ObservabilityConventionTestsBase that runs the shipped alert-to-runbook pairing gate over the framework's own deployment sample, samples/deployment, instead of a fixture. It is the one place in this repository the base runs against real infrastructure.
  • Depends on - ObservabilityConventionTestsBase (public sealed class SampleDeploymentObservabilityTests : ObservabilityConventionTestsBase, SampleDeploymentObservabilityTests.cs:12) and the sample's own IaC pair, embedded under the logical names samples.deployment.main.bicep and samples.deployment.OPERATIONS.md (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/MMCA.Common.Architecture.Tests.csproj:26-:30, sourced from MMCA.Common/samples/deployment/main.bicep and .../OPERATIONS.md).
  • Concept - cross-references the assembly-boundary concept introduced by ObservabilityConventionTestsBaseTests: both are subclasses of the same base that re-point its resource names, but where that class proves the resolution mechanism against a minimal fixture, this class proves the mechanism's real target. The sample is the artifact consumers copy (samples/deployment/DEPLOYMENT.md, per MMCA.Common/CLAUDE.md), so a runbook section that went missing there would propagate into every repository that lifted it; this is the gate proving its own worked example (SampleDeploymentObservabilityTests.cs:5-:10). [Rubric §13 - Observability & Operability] is the category directly exercised, the same alert-to-runbook pairing rule as its sibling; [Rubric §33 - Developer Experience] applies because the sample is a teaching artifact, and a silently stale one would mislead every adopter rather than just this repository.
  • Walkthrough - four property overrides. Two re-point the base at the sample's real files: BicepResource => "samples.deployment.main.bicep" (:14) and RunbookResource => "samples.deployment.OPERATIONS.md" (:16), replacing the base defaults infra.main.bicep and infra.OPERATIONS.md. MinimumAlertSpecs => 4 (:20) raises the base's default of 3, because the sample provisions four alerts: failed-requests, server-response-time, availability and ai-token-spend (:18-:19). RequireWorkbook => true (:24) opts into the base's monitoring-view check, which is off by default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:58). No [Fact]s of its own; inheritance supplies the base's four facts, run against the real sample bicep and runbook instead of a fixture: the alert-to-runbook pairing and severity facts, plus MonitoringWorkbookOrDashboard_IsProvisioned_WhenRequired (ObservabilityConventionTestsBase.cs:67-:77), which returns early unless RequireWorkbook is on and here asserts the sample bicep declares a Microsoft.Insights/workbooks or Microsoft.Portal/dashboards resource.
  • Why it's built this way - the class doc states the comment inline (:18-:19): raising MinimumAlertSpecs is meant to track the sample provisioning a new alert, never to paper over one going missing, since lowering the count to make a red run go green would defeat the gate's purpose. The RequireWorkbook opt-in carries its own reason (:22-:23): because consumers copy the sample, it must also ship the operator-facing view the alerts point at (rubric section 13). The base keeps the check off by default so a consumer that does not opt in keeps exactly the alert-to-runbook checks it had (ObservabilityConventionTestsBase.cs:53-:57). sample-deployment-validate in ci.yml (MMCA.Common/CLAUDE.md) is the surrounding CI job that keeps the sample itself deployable; this class is the one that keeps its alerts and runbook in lockstep.
  • Where it's used - an independent class in the Common architecture suite; it has no consumers outside CI, since it is the terminal check on the framework's own sample rather than a fixture other tests build on.

RightModel

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CycleFixtures.Right · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Right/RightFixtures.cs:6 · Level 1 · class

  • What it is - the other arm of the deliberate namespace cycle: a type in the Right fixture namespace that derives from a base in Left, supplying the Right -> Left edge.
  • Depends on - LeftModelBase (public sealed class RightModel : LeftModelBase, RightFixtures.cs:6, with the using of the Left namespace at :1).
  • Concept - cross-references the namespace-cycle concept introduced by LeftModelBase. This half contributes the inheritance edge; LeftService contributes the property-type edge in the other direction. Two edges pointing opposite ways between two namespace nodes is the minimum strongly connected component the rule can report.
  • Walkthrough - one auto-property, string Name initialized to string.Empty (:9), present only so the type is not empty.
  • Where it's used - the property type of LeftService.Model, which is what closes the loop; both are scanned by NamespaceCycleFitnessTests.

RoleGatedPage

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:48 · Level 1 · class

  • What it is - an empty public fixture page nested in the route-authorization guard, routed at /fixture/role-gated and decorated [Authorize(Roles = AdminRole)] (RouteAuthorizationTestsBaseTests.cs:46-:48). It is deliberately unsealed so a subclass can inherit its role gate.
  • Depends on - AuthorizeAttribute and RouteAttribute; the AdminRole constant of the enclosing test class.
  • Concept - attribute inheritance is part of the role check. RequiresRole (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:86) must see a role declared on a base class; this page is that base, and BareOverRoleGatedPage derives from it.
  • Walkthrough - no members. The theory at RouteAuthorizationTestsBaseTests.cs:19 asserts RoleProbe.Requires(typeof(RoleGatedPage), AdminRole) is true.
  • Where it's used - only inside its defining file, as a [InlineData] case and as the base of BareOverRoleGatedPage.

RoleProbe

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:76 · Level 1 · class

  • What it is - a private sealed subclass of RouteAuthorizationTestsBase whose only job is to expose the protected static RequiresRole to the test as a public Requires(Type page, string role) (:76-:85).
  • Depends on - RouteAuthorizationTestsBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs): it supplies the abstract RequiredRole (:31) and IsGovernedPage (:40) members this probe must override.
  • Concept - a probe subclass satisfies the abstract contract trivially so a protected helper can be tested directly. TargetAssembly is this assembly, RequiredRole is AdminRole, and IsGovernedPage returns false, so none of the base's inherited facts govern any page here.
  • Walkthrough - Requires forwards to RequiresRole(page, role) and nothing else; the theory feeds it the fixture pages and compares the result to the expected boolean (:27).
  • Where it's used - only inside its defining file.

RoleThenPolicyPage

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:58 · Level 1 · class

  • What it is - a sealed fixture page routed at /fixture/role-then-policy that stacks two attributes: [Authorize(Roles = AdminRole)] first, then [Authorize(Policy = "Fixture")] (:55-:58).
  • Depends on - AuthorizeAttribute, RouteAttribute.
  • Concept - cross-references the attribute-stacking idea of the sibling PolicyThenRolePage (:50-:53), which reverses the order. Together the two prove the role check finds the role regardless of which [Authorize] appears first.
  • Walkthrough - no members; the theory expects RequiresRole to be true for it (:21).
  • Where it's used - only inside its defining file: the theory case at :21 and a governed-page match at :95.

UntaggedTemplate

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance.Deployment · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/CostTagConventionTestsBaseTests.cs:50 · Level 1 · class

  • What it is - a private, sealed negative-control subclass nested inside CostTagConventionTestsBaseTests. It points the cost-tag gate at a fixture bicep in which a container app carries no service tag, so the inherited fact must fail.
  • Depends on - CostTagConventionTestsBase and its three configuration points: the abstract MinimumContainerApps (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs:36) and DatabaseNamePrefix (:42), and the virtual BicepResource defaulting to infra.main.bicep (:45).
  • Concept - a negative control proves the gate can fail. The positive case in the outer class runs against fixtures.costtag-main.bicep and passes; this subclass swaps in fixtures.costtag-untagged.bicep while keeping every other knob identical (MinimumContainerApps => 2, DatabaseNamePrefix => "Fixture_", CostTagConventionTestsBaseTests.cs:50-:57), so the resource name is the only variable.
  • Walkthrough - the outer test UntaggedContainerApp_IsFlaggedByName takes EveryContainerApp_CarriesAServiceTag (CostTagConventionTestsBase.cs:59) from a new UntaggedTemplate as a delegate and asserts it throws with a message containing uiApp, so the failure must name the app whose spend is unattributed.
  • Where it's used - only inside its defining file.

WrongPrefixTemplate

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance.Deployment · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/CostTagConventionTestsBaseTests.cs:68 · Level 1 · class

  • What it is - a private, sealed negative-control subclass nested inside CostTagConventionTestsBaseTests. It reads the well-formed fixtures.costtag-main.bicep but declares DatabaseNamePrefix => "Other_" instead of the fixture's Fixture_ (CostTagConventionTestsBaseTests.cs:68-:75).
  • Depends on - CostTagConventionTestsBase; the prefix is consumed by ServiceDatabases_CarryTheServiceTagOfTheOwningApp, which builds the expected tag text tags: union(commonTags, { service: toLower(replace(dbName, '<prefix>', '')) }) from it (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs:83-:88).
  • Concept - cross-references the negative-control idea introduced by UntaggedTemplate. Here the template text is correct and the subclass's configuration is wrong, which proves the gate derives the database tag from the repo's own prefix rather than matching any prefix.
  • Walkthrough - the outer test DatabaseTag_UnderAnotherPrefix_IsFlagged takes ServiceDatabases_CarryTheServiceTagOfTheOwningApp from a new WrongPrefixTemplate as a delegate and asserts it throws.
  • Where it's used - only inside its defining file.

ObservabilityConventionTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/ObservabilityConventionTestsBaseTests.cs:14 · Level 1 · class

  • What it is - a cross-assembly regression guard for the shared SLO alert-to-runbook pairing rule. It subclasses ObservabilityConventionTestsBase from an assembly other than the one the base ships in, re-points it at a fixture IaC pair embedded in this test project, and asserts the base resolves its manifest resources from the derived type's assembly.
  • Depends on - ObservabilityConventionTestsBase (public sealed class ObservabilityConventionTestsBaseTests : ObservabilityConventionTestsBase, ObservabilityConventionTestsBaseTests.cs:14) and the two fixture resources embedded by the csproj under the logical names fixtures.observability-main.bicep and fixtures.observability-OPERATIONS.md (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/MMCA.Common.Architecture.Tests.csproj:17-:22). Externals: System.Reflection.Assembly, xUnit [Fact], AwesomeAssertions.
  • Concept introduced - proving a shipped base class's defaults from outside its own assembly. The rule library ships as the MMCA.Common.Testing.Architecture package (ArchitectureRules plus its *TestsBase family), so a base can carry a default that is only ever wrong for a consumer: ResourceAssembly => GetType().Assembly (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:51) must bind to the subclass's assembly, because the base's own assembly never contains a consumer's bicep. Testing that default inside the base's own assembly would prove nothing (both assemblies would be the same one), so the guard is deliberately located here, one assembly away. [Rubric §13 - Observability & Operability] assesses whether alerts stay operable, and the rule this class exercises is the one that keeps every provisioned SLO alert paired with a severity-correct runbook section. [Rubric §14 - Testability] applies because this is a meta-test that keeps a shared gate honest, and [Rubric §33 - Developer Experience] because the alternative failure mode is a break that surfaces only in the first downstream repo to adopt the base.
  • Walkthrough - two property overrides re-point the base's resource names at the fixtures: BicepResource => "fixtures.observability-main.bicep" (:16) and RunbookResource => "fixtures.observability-OPERATIONS.md" (:18), replacing the base defaults infra.main.bicep and infra.OPERATIONS.md (ObservabilityConventionTestsBase.cs:42,:45). One own [Fact], ResourceAssembly_DefaultsToTheDerivedTypesAssembly (:24-:29), asserts the resolved ResourceAssembly is the same object as this class's assembly and is not the base's assembly. A second member, the [Theory] HeadingNamesAlert_MatchesTheKeyAsAWholeToken (:38-:61, 21 [InlineData] rows at :39-:59), pins the predicate both pairing facts read headings through, HeadingNamesAlert (ObservabilityConventionTestsBase.cs:168): an alert key matches a runbook heading only as a whole token, -alert-{key} then an optional version suffix -v{digits} then end of line, whitespace, ( or a closing backtick (regex at :175). The rows cover the accepted shapes (a provisioned -v2 or -v12 suffix, a backticked <prefix>-alert-... heading, a trailing \r, no space before (sev) and the rejected ones, where a longer hyphenated sibling must not answer for a shorter key (server-response-time-p99 versus server-response-time, x-extra versus x) and a malformed suffix (-v, -v2x, -v2-extra) is a different alert; this is the 062 SLO-alerting-as-code pairing contract (ADR-062). Inheritance supplies the rest: the base's three [Fact]s (ObservabilityConventionTestsBase.cs:81,:63,:91) run against the fixture pair, so the whole discovery-and-pairing path is exercised across the assembly boundary. The fixture bicep declares exactly three well-formed specs plus the two parse anchors the base looks for, var sloAlertSpecs and resource sloAlerts (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Fixtures/observability-main.bicep:6-:25), which meets the base's default MinimumAlertSpecs => 3 (ObservabilityConventionTestsBase.cs:39); the fixture runbook carries a matching ### fixture-alert-<key> (sev N) heading per spec (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Fixtures/observability-OPERATIONS.md:9-:19), so both pairing directions and the severity check pass.
  • Why it's built this way - the class doc records the exact regression it exists for (:5-:13): resolving against the base's own assembly instead of the subclass's is a silent break, since the framework's CI would stay green and only the first adopting consumer would fail. Keeping the guard in a different assembly is the only way to make that difference observable. It is the packaging discipline ADR-058 describes for shipped conformance suites, applied to a fitness base; the rule it guards implements the alerting side of ADR-041.
  • Where it's used - an independent class in the Common architecture suite. The real adopters are ObservabilityConventionTests in MMCA.ADC and MMCA.Store (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7), which subclass the same base with no overrides at all and rely entirely on the default this class pins.
  • Caveats / not-in-source - the fixture bicep is deliberately minimal (its own header comment says so, observability-main.bicep:1-:4): it proves the base's parse-and-pair logic, not that any real consumer template is well formed. The consumers' own subclasses do that against their real infra/ files.

OtherRolePlusBarePage

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:71 · Level 1 · class

  • What it is - a nested public sealed page fixture carrying [Route("/fixture/other-role")], [Authorize(Roles = "Other")] and a bare [Authorize] (RouteAuthorizationTestsBaseTests.cs:68-:71). It is the negative case for the role check: the role gate names a role other than Admin, and the bare attribute adds nothing.
  • Depends on - Microsoft.AspNetCore.Authorization.AuthorizeAttribute and RouteAuthorizationTestsBase.RequiresRole (exposed by the RoleProbe subclass, :76-:85).
  • Concept - stacked [Authorize] attributes are all enforced, so the role check must read every one. A page keeps its gate when any attribute names the required role (class doc, :7-:13). This page proves the converse: no attribute names Admin, so RequiresRole is false (:24) and a governed page like it must still fail.
  • Walkthrough - no members. RequiresRole_ReadsEveryAuthorizeAttribute expects false for it (:24, :26-:27); MissingRoleTests governs only this page (:105), and GovernedPages_WithoutTheRole_StillFail asserts the failure message contains its name (:37-:44).
  • Where it's used - only inside its defining file.

PolicyThenRolePage

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:53 · Level 1 · class

  • What it is - a nested public sealed page fixture carrying [Route("/fixture/policy-then-role")], a policy-only [Authorize(Policy = "Fixture")] and then [Authorize(Roles = "Admin")] (RouteAuthorizationTestsBaseTests.cs:50-:53).
  • Depends on - Microsoft.AspNetCore.Authorization.AuthorizeAttribute and the AdminRole constant (:16).
  • Concept - a sibling policy attribute must not hide the role gate. AuthorizeAttribute allows multiple instances and ASP.NET Core enforces each one, so the role is present even though the first attribute names none. Its mirror RoleThenPolicyPage reverses the order (:55-:58), so the check is order-independent.
  • Walkthrough - no members. RequiresRole_ReadsEveryAuthorizeAttribute expects true (:20); StackedAttributeTests lists it as governed (:93-:96) and GovernedPages_WithStackedOrInheritedAttributes_KeepTheirRoleGate asserts the gate does not throw (:29-:35).
  • Where it's used - only inside its defining file.

BareOverRoleGatedPage

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:62 · Level 2 · class

  • What it is - a sealed fixture page routed at /fixture/bare-over-role-gated that derives from RoleGatedPage and adds a bare [Authorize] (:60-:62).
  • Depends on - RoleGatedPage for the inherited [Authorize(Roles = AdminRole)].
  • Concept - cross-references RoleGatedPage. A bare [Authorize] on a derived class must not hide the role gate inherited from its base, so RequiresRole is expected to be true; this is the case that distinguishes a role-aware check from one that only looks at the most-derived attribute. The contrast is BareOnlyPage (:64-:66), which has no role anywhere.
  • Walkthrough - no members; it is the theory case at :22 and is also matched as a governed page at :96.
  • Where it's used - only inside its defining file.

CostTagConventionTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance.Deployment · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/CostTagConventionTestsBaseTests.cs:11 · Level 2 · class

  • What it is - the cross-assembly guard for the shipped cost-attribution gate. It subclasses CostTagConventionTestsBase (public sealed class CostTagConventionTestsBaseTests : CostTagConventionTestsBase), supplies a fixture bicep embedded in this test project, and adds four own facts that prove both the happy path and each way the gate must fail.
  • Depends on - CostTagConventionTestsBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs) and three nested subclasses: UntaggedTemplate, WrongPrefixTemplate and HighFloorTemplate (MinimumContainerApps => 3 against a fixture with two apps). Overrides are MinimumContainerApps => 2, DatabaseNamePrefix => "Fixture_" and BicepResource => "fixtures.costtag-main.bicep".
  • Concept - cross-references ObservabilityConventionTestsBaseTests, which introduced proving a shipped base's defaults from outside its own assembly. The base's ResourceAssembly defaults to the derived type's assembly (CostTagConventionTestsBase.cs:51), so the guard lives one assembly away. This class adds the negative-control triple: each failure mode of the gate has a template that must make it throw.
  • Walkthrough - ResourceAssembly_DefaultsToTheDerivedTypesAssembly asserts the resolved assembly is this class's assembly. UntaggedContainerApp_IsFlaggedByName expects the failure to name uiApp. TooFewContainerApps_FailsTheFloor expects a throw when fewer apps are discovered than the floor, because a drifted parse anchor would otherwise pass vacuously. DatabaseTag_UnderAnotherPrefix_IsFlagged expects a throw when the database tag is derived from another prefix. The inherited facts EveryContainerApp_CarriesAServiceTag (CostTagConventionTestsBase.cs:59) and ServiceDatabases_CarryTheServiceTagOfTheOwningApp (:82) also run against the main fixture and must pass.
  • Where it's used - an independent class in the Common architecture suite (CostTagConventionTestsBaseTests.cs:11-:76); no consumers outside CI.

InnocentHandler

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:95 · Level 2 · class

  • What it is - the negative control for the domain-event handler save rule: a handler that mutates in-memory state and returns, which is what a conforming handler looks like.
  • Depends on - IDomainEventHandler<in TDomainEvent> closed over FixtureDomainEvent (DomainEventSaveFixtures.cs:95, imports at :1).
  • Concept - cross-references the handler-save concept from IBadgeGranter. Its contribution is discrimination. The rule walks the call graph out of every method on a handler type up to a depth bound, and a walker that reported any handler with any outbound call would be useless. This fixture makes calls (GetType(), List<string>.Add, ArgumentNullException.ThrowIfNull) and reaches no save, so a green result for it is a statement that the classifier looked and found nothing rather than that it stopped looking.
  • Walkthrough - one private List<string> _seen initialized with a collection expression (:97), and a HandleAsync that guards its argument, records the event's type name, and returns Task.CompletedTask (:99-:105). No constructor and no injected collaborator, which is the point: nothing it holds can lead to a unit of work.
  • Why it's built this way - the doc states the ruling directly (:91-:94): this is what the rule protects, so it must never appear in the report.
  • Where it's used - asserted absent from the failure message by HandlerThatOnlyMutates_IsNotFlagged (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:58-:67).

InterfaceDispatchSavingHandler

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:81 · Level 2 · class

  • What it is - the handler fixture whose save is reachable only through an abstraction: it calls IBadgeGranter and never touches a unit of work itself.
  • Depends on - IDomainEventHandler<in TDomainEvent>, FixtureDomainEvent, and IBadgeGranter (the constructor-injected collaborator, DomainEventSaveFixtures.cs:83,:85).
  • Concept - cross-references IBadgeGranter. The mechanism this fixture forces is CallGraphIndex.TargetsOf, which the walk calls for every callee that is not itself a save (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:170-:177): an interface method reference is expanded into the implementations present in the scanned modules, and each unvisited, non-allowlisted target is queued one depth deeper. Without that expansion the walk would see IBadgeGranter.GrantAsync and stop, because an interface method has no body to read.
  • Walkthrough - one field, one constructor, and a HandleAsync that returns _granter.GrantAsync(cancellationToken) directly (:87-:88). It is not async, which is deliberate: the save is exactly two IL calls away with no state machine in between, so the reported chain stays short and the fixture isolates interface resolution from the state-machine traversal that TransitiveSavingHandler exercises.
  • Where it's used - asserted present in the failure message by SaveBehindAnInterface_IsResolvedToItsImplementation (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:47-:56), whose because states the ruling: IL records the interface call, so the walk must expand it to the implementations in scope.

LeftService

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CycleFixtures.Left · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Left/LeftFixtures.cs:6 · Level 2 · class

  • What it is - the Left -> Right half of the fixture cycle: a Left type whose property type lives in the Right namespace.
  • Depends on - RightModel (the nullable Model property, LeftFixtures.cs:9, with the using at :1).
  • Concept - cross-references LeftModelBase. What this type adds is the second edge kind: a property type rather than a base type. The cycle rule records both (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:25-:27), and having one of each in the fixture means a regression that drops either edge kind from the graph builder is caught instead of being masked by the other.
  • Walkthrough - one member, public RightModel? Model { get; set; } (:9). Nullability is irrelevant to the rule (it reads the property type); the reference itself is the fixture.
  • Where it's used - referenced by AcyclicConsumer and scanned by NamespaceCycleFitnessTests.

SharedCodeErrors

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ErrorCodeFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:52 · Level 2 · class

  • What it is - a fixture that reuses a generic framework static, Error.NotFoundError("Error.NotFound", ...) (:54), which the allowed-shared-codes list exempts from both the uniqueness and prefix rules.
  • Depends on - Error (MMCA.Common.Shared.Abstractions, :1,:54).
  • Concept introduced - an allowlist is not the same guard as absence of a rule. TicketErrors and DuplicateTicketErrors prove the uniqueness rule catches a real collision; this fixture proves it can still stay silent about a code that is deliberately shared across the codebase (a generic "not found" convenience) once that code is named on the SharedCodes allowlist passed into ArchitectureRules.ErrorCodesAreUnique.
  • Walkthrough - one member, Missing() => Error.NotFoundError("Error.NotFound", "Generic shared code") (:54).
  • Where it's used - SharedCode_OnTheAllowList_IsExemptFromUniqueness asserts its name absent from the uniqueness failure message (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:44-:53); PrefixedCodes_AndAllowedSharedCodes_PassThePrefixRule asserts "Error.NotFound" absent from the prefix-rule failure message (:67-:76); DistinctCodeCount_CountsTheLiteralCatalog counts it among the five literal codes the catalog must contain (:89-:97).

TicketErrors

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ErrorCodeFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:11 · Level 2 · class

  • What it is - the well-formed baseline for the error-catalog fitness rule: two properly prefixed codes, Tickets.NotFound and Tickets.AlreadyClosed.
  • Depends on - Error (NotFoundError, Conflict, :13,:15).
  • Concept introduced - the control every other fixture in the file is measured against: DuplicateTicketErrors collides with one of its codes from a second type, TwoBranchTicketErrors reuses a code inside a single type without colliding, and UnprefixedErrors breaks the prefix convention this type follows correctly.
  • Walkthrough - two members, NotFound() (:13) and AlreadyClosed() (:15), each a one-line Error factory call.
  • Where it's used - DuplicateCode_AcrossTwoTypes_IsFlagged asserts both "Tickets.NotFound" and nameof(TicketErrors) appear in the uniqueness failure message (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:22-:31); PrefixedCodes_AndAllowedSharedCodes_PassThePrefixRule asserts "Tickets.AlreadyClosed" absent from the prefix-rule failure message (:67-:76); its assembly anchors FixtureModuleMap's single module (typeof(TicketErrors).Assembly, :112).

TwoBranchTicketErrors

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ErrorCodeFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:28 · Level 2 · class

  • What it is - reuses one code, Tickets.Invalid, across two branches of the same type. One error with two exits is not a catalog collision, so the uniqueness rule must stay silent about it.
  • Depends on - Error.Validation (:32-:33).
  • Concept introduced - cross-references TicketErrors. Its contribution is discrimination: a uniqueness rule that flagged any code appearing at more than one call site would also flag this fixture, so the rule must key on the owning type, not the code string alone. DuplicateTicketErrors is the positive control for the same code shared across two types; this is the negative control for the same code repeated inside one.
  • Walkthrough - one member, Invalid(bool alternate), a ternary choosing between "Alternate reason" and "Primary reason" while the code string "Tickets.Invalid" is identical on both branches (:30-:33).
  • Where it's used - CodeReusedAcrossBranchesOfOneType_IsNotADuplicate asserts "Tickets.Invalid" absent from the uniqueness failure message (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:34-:42).

UnprefixedErrors

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ErrorCodeFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:37 · Level 2 · class

  • What it is - carries no module prefix at all ("SomethingBroke"), so the prefix rule must flag it.
  • Depends on - Error.Validation (:39).
  • Concept introduced - the negative control for ArchitectureRules.ErrorCodesUseAnAllowedPrefix, paired with TicketErrors (the positive control) and SharedCodeErrors (the allowlisted exemption).
  • Walkthrough - one member, Broken() => Error.Validation("SomethingBroke", "No module prefix at all") (:39).
  • Where it's used - UnprefixedCode_IsFlagged asserts both "SomethingBroke" and nameof(UnprefixedErrors) appear in the prefix-rule failure message, so the report names both the code and its owning type (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:56-:64).

DuplicateTicketErrors, DynamicErrors, SharedCodeErrors, TicketErrors, TwoBranchTicketErrors, UnprefixedErrors

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ErrorCodeFixtures · (see per-type table) · Level 2 · class

Six internal static classes in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs), each holding one or two expression-bodied factory methods that construct an Error. Together they compile the error catalog the two catalog rules have to judge: well-formed codes, a cross-type collision, an unprefixed code, one code reused across two branches of a single type, a shared framework static, and a code built at run time.

  • Depends on - Error and its static factories NotFoundError, Conflict and Validation (ErrorCodeFixtures.cs:1). Nothing else.
  • Concept introduced - the error code as the module's public vocabulary, read out of IL. A client switches on Order.NotFound and a support ticket quotes it, so two modules both shipping Item.Invalid make that vocabulary ambiguous in a way that only surfaces in production (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.ErrorCatalog.cs:32-:37). Two rules police it. ErrorCodesAreUnique (:62) fails when the same literal code is constructed by more than one type, and the choice of "type" as the unit is deliberate: one error with two exits in the same class is not a collision, which keeps a normal two-branch handler out of the report (:45-:50). ErrorCodesUseAnAllowedPrefix (:102) fails on any literal code whose prefix a caller-supplied delegate rejects, so the convention stays the consumer's rather than the framework's (:92-:98). Both read the code out of the compiled IL: the scan looks for calls to any of nine Error factory names plus .ctor (:17-:29) and reads the first argument, and both deliberately skip the record copy constructor through a first-parameter-is-string check so error with { Source = ... } is not mistaken for a new code (:11-:15). Codes that are not literals cannot be judged statically, and the rules refuse to guess: they are listed as UNVERIFIABLE in the failure message so the reader knows the catalog has a blind spot (:51-:55). [Rubric §9 - API & Contract Design] assesses whether the published failure vocabulary is governed; [Rubric §15 - Best Practices & Code Quality] covers traceability, since a prefixed code alone says where it came from; [Rubric §14 - Testability] covers the fixtures.
  • Walkthrough - scope is the per-module Domain and Application assemblies only (:38-:44), which is why the self-test map registers this assembly as a module Application layer rather than a framework one.
Type File:Line What it models
TicketErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:11 The clean catalog: Tickets.NotFound via Error.NotFoundError (:13) and Tickets.AlreadyClosed via Error.Conflict (:15). Correctly prefixed, so it must pass the prefix rule, while being one half of the collision below.
DuplicateTicketErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:19 Ships Tickets.NotFound a second time from a different type, with a different message (:21). The collision the uniqueness rule exists to catch, and the failure message must name both owning types.
TwoBranchTicketErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:28 The near-miss: one method with a ternary that constructs Tickets.Invalid on both branches with different reasons (:30-:33). Two construction sites, one owning type, so it is not a collision and the rule must stay silent.
UnprefixedErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:37 Error.Validation("SomethingBroke", ...) (:39), a code with no module prefix at all. The prefix rule must report it and name the owning type.
DynamicErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:46 The blind spot: Error.Validation("Tickets." + entity, ...) (:48). The code argument is a concatenation rather than a literal, so the scan reports the site as UNVERIFIABLE rather than passing or failing it.
SharedCodeErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:52 Error.NotFoundError("Error.NotFound", ...) (:54), reusing one of the generic statics on Error itself. The allowed-shared-codes list exempts it from both rules, which is what those generics exist for.
  • Why they're built this way - the pairs are what make each rule discriminating rather than merely loud. TicketErrors and DuplicateTicketErrors are the collision; TwoBranchTicketErrors is the shape a naive "count the construction sites" implementation would wrongly flag; SharedCodeErrors proves the exemption is honored on the prefix rule as well as the uniqueness rule. Keeping every fixture internal static with expression-bodied members means the IL is minimal and the reported owner name is unambiguous. See ADR-015.
  • Where they're used - compiled into this assembly and scanned by ErrorCatalogFitnessTests through its FixtureModuleMap, which anchors the module Application layer on typeof(TicketErrors).Assembly (ErrorCatalogFitnessTests.cs:125).

DuplicateTicketErrors, DynamicErrors

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ErrorCodeFixtures · (see per-type table) · Level 2 · class

Two of the compiled Error-construction sites ErrorCatalogFitnessTests judges: one ships a code that collides with another type, the other builds its code at run time so no static scan can read it.

  • Depends on - Error (MMCA.Common.Shared.Abstractions, :1) and its static factories, Error.NotFoundError and Error.Validation.
  • Concept - cross-references the IL-scanned catalog concept the fixture-set class doc introduces (ErrorCodeFixtures.cs:5-:10): the rule reads error codes out of compiled IL rather than source, so every behavior it must judge (a well-formed code, a cross-type collision, same-type branch reuse, a missing prefix, an allowlisted shared code, a runtime-built code) has to exist as a real compiled call site. DuplicateTicketErrors is the collision case; DynamicErrors is the blind spot the rule cannot resolve by static analysis and must instead report as UNVERIFIABLE rather than silently passing or failing it.
  • Walkthrough - DuplicateTicketErrors.NotFound() calls Error.NotFoundError("Tickets.NotFound", "The ticket is missing") (:21), the same code string TicketErrors.NotFound() constructs (:13), so the two types collide on Tickets.NotFound. DynamicErrors.For(string entity) builds its code by string concatenation, "Tickets." + entity (:48), so the code argument is not a literal the scan can read.
  • Where they're used - DuplicateCode_AcrossTwoTypes_IsFlagged asserts the uniqueness rule's failure message names both TicketErrors and DuplicateTicketErrors (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:21-:31); DynamicCode_IsReportedAsUnverifiable asserts the prefix rule's failure message contains UNVERIFIABLE and names DynamicErrors (:78-:87).
Type File:Line The case it models
DuplicateTicketErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:19 Ships "Tickets.NotFound", the same code TicketErrors constructs, from a second type: the collision the uniqueness rule catches.
DynamicErrors MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:46 Builds its code by concatenation at run time, so the argument is not a literal and the rule reports the site as UNVERIFIABLE instead of guessing.

FakeDependentModule

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:31 · Level 2 · class

  • What it is - the "hard" module fixture: a module that declares dependencies, refuses to start without them, and exports a disabled stub. It exercises every member of the module contract that a leaf module leaves at its default.
  • Depends on - IModule (public sealed class FakeDependentModule : IModule, ModuleConformanceTestsBaseTests.cs:31), ApplicationSettings (the Register parameter, :39), IFakeExportService and DisabledFakeExportService (the stub pair, :44), plus IServiceCollection and IConfigurationBuilder from Microsoft.Extensions.* (:1-:2).
  • Concept introduced - the full module contract as a testable surface. IModule has five members, three of which are defaulted (Dependencies => [], RequiresDependencies => false, RegisterDisabledStubs as an empty body, MMCA.Common/Source/Core/MMCA.Common.Application/Modules/IModule.cs:17,:23,:34), so the entire contract ModuleLoader registers on can be silently wrong without a compile error. This fixture is the one that overrides all three, so the conformance base is exercised against real (not defaulted) values. [Rubric §14 - Testability] covers the fixture role; [Rubric §7 - Microservices Readiness] is what the stub half protects, since a module that can be switched off without breaking the modules that import its contract is a module that can also be extracted (ADR-059).
  • Walkthrough
    • Name => "FakeDependent" (:33): the key ModulesSettings entries and other modules' dependency lists match on.
    • Dependencies => ["FakeLeaf", "FakeOther"] (:35): two entries, one of which (FakeLeafModule) exists in the fixture set and one of which deliberately does not, so the list is a pure data declaration rather than a resolvable graph.
    • RequiresDependencies => true (:37): the flag that turns a disabled dependency into a startup failure instead of a substituted stub.
    • Register(...) (:39-:41): an empty body. Registration behavior is not what the conformance base asserts, so the fixture spends nothing on it.
    • RegisterDisabledStubs(IServiceCollection services) (:43-:44): a single expression-bodied services.AddSingleton<IFakeExportService, DisabledFakeExportService>(), which is the only member with observable behavior and the one FakeDependentModuleConformanceTests inspects.
  • Why it's built this way - the doc comment (:30) names the intent: this is the shape of the two real consumer modules that are not leaves (Store Sales, ADC Notification). Modeling them locally means the shared base is proven against both module shapes inside MMCA.Common's own CI, before any consumer sees it (ADR-015).
  • Where it's used - the TModule of FakeDependentModuleConformanceTests (:60) and of the deliberately-drifted DriftedTests (:131).

FakeLeafModule

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:15 · Level 2 · class

  • What it is - the minimal module fixture: Name plus an empty Register, and nothing else. Its whole purpose is to not override Dependencies or RequiresDependencies, so the conformance base has to reach the interface's default implementations to read them.
  • Depends on - IModule (public sealed class FakeLeafModule : IModule, ModuleConformanceTestsBaseTests.cs:15) and ApplicationSettings (the Register parameter, :19).
  • Concept introduced - default interface implementations are only reachable through the interface. A default member declared on IModule (IModule.cs:17,:23) does not exist on the concrete class, so module.Dependencies will not compile against FakeLeafModule and a reflection lookup on the concrete type finds nothing. The shared base solves this by locating the IModule interface by full name and reading the property off the interface (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:80-:99), which dispatches to the override when there is one and to the framework default when there is not. Before the base existed, the hand-written consumer tests needed an explicit (IModule) cast to get the same reach (class doc, :9-:14). [Rubric §14 - Testability] and [Rubric §1 - SOLID] both apply: the fixture exists so the base's interface-dispatch contract is provable rather than assumed.
  • Walkthrough - Name => "FakeLeaf" (:17) and an empty Register(...) (:19-:21). That is the entire type; the absence of members is the fixture.
  • Why it's built this way - this is the exact shape the three byte-identical consumer {X}ModuleTests files collapse into (class doc, :12-:13), so the leaf path is the most-travelled one and the one whose silent breakage would be widest.
  • Where it's used - the TModule of FakeLeafModuleConformanceTests (:51), which in turn is driven directly by two of ModuleConformanceTestsBaseTests's facts (:112-:129).

FixtureSpanishTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance.Localization · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Localization/SpanishAccentFitnessTests.cs:37 · Level 2 · class

  • What it is - a private sealed nested subclass of SpanishAccentTestsBase whose only job is to point the shipped Spanish accent gate at a folder chosen per test, so the enclosing SpanishAccentFitnessTests can run the base's fact over a compliant and an offending fixture tree.
  • Depends on - SpanishAccentTestsBase (the base, SpanishAccentFitnessTests.cs:37; its abstract ResourceRoot is MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SpanishAccentTestsBase.cs:46). Nothing else.
  • Concept - cross-references SpanishAccentTestsBase. Its contribution is making the gate testable from both sides: the base's fact Spanish_resources_keep_their_accents (base :68) scans every *.es.resx under ResourceRoot (base :74), and a reusable gate is only trustworthy if a fixture proves it both passes clean input and fails a real mistake. The class doc names the case it pins (SpanishAccentFitnessTests.cs:6-:11): unaccented aun ("even") is correct Spanish and must be accepted, while a word whose unaccented spelling is the error must still be rejected. [Rubric §14 - Testability] covers the technique.
  • Walkthrough - a primary constructor taking string resourceRoot and one override, protected override string ResourceRoot => resourceRoot; (:37-:40). No fields beyond the captured parameter.
  • Why it's built this way - it is private, so xUnit does not discover it and run the inherited fact against a meaningless root (class doc, :9-:10); the enclosing tests construct it directly and invoke the fact as a delegate. Taking the root as a constructor argument lets one subclass serve both fixture folders instead of one subclass per folder.
  • Where it's used - UnaccentedAun_MeaningEven_IsAccepted constructs it over SpanishFixtures/Compliant and asserts the fact does not throw (:21-:27); AWordMissingItsAccent_IsStillRejected constructs it over SpanishFixtures/Offending and asserts an XunitException whose message contains todavia (:29-:35). The fixture root is resolved from the repo root via ArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx") (:14-:19). No usage sites outside the defining file.

MessageBusBackpressureTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance.Deployment · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/MessageBusBackpressureTestsBaseTests.cs:14 · Level 2 · class

  • What it is - the self-test for the shipped broker-backpressure gate: it subclasses MessageBusBackpressureTestsBase with one fixture service name (:16) and drives the base's public static Check over inline JSON documents, so every violation message the gate can produce is proven to fire.
  • Depends on - MessageBusBackpressureTestsBase (the base, :14; its Check is MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/MessageBusBackpressureTestsBase.cs:93) and MessageBusSettings (the settings type whose SectionName and property names the first fact pins, :21-:23), plus externals xUnit and AwesomeAssertions.
  • Concept introduced - a gate that parses JSON instead of binding the settings type must be pinned to that type from the side that can see both. The base reads appsettings.json with System.Text.Json because the testing package takes no framework reference (base class doc, MessageBusBackpressureTestsBase.cs:24-:28), so its key names (MessageBus, PrefetchCount, ConcurrentMessageLimit, base :34-:40) are plain strings. This test class is the pin: KeyNames_MatchTheSettingsTypeTheHostBinds asserts each constant equals MessageBusSettings.SectionName or nameof(...) of the property (:19-:24), so a rename in Common fails here before a consumer's gate silently stops matching. [Rubric §12 - Resilience & Reliability] assesses bounded consumers (the base's because text cites it, base :82); [Rubric §14 - Testability] covers the technique of testing a reusable gate over fixture documents.
  • Walkthrough - Services is overridden to ["Fixture"] (:16). Each fact calls Check("Svc", <raw JSON>) and asserts on the returned list:
    • KeyNames_MatchTheSettingsTypeTheHostBinds (:19-:24): the constant-to-settings pin above.
    • BoundedSettings_HaveNoViolations (:27-:29): PrefetchCount 16 with ConcurrentMessageLimit 8 returns an empty list.
    • MissingSection_IsAViolation (:32-:34): a document with no MessageBus section yields the single message "Svc declares no MessageBus section".
    • MissingPrefetch_IsAViolation (:37-:39) and ZeroConcurrency_IsAViolation (:42-:44): an absent or non-positive knob yields one violation naming that key (base :109-:117).
    • PrefetchBelowConcurrency_IsAViolation (:47-:49): prefetch 4 against concurrency 8 yields "prefetches 4 but processes 8" (base :119-:124).
    • EmptyServiceList_FailsRatherThanCheckingNothing (:52-:57): a private nested NoServices subclass (:59-:62) returns an empty Services; invoking the base fact on it must throw, which proves the non-vacuity guard (base :72-:73).
  • Why it's built this way - the base's real fact reads embedded resources from the consuming project, so it cannot be fed a bad document. Making Check public (base doc, :86-:89) lets this class exercise the parse directly; the empty-list case reuses the same trick as the other *BaseTests in this part, a nested subclass configured to be wrong.
  • Where it's used - an independent class in the Common architecture suite; no usage sites outside the defining file.

MissingRoleTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:99 · Level 2 · class

  • What it is - the negative control for the role check: a private nested subclass of RouteAuthorizationTestsBase that governs a single page, OtherRolePlusBarePage, which carries [Authorize(Roles = "Other")] plus a bare [Authorize] and never names Admin.
  • Depends on - RouteAuthorizationTestsBase (the base, RouteAuthorizationTestsBaseTests.cs:99) and the sibling fixture page OtherRolePlusBarePage (:68-:71), plus the AdminRole constant (:16).
  • Concept - cross-references StackedAttributeTests, its positive twin. StackedAttributeTests proves a role gate survives when ANY of a page's authorize attributes names the role; this type proves the loosened check has not become vacuous, because a page whose attributes name a different role must still fail. Without it, a regression that made the check accept any [Authorize] would leave the stacked-attribute cases green.
  • Walkthrough - three overrides (:99-:106): TargetAssembly is the test assembly (:101), RequiredRole is AdminRole (:103), and IsGovernedPage matches only typeof(OtherRolePlusBarePage) (:105).
  • Why it's built this way - it is private sealed, so xUnit does not collect its inherited facts as tests of their own (class doc, :12); the enclosing test grabs the inherited GovernedPages_RequireDeclaredRole as a delegate instead of running it.
  • Where it's used - GovernedPages_WithoutTheRole_StillFail constructs it and asserts the delegate throws with a message containing nameof(OtherRolePlusBarePage) (RouteAuthorizationTestsBaseTests.cs:37-:44). No usage sites outside the defining file.

NoDomainEventDispatcher

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:128 · Level 2 · class

  • What it is - a no-op IDomainEventDispatcher stub: DispatchAsync returns Task.CompletedTask and touches nothing (:128-:132).
  • Depends on - IDomainEventDispatcher (:128) and IDomainEvent (the IEnumerable<IDomainEvent> parameter, :130).
  • Concept introduced - building a real DbContext model without a real dispatcher. DeleteBehaviorConventionTests assembles a service provider (:90-:97) purely so DomainEventSaveChangesInterceptor can be constructed and EF's convention pipeline driven over the model; nothing in a delete-behavior assertion ever raises an event, so the fixture is deliberately inert rather than a mock that records calls it will never receive.
  • Walkthrough - one member, DispatchAsync(IEnumerable<IDomainEvent> domainEvents, CancellationToken cancellationToken = default) => Task.CompletedTask (:130-:131). No fields, no constructor.
  • Why it's built this way - the doc comment states the ruling directly (:127): "Never invoked: no model build dispatches an event." Two sibling stubs beside it in the same file carry the identical rationale for the outbox signal and the entity data-source registry.
  • Where it's used - constructs the IServiceCollection behind the fixture's service-provider helper alongside the sibling stubs, wired into new DomainEventSaveChangesInterceptor(new NoDomainEventDispatcher(), ...) (DeleteBehaviorConventionTests.cs:92-:95). Not referenced outside the defining file.

PasswordHashingFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PasswordHashingFitnessTests.cs:15 · Level 2 · class

  • What it is - a structural credential-storage gate asserted against compiled IL rather than source text: PasswordHasher must depend on Rfc2898DeriveBytes (a deliberately slow key-derivation function) and on CryptographicOperations (the constant-time comparison), so a rewrite cannot quietly swap either out.
  • Depends on - PasswordHasher as the assembly anchor and the type under scan (PasswordHashingFitnessTests.cs:1,:21), ArchitectureAssert (:37,:50), NetArchTest's Types / PredicateList query API (:56-:59), and externals xUnit plus AwesomeAssertions.
  • Concept introduced - asserting on a dependency edge instead of on an output value. A hashing routine can be verified two ways. A known-answer test pins the values (iteration count, salt length, digest length) and is what PasswordHasherSecurityTests does; this class pins the shape, which catches a class of change the value tests cannot. Swap PBKDF2 for a single SHA-512 pass and keep the same 32-byte output length and the known-answer pin can be regenerated by whoever made the change, but the Rfc2898DeriveBytes reference disappears from the IL and this test goes red. Same for replacing CryptographicOperations.FixedTimeEquals with SequenceEqual: identical behavior on every test input, and a timing side channel in production. The two because strings spell out the attacks precisely: commodity GPUs trying billions of candidates per second against a stolen table (:38-:39), and a short-circuiting comparison leaking the matching prefix length so a digest is recovered byte by byte (:51-:53). [Rubric §11 - Security] assesses whether credential storage resists offline cracking and side channels; this is the build-time half of that guarantee. [Rubric §14 - Testability] covers the technique, and [Rubric §32 - Dependency & Supply-Chain] applies at the edges, since the assertion is that a specific BCL cryptographic primitive is actually reached.
  • Walkthrough - two private const string fully-qualified type names, CryptographicOperationsType (:17) and Rfc2898DeriveBytesType (:19), keep the matched names in one place. Infrastructure (:21) anchors the scanned assembly through typeof(PasswordHasher).Assembly, and the private PasswordHasherTypes() helper (:56-:59) narrows it with Types.InAssembly(Infrastructure).That().HaveName(nameof(PasswordHasher)).
    • ScannedPasswordHasherSet_IsNotEmpty (:23-:27): the non-vacuity guard, and the first fact for a reason. HaveName matches on the simple name, so a renamed or moved type would leave the predicate list empty and both dependency assertions would pass having inspected nothing. ContainSingle on the full name (:25-:26) closes that.
    • PasswordHasher_DependsOnASlowKeyDerivationFunction (:29-:40) and PasswordHasher_DependsOnConstantTimeComparison (:42-:54): each runs .Should().HaveDependencyOnAll(<type name>).GetResult() and routes the result through ArchitectureAssert.NoViolations. HaveDependencyOnAll is the positive form (the dependency must be present), which is the unusual direction for an architecture rule and the right one here.
  • Why it's built this way - ADR-032 fixes the hashing scheme; the real implementation calls Rfc2898DeriveBytes.Pbkdf2 (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Auth/PasswordHasher.cs:35) and CryptographicOperations.FixedTimeEquals (:58). Pinning the decision as a fitness function rather than a code-review convention is the ADR-015 approach, and it is the one that survives a refactor by someone who has not read the ADR.
  • Where it's used - an independent class in the Common architecture suite. It is the shape half of a pair; the parameter values are pinned by PasswordHasherSecurityTests in the Infrastructure unit-test project (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Tests/Auth/PasswordHasherSecurityTests.cs:21), which holds PBKDF2-HMAC-SHA512 at 600,000 iterations with a 32-byte salt and a 64-byte output as reflected private constants (:20-:22). The class doc here records the division of labour (PasswordHashingFitnessTests.cs:10-:13).
  • Caveats / not-in-source - HaveDependencyOnAll reports a reference in the compiled IL, not that the reference is on the hashing path. It cannot tell an actually-used PBKDF2 call from a dead one, which is why the value pins in the companion test remain load-bearing.

ThrottledErrors

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ErrorCodeFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:61 · Level 2 · class

  • What it is - an internal static fixture that owns one compiled Error construction site built through the Error.TooManyRequests factory, so the error-catalog rule has a rate-limit code to read out of IL.
  • Depends on - Error (internal static Error TooFast() => Error.TooManyRequests("TooFast", "Slow down"), ErrorCodeFixtures.cs:61-:64). Its siblings in the same file, such as UnprefixedErrors, cover the other catalog shapes.
  • Concept introduced - a factory you do not cover is a hole in the catalog. The rule reads codes from compiled IL (file doc, ErrorCodeFixtures.cs:6-:7), so every Error factory the scanner recognizes needs a fixture proving its code is picked up. The code TooFast carries no module prefix on purpose: if TooManyRequests were ignored, the code would escape both the prefix gate and the uniqueness gate unreported.
  • Walkthrough - one static method, TooFast() (:63), returning an Error with the code "TooFast" and message "Slow down". No state, no branches.
  • Where it's used - the subject of TooManyRequestsCode_IsReadIntoTheCatalog in ErrorCatalogFitnessTests (ErrorCatalogFitnessTests.cs:66-:77), which runs ArchitectureRules.ErrorCodesUseAnAllowedPrefix and asserts the thrown message contains "TooFast" and nameof(ThrottledErrors) (:75-:76).

PasswordRuleParityTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/PasswordRuleParityTests.cs:21 · Level 2 · class

  • What it is - a client/server validation parity test for the password rule: for every row it asks the Register form's PasswordComplexityAttribute and the server's StrongPasswordRules<T> for a verdict and fails when the two disagree (PasswordRuleParityTests.cs:40-:55).
  • Depends on - PasswordComplexityAttribute (MMCA.Common.UI.Pages.Auth, :3,:45), StrongPasswordRules<T> (MMCA.Common.Application.Validation, :2,:48), System.ComponentModel.DataAnnotations for ValidationContext and ValidationResult (:1), and externals xUnit plus AwesomeAssertions. Both sides evaluate the shared PasswordComplexity definition (MMCA.Common/Source/Core/MMCA.Common.Shared/Auth/PasswordComplexity.cs:18), which this test never names directly: it asserts the agreement, not the mechanism.
  • Concept introduced - a rule implemented twice is tested as a pair, not twice. The form and the API each need the password rule, and the layering forbids them from sharing a validator (UI may depend on Shared only). The fix in source is one definition in Shared that both sides call: the attribute calls PasswordComplexity.Evaluate (MMCA.Common/Source/Presentation/MMCA.Common.UI/Pages/Auth/PasswordComplexityAttribute.cs:25) and the server rule chains PasswordComplexity.MinimumLength, MaximumLength and the four regexes (MMCA.Common/Source/Core/MMCA.Common.Application/Validation/CommonValidationRules.cs:194-:201). This test is what keeps that true: it does not assert either verdict, only that they are equal, so a change that moves one side (a new character class, an ASCII-only regex) goes red even when each side's own unit tests stay green. A disagreement means the form either rejects a password the API would accept, or lets one through that the API then refuses after a round trip (class doc, :8-:12). The rows lean on the Unicode cases because that is where a naive [A-Z] / [^a-zA-Z0-9] implementation diverges: PasswordComplexity treats any \p{L} (ideographs included) as a letter and any \p{Nd} as a digit (PasswordComplexity.cs:12-:15,:27-:43), so an accented or CJK letter never counts as the special character. [Rubric §24 - Forms, Validation & UX Safety] assesses exactly this client/server parity; [Rubric §14 - Testability] covers the technique.
  • Walkthrough
    • ClientPasswordComplexity_MatchesServerStrongPasswordRules(string password, string scenario) (:27-:55): a [Theory] over twelve [InlineData] rows (:28-:39). Seven are ASCII: a valid password, a valid one at exactly the eight-character minimum, then one row each for no uppercase, no lowercase, no digit, no special character, and seven characters. Five are non-ASCII: an accented lowercase letter as the only candidate special character (:35), an uppercase umlaut as the only uppercase (:36), CJK ideographs as the only non-ASCII-alphanumeric characters (:37), CJK beside a full ASCII complement (:38), and a full-width digit as the only digit (:39).
    • The client verdict is computed the way the EditForm computes it: GetValidationResult with a ValidationContext whose MemberName is nameof(PasswordProbe.Password), compared against ValidationResult.Success (:44-:47).
    • The server verdict binds a StrongPasswordRules<PasswordProbe> to p => p.Password and reads Validate(probe).IsValid (:48-:50), the same way the API's FluentValidation validators run it over a model.
    • clientVerdict.Should().Be(serverVerdict, ...) (:52-:54) puts the scenario label and the server's verdict in the because, so a red row reads as "the Register form and the API must agree on 'full-width digit as the only digit' (server says valid)" rather than a bare boolean mismatch.
    • PasswordProbe (:58) is a private sealed record with one Password property: the smallest model the generic server rule can be bound to.
  • Why it's built this way - the class doc records the placement (:14-:15): the architecture test project is the one project that references both sides, since the UI test project references MMCA.Common.UI only. The non-ASCII rows are written as \u escapes so the test source stays ASCII (:16-:18).
  • Where it's used - an independent class in the Common architecture suite; no usage sites outside the defining file. Its subjects are the PasswordComplexityAttribute on the Register form and the StrongPasswordRules<T> the API's validators compose.
  • Caveats / not-in-source - parity is asserted only over the twelve rows, and none of them is empty or longer than 128 characters. Those are the two inputs where the sides are not the same rule by design: the attribute returns success on null or empty input and leaves that to RequiredAttribute (PasswordComplexityAttribute.cs:25), and PasswordComplexity.Evaluate does not check the upper bound (PasswordComplexity.cs:46-:57), while the server rule applies NotEmpty and MaximumLength(PasswordComplexity.MaximumLength) (CommonValidationRules.cs:195,:197).

AcyclicConsumer

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CycleFixtures.Acyclic · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Acyclic/AcyclicFixtures.cs:6 · Level 3 · class

  • What it is - the negative control for the namespace-cycle rule: a third fixture namespace that points into Left and that nothing points back at, so it must never appear in a cycle report.
  • Depends on - LeftService (the one-way Service property, AcyclicFixtures.cs:9, using at :1).
  • Concept - cross-references LeftModelBase. Its role is the discrimination half of the proof: a rule that reported every namespace with any edge would also "pass" the cycle test, so the fixture set needs a namespace that is genuinely coupled yet genuinely acyclic. Depending on Left (a namespace that is in a cycle) is deliberate: it proves the rule reports strongly connected components, not merely everything reachable from a cycle.
  • Walkthrough - one member, public LeftService? Service { get; set; } (:9).
  • Where it's used - asserted absent from the failure message by Rule_FlagsTwoNamespaceCycle_ButNotAcyclicNamespaces (NamespaceCycleFitnessTests.cs:23-:25).

MutableFixtureId, ExtraStateFixtureId

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.StronglyTypedIdFixtures · (see per-type table) · Level 3 · record struct

  • What it is - two of the three offending shapes the StronglyTypedIdsAreReadonlyRecordStructs fitness rule (ADR-115) is proven against: a record struct missing readonly, and a readonly record struct that wraps a second value alongside the primitive.
  • Depends on - IStronglyTypedId<TSelf, TValue>, the interface every wrapper in this file (StronglyTypedIdFixtures.cs) implements. Both sit alongside two compliant siblings, CompliantFixtureId and CompliantStringFixtureId, and one non-record sibling, NotARecordFixtureId, all in the same file, none of which the rule flags for the two shapes below.
  • Concept introduced - why the wrapper shape itself is the contract, not just its interface. The strongly-typed-identifier opt-in (MMCA.Common/CLAUDE.md, "Strongly typed identifiers") is two lines: a readonly record struct plus a From factory. MutableFixtureId drops readonly (record struct MutableFixtureId(int Value) : IStronglyTypedId<MutableFixtureId, int>, StronglyTypedIdFixtures.cs:53), so its wrapped value could be reassigned after an entity has been keyed on it. ExtraStateFixtureId carries a second field, Label (readonly record struct ExtraStateFixtureId(int Value, string Label) : IStronglyTypedId<ExtraStateFixtureId, int>, :63-:64), which makes the EF column mapping ambiguous and the converter's round trip lossy; its From factory only ever fills the primitive and leaves Label at string.Empty (:67), so the extra state is dead weight by construction. Neither shape fails to compile: only reflection over the compiled type (record-ness, readonly-ness, declared instance field count) can catch them, which is why they are compiled fixtures rather than inline test doubles. [Rubric §14 - Testability] covers the fixture technique; [Rubric §5 - Domain Model] is what the rule protects, a value object that cannot silently mutate or smuggle untyped state past its wrapper.
  • Walkthrough - MutableFixtureId.From(int value) => new(value) (:56) is otherwise identical to the compliant declaration; the only diff is the missing readonly modifier on the struct itself. ExtraStateFixtureId.From(int value) => new(value, string.Empty) (:67) is the whole type: a two-line factory that proves the second constructor parameter is reachable but never legitimately populated by the framework's own convention.
Type File:Line Notes (what differs)
MutableFixtureId MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/StronglyTypedIdFixtures/StronglyTypedIdFixtures.cs:53 record struct, not readonly record struct: the missing modifier is the offense.
ExtraStateFixtureId MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/StronglyTypedIdFixtures/StronglyTypedIdFixtures.cs:63 readonly record struct but wraps a second field, Label, beyond the primitive Value.
  • Where it's used - both are the subject of one [Fact] each on StronglyTypedIdFitnessTests: Rule_FlagsAWrapperThatIsNotReadonly asserts the thrown message contains nameof(MutableFixtureId) and the word "readonly" (StronglyTypedIdFitnessTests.cs:23-:31), and Rule_FlagsAWrapperCarryingExtraState asserts it contains nameof(ExtraStateFixtureId) and "Label" (:33-:41).

EmptyScanTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:180 · Level 3 · class

  • What it is - the adversarial fixture for the anonymous-endpoint gate's non-vacuity guard: a subclass pointed at an assembly that contains no controllers and no routable components at all, so the scan finds nothing and the guard must fail.
  • Depends on - AnonymousEndpointTestsBase (private sealed class EmptyScanTests : AnonymousEndpointTestsBase, AnonymousEndpointTestsBaseTests.cs:180) and the MMCA.Common.Shared assembly reached through typeof(Shared.Abstractions.Result).Assembly (:121).
  • Concept introduced - guarding the guard: why a fitness function needs a floor. The allow-list assertion is offenders.Should().BeEmpty() (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:86-:62), and an empty scan produces zero offenders, so the gate passes loudest exactly when it has stopped looking. A renamed assembly, a moved anchor type, or a repo re-layout all produce that failure mode, and none of them is visible in a green run. The base therefore ships a third fact, ScannedEndpointSet_IsNotEmpty (:66-:76), that counts the discovered controller and routable-component types and requires at least MinimumScannedTypes, whose default is 1 (:51). This fixture is what proves the counter is real: point it at an assembly that genuinely has neither shape and the fact must throw. Compare the equivalent floors elsewhere in the suite: MinimumScannedTypes => 21 on AnonymousEndpointTests, MinimumBaseResources => 3 on LocalizationResourceTests, MinimumRoutes = 8 in NavigationContractTests, and the DistinctErrorCodeCount and HandledCommandCount inventories the catalog and validator rules expose for the same purpose. [Rubric §14 - Testability] assesses whether the guardrails are themselves trustworthy, and a vacuity floor is the cheapest thing that keeps one honest over a decade of refactors.
  • Walkthrough - two overrides and an inline comment that states why the chosen assembly works: the Shared package has neither controllers nor routable components (:119). TargetAssemblies returns that one assembly (:120-:121), and AllowedAnonymousEndpoints is empty (:123), which is irrelevant here because nothing is discovered to compare against. Being private and nested is what keeps xUnit from collecting its three inherited facts as deliberately-red tests of their own (class doc, :10-:11).
  • Where it's used - the subject of Base_Fails_WhenNothingWasScanned (AnonymousEndpointTestsBaseTests.cs:37-:43), which converts the inherited ScannedEndpointSet_IsNotEmpty into a delegate and asserts it throws (:40-:42).
  • Caveats / not-in-source - that fact asserts only that an exception is thrown, not what its message says. It proves the floor bites; it does not pin the wording.

CompliantStringFixtureId

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.StronglyTypedIdFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/StronglyTypedIdFixtures/StronglyTypedIdFixtures.cs:24 · Level 3 · record struct

  • What it is - the same positive control as CompliantFixtureId, over a string primitive instead of int.
  • Depends on - IStronglyTypedId<TSelf, TValue> closed over itself and string (public readonly record struct CompliantStringFixtureId(string Value) : IStronglyTypedId<CompliantStringFixtureId, string>, StronglyTypedIdFixtures.cs:24-:25).
  • Concept introduced - cross-references CompliantFixtureId. Its contribution is discrimination across the primitive type: the fitness rule inspects the shape of the wrapper, not its backing type, and a rule that only ever saw an int-backed compliant declaration could not prove it treats a string-backed one the same way.
  • Walkthrough - the primary constructor parameter Value (:24) and one static factory, From(string value) => new(value) (:27).
  • Where it's used - the same IdentifierFixtureMap scan and the same Rule_SaysNothingAboutTheCanonicalDeclaration assertion as CompliantFixtureId (StronglyTypedIdFitnessTests.cs:50).

ArchiveTicketHandler

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CommandValidatorFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:62 · Level 3 · class

  • What it is - the handler for ArchiveTicketCommand, a command that carries data (TicketId) and has a handler but no validation of any kind: the shape the command-validator coverage rule must flag.
  • Depends on - ICommandHandler<ArchiveTicketCommand, Result> (MMCA.Common.Application.UseCases.Contracts) and ArchiveTicketCommand (the record it handles, :60,:62).
  • Concept introduced - cross-references CreateTicketHandler. Its contribution is discrimination at the far end of the coverage spectrum: CreateTicketHandler has direct coverage and UpdateTicketHandler has bridge coverage, while this one has neither, so it is what proves the rule actually detects an uncovered command rather than passing everything with a handler.
  • Walkthrough - one member, HandleAsync(ArchiveTicketCommand command, CancellationToken cancellationToken = default) => Task.FromResult(Result.Success()) (:64-:65). No constructor, no validator anywhere in the file for ArchiveTicketCommand.
  • Where it's used - CommandWithNoValidation_IsFlagged asserts nameof(ArchiveTicketCommand) present in the coverage failure message (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:23-:31); AllowlistedCommand_IsExempt reuses it as the fixture that must stay flagged once a different command is allowlisted, proving the allowlist narrows to exactly the name it lists (:78-:89).

CompliantFixtureId

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.StronglyTypedIdFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/StronglyTypedIdFixtures/StronglyTypedIdFixtures.cs:17 · Level 3 · record struct

  • What it is - the positive control for the strongly typed ID fitness function (ADR-115): the canonical declaration shape, an int-backed wrapper, that the rule must say nothing about.
  • Depends on - IStronglyTypedId<TSelf, TValue> closed over itself and int (public readonly record struct CompliantFixtureId(int Value) : IStronglyTypedId<CompliantFixtureId, int>, StronglyTypedIdFixtures.cs:17).
  • Concept introduced - the two-line strongly typed ID is exactly two lines, no more. A readonly record struct with a positional Value and a static From factory is the whole contract; nothing else is required and nothing else is permitted before the rule starts flagging (see NotARecordFixtureId, MutableFixtureId, ExtraStateFixtureId for the three ways to fail it). This fixture is the shape those three are measured against: a rule that flagged it too would be unusable, since it is the identical shape every real identifier in Source/ would take if one adopted the opt-in.
  • Walkthrough - the primary constructor parameter Value (:17) and one static factory, From(int value) => new(value) (:18), documented as "the one member the canonical declaration writes" (:18).
  • Where it's used - the IdentifierFixtureMap assembly scan in StronglyTypedIdFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/StronglyTypedIdFitnessTests.cs:53-:58) includes it by assembly membership, and Rule_SaysNothingAboutTheCanonicalDeclaration asserts its name is absent from the failure message (:44-:50).

CreateTicketHandler

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CommandValidatorFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:21 · Level 3 · class

  • What it is - the handler for CreateTicketCommand, the direct-coverage positive control: an explicit CreateTicketCommandValidator (AbstractValidator<CreateTicketCommand>, :16-:19) validates the command itself.
  • Depends on - ICommandHandler<CreateTicketCommand, Result> and CreateTicketCommand (:13,:21).
  • Concept introduced - cross-references ArchiveTicketHandler. Its contribution is the coverage baseline: AddValidatorsFromAssembly picks up CreateTicketCommandValidator by its IValidator<CreateTicketCommand> closure with no bridge involved, which is the simplest of the three coverage shapes the rule must recognize (direct, bridge, and none).
  • Walkthrough - one member, HandleAsync(CreateTicketCommand command, CancellationToken cancellationToken = default) => Task.FromResult(Result.Success()) (:23-:24).
  • Where it's used - CommandWithItsOwnValidator_IsCovered asserts nameof(CreateTicketCommand) absent from the coverage failure message (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:34-:42); its assembly anchors FixtureModuleMap's single module (typeof(CreateTicketCommand).Assembly, :121).

ArchiveTicketHandler, CreateTicketHandler, PurgeTicketsHandler, RebuildTicketIndexHandler, ReopenTicketHandler, UpdateTicketHandler

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CommandValidatorFixtures · (see per-type table) · Level 3 · class

Six byte-identical handlers, one per fixture command, each a single expression-bodied HandleAsync returning Task.FromResult(Result.Success()). They do nothing, and that is the design: their only job is to make their command handled, which is the condition that puts a command into the coverage rule's scope at all.

  • Depends on - ICommandHandler<in TCommand, TResult> closed over the fixture command and Result (CommandValidatorFixtures.cs:2-:3).
  • Concept introduced - "has a handler" is the scoping predicate, not "is named Command". The rule never matches on a naming convention. It projects every concrete class in the module Application assemblies through the closed generic arguments of the command-handler interface and takes the first type argument (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:96-:102). A record that nobody handles is not a command the repo dispatches, so it is not a command the pipeline can fail to validate, and inventing a naming rule instead would both miss unconventionally-named commands and flag DTOs that merely end in Command. These six handlers exist so the fixture commands satisfy that predicate honestly rather than by declaration.
  • Walkthrough - every one has the same body: public Task<Result> HandleAsync({Command} command, CancellationToken cancellationToken = default) => Task.FromResult(Result.Success());. Note the trailing token with its conventional name, which keeps these fixtures compliant with the framework's own cancellation rule even though nothing here awaits.
Type File:Line Its command
CreateTicketHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:21 CreateTicketCommand, the directly-validated case.
UpdateTicketHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:38 UpdateTicketCommand, covered through the request bridge.
ReopenTicketHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:53 ReopenTicketCommand, the half-bridge offender.
ArchiveTicketHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:62 ArchiveTicketCommand, the plain offender.
PurgeTicketsHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:71 PurgeTicketsCommand, the offender the allowlist fact exempts.
RebuildTicketIndexHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:80 RebuildTicketIndexCommand, the payload-free marker. Handled like the rest, and still skipped, because the skip is decided by the payload test rather than by the handler.
  • Why they're built this way - one handler per command, with no shared abstract base, keeps the discovery pass honest: an abstract or open-generic handler would contribute a type parameter rather than a command and would silently shrink the inventory. The uniform empty body means no handler can accidentally become the reason a fact passes.
  • Where they're used - counted by HandledCommandCount (ArchitectureRules.CommandValidators.cs:80), which CommandInventory_CountsTheDataCarryingCommands pins at exactly 5, the five data-carrying commands with the payload-free marker excluded (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:100-:108).

FakeDependentModuleConformanceTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:60 · Level 3 · class

  • What it is - the shared module-conformance base wired up against the "hard" fixture module: the subclass shape a real non-leaf module (Store Sales, ADC Notification) uses, with all three expectations declared and the disabled-stub hook implemented.
  • Depends on - ModuleConformanceTestsBase<TModule> (public sealed class FakeDependentModuleConformanceTests : ModuleConformanceTestsBase<FakeDependentModule>, ModuleConformanceTestsBaseTests.cs:60), FakeDependentModule, IFakeExportService, DisabledFakeExportService, and Microsoft.Extensions.DependencyInjection's ServiceCollection / ServiceLifetime (:70,:77).
  • Concept introduced - asserting on a ServiceCollection instead of a built container. The disabled-stub hook is a registration contract, not a resolution one, so the fixture never builds a provider: it constructs a bare ServiceCollection (:70), calls RegisterDisabledStubs on it (:72), and then inspects the resulting ServiceDescriptor directly (:74). That is what lets one assertion cover all three things that can go wrong (wrong service type, wrong implementation type, wrong lifetime) without any of the module's real dependencies having to exist. [Rubric §7 - Microservices Readiness] is the property under guard: a module that can be switched off while its exported contract stays resolvable is a module that can be extracted. [Rubric §14 - Testability] covers the technique.
  • Walkthrough - three property overrides declare the expectations: ExpectedName => "FakeDependent" (:62), ExpectedDependencies => ["FakeOther", "FakeLeaf"] (:64), and ExpectedRequiresDependencies => true (:66). The dependency list is written in the opposite order to the module's own declaration (:35), which is deliberate: the base compares with BeEquivalentTo (ModuleConformanceTestsBase.cs:51), so order must not matter, and writing it reversed proves it does not. AssertDisabledStubs(FakeDependentModule module) (:68-:78) is the one real override: single descriptor for IFakeExportService (:74-:75), implementation type DisabledFakeExportService (:76), lifetime Singleton (:77).
  • Where it's used - collected and run directly by xUnit (it is public and sealed, so its four inherited [Fact]s are real tests of the fixture module), and referenced by ModuleConformanceTestsBaseTests as the non-leaf half of the base's coverage.

DriftedTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:131 (and a same-named sibling in Api/AnonymousEndpointTestsBaseTests.cs:100) · Level 3 · class

  • What it is - the name is used twice in this assembly, once per fitness base under proof, and both are the same idea: a private sealed subclass of a shipped *TestsBase whose configuration is deliberately wrong, so the base's assertions can be shown to actually fail on the drift they claim to catch. They are nested in different outer classes, so their full names differ and neither is visible outside its own file.
  • Depends on - the base each one drifts from, plus the fixture it points at: ModuleConformanceTestsBase<TModule> with FakeDependentModule for one, AnonymousEndpointTestsBase with the fixture controllers for the other.
  • Concept introduced - negative fixtures, and hiding them from test discovery. A fitness base that asserts nothing passes everywhere; the only way to know an assertion bites is to feed it a case that must fail. But a public subclass of an xUnit base is itself collected, so its inherited [Fact]s would run and report as red tests of their own. Declaring the drifted subclass private and nested keeps xUnit from collecting it, while the enclosing class can still instantiate it and invoke the inherited methods directly as delegates (var assert = new DriftedTests().Module_ShouldDeclare_ExpectedName;, ModuleConformanceTestsBaseTests.cs:91). Both class docs record that reasoning in the same words (ModuleConformanceTestsBaseTests.cs:81-:85, AnonymousEndpointTestsBaseTests.cs:10-:11). [Rubric §14 - Testability] assesses whether the guardrails themselves are trustworthy; this is the fixture shape that earns that trust. Compare NavigatingSpec, the same negative-fixture technique applied to a specification rule, and ProbeTests, which parameterizes the drift instead of hard-coding it.
Type File:Line What is deliberately wrong
DriftedTests (in AnonymousEndpointTestsBaseTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:163 Points TargetAssemblies at this test assembly (:102-:103), so the scan finds the fixture controllers' [AllowAnonymous] attributes, and then supplies an empty AllowedAnonymousEndpoints (:105). Every discovered endpoint is therefore an offender, and the failure message must name AnonymousFixtureController.
DriftedTests (in ModuleConformanceTestsBaseTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:131 Three overrides, each wrong in a different way against FakeDependentModule's real declarations: ExpectedName => "NotTheDeclaredName" (:133) against the module's "FakeDependent" (:33); ExpectedDependencies => ["FakeLeaf"] (:135) against the module's two-entry ["FakeLeaf", "FakeOther"] (:35), so one dependency is missing; and ExpectedRequiresDependencies => false (:137) against the module's true (:37). AssertDisabledStubs is deliberately not overridden, so the fourth inherited fact stays vacuous here.
  • Walkthrough - one wrong value per assertion, and no more. If a single drifted subclass were wrong in three ways at once and the base only ever threw on the first, the other two assertions could rot undetected; the module-side fixture avoids that by being driven three separate times, once per fact.
  • Where it's used - the anonymous-endpoint one drives Base_Fails_WhenAnAnonymousEndpointIsNotAllowListed (AnonymousEndpointTestsBaseTests.cs:15-:24); the module one is instantiated three times by ModuleConformanceTestsBaseTests (:91,:99,:107), once per assertion under proof.

FixtureCountEvent, FixtureNullableCountEvent

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ContractShapeFixtures.IntegrationEvents · (see per-type table) · Level 3 · record

A twin pair of compiled integration-event fixtures that differ in exactly one thing: whether their single Count member is int or int?. Each one's summary names the other as its twin "differing only in the member's nullability" (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:31,:35).

  • Depends on - BaseIntegrationEvent (both derive from it directly, ContractShapeFixtures.cs:33,:37). They are scanned by the frozen-contract gate IntegrationEventContractTestsBase through the self-test's private FixtureMap (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventContractTestsBaseTests.cs:254).
  • Concept introduced - a twin fixture isolates one variable. Two types identical in every other respect mean any difference in their rendered contract line can only come from the one axis that differs, here value-type nullability. An int retyped to int? changes what a consumer must accept, so a snapshot that printed both as Int32 would let a breaking wire change through silently. This is the same isolate-one-variable technique the negative fixtures in this chapter use (see DriftedTests). [Rubric §14 - Testability] assesses whether the code's guarantees are provable by tests; this pair proves the contract renderer does not erase Nullable<T>.
  • Walkthrough - each is a one-line positional internal sealed record with one member; there is no body.
Type File:Line What differs
FixtureCountEvent MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:33 (int Count): the non-nullable member.
FixtureNullableCountEvent MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:37 (int? Count): the nullable member.
  • Why they're built this way - the assertion compares the two rendered member lists against each other rather than against hard-coded strings, so it stays true whatever spelling the renderer chooses for a nullable value type, as long as the two differ.
  • Where they're used - Contract_Differs_WhenTheOnlyChangeIsIntToNullableInt in IntegrationEventContractTestsBaseTests (IntegrationEventContractTestsBaseTests.cs:117-:124), which asserts MembersOf(contract, ".FixtureNullableCountEvent ") is not equal to MembersOf(contract, ".FixtureCountEvent ").

FixtureEventNameMissingEvent

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.EventNameFixtures.IntegrationEvents · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/EventNameFixtures/IntegrationEvents/EventNameFixtures.cs:12 · Level 3 · record

  • What it is - the deliberately bad integration event for the event-name rule: it carries no [EventName], so its outbox rows would store a CLR type name that a rename would orphan, and the rule must name it (summary, EventNameFixtures.cs:6-:10).
  • Depends on - BaseIntegrationEvent (internal sealed record FixtureEventNameMissingEvent(string Reference) : BaseIntegrationEvent;, EventNameFixtures.cs:12). Its well-formed twin is FixtureEventNameDeclaredEvent.
  • Concept introduced - a stable wire identity must be declared, never inferred from the CLR name. The outbox persists the event's name, so an event with no EventName ties stored rows to a type name that a rename or namespace move would orphan. [Rubric §7 - Microservices Readiness] assesses stable contracts between services; [Rubric §14 - Testability] covers the negative/positive fixture pair.
  • Walkthrough - one plain string Reference member and no body; the absence of the attribute is the whole fixture.
  • Where it's used - EventWithoutEventName_IsFlagged in EventNameConventionTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventNameConventionTests.cs:16-:24) asserts the failure message of ArchitectureRules.IntegrationEventsDeclareEventName contains nameof(FixtureEventNameMissingEvent) (:22); the test's FixtureProbeMap also uses its assembly to scope the scan (:48).

FixtureEventNameDeclaredEvent

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.EventNameFixtures.IntegrationEvents · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/EventNameFixtures/IntegrationEvents/EventNameFixtures.cs:20 · Level 3 · record

  • What it is - the well-formed twin of FixtureEventNameMissingEvent: it declares [EventName("Fixture.EventNameDeclared.v1")] (EventNameFixtures.cs:19), so the rule must leave it out of the failure message (summary, :14-:17).
  • Depends on - BaseIntegrationEvent and EventNameAttribute (MMCA.Common.Domain.Attributes, :1): internal sealed record FixtureEventNameDeclaredEvent(string Reference) : BaseIntegrationEvent; (:20).
  • Concept introduced - cross-references a stable wire identity must be declared from FixtureEventNameMissingEvent: this is the control proving the rule is selective rather than flagging every event. [Rubric §14 - Testability] applies.
  • Where it's used - EventWithEventName_IsNotFlagged in EventNameConventionTests (EventNameConventionTests.cs:27-:35) asserts the rule's message does not contain nameof(FixtureEventNameDeclaredEvent) (:33).

FixtureCompliantV1, FixtureCompliantV2, FixtureCompliantV3, FixtureContestedV1, FixtureContestedV2, FixtureContestedV3, FixtureBackwardsV1, FixtureBackwardsV2

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.UpcasterFixtures.IntegrationEvents · (see per-type table) · Level 3 · record

Eight throwaway integration-event contracts in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs), forming three groups: a compliant three-rung version ladder, a pair of rival successors to one contested source, and a backwards pair whose "successor" is older than its source. They are the data the two event-upcaster fitness rules are proven against.

  • Depends on - BaseIntegrationEvent (every one of them derives from it, EventUpcasterFixtures.cs:2). Each is a one-parameter positional record carrying a single string Sku.
  • Concept introduced - SchemaVersion as the ordering a fixture set has to make visible. BaseIntegrationEvent declares public virtual int SchemaVersion => 1 (MMCA.Common/Source/Core/MMCA.Common.Domain/DomainEvents/BaseIntegrationEvent.cs:32), so a contract that overrides nothing is version 1 and a successor states its own number (ADR-010). That default is what lets these fixtures be as small as they are: the V1 of each group is a bare record and only the successors carry an override. The rule reads the property without running a constructor, through RuntimeHelpers.GetUninitializedObject (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:118), which is why a get-only virtual returning a literal is the required shape and why no event needs a parameterless factory just to be inspected. [Rubric §6 - CQRS & Event-Driven] assesses whether event contracts are versioned and governed rather than edited in place; [Rubric §9 - API & Contract Design] applies because a published event is a wire contract; [Rubric §14 - Testability] covers the fixtures.
  • Walkthrough - the file's header doc records a subtlety worth internalizing (:9-:13): the contracts sit in a *.IntegrationEvents namespace so that the residency rule, which EventScopeFitnessTests exercises over this same assembly through a consumer-shaped map, stays satisfied. A fixture that broke a neighbouring rule to prove its own would be a poor fixture. They also never leave this test assembly, so no shipped event contract churns because of them.
Type File:Line SchemaVersion Role
FixtureCompliantV1 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:15 1 (inherited default) First rung of the compliant ladder, and the source of exactly one upcaster.
FixtureCompliantV2 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:18 2 (:20) The middle rung: a target of one upcaster and the source of the next. That dual role is the whole point, since it is a chain rather than a duplicate claim.
FixtureCompliantV3 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:24 3 (:26) Terminal contract of the ladder.
FixtureContestedV1 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:30 1 (inherited default) The offending source: two upcasters both read it, which is what the unique-source rule must report.
FixtureContestedV2 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:33 2 (:35) One of the two rival successors.
FixtureContestedV3 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:39 3 (:41) The other rival successor. Both targets are legal on their own; the offence is the shared source.
FixtureBackwardsV1 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:45 1 (inherited default) The older contract of the backwards pair, used as the upcaster's target, which is the offence.
FixtureBackwardsV2 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:48 2 (:50) The newer contract, used as the upcaster's source.
  • Why they're built this way - three groups because the two rules have three distinct outcomes to prove between them (clean, duplicate claim, wrong direction), and because a chain has to be distinguishable from a duplicate claim. Nothing but Sku on any of them: the payload is irrelevant to both rules, and every byte of fixture that is not load-bearing is a byte that can mislead a future reader about what is being tested. See ADR-090.
  • Where they're used - the type arguments of the five fixture upcasters (FixtureCompliantV1ToV2Upcaster and family), named by nameof in the assertions of EventUpcasterFitnessTests, and, because they are this assembly's only integration events, the live contract IntegrationEventContractTestsBaseTests mutates.

FixtureHopV1, FixtureHopV2

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.UpcasterFixtures.IntegrationEvents · (see per-type table) · Level 3 · record

The first two rungs of a three-rung "double-hop" ladder (FixtureHopV1, FixtureHopV2, FixtureHopV3) declared in MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/DoubleHopUpcasterFixtures.cs. They exist only to be the type arguments of one offending upcaster class, FixtureDoubleHopUpcaster, which declares two upcaster contracts at once (DoubleHopUpcasterFixtures.cs:29-:30).

  • Depends on - BaseIntegrationEvent (both derive from it, DoubleHopUpcasterFixtures.cs:8,:11). They are consumed as the generic arguments of IEventUpcaster in IEventUpcaster<FixtureHopV1, FixtureHopV2>, IEventUpcaster<FixtureHopV2, FixtureHopV3> (:30).
  • Concept introduced - one upcaster class, one hop. The class summary spells out why a class implementing two IEventUpcaster<,> contracts is unsafe (:22-:28): the two default interface implementations conflict, so the class must declare the non-generic SourceType, TargetType and Upcast explicitly, and those can report only one source. SourceType returns typeof(FixtureHopV1) and TargetType returns typeof(FixtureHopV2) (:32,:34), so the registry would index the class as a single V1-to-V2 hop and the V2-to-V3 rung would silently vanish. FixtureHopV2 sits in the middle of that: it is the declared target of the first contract and the source of the second, the rung the runtime would lose. [Rubric §6 - CQRS & Event-Driven] assesses governed event versioning (ADR-090); [Rubric §14 - Testability] covers the fixture.
  • Walkthrough - each is a one-parameter positional internal sealed record carrying only string Sku. FixtureHopV1 has no body and so takes the inherited SchemaVersion of 1 from BaseIntegrationEvent (MMCA.Common/Source/Core/MMCA.Common.Domain/DomainEvents/BaseIntegrationEvent.cs:32); FixtureHopV2 overrides it to 2.
Type File:Line SchemaVersion Role
FixtureHopV1 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/DoubleHopUpcasterFixtures.cs:8 1 (inherited default) First rung: the source the offending class reports through SourceType (:32).
FixtureHopV2 MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/DoubleHopUpcasterFixtures.cs:11 2 (:13) Second rung: target of the first contract and source of the second, the hop the registry would drop.
  • Why they're built this way - the versions ascend correctly (1, 2, 3), so the schema-version rule has nothing to object to and the only defect left is the two-contracts-in-one-class shape. They live in a separate file from the FixtureCompliantV1 family but in the same *.IntegrationEvents namespace, so the residency rule stays satisfied.
  • Where they're used - nowhere outside the defining file. They reach the tests only through FixtureDoubleHopUpcaster, which UniqueSourceRule_FlagsAClassDeclaringTwoUpcasterContracts in EventUpcasterFitnessTests expects in the unique-source rule's message, together with the text "exactly one hop" (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventUpcasterFitnessTests.cs:37-:44).

FixtureCleanEvent

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.PayloadPurityFixtures.IntegrationEvents · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/PayloadPurityFixtures/IntegrationEvents/PayloadPurityFixtures.cs:27 · Level 3 · record

  • What it is - the control fixture for the integration-event payload-purity rule: a one-parameter positional record whose only member is a primitive string Sku, proving the rule stays quiet on a contract that is actually clean.
  • Depends on - BaseIntegrationEvent (internal sealed record FixtureCleanEvent(string Sku) : BaseIntegrationEvent;, PayloadPurityFixtures.cs:27). Sits beside its offending sibling, FixtureLeakingEvent, which nests a payload typed on AuditableBaseEntity<int> (:12-:15, :20) purely to give the rule something to flag.
  • Concept introduced - cross-references SchemaVersion as the ordering a fixture set has to make visible from the FixtureCompliantV1 and family section: both fixture sets sit in the same PayloadPurityFixtures/UpcasterFixtures *.IntegrationEvents namespace convention so the residency rule stays satisfied while they prove an unrelated rule. [Rubric §9 - API & Contract Design] applies because a published event is a wire contract, and payload purity (no domain entities crossing the wire) is part of that contract; [Rubric §14 - Testability] covers the positive/negative fixture pair.
  • Where it's used - the clean half of IntegrationEventPayloadPurityTests (2 references) and one more site, proving the rule does not flag a contract carrying only primitives.

FakeLeafModuleConformanceTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:51 · Level 3 · class

  • What it is - the same shared base wired up against the leaf fixture, declaring only ExpectedName and leaving every other expectation at the base's default. It is the exact subclass shape the three byte-identical consumer {X}ModuleTests files collapse into.
  • Depends on - ModuleConformanceTestsBase<TModule> (public sealed class FakeLeafModuleConformanceTests : ModuleConformanceTestsBase<FakeLeafModule>, ModuleConformanceTestsBaseTests.cs:51) and FakeLeafModule.
  • Concept - cross-references the default-interface-dispatch concept from FakeLeafModule. The pairing is what makes the proof work: the fixture module declares neither Dependencies nor RequiresDependencies, and this subclass declares neither expectation, so the base's defaults ([] and false, ModuleConformanceTestsBase.cs:29,:35) must line up with IModule's defaults read through the interface. If either side stopped reaching the interface, this pair would go red.
  • Walkthrough - one member: ExpectedName => "FakeLeaf" (:53). Everything else is inherited, including the vacuous AssertDisabledStubs default, which is exactly what makes the fourth fact a no-op here.
  • Where it's used - collected by xUnit in its own right, and driven directly (as an object, not as a test class) by two of ModuleConformanceTestsBaseTests's facts (:112-:129).

FixtureHopV3

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.UpcasterFixtures.IntegrationEvents · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/DoubleHopUpcasterFixtures.cs:17 · Level 3 · record

  • What it is - the third rung of the double-hop upcaster fixture (summary, DoubleHopUpcasterFixtures.cs:16): an integration event at schema version 3, the target of the second IEventUpcaster<,> contract that the offending FixtureDoubleHopUpcaster declares (:29-:30).
  • Depends on - BaseIntegrationEvent (:17). Its siblings in the same file are FixtureHopV1 (:8, default schema version) and FixtureHopV2 (:11-:14, version 2); FixtureDoubleHopUpcaster maps V1 to V2 and V2 to V3 through IEventUpcaster.
  • Concept introduced - a rung that would silently vanish. The upcaster's class doc (:22-:28) explains the trap: a class declaring two IEventUpcaster<,> contracts must implement the non-generic SourceType, TargetType and Upcast explicitly, and those can report only ONE source (:32-:34 pin FixtureHopV1 to FixtureHopV2), so the registry would index the class as a single V1 to V2 hop and the V2 to V3 step toward this record would never run. FixtureHopV3 exists so that lost rung is a real, compiled type. [Rubric §14 - Testability] applies.
  • Walkthrough - a positional internal sealed record FixtureHopV3(string Sku) whose only body is public override int SchemaVersion => 3; (:19); Sku is the single payload member every rung carries forward (:36, :38).
  • Why it's built this way - a third version is the minimum that makes a two-contract upcaster possible, since each contract needs its own distinct source and target; the version override keeps the ladder monotonic so only the unique-source defect is under test.
  • Where it's used - only through FixtureDoubleHopUpcaster (:30, :38); UniqueSourceRule_FlagsAClassDeclaringTwoUpcasterContracts in EventUpcasterFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventUpcasterFitnessTests.cs:37-:44) asserts the rule's message names that upcaster and contains "exactly one hop". No site outside the defining file names FixtureHopV3 itself.

FixtureShapedBase

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ContractShapeFixtures.IntegrationEvents · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:10 · Level 3 · record

  • What it is - an abstract intermediate integration-event base sitting between the framework envelope and a concrete fixture event. Its member is part of every derived event's wire shape, so the frozen contract must pin it (summary, ContractShapeFixtures.cs:6-:9).
  • Depends on - BaseIntegrationEvent (:10). Its one subclass is FixtureShapedEvent (:22), which adds int? Count and IReadOnlyList<string> Tags.
  • Concept introduced - two inheritance layers, two different owners. A consumer may put shared members on its own base record below BaseIntegrationEvent; those members travel on the wire like any other and belong in the event's contract. The framework envelope's own members, by contrast, are the framework's contract, not the event's. This fixture is what lets the self-test assert both halves of that line at once. [Rubric §14 - Testability] applies.
  • Walkthrough - public string Origin { get; init; } = string.Empty; (:13) is the single inherited member; being abstract, the record is never itself an event in the contract.
  • Why it's built this way - a renderer that read only DeclaredOnly properties of the concrete event would drop Origin; one that walked the whole chain would leak the envelope. The expected line, FixtureShapedEvent { Count:Nullable<Int32>, Origin:String, Tags:IReadOnlyList<String> }, rules out both.
  • Where it's used - only through FixtureShapedEvent: Contract_SpellsOutGenericArguments_AndPinsAnIntermediateBase in IntegrationEventContractTestsBaseTests (IntegrationEventContractTestsBaseTests.cs:86-:98) asserts that line, and that no ContractShapeFixtures line contains SchemaVersion: or DateOccurred: (:94-:97). No site outside the defining file names FixtureShapedBase itself.

FixtureTallyEvent

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ContractShapeFixtures.IntegrationEvents · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:29 · Level 3 · record

  • What it is - an integration-event fixture whose one member is a two-argument generic, IReadOnlyDictionary<string, int> Counts (ContractShapeFixtures.cs:29), so its contract line carries a comma inside angle brackets that the snapshot parser must not split on (summary, :24-:27).
  • Depends on - BaseIntegrationEvent and IReadOnlyDictionary<TKey, TValue> (BCL). Scanned by IntegrationEventContractTestsBase.
  • Concept introduced - a member-list parser must respect generic nesting. The snapshot line separates members with commas, and a naive split would cut IReadOnlyDictionary<String, Int32> into two bogus members, so a retyped argument would surface as a confusing add-and-remove rather than one changed member. Cross-references the twin-fixture technique at FixtureCountEvent, FixtureNullableCountEvent; [Rubric §14 - Testability] applies.
  • Walkthrough - a one-line positional internal sealed record with no body.
  • Where it's used - Base_Fails_WhenAGenericArgumentOfAMultiArgumentMemberChanges in IntegrationEventContractTestsBaseTests (IntegrationEventContractTestsBaseTests.cs:101-:114): it replaces <String, Int32> with <String, Int64> on the FixtureTallyEvent line of a copy of the live contract and asserts the gate throws naming "member Counts changed type".

FixtureTwoContractHandlerBase

MMCA.Common.Architecture.Tests.SliceFixtures · MMCA.Common.Architecture.Tests.SliceFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/FixtureTwoContractHandlerBase.cs:12 · Level 3 · class

  • What it is - an abstract query-handler base that serves TWO query contracts, both declared one namespace away in SliceFixtures.Stranded (summary, FixtureTwoContractHandlerBase.cs:7-:11): the multi-contract negative fixture for slice cohesion.
  • Depends on - IQueryHandler<GetFixtureFarQuery, Result> and IQueryHandler<GetFixtureSecondFarQuery, Result> (MMCA.Common.Application.UseCases.Contracts, :13), with Result from MMCA.Common.Shared.Abstractions (:3). The two contracts are internal sealed record GetFixtureFarQuery(int Id) (SliceFixtures/Stranded/GetFixtureFarQuery.cs:7) and internal sealed record GetFixtureSecondFarQuery(int Id) (SliceFixtures/Stranded/GetFixtureSecondFarQuery.cs:4), reached through using MMCA.Common.Architecture.Tests.SliceFixtures.Stranded; (:2).
  • Concept introduced - every handler interface is a registration, so every contract is checked. Registration wires the class for each IQueryHandler<,> it implements, so HandlersAreCoLocatedWithTheirContracts (ArchitectureRules.Slices.cs:40) must report both stranded contracts rather than whichever interface reflection happens to list first (:8-:10). It is the inverse layout of StrandedFixtureHandlerBase: there the handler sits in Stranded and the contract outside; here the contracts sit in Stranded and the handler outside. [Rubric §14 - Testability] applies.
  • Walkthrough - two HandleAsync overloads, one per contract (:15-:16, :18-:19), each returning Task.FromResult(Result.Success()), the same trivial body every slice fixture uses.
  • Why it's built this way - a rule that inspected only the first handler interface would still flag this class for one contract and pass the self-test; asserting on both contract names is what proves the scan walks every interface.
  • Where it's used - AHandlerServingTwoStrandedContracts_IsFlaggedForBoth in SliceCohesionFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:56-:64), which asserts the thrown message contains both GetFixtureFarQuery and GetFixtureSecondFarQuery. The two contract files name it in their summaries.

RebuildFixtureProjectionHandler

MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures · MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs:33 · Level 3 · class

  • What it is - a compiled command-handler fixture with three injected constructor dependencies, the command-side half of the population ConstructorDependencyCountTestsBase's handler ceiling scans, paired with GetFixtureProjectionHandler as the query-side half of the same proof.
  • Depends on - ICommandHandler<RebuildFixtureProjectionCommand, Result> (MMCA.Common.Application.UseCases.Contracts, ConstructorDependencyFixtures.cs:34) and the sibling internal sealed record RebuildFixtureProjectionCommand (:30), a payload-free marker chosen so the separate command-validator coverage rule that points a map at this assembly does not have to account for it.
  • Concept introduced - why the command carries no payload. The file's class doc states this directly (:12-:14): the command is a marker with nothing for the validator-coverage rule to count, so pointing a map at this assembly still counts the same five data-carrying commands it did before this fixture existed. Every injected collaborator (first, second, third) is read back through Collaborators (:36), the same unread-parameter guard GetFixtureProjectionHandler uses. [Rubric §14, Testability] is the concern.
  • Walkthrough - HandleAsync(RebuildFixtureProjectionCommand command, CancellationToken cancellationToken = default) (:38-:41) returns Task.FromResult(Result.Success()), the same trivial body every fixture in the file uses.
  • Why it's built this way - three constructor parameters put it one dependency over a tight ceiling (MaxHandlerConstructorDependencies => 2, ConstructorDependencyCountTestsBaseTests.cs:153) and exactly on a looser one (=> 3, :116); using an ICommandHandler here and an IQueryHandler on its sibling proves the ceiling scans both contract shapes, not just one.
  • Where it's used - Handlers_AreFlagged_WhenEitherHandlerContractExceedsTheCeiling (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:36-:49), which asserts the failure message names RebuildFixtureProjectionHandler (3 ctor dependencies); Handlers_AtTheCeiling_AreNotFlagged (:51-:58) reuses the same fixture, over the same FixtureMap (:74-:84), against the looser ceiling and asserts no throw.

StrandedFixtureHandlerBase

MMCA.Common.Architecture.Tests.SliceFixtures.Stranded · MMCA.Common.Architecture.Tests.SliceFixtures.Stranded · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/Stranded/StrandedFixtureHandler.cs:11 · Level 3 · class

  • What it is - the negative fixture for slice cohesion: an abstract query-handler base over the same GetFixturePreferencesQuery contract as GetFixturePreferencesHandlerBase, but declared one namespace away, in its own Stranded sub-folder.
  • Depends on - IQueryHandler<GetFixturePreferencesQuery, Result> (MMCA.Common.Application.UseCases.Contracts, StrandedFixtureHandler.cs:11) and GetFixturePreferencesQuery from the sibling SliceFixtures namespace, reached across the namespace boundary the fixture exists to create.
  • Concept introduced - the violation the rule could not see while it read concrete classes only. The class doc states this directly (:6-:10): an abstract handler base whose contract lives one namespace away is exactly the drift HandlersAreCoLocatedWithTheirContracts (ArchitectureRules.Slices.cs:40) is meant to catch, and it sits in its own folder because the namespace has to follow the folder (IDE0130). [Rubric §14, Testability] is the concern; compare the negative-fixture technique cross-referenced at DriftedTests.
  • Walkthrough - HandleAsync(GetFixturePreferencesQuery query, CancellationToken cancellationToken = default) (:13-:16) returns Task.FromResult(Result.Success()), the same trivial body as its co-located sibling; only the namespace differs.
  • Why it's built this way - reusing the same contract type as GetFixturePreferencesHandlerBase isolates the one variable the rule checks (namespace distance), rather than introducing a second, unrelated contract that could confound the result.
  • Where it's used - AnAbstractHandlerStrandedFromItsContract_IsFlagged (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:21-:30), which asserts the rule's failure message names it.

StubMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleIsolationTestsBaseTests.cs:93 · Level 3 · class

  • What it is - a hand-built IArchitectureMap implementation, written from scratch rather than derived from ArchitectureMapBase, so a test can declare exactly one forbidden namespace for exactly one layer.
  • Depends on - IArchitectureMap, LayerRef and the Layer enum (ModuleIsolationTestsBaseTests.cs:93-:79). Four constructor parameters carry the whole configuration: fromLayer, forbiddenLayer, forbiddenNamespace, and the assembly to register.
  • Concept introduced - when the map base is the wrong tool, and why implementing the interface directly is the honest alternative. Every other map in this chapter subclasses ArchitectureMapBase, which derives every other module's namespaces mechanically from the repo token. That derivation is exactly what makes a real map cheap and exactly what makes it useless here: the isolation rules ask a map for OtherModuleNamespaces(module, layer), and to test one specific cross-layer pair the answer has to be one namespace for one layer and empty for the rest. The base cannot express that, so this stub implements all eleven interface members itself (:81-:110) and puts the whole test in OtherModuleNamespaces, a one-line conditional (:109-:110). The class doc states the reasoning verbatim (:70-:74). [Rubric §7 - Microservices Readiness] is the property the rules under test defend; [Rubric §1 - SOLID] is why this substitution is even possible, since the rule library consumes only the interface and never the base; [Rubric §14 - Testability] is the technique.
  • Walkthrough
    • RepoToken => "MMCA.Fixture" (:81) and ModuleNames => ["Beta", "Gamma"] (:83): two declared modules, so the isolation rules have a cross product to iterate, even though only one of them registers an assembly.
    • Layers (:85-:86) is a single LayerRef for module "Beta" at the caller's fromLayer, pointing at the caller's assembly with a derived root namespace.
    • OfLayer and the five convenience accessors (:88-:99) all project off that one list, so a layer with no entry yields an empty sequence and the corresponding rule is vacuous rather than broken.
    • OtherModuleNamespaces(module, layer) (:109-:110) returns the single forbidden namespace when layer == forbiddenLayer and an empty array otherwise. That single line is what isolates the pair under test from every other pair the rules would otherwise check.
  • Why it's built this way - the test it serves needs a violation that is real: the registered assembly genuinely does depend on MMCA.Common.Application (it is the Infrastructure assembly, :25), and the map declares that namespace as another module's Application layer. A hand-written map is the only way to arrange a true dependency edge into a fabricated module boundary without inventing assemblies. See ADR-059 for the module boundary the rules enforce.
  • Where it's used - constructed twice in ModuleIsolationTestsBaseTests: once as the static CrossLayerViolation shared by two facts (:21-:25) and once as a clean map pointed at a namespace nothing references (:59-:63).

NotARecordFixtureId

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.StronglyTypedIdFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/StronglyTypedIdFixtures/StronglyTypedIdFixtures.cs:40 · Level 3 · struct

  • What it is - a negative fixture: a strongly-typed identifier wrapper implemented as a plain readonly struct rather than a readonly record struct, so the strongly-typed-id fitness rule can be proven to reject that shape.
  • Depends on - IStronglyTypedId<TSelf, TValue>, which it implements over int (public readonly struct NotARecordFixtureId(int value) : IStronglyTypedId<NotARecordFixtureId, int>, StronglyTypedIdFixtures.cs:40).
  • Concept introduced - cross-references the negative-fixture technique explained at DriftedTests (a deliberately-wrong subclass or shape used to prove an assertion actually bites). Here the "wrong" axis is the CLR shape itself, not a configuration override: the rule requires record struct so it gets value equality and an immutable, printable wrapper for free, and this fixture is the one type in the suite that withholds exactly that. [Rubric §14 - Testability] applies.
  • Walkthrough - a primary-constructor readonly struct with one member, public int Value { get; } = value; (:29), and the required factory public static NotARecordFixtureId From(int value) => new(value); (:32). Both satisfy IStronglyTypedId<TSelf, TValue>'s contract; only the record keyword is missing.
  • Why it's built this way - a rule that only checked the interface would pass this fixture, which is precisely the gap StronglyTypedIdsAreReadonlyRecordStructs closes by additionally reflecting on the CLR type shape.
  • Where it's used - Rule_FlagsAWrapperThatIsNotARecord (StronglyTypedIdFitnessTests.cs:13-:21), which asserts the thrown message names NotARecordFixtureId and mentions "record".

NoEntityDataSources

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:112 · Level 3 · class

  • What it is - a private nested stub IEntityDataSourceRegistry that answers every lookup with the default SQL Server key and every TryGet with false, used to build the minimal, real ApplicationDbContext model the delete-behavior convention test needs without wiring the framework's actual multi-source registry.
  • Depends on - IEntityDataSourceRegistry (the interface it implements, DeleteBehaviorConventionTests.cs:112) and DataSourceKey / DataSource (DataSourceKey.Default(DataSource.SQLServer), :114,:116). Registered as the singleton behind a bare ServiceCollection built by DeleteBehaviorConventionTests's own model-building helper (services.AddSingleton<IEntityDataSourceRegistry>(new NoEntityDataSources());, :96).
  • Concept introduced - a no-op collaborator has to be honest about doing nothing. The class doc (:107-:111) explains the shape: the two GetDataSourceKey overloads answer the default key rather than throw, and TryGetDataSourceKey always answers false, because nothing in this test's model-building path calls the "get" overloads and a throwing fixture would trip this repo's own domain-throw architecture rule. With every entity answering TryGet = false, CrossDataSourceDegradeConvention (MMCA.Common/CLAUDE.md, "Multi-Database Strategy") treats every relationship as single-source, which is exactly the FK-constraints-intact case the delete-behavior rule is checking. [Rubric §14 - Testability] is the concern: a stub collaborator that only ever answers the one shape the test under proof needs, and nothing more.
  • Walkthrough - GetDataSourceKey(Type) and GetDataSourceKey(string) both return DataSourceKey.Default(DataSource.SQLServer) (:114,:116); TryGetDataSourceKey(string, out DataSourceKey) sets key = default and returns false (:118-:122); GetPhysicalSourcesInUse() returns an empty array (:124).
  • Where it's used - registered by the private Services() helper (:88-:98) that builds the ServiceProvider behind the model-building test context in DeleteBehaviorConventionTests; no site outside the defining file references it.

GetFixtureEntityHandlerBase<TQuery>

MMCA.Common.Architecture.Tests.SliceFixtures · MMCA.Common.Architecture.Tests.SliceFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/SliceFixtures.cs:33 · Level 3 · class

  • What it is - an abstract query-handler base parameterized over its own contract type, the shape the framework's own *HandlerBase types use: TQuery is a generic parameter, not a concrete type any folder can hold.
  • Depends on - IQueryHandler<TQuery, Result> (MMCA.Common.Application.UseCases.Contracts, SliceFixtures.cs:33) and Result (MMCA.Common.Shared.Abstractions).
  • Concept introduced - a contract that is a type parameter cannot be co-located, so the rule must exempt it. HandlersAreCoLocatedWithTheirContracts (MMCA.Common.Testing.Architecture, ArchitectureRules.Slices.cs:40) checks that an abstract handler base sits in the same namespace as the concrete request it serves, but a generic contract has no concrete namespace to compare against, so the rule must skip this shape rather than misfire on it. This fixture proves that exemption holds. [Rubric §14, Testability] is the concern.
  • Walkthrough - HandleAsync(TQuery query, CancellationToken cancellationToken = default) (:35-:36) returns Task.FromResult(Result.Success()), the same trivial body every fixture in this file uses; the type exists for its shape, not its behavior.
  • Why it's built this way - the class doc states the reasoning directly (:27-:32): it mirrors the framework's own generic *HandlerBase types, so the self-test proves the rule tolerates the exact pattern the framework ships.
  • Where it's used - AnAbstractHandlerOverAGenericContract_IsSkipped (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:43-:52), which asserts the rule's failure message never names it.

GetFixturePreferencesHandlerBase

MMCA.Common.Architecture.Tests.SliceFixtures · MMCA.Common.Architecture.Tests.SliceFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/SliceFixtures.cs:19 · Level 3 · class

  • What it is - an abstract query-handler base declared beside its concrete contract, GetFixturePreferencesQuery (SliceFixtures.cs:13), in the same file and namespace: the co-located case HandlersAreCoLocatedWithTheirContracts must pass.
  • Depends on - IQueryHandler<GetFixturePreferencesQuery, Result> (MMCA.Common.Application.UseCases.Contracts, SliceFixtures.cs:19) and the sibling internal sealed record GetFixturePreferencesQuery(int UserId) (:13).
  • Concept introduced - the rule scans abstract handler bases, not only concrete classes. The class doc on GetFixturePreferencesQuery (:6-:12) states that every handler in this file is deliberately abstract: HandlersAreCoLocatedWithTheirContracts used to read concrete classes only, and these fixtures are exactly what it started seeing once it was widened to abstract bases too. Being query handlers rather than ICommandHandlers also keeps them invisible to the separate command-validator coverage rule that points a map at this same assembly. [Rubric §14, Testability] applies.
  • Walkthrough - HandleAsync(GetFixturePreferencesQuery query, CancellationToken cancellationToken = default) (:21-:24) returns Task.FromResult(Result.Success()).
  • Why it's built this way - co-location with the concrete contract is the positive case the rule protects; pairing it in the same file as StrandedFixtureHandlerBase, the same base moved one namespace away, isolates the one variable the rule actually checks.
  • Where it's used - AnAbstractHandlerBesideItsContract_IsNotFlagged (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:32-:41), which asserts the rule's failure message never names it.

GetFixtureProjectionHandler

MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures · MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs:48 · Level 3 · class

  • What it is - a compiled query-handler fixture with three injected constructor dependencies, the query-side half of the population ConstructorDependencyCountTestsBase's handler ceiling scans, paired with RebuildFixtureProjectionHandler as the command-side half of the same proof.
  • Depends on - IQueryHandler<GetFixtureProjectionQuery, Result> (MMCA.Common.Application.UseCases.Contracts, ConstructorDependencyFixtures.cs:49) and the sibling internal sealed record GetFixtureProjectionQuery(int ProjectionId) declared right above it (:45), kept in the same file "for slice cohesion" per its own summary (:44).
  • Concept introduced - why the fixture is top-level rather than nested. The file's class doc states it directly (:7-:14): the ceiling gate skips nested types, so a fixture nested inside the test class would never be scanned, and every injected collaborator (first, second, third) is read back through Collaborators (:51) so none is an unread primary-constructor parameter (CS9113). [Rubric §14, Testability] is the concern.
  • Walkthrough - HandleAsync(GetFixtureProjectionQuery query, CancellationToken cancellationToken = default) (:53-:56) returns Task.FromResult(Result.Success()), the same trivial body every fixture in the file uses.
  • Why it's built this way - three constructor parameters put it one dependency over a tight ceiling (MaxHandlerConstructorDependencies => 2, ConstructorDependencyCountTestsBaseTests.cs:153) and exactly on a looser one (=> 3, :116), the two-sided proof a single fixture with the wrong count could not give on its own.
  • Where it's used - Handlers_AreFlagged_WhenEitherHandlerContractExceedsTheCeiling (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:36-:49), which asserts the failure message names GetFixtureProjectionHandler (3 ctor dependencies); Handlers_AtTheCeiling_AreNotFlagged (:51-:58) reuses the same fixture, over the same FixtureMap (:74-:84), against the looser ceiling and asserts no throw.

StackedAttributeTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:87 · Level 3 · class

  • What it is - a private nested subclass of RouteAuthorizationTestsBase that governs exactly three fixture pages, each of which carries [Authorize(Roles = "Admin")] somewhere among several authorize attributes. It is the positive-case drifted subclass for the stacked and inherited attribute coverage.
  • Depends on - RouteAuthorizationTestsBase (private sealed class StackedAttributeTests : RouteAuthorizationTestsBase, RouteAuthorizationTestsBaseTests.cs:87) and the three fixture pages it names: PolicyThenRolePage (:53), RoleThenPolicyPage (:58) and BareOverRoleGatedPage (:62).
  • Concept introduced - a role gate survives sibling and inherited authorize attributes. The suite's class doc (:7-:13) states the rule: AuthorizeAttribute allows multiple instances and is inherited, and ASP.NET Core enforces every one, so a page keeps its role gate when ANY of its authorize attributes names the role. A policy-only attribute stacked beside the role, in either order, or a derived page's bare [Authorize] must not hide it. [Rubric §14 - Testability] and [Rubric §11 - Security] apply: a role check that read only the first attribute would report a false gap or, worse, a false pass.
  • Walkthrough - TargetAssembly is the test assembly itself (:89), RequiredRole is AdminRole (:91), and IsGovernedPage returns true only for the three pages above (:93-:96), so the base's scan sees exactly the stacked and inherited shapes and no others.
  • Why it's built this way - it is private, so xUnit does not collect its inherited facts as tests of their own (:12); the sibling MissingRoleTests (:99) is the negative twin that must still fail.
  • Where it's used - GovernedPages_WithStackedOrInheritedAttributes_KeepTheirRoleGate (:29-:35), which takes new StackedAttributeTests().GovernedPages_RequireDeclaredRole as a delegate and asserts it does not throw. No site outside the defining file references it.

FixtureLabelEvent, FixtureNullableLabelEvent

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ContractShapeFixtures.IntegrationEvents · (see per-type table) · Level 3 · record

The reference-type counterpart of FixtureCountEvent, FixtureNullableCountEvent: a twin pair whose only difference is nullable reference annotations, both on a plain member (Label) and on a generic argument (Notes). Each summary names the other as its twin "differing only in nullable annotations" (ContractShapeFixtures.cs:39,:44).

  • Depends on - BaseIntegrationEvent (ContractShapeFixtures.cs:42,:47) and IReadOnlyList<T> (BCL). Scanned by IntegrationEventContractTestsBase.
  • Concept introduced - nullable reference annotations are not in the CLR type. Unlike int?, a string? is the same runtime type as string; the annotation lives in compiler-emitted nullability metadata. A contract renderer that read only PropertyType would print both twins identically, so this pair proves the renderer reads the annotation too, including on a generic argument (IReadOnlyList<string?>). A member that may now be null breaks a consumer that relied on it never being null, which is why the snapshot must see it. Cross-references the isolate-one-variable technique at FixtureCountEvent, FixtureNullableCountEvent; [Rubric §14 - Testability] applies.
  • Walkthrough - one-line positional internal sealed records with two members each and no body.
Type File:Line What differs
FixtureLabelEvent MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:42 (string Label, IReadOnlyList<string> Notes); renders as { Label:String, Notes:IReadOnlyList<String> } (IntegrationEventContractTestsBaseTests.cs:134-:135).
FixtureNullableLabelEvent MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:47 (string? Label, IReadOnlyList<string?> Notes); renders as { Label:String?, Notes:IReadOnlyList<String?> } (:131-:132).
  • Why they're built this way - unlike the value-type pair, these are pinned to exact strings, because the ? suffix is the very spelling under test: it is what makes the nullable twin render differently from the non-nullable one.
  • Where they're used - in IntegrationEventContractTestsBaseTests: Contract_Differs_WhenTheOnlyChangeIsANullableReferenceAnnotation (IntegrationEventContractTestsBaseTests.cs:127-:137) asserts both rendered member lists; Base_Fails_WhenOnlyAMembersNullabilityChanges (:140-:153) rewrites the FixtureNullableLabelEvent line from Label:String? to Label:String in a copy of the live contract and asserts the gate throws naming "member Label changed type".

FixtureNamedEvent

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ContractShapeFixtures.IntegrationEvents · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:55 · Level 3 · record

  • What it is - an integration-event fixture that declares a stable wire identity with [EventName("Fixture.Named.v1")] (ContractShapeFixtures.cs:54), so the frozen contract must key it on that name rather than on its CLR type name (summary, :49-:52).
  • Depends on - BaseIntegrationEvent and EventNameAttribute (MMCA.Common.Domain.Attributes, :1). Scanned by IntegrationEventContractTestsBase.
  • Concept introduced - the wire identity, not the CLR name, is the contract key. The [EventName] value is what the outbox stores, so a CLR rename or namespace move of an event that declares one must not churn the snapshot. [Rubric §7 - Microservices Readiness] assesses whether services can evolve independently over stable contracts; this fixture proves the contract gate keys on the identity a consumer actually sees. [Rubric §14 - Testability] applies too.
  • Walkthrough - internal sealed record FixtureNamedEvent(int Value) : BaseIntegrationEvent; (:55): one plain member, no body.
  • Where it's used - Contract_KeysAnEventOnItsEventName_WhenItDeclaresOne in IntegrationEventContractTestsBaseTests (IntegrationEventContractTestsBaseTests.cs:156-:166), which asserts the live contract contains Fixture.Named.v1 { Value:Int32 } and that no line contains FixtureNamedEvent.

TestingAspireBoundaryTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/TestingAspireBoundaryTests.cs:21 · Level 3 · class

  • What it is - a layering fitness test for MMCA.Common.Testing.Aspire, the AppHost test-infrastructure assembly: five facts asserting it depends on none of Domain, Infrastructure, API, UI, or the MMCA.Common.Aspire service-defaults namespace. It is the runtime half of the boundary; the compile-time half is EnforceTestingAspireLayerBoundary in Source/Build/MMCA.Common.LayerEnforcement.targets, and this half catches a forbidden dependency that arrives transitively rather than through a declared ProjectReference (:6-:12).
  • Depends on - AppHostFixtureBase only as the anchor for TestingAspire => typeof(AppHostFixtureBase).Assembly (:23); ArchitectureAssert for the shared verdict-reporting helper; NetArchTest's Types.InAssembly(...).ShouldNot().HaveDependencyOnAny(...) fluent API.
  • Concept introduced - reuses the dependency-direction assertion pattern this suite applies throughout its layering tests (ArchitectureAssert.NoViolations over a NetArchTest result), applied here to a fifth kind of boundary: a testing package's own layer discipline, not a production module's. The reasons differ per forbidden namespace and are stated inline rather than shared: Domain and Infrastructure are ruled out because AppHost test infra drives services over HTTP and has no business coupling to either (:28-:29, :34-:35); API is reached only transitively through MMCA.Common.Testing (:40-:41); UI is out because the tests exercise HTTP endpoints, never rendered components (:46-:47); and MMCA.Common.Aspire (service defaults) is excluded so the package's own probe-path constants never accidentally pull in the graph they are meant to mirror (:54-:55). [Rubric §7 - Microservices Readiness] is the property under test, since a testing package that leaks a production-layer dependency is a package that cannot be referenced from a thin AppHost without dragging the whole stack with it; [Rubric §14 - Testability] is the technique.
  • Walkthrough
    • TestingAspire (:23): the assembly under test, resolved off AppHostFixtureBase rather than Assembly.GetExecutingAssembly(), so a rename or move of the fixture base is a compile error rather than a silent scope change.
    • TestingAspire_ShouldNotDependOn_Domain / _Infrastructure / _Api / _Ui (:25-:47): four one-line facts, each calling the shared AssertNoDependency with a forbidden namespace and a because reason.
    • TestingAspire_ShouldNotDependOn_ServiceDefaults (:49-:55): the fifth fact, against "MMCA.Common.Aspire" with no trailing dot. A dotted NetArchTest namespace entry matches nothing, so the bare segment form is the one that covers MMCA.Common.Aspire and every namespace beneath it (:51-:52).
    • AssertNoDependency(forbiddenNamespace, reason) (:57-:65): the shared helper every fact calls, running the NetArchTest scan and routing the result through ArchitectureAssert.
  • Why it's built this way - ADR-117 introduces the AppHost integration-test base this suite protects; keeping the test package's own dependency graph thin is what lets it be referenced from an AppHost project without pulling Domain or Infrastructure along for the ride. See ADR-117.
  • Where it's used - an independent fitness test in the Common architecture suite; it has no consumers of its own.

ConformantTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:225 · Level 4 · class

  • What it is - the positive fixture for the anonymous-endpoint gate: a subclass whose allow-list correctly names every anonymous endpoint in the fixture set, in both identifier shapes, so all three inherited facts must pass.
  • Depends on - AnonymousEndpointTestsBase (private sealed class ConformantTests : AnonymousEndpointTestsBase, AnonymousEndpointTestsBaseTests.cs:225) and the three fixture controllers it allow-lists.
  • Concept introduced - building allow-list entries with typeof(...).FullName and nameof(...) rather than string literals. Each of the three entries is interpolated from live metadata (:133-:135): $"{typeof(AnonymousFixtureController).FullName}.{nameof(AnonymousFixtureController.PeekAsync)}" for the method-level shape, the same construction against the abstract base for the inherited action, and a bare typeof(TypeLevelAnonymousFixtureController).FullName! for the type-level shape. Renaming any fixture is then a compile error instead of a silently-drifting test. That discipline is not available to a real consumer's list (the framework's endpoints are strings there, as in AnonymousEndpointTests), which is precisely why the stale-entry fact exists to catch what nameof would have caught. [Rubric §15 - Best Practices & Code Quality] and [Rubric §14 - Testability] apply.
  • Walkthrough - TargetAssemblies is this test assembly (:128-:129), so the scan sees the nested fixture controllers. AllowedAnonymousEndpoints (:131-:136) holds the three entries described above; note the abstract base is listed at its own name rather than the deriving controller's, which is the ruling InheritingFixtureController pins. MinimumScannedTypes is left at the base's default of 1, which the fixture set clears comfortably. One member is added beyond the base's surface: internal IReadOnlyCollection<string> AnonymousEndpointsForTest() => [.. AnonymousEndpoints()]; (:138), which materializes the base's protected enumeration so the enclosing test class can assert on the emitted set directly. The base exposes AnonymousEndpoints() as protected for exactly this kind of extension (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:178-:125).
  • Where it's used - Base_Accepts_TypeLevelAndMethodLevelEntries (AnonymousEndpointTestsBaseTests.cs:45-:59) runs its three inherited facts, and Base_DoesNotReport_AnInheritedAttributeOnTheDerivedController (:61-:71) reads its AnonymousEndpointsForTest() output.

DataSourceBranchingFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DataSourceBranchingFitnessTests.cs:17 · Level 4 · class

  • What it is - a text-scanning fitness test over the framework's own Source/**/*.cs that fails whenever a line names a concrete database engine (a DataSource.X value or an engine-specific context type) outside a fixed per-file allow-list, plus two facts that keep that allow-list exact and four detector facts that pin the regex itself.
  • Depends on - ArchitectureAssert for the verdict (:87, :99), ArchitectureMapBase only for its static FindRepoRoot("MMCA.Common.slnx") (:122); the rule it protects is the IDataSourceEngine strategy split keyed by DataSource. Externals: a source-generated Regex ([GeneratedRegex], :150-:154), System.IO.Directory/File, xUnit [Fact]/[Theory], and AwesomeAssertions.
  • Concept introduced - a text-level fitness test for a rule the type system cannot see. The class doc states the gap (:7-:16): engine-specific behavior is meant to live in one IDataSourceEngine strategy per engine (ADR-130), but nothing in the compiler stops a new if (engine == DataSource.Sqlite) or context is SqliteDbContext branch from growing back anywhere else. A NetArchTest dependency rule cannot help either, because the offending code references types the file is allowed to reference; what is wrong is the branch, not the dependency. So the gate reads every source file as text and matches one regex (:151): DataSource\.(CosmosDB|Sqlite|SQLServer|PostgreSQL) for an enum value, or is (not )?\w*(SQLServer|Cosmos|Sqlite|PostgreSQL|Npgsql)\w*DbContext for a type test against an engine context. The allow-list is keyed at file granularity and is two-sided: a hit outside it fails, and a listed file that no longer produces a hit also fails, so the list shrinks with the code instead of keeping a pass for a file that could regress later (:13-:15). [Rubric §34 - Architecture Governance & Documentation] assesses whether architectural decisions are enforced rather than only written down; this test is ADR-130's executable form. [Rubric §8 - Data Architecture] is the property at stake, since a scattered engine branch is what makes a fifth engine or a changed engine rule a hunt rather than one class. [Rubric §14 - Testability] covers the detector facts that prove the regex itself.
  • Walkthrough
    • Remedy (:27): the one-line fix text appended to every failure, "route engine-specific behavior through IDataSourceEngine (ADR-130)".
    • AllowedFiles (:32-:76): an ordinal Dictionary<string, string> of Source/-relative paths (forward slashes) to the reason each may name an engine. Twenty-four entries in eight commented groups: the four engine strategies (:35-:38); the four sealed per-engine contexts, one class per engine under ADR-006 (:42-:45); the four EntityTypeConfiguration* shim bases, since [UseDataSource(DataSource.X)] is how an entity picks its engine (:48-:51); four settings classes whose default engine is a value rather than a branch (:54-:57); DesignTimeDbContextHelper with its per-engine design-time entry points (:60); IndexBuilderExtensions, whose engine parameter defaults to SQL Server (:64); DataSourceResolver, which owns the framework-default engine constant (:67); and five files that ask for the framework tables' source via ResolveLogical(DataSource.SQLServer, Default), which the resolver maps onto whatever the host configured (:71-:75).
    • ConcreteEngineBranching_StaysInsideTheEngineStrategies (:78-:88): scans, drops every hit whose file is allow-listed, and reports each remaining one as Source/{file}:{line}: {text}.
    • AllowList_HasNoStaleEntries (:90-:102): collects the set of files that produced any hit and fails on every allow-list key absent from it.
    • Detector_Flags_ConcreteEngineBranching (:104-:111): five [InlineData] lines the regex must match, covering an equality test, a case label, is, is not with a pattern variable, and the Npgsql spelling of a context name.
    • Detector_Accepts_EngineNeutralCode (:113-:118): three lines it must not match, including dataSourceEngines.Resolve(key.Engine) (the sanctioned route), a DataSource property declaration, and a type test against the engine-neutral ApplicationDbContext.
    • ScanSource() (:120-:148): enumerates *.cs under {repo root}/Source excluding bin and obj, in ordinal order, asserts more than 500 files were found (:131, the non-vacuous floor so a wrong scan root cannot pass having read nothing), then matches line by line and records an EngineHit with a 1-based line number and the trimmed text.
    • EngineReference (:150-:154): the generated regex, CultureInvariant | ExplicitCapture, with a 1000 ms match timeout. EngineHit (:157) is a private record of file, line and text.
  • Why it's built this way - ADR-130 moves engine-specific behavior behind one strategy per engine; this gate is what stops the decision decaying one convenient branch at a time. A file-level allow-list with a reason per entry keeps every exception reviewable in one place, and the stale-entry fact means that removing an engine reference from a listed file forces the entry out in the same change.
  • Where it's used - an independent fitness test in the Common architecture suite; it has no consumers and scans only Common's own Source/ tree (no *TestsBase is shipped for Store or ADC to subclass).
  • Caveats / not-in-source - the scan is textual, so it does not distinguish code from comments or string literals: a doc comment naming DataSource.SQLServer counts as a hit, which is why several entries (for example IndexBuilderExtensions and DataSourceResolver) cite "and its doc" or "and doc" as part of the reason.

FailingAnonymizeSubject

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.AnonymizationFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AnonymizationFixtures/AnonymizationFixtures.cs:37 · Level 4 · class

  • What it is - an IAnonymizable fixture whose Anonymize() always returns Result.Failure(Error.Invariant("Fixture.CannotErase", ...)) (AnonymizationFixtures.cs:37-:43), so the erasure reports failure on its sample instead of overwriting anything.
  • Depends on - IAnonymizable and Result/Error (AnonymizationFixtures.cs:1-:2); it carries one Nickname property initialised to "Ada" and never touched, and no [Pii] marker (the fixtures' header comment, :6-:9, explains why: the personal-data inventory self-test scans this assembly for [Pii] members).
  • Concept - the reverse-PII rule PropertiesOverwrittenByAnonymizeCarryPii runs each entity's Anonymize() on a sample to see which text properties it overwrites. An erasure that fails cannot be inspected, and silently skipping it would hide a broken erasure path, so the rule must report it. ReverseRule_AnonymizeThatFails_IsReported proves exactly that: the thrown message contains FailingAnonymizeSubject: Anonymize() did not succeed (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/AnonymizationRulesTests.cs:72-:80).
  • Where it's used - registered as a sample factory in that single test (AnonymizationRulesTests.cs:76).

FakeArchitectureMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyOverrideTests.cs:127 · Level 4 · class

  • What it is - a map that declares exactly the module and layer pairs a test hands it, with no relation to any real assembly layout. Its only purpose is to let the map-completeness rule's arithmetic be tested directly.
  • Depends on - ArchitectureMapBase (private sealed class FakeArchitectureMap(params IEnumerable<LayerRef>[] modules) : ArchitectureMapBase, LayerDependencyOverrideTests.cs:127) and LayerRef.
  • Concept - cross-references the map concept from CommonArchitectureMap. What is different here is that the map is data, not a description of reality: a primary constructor takes any number of LayerRef sequences and DefineLayers() simply flattens them (:131). Every LayerRef points at this same test assembly with a fabricated root namespace of the form MMCA.Fake.{module}.{layer} (:123-:124), and RepoToken is "MMCA.Fake" (:129), so nothing about the entries can be confused with the framework's own map. The class doc says why that is legitimate (:14-:16): the point under test is the rule's arithmetic over module names and layers, not any particular assembly, so a fixture map is more honest than borrowing a real one.
  • Walkthrough - the test class supplies the data through two helpers rather than the map: Full(module) yields a LayerRef for all five layers, and Thin(module) yields only Shared, Application and Api (:117-:121), which is the shape of a module with no aggregate and no persistence. Ref(module, layer) (:123-:124) builds each entry.
  • Why it's built this way - a real map cannot be made to declare a module that is missing a layer without deleting an assembly, which is not something a test can arrange. Constructing the map from literal layer lists turns "module Notification declares no Domain assembly" into a one-line fixture.
  • Where it's used - constructed once per fact in LayerDependencyOverrideTests (:26,:39,:59,:80,:99,:109).

DriftedTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests · (see per-type table) · Level 4 · class

The name is used twice in this assembly, once per fitness base under proof, and both are the same idea: a private sealed subclass of a shipped *TestsBase whose configuration is deliberately wrong, so the base's assertions can be shown to actually fail on the drift they claim to catch. They are nested in different outer classes, so their full names differ and neither is visible outside its own file.

  • Depends on - the base each one drifts from, plus the fixture it points at: ModuleConformanceTestsBase<TModule> with FakeDependentModule for one, AnonymousEndpointTestsBase with the fixture controllers for the other.
  • Concept introduced - negative fixtures, and hiding them from test discovery. A fitness base that asserts nothing passes everywhere; the only way to know an assertion bites is to feed it a case that must fail. But a public subclass of an xUnit base is itself collected, so its inherited [Fact]s would run and report as red tests of their own. Declaring the drifted subclass private and nested keeps xUnit from collecting it, while the enclosing class can still instantiate it and invoke the inherited methods directly as delegates (var assert = new DriftedTests().Module_ShouldDeclare_ExpectedName;, ModuleConformanceTestsBaseTests.cs:91). Both class docs record that reasoning in the same words (ModuleConformanceTestsBaseTests.cs:81-:85, AnonymousEndpointTestsBaseTests.cs:10-:11). [Rubric §14 - Testability] assesses whether the guardrails themselves are trustworthy; this is the fixture shape that earns that trust. Compare NavigatingSpec, the same negative-fixture technique applied to a specification rule, and ProbeTests, which parameterizes the drift instead of hard-coding it.
Type File:Line What is deliberately wrong
DriftedTests (in AnonymousEndpointTestsBaseTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:163 Points TargetAssemblies at this test assembly (:102-:103), so the scan finds the fixture controllers' [AllowAnonymous] attributes, and then supplies an empty AllowedAnonymousEndpoints (:105). Every discovered endpoint is therefore an offender, and the failure message must name AnonymousFixtureController.
DriftedTests (in ModuleConformanceTestsBaseTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:131 Three overrides, each wrong in a different way against FakeDependentModule's real declarations: ExpectedName => "NotTheDeclaredName" (:133) against the module's "FakeDependent"; ExpectedDependencies => ["FakeLeaf"] (:135) against the module's two-entry list, so one dependency is missing; and ExpectedRequiresDependencies => false (:137) against the module's true. AssertDisabledStubs is deliberately not overridden, so the fourth inherited fact stays vacuous here.
  • Why they're built this way - one wrong value per assertion, and no more. If a single drifted subclass were wrong in three ways at once and the base only ever threw on the first, the other two assertions could rot undetected; the module-side fixture avoids that by being driven three separate times, once per fact.
  • Where they're used - the anonymous-endpoint one drives Base_Fails_WhenAnAnonymousEndpointIsNotAllowListed (AnonymousEndpointTestsBaseTests.cs:15-:24); the module one is instantiated three times by ModuleConformanceTestsBaseTests (:91,:99,:107), once per assertion under proof.

FixtureApplicationMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:70 · Level 4 · class

  • What it is - a one-entry map, private to SliceCohesionFitnessTests, that registers this test assembly as the map's single Application layer so the slice-cohesion rule reads its handler fixtures and nothing else.
  • Depends on - ArchitectureMapBase (private sealed class FixtureApplicationMap : ArchitectureMapBase, SliceCohesionFitnessTests.cs:70) and LayerRef.
  • Concept - cross-references the map concept taught at FixtureAssemblyMap: a fixture map's only job is to scope a rule's scan to the deliberately-built fixtures instead of the whole repo. HandlersAreCoLocatedWithTheirContracts reads map.OfLayer(Layer.Application) (SliceCohesionFitnessTests.cs:24,:35,:46), so this map declares Framework(Layer.Application, typeof(GetFixturePreferencesQuery).Assembly) (:64) and nothing else. The three facts it feeds prove the rule's newest behavior: an abstract handler base now counts as a slice member, not just its concrete implementation, so a base stranded away from its contract's folder must be flagged (AnAbstractHandlerStrandedFromItsContract_IsFlagged, :22-:30), a base declared beside its contract must not be (AnAbstractHandlerBesideItsContract_IsNotFlagged, :32-:41), and a base parameterized over a generic contract, which has no folder any type could co-locate with, must be skipped entirely (AnAbstractHandlerOverAGenericContract_IsSkipped, :43-:52). [Rubric §14, Testability] assesses whether the co-location guard is provable with focused inputs; [Rubric §34, Architecture Governance and Documentation] is why the guard exists, since slice cohesion is what keeps a CQRS handler and the contract it serves in one folder a reviewer can see at once.
  • Walkthrough - RepoToken => "MMCA.Common" (:60) and a single-entry DefineLayers() returning Framework(Layer.Application, typeof(GetFixturePreferencesQuery).Assembly) (:62-:65).
  • Where it's used - constructed once as a readonly field on SliceCohesionFitnessTests (:19) and passed to ArchitectureRules.HandlersAreCoLocatedWithTheirContracts in all three facts (:24,:35,:46).

FixtureAssemblyMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · (see per-type table) · Level 4 · class

The name is used seven times in this assembly, once per IL-reading rule under proof, and all seven are the same one-entry map: this test assembly registered as a single framework layer, so the rule's whole-layer scan lands on the fixtures. They are nested in different outer classes, so their full names differ and none is visible outside its own file.

  • Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
  • Concept introduced - the layer a fixture map declares is not decoration, it is the scope selector. Each of these seven rules starts from a different part of the map, so each map has to name the matching layer or the rule reads nothing and passes vacuously. The cascade and hard-delete rules walk every assembly the map registers, through ScannableAssemblyLocations (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:88-:92), so their layer choice is nominal; the domain-throw rule reads map.OfLayer(Layer.Domain) and nothing else (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:111-:111), so registering Domain is mandatory there. All four use Framework(...) rather than Module(...), which is the opposite choice from FixtureModuleMap and correct for the same reason: these rules do not filter on module ownership, so the simplest entry is the honest one. Reading assemblies off disk by Assembly.Location also explains a shared limitation: a rule in this family cannot inspect a dynamic or single-file assembly, and the helper silently skips any location that does not exist on disk. [Rubric §14 - Testability] assesses whether a guardrail is provable with focused inputs, and a map that scopes the scan to fixtures is what makes each of these seven rules provable at all.
  • Walkthrough - each declares RepoToken => "MMCA.Common" and a single-entry DefineLayers() naming one layer and one anchor type from the fixture namespace it serves. Anchoring by typeof(SomeFixture).Assembly rather than by Assembly.GetExecutingAssembly() is deliberate: it ties the map to a type a compiler would fail on if the fixtures moved.
Type File:Line Layer and anchor The rule it scopes
FixtureAssemblyMap (in CascadeSoftDeleteFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:97 Framework(Layer.Domain, typeof(CascadeChildFixture).Assembly) (:103) AggregatesCascadeSoftDeleteToChildren.
FixtureAssemblyMap (in SoftDeleteEnforcementFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementFitnessTests.cs:77 Framework(Layer.Infrastructure, typeof(FixtureEntity).Assembly) (:83) HardDeletesOnlyInAllowedTypes.
FixtureAssemblyMap (in DomainEventHandlerSaveFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:110 Framework(Layer.Application, typeof(FixtureDomainEvent).Assembly) (:116) DomainEventHandlersDoNotSave.
FixtureAssemblyMap (in DomainThrowFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:150 Framework(Layer.Domain, typeof(NonThrowingFixture).Assembly) (:156) DomainThrowsOnlyArgumentGuards, which reads the Domain layer only, so this entry is load-bearing.
FixtureAssemblyMap (in ClockReadTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:98 Framework(Layer.Domain, typeof(InjectedClockFixture).Assembly) (:104) DomainAndApplicationDoNotReadTheClock (:88).
FixtureAssemblyMap (in QueryHandlerReadRepositoryTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/Repositories/QueryHandlerReadRepositoryTests.cs:58 Framework(Layer.Application, typeof(ReadRepositoryQueryHandlerFixture).Assembly) (:64) QueryHandlersUseReadRepositories (:48).
FixtureAssemblyMap (in SubstitutabilityRuleTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/SubstitutabilityRuleTests.cs:73 Framework(Layer.Domain, typeof(FixtureRefusingShape).Assembly) (:79) SubstitutableMembersDoNotRefuseTheContract (:25), which flags an override or interface implementation that throws NotSupportedException or NotImplementedException.
  • Why they're built this way - seven nested six-line classes rather than one shared fixture map, so each test file is readable end to end and no test can change another's scope by editing a shared file. It is the same trade-off FixtureModuleMap makes.
  • Caveats / not-in-source - because each map registers the whole test assembly, a rule in this family sees every type in it, not just the fixtures in its own namespace. Most are unaffected because their fixtures are the only matching shapes present; the domain-throw rule is not, which is why it carries an explicit allowlist of the assembly's non-fixture throws (DomainThrowFitnessTests.cs:29-:33).
  • Where they're used - constructed once as a readonly field per test class (CascadeSoftDeleteFitnessTests.cs:21, SoftDeleteEnforcementFitnessTests.cs:19, DomainEventHandlerSaveFitnessTests.cs:21, DomainThrowFitnessTests.cs:36, ClockReadTests.cs:18, QueryHandlerReadRepositoryTests.cs:18, SubstitutabilityRuleTests.cs:20) and passed to every rule call in each.

FitnessPrincipal

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:57 · Level 4 · class

  • What it is - a throwaway "principal" entity used only as test data for the specification-navigation fitness function. It is the entity that a dependent record points at, so a specification can be written that tries to reach across the navigation into it.
  • Depends on - AuditableBaseEntity<TIdentifierType> (it is a public sealed class FitnessPrincipal : AuditableBaseEntity<int>, SpecificationFitnessTests.cs:57).
  • Concept introduced - test fixture entities for a fitness function. A fitness function is an executable architecture rule (see ArchitectureRules); to prove such a rule actually fires you feed it a deliberately-crafted model rather than the real domain. FitnessPrincipal is one half of that crafted model: a bare entity carrying a single scalar (IsActive, SpecificationFitnessTests.cs:59) so a specification can navigate to it. [Rubric §14 - Testability] assesses whether rules are provable with focused inputs; this fixture exists precisely so the guard is tested against a known-unsafe shape instead of hoping a real specification trips it.
  • Walkthrough - one auto-property, bool IsActive (:59). That is the only member; identity and audit fields come from the base. It is the navigation target referenced by FitnessDependent.Principal. Deriving from AuditableBaseEntity<int> is load-bearing rather than cosmetic: the rule's navigation walker only counts a property as an entity navigation when its type (or its collection element type) inherits the auditable entity base (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:121-:138).
  • Why it's built this way - the fitness test must be non-vacuous: it needs a real cross-entity navigation to flag. A minimal principal with a single scalar is the smallest thing a specification can legally navigate into.
  • Where it's used - referenced by FitnessDependent and, through it, by NavigatingSpec and NavigatingQuerySpec; the whole fixture set drives SpecificationFitnessTests.

BareMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:133 · Level 4 · class

  • What it is - a map whose Api and Application layers both point at this test assembly, deliberately chosen because it holds neither a controller nor a CQRS handler. It is the vacuous-scan fixture for ConstructorDependencyCountTestsBase's two newer populations.
  • Depends on - ArchitectureMapBase (private sealed class BareMap : ArchitectureMapBase, :87) and Common.Shared.Abstractions.Result purely as an anchor assembly, not because Result itself is scanned.
  • Concept - cross-references the map concept introduced at FixtureAssemblyMap: a fixture map's job is to scope a rule's scan to a deliberately-built input rather than the whole repo. Where that family proves a rule fires correctly on a real offender, BareMap proves the opposite failure mode, that a scan finding zero candidates must not pass silently. It declares Module("Bare", Layer.Api, typeof(Common.Shared.Abstractions.Result).Assembly) and the same for Layer.Application (:92-:93), so both layers resolve to an assembly with no controller and no handler in it.
  • Walkthrough - RepoToken => "MMCA.Common" (:89) and a two-entry DefineLayers() (:91-:94), one per scanned layer.
  • Why it's built this way - reusing Result's assembly as the anchor, rather than declaring a fresh fixture type, keeps the map to a two-line body: the assembly's contents are irrelevant, only that it contains neither shape the rule looks for.
  • Where it's used - the Map of EmptyScanTests (ConstructorDependencyCountTestsBaseTests.cs:180), which BothPopulations_FailVacuousScans (:60-:72) drives to assert both the controller and the handler ceiling facts throw on a scan that finds nothing to check.

AnonymousEndpointTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:13 · Level 4 · class

  • What it is - the meta-test for the shipped anonymous-endpoint base: ten facts proving each assertion fails on the drift it claims to catch, that both allow-list identifier shapes are accepted, that an inherited attribute is not double-reported on the derived controller, and (SEC-ADC-03) that the default-on "undecorated endpoint" scan carries the same fail/accept/stale-entry guarantees without ever double-reporting an endpoint the anonymous scan already covers.
  • Depends on - AnonymousEndpointTestsBase (the type under test), its own nested fixtures (AnonymousFixtureController and family, InheritingFixtureController, UndecoratedFixtureController, DriftedTests, StrictDriftedTests, StaleAllowListTests, StaleUndecoratedAllowListTests, EmptyScanTests, ConformantTests, StrictConformantTests), plus xUnit [Fact] and AwesomeAssertions' delegate assertions.
  • Concept introduced - cross-references the "test a shipped test base from the outside by invoking its facts as delegates" technique introduced by ModuleConformanceTestsBaseTests. The base's original job was one scan: fail on an anonymous endpoint the allow-list missed, and accept both identifier shapes (method-level and type-level [AllowAnonymous]) the list is written in. SEC-ADC-03 adds a second scan, on by default, for the gap that leaves: a controller that never got [Authorize] at all is invisible to an allow-list keyed on [AllowAnonymous], because it carries no attribute for the scan to find. RequireExplicitAuthorizationDecision defaults to true (AnonymousEndpointTestsBase.cs:69; a repo can override it to opt out), and this suite proves the scan symmetrically to the original: fails when an undecorated controller is unlisted (StrictDriftedTests), does not double-report an endpoint whose decision is inherited from an abstract base, accepts a reviewed undecorated entry, fails on a stale undecorated entry, and, critically, fails by default on a subclass that overrides nothing, so the gate cannot be silently absent. The class doc states the reasoning (:8-:11). [Rubric §14 - Testability] assesses whether guardrails are trustworthy; [Rubric §11 - Security] is the property at stake, since a broken or unadopted undecorated-endpoint gate is exactly the "forgot [Authorize]" class of bug SEC-ADC-03 names; [Rubric §33 - Developer Experience] covers the failure mode, a break that would only appear downstream after a release (ADR-058).
  • Walkthrough
    • Base_Fails_WhenAnAnonymousEndpointIsNotAllowListed (:15-:24): drives DriftedTests and asserts the thrown message names AnonymousFixtureController (:22), with the because stating the ruling, that the offender message must name the endpoint that lost its gate (:23).
    • Base_Fails_WhenTheAllowListHasAStaleEntry (:26-:35): drives StaleAllowListTests and asserts the message contains "NoLongerAnonymous" (:33), the type name from an entry that matches nothing.
    • Base_Fails_WhenNothingWasScanned (:37-:43): drives EmptyScanTests and asserts the non-vacuity fact throws (:42).
    • Base_Accepts_TypeLevelAndMethodLevelEntries (:45-:59): the positive case. It calls all three of ConformantTests's inherited facts inside one delegate (:50-:55) and asserts NotThrow (:57-:58). Running all three together matters: the allow-list is only correct if it is simultaneously complete (no offenders) and exact (no stale entries).
    • Base_DoesNotReport_AnInheritedAttributeOnTheDerivedController (:61-:71): reads the scan output through ConformantTests's AnonymousEndpointsForTest() and asserts the derived-controller identifier is absent (:69-:70). This is the only fact that inspects the emitted set rather than an assertion's outcome.
    • Base_Fails_WhenAControllerCarriesNoAuthorizationDecisionAtAll (:73-:84): drives StrictDriftedTests (RequireExplicitAuthorizationDecision => true, empty allow-list) and asserts the thrown message names UndecoratedFixtureController (:81), because "a controller that forgot [Authorize] is invisible to the [AllowAnonymous] allow-list" (:82-:83).
    • Base_DoesNotReport_AControllerThatInheritsItsDecisionFromAnAbstractBase (:86-:97): reads StrictConformantTests().UndecoratedEndpointsForTest() and asserts neither InheritingFixtureController nor the abstract base itself is reported (:91-:96), the same inherited-attribute discipline the anonymous scan already applies.
    • Base_Accepts_AnUndecoratedEndpointThatIsAllowListed (:99-:105): drives StrictConformantTests, whose EndpointsWithoutAuthorizationAttribute is the scan's own output fed back in, and asserts NotThrow.
    • Base_Fails_WhenTheUndecoratedAllowListHasAStaleEntry (:107-:114): drives StaleUndecoratedAllowListTests and asserts the message contains "NoLongerUndecorated" (:113).
    • Base_RequiresAnExplicitDecisionByDefault (:118-:125): drives plain DriftedTests, which never overrides RequireExplicitAuthorizationDecision, against the same fixture assembly that contains UndecoratedFixtureController, and asserts the thrown message names it (:124). The gate is on by default (AnonymousEndpointTestsBase.cs:69); the comment (:116-:117) records why the old off-by-default branch was replaced: it asserted two disjoint-by-construction scans did not intersect, which could never fail.
  • Why it's built this way - all six drifted and conformant subclasses (including the two SEC-ADC-03 additions, StrictDriftedTests and StaleUndecoratedAllowListTests) are private, which is what keeps xUnit from collecting their inherited facts as deliberately-failing tests of their own (class doc, :10-:11). The base ships in the MMCA.Common.Testing.Architecture package, so proving it here is proving it before a release rather than after one.
  • Where it's used - an independent class in the Common architecture suite. The base it protects is bound for real by AnonymousEndpointTests here and by the equivalent subclass in each consumer repo.

CascadeChildFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CascadeFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:17 · Level 4 · class

  • What it is - the child entity every cascade fixture aggregate owns a collection of. It is what makes those collections child-entity collections in the rule's eyes rather than ordinary lists.
  • Depends on - AuditableBaseEntity<TIdentifierType> closed over int (CascadeFixtures.cs:17, import at :1).
  • Concept introduced - soft-delete does not cascade, and the shape of a rule that can tell. Setting IsDeleted = true on an aggregate root hides the root behind the global query filter and leaves every child row ACTIVE: an order line of a deleted order, a session of a deleted event. Nothing reads those rows through the root any more, so the orphans are invisible until a report, an export or a per-child query surfaces them, and an erasure request that walks the child table finds data whose owner was "deleted" months ago. The database cannot help either, because a soft delete is an ordinary UPDATE and there is no ON DELETE CASCADE to fire (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.CascadeSoftDelete.cs:35-:42). AggregatesCascadeSoftDeleteToChildren (:100) therefore fails any concrete type deriving from AuditableAggregateRootEntity<T> that declares a collection of AuditableBaseEntity<T> children and does not delete them in its own Delete() override. The child-collection test is deliberately narrow: an instance field on the aggregate whose type is one of six named generic collection types (:25-:33) over an element type deriving from the auditable entity base (:200). Auto-properties are covered by the same pass, because an auto-property is a compiler-generated instance field, and the backing field name is normalized back to the property name for the report (:192-:197,:242). The aggregate's own _domainEvents escapes twice over: it is declared on the base rather than on the aggregate, and IDomainEvent is not an entity (:66-:68). [Rubric §8 - Data Architecture] assesses whether the data model's lifecycle rules actually hold; [Rubric §30 - Compliance/Privacy/Data Governance] is the sharper consequence, since an orphaned child row is data nobody accounts for at erasure time (ADR-005); [Rubric §14 - Testability] covers the fixtures.
  • Walkthrough - one member, public string Label { get; init; } = string.Empty; (:19). Deriving from AuditableBaseEntity<TIdentifierType> is the entire fixture: strip that base and every collection in the file becomes invisible to the rule, which is precisely what ChildlessFixture's List<string> demonstrates from the other side.
  • Why it's built this way - the file doc explains why every fixture here is internal and in its own namespace (:10-:15): it keeps them invisible to the framework rules that run over CommonArchitectureMap, which registers the Source/ assemblies and never this test assembly.
  • Where it's used - the element type of five collections across the six cascade aggregates, and the assembly anchor of CascadeSoftDeleteFitnessTests's FixtureAssemblyMap (CascadeSoftDeleteFitnessTests.cs:103).

AbstractAnonymizableSubject

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.AnonymizationFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AnonymizationFixtures/AnonymizationFixtures.cs:52 · Level 4 · class

  • What it is - an internal abstract fixture that implements IAnonymizable but leaves Anonymize() abstract. Its summary calls it "an abstract anonymizable base: not a concrete entity, so it needs no sample" (AnonymizationFixtures.cs:51).
  • Depends on - IAnonymizable (internal abstract class AbstractAnonymizableSubject : IAnonymizable, :52) and Result for the abstract public abstract Result Anonymize(); (:54).
  • Concept introduced - a coverage rule must skip what cannot be instantiated. AnonymizableEntitiesAreCoveredBySamples (on ArchitectureRules) demands a sample factory for every concrete anonymizable entity. An abstract base has no instance to sample, so demanding one would misfire. This fixture is the abstract candidate that proves the exemption holds, alongside the concrete UnsampledSubject that must be reported and the non-anonymizable NotAnonymizableSubject that must not. [Rubric §14 - Testability] and [Rubric §30 - Compliance/Privacy/Data Governance] apply.
  • Walkthrough - one abstract member, Anonymize() (:54); no state and no body, since only its type shape matters to the rule.
  • Why it's built this way - keeping it in the candidate list beside the concrete subjects lets one assertion cover both directions: the failure message names the unsampled concrete type and never names the abstract base.
  • Where it's used - the candidate arrays of CoverageRule_AnonymizableEntityWithoutSample_IsReported (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/AnonymizationRulesTests.cs:110, asserted absent from the message at :123) and CoverageRule_EveryConcreteAnonymizableSampled_Passes (:134), where sampling only the two concrete subjects is enough for the rule to pass.

FixtureModuleMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts and MMCA.Common.Architecture.Tests.Cqrs · (see per-type table) · Level 4 · class

The name is used twice in this assembly, once per catalog-style rule under proof, and both are the same one-entry map: this test assembly registered as a module Application layer so a consumer-facing rule sees the fixtures. They are nested in different outer classes, so their full names differ.

  • Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
  • Concept introduced - why a self-test sometimes has to pretend to be a consumer. Several rules deliberately read only the per-module assemblies and skip framework layers, because a framework's own types are not the consumer's catalog: the error-catalog rules take ModuleDomain() plus ModuleApplication() (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.ErrorCatalog.cs:39-:44), and the validator-coverage rule takes ModuleApplication() alone, mirroring where the framework's AddValidatorsFromAssembly scan looks (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:88-:89). A map built from Framework(...) entries has Module set to the empty string (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:94-:95) and the module-layer lookup keeps only entries with l.Module.Length > 0 (:101-:102), so ModuleApplication() returns nothing and both rules would pass having read nothing at all. Using Module("Tickets", ...) is therefore not decoration: it is the one thing that puts the fixtures in scope. Compare FixtureAssemblyMap, whose rules read whole layers and which uses Framework(...) for the opposite reason, and FakeConsumerMap, which flips the same switch to prove a rule excludes framework types.
  • Walkthrough - both declare RepoToken => "MMCA.Common" and a single-entry DefineLayers() returning Module("Tickets", Layer.Application, <anchor>.Assembly). The module name "Tickets" is arbitrary except that the error-catalog self-test's prefix delegate closes over the same word (ErrorCatalogFitnessTests.cs:113), which is how a consumer's real prefix convention is modelled.
Type File:Line The rule it scopes
FixtureModuleMap (in CommandValidatorCoverageFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:115 Anchored on typeof(CreateTicketCommand).Assembly (:121). Its doc records the mirroring intent: the rule reads commands and validators from the per-module Application assemblies, exactly where the framework's validator scan looks (:110-:114).
FixtureModuleMap (in ErrorCatalogFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:119 Anchored on typeof(TicketErrors).Assembly (:112). Its doc notes that the catalog rules read the per-module Domain and Application assemblies and deliberately skip framework layers (:102-:105).
  • Why they're built this way - each is nested private sealed inside the test class that uses it, so neither is visible to the other and neither can be mistaken for a repo map. Duplicating eight lines is cheaper than a shared fixture map whose meaning would have to be read from another file.
  • Where they're used - constructed once as a readonly field per test class (CommandValidatorCoverageFitnessTests.cs:20, ErrorCatalogFitnessTests.cs:19) and passed to every rule call in each.

FixtureShapedEvent

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ContractShapeFixtures.IntegrationEvents · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:22 · Level 4 · record

  • What it is - an internal sealed integration-event fixture with two constructed-generic members, int? Count and IReadOnlyList<string> Tags, that derives from an intermediate consumer base instead of directly from the framework envelope (ContractShapeFixtures.cs:22). It exists so the frozen wire-contract snapshot can be proven to spell generic arguments out and to include a member inherited from a consumer base.
  • Depends on - the private fixture base FixtureShapedBase (ContractShapeFixtures.cs:10), which adds one member, string Origin (:13), and itself derives from BaseIntegrationEvent (:10).
  • Concept - cross-references the frozen-contract concept taught at IntegrationEventContractTestsBase. The snapshot line for an event lists its public instance properties sorted by name, drops anything declared in the framework envelope namespace MMCA.Common.Domain.DomainEvents (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:75,:68), and renders each type by its simple name with generic arguments spelled out (:91-:96). This fixture carries the two shapes a naive renderer gets wrong. A Nullable<Int32> or IReadOnlyList<String> printed as the open-generic name would hide a retyped argument, and an intermediate base sitting between the event and the envelope is still part of the wire shape, so its Origin member must appear while the envelope's SchemaVersion and DateOccurred must not. [Rubric §9, API & Contract Design] assesses whether published contracts are versioned and guarded against silent breaks; this fixture is what makes the snapshot's type rendering checkable rather than assumed. [Rubric §14, Testability] covers the self-test it feeds.
  • Walkthrough - a positional record with no body. Its siblings in the same file each isolate one other rendering rule: FixtureTallyEvent a comma inside angle brackets (ContractShapeFixtures.cs:29), FixtureCountEvent / FixtureNullableCountEvent value nullability (:33,:37), FixtureLabelEvent / FixtureNullableLabelEvent reference nullability (:42,:47), and FixtureNamedEvent an [EventName] wire key (:54-:55).
  • Where it's used - read through the fixture map by IntegrationEventContractTestsBaseTests, whose Contract_SpellsOutGenericArguments_AndPinsAnIntermediateBase asserts the live contract contains the exact line ...FixtureShapedEvent { Count:Nullable<Int32>, Origin:String, Tags:IReadOnlyList<String> } and that no ContractShapeFixtures line carries SchemaVersion: or DateOccurred: (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventContractTestsBaseTests.cs:85-:98). The live contract comes from ArchitectureRules.BuildIntegrationEventContract(new FixtureMap()) (:198-:199).
  • Caveats / not-in-source - being internal keeps it off the test assembly's public surface; the contract builder evidently enumerates non-public types, since the assertion at :90-:93 passes against it, but the enumeration helper itself was not read for this section.

ModuleConformanceTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:86 · Level 5 · class

  • What it is - the meta-test for the shipped per-module conformance base: five facts that prove each of the base's assertions actually fails on the drift it claims to catch, that the base reaches IModule's default implementations for a leaf module, and that its disabled-stub hook is harmlessly vacuous when a subclass does not override it.
  • Depends on - ModuleConformanceTestsBase<TModule> (the type under test), its own fixtures DriftedTests, FakeLeafModuleConformanceTests, FakeLeafModule and FakeDependentModule, plus xUnit [Fact] and AwesomeAssertions' Should().Throw<Exception>() / NotThrow() delegate assertions.
  • Concept introduced - testing a shipped test base, from the outside, by invoking its facts as delegates. The base lives in the MMCA.Common.Testing.Architecture package and is consumed by five subclasses across MMCA.ADC and MMCA.Store, so a regression in it would surface as green consumer suites that assert nothing. This class converts each inherited [Fact] into a method group (var assert = new DriftedTests().Module_ShouldDeclare_ExpectedName;, :91) and asserts on the delegate's behavior, which is what lets one xUnit test observe another test method failing. [Rubric §14 - Testability] assesses whether guardrails are themselves trustworthy; [Rubric §34 - Architecture Governance & Documentation] is the reason the guardrail exists at all (ADR-015 makes module conformance build-gating); and [Rubric §33 - Developer Experience] covers the failure mode being prevented, a silent break that would only appear in a downstream repo after a release (ADR-058).
  • Walkthrough
    • Three adversarial facts, one per assertion the base makes, each pointing at DriftedTests and asserting the delegate throws: Base_Fails_WhenTheNameDrifts (:88-:94), Base_Fails_WhenADependencyIsMissing (:96-:102), and Base_Fails_WhenRequiresDependenciesDrifts (:104-:110).
    • Base_ReadsDefaultInterfaceImplementations_ForALeafModule (:112-:121): instantiates FakeLeafModuleConformanceTests and calls the two inherited assertions directly (:119-:120). Passing proves the base reached IModule's defaults, because the leaf fixture declares neither member (inline comment, :115-:116).
    • Base_DisabledStubHook_IsVacuous_ByDefault (:123-:129): asserts the fourth inherited fact does not throw for a module that exports nothing, so a leaf subclass never has to override AssertDisabledStubs (the base's default is an empty body, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:75-:77).
    • The nested private sealed class DriftedTests (:131-:138) closes the file; being private is what keeps xUnit from collecting its three deliberately-failing inherited facts as tests of their own (class doc, :81-:85).
  • Why it's built this way - the three module members the base asserts on are the entire contract ModuleLoader registers on: it topologically orders on Dependencies, matches ModulesSettings entries by Name, and chooses between a hard start failure and stub registration from RequiresDependencies (the base's own because clauses spell each one out, ModuleConformanceTestsBase.cs:40,:52,:61). None of those drifts throws at build time, so the fitness base is the only guard, and this class is the guard on the guard (ADR-059).
  • Where it's used - an independent class in the Common architecture suite. The base it protects is subclassed by five real module tests: MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.API.Tests/NotificationModuleTests.cs:8, MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.API.Tests/IdentityModuleTests.cs:5, MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.API.Tests/SalesModuleTests.cs:5, MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.API.Tests/IdentityModuleTests.cs:5, and MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.API.Tests/CatalogModuleTests.cs:5.
  • Caveats / not-in-source - the three adversarial facts assert only that an exception is thrown (Should().Throw<Exception>(), :93,:101,:109), not which assertion produced it or what its message says. They prove the base is not vacuous; they do not pin its failure text.

FixtureProbeMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventNameConventionTests.cs:43 · Level 4 · class

  • What it is - a private sealed one-entry architecture map nested in EventNameConventionTests: RepoToken => "MMCA.Common" (EventNameConventionTests.cs:45) and a single Framework(Layer.Shared, typeof(FixtureEventNameMissingEvent).Assembly) layer (:47-:48), so the event-name rule sees this test assembly's fixture events and nothing else.
  • Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
  • Concept - cross-references the scoped-fixture-map pattern taught at FixtureAssemblyMap: register only the layer the rule reads so the assertion is provable against a known shape. This map uses Framework(...) (not Module(...)) because the [EventName] rule reads the shared layer. [Rubric §14, Testability] covers the focused input; [Rubric §9, API & Contract Design] is why an event without [EventName] matters (its assertion message says a CLR name would be persisted and a rename would orphan it, :22).
  • Walkthrough - the map is built once per fact in EventWithoutEventName_IsFlagged (:16) and EventWithEventName_IsNotFlagged (:27). The third fact, FrameworkEvents_AllDeclareEventName (:38), runs against the real framework instead, so the fixture map is not used there.
  • Where it's used - only inside EventNameConventionTests; nothing references it outside its own file.

PluralSentenceResourceTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/PluralSentenceResourceTests.cs:15 · Level 4 · class

  • What it is - a resource-file fitness test: one fact that scans every framework .resx for a count rendered inside a fixed plural sentence ("{0} items in your cart"), plus three detector self-tests that pin what the scanner flags and what it accepts.
  • Depends on - ArchitectureMapBase for FindRepoRoot("MMCA.Common.slnx") (PluralSentenceResourceTests.cs:20), ArchitectureAssert for NoViolations (:35-:38), and the plural API it steers toward, StringLocalizerPluralExtensions. Externals: System.Xml.Linq.XDocument for reading .resx entries (:72-:77), a source-generated Regex (:111-:112), xUnit and AwesomeAssertions.
  • Concept introduced - a count inside a single-form sentence is a localization defect, and it is detectable mechanically. "{0} items" reads wrongly for a count of one, and translators cannot fix it because the key has only one form. The framework's answer is a pair of sibling keys, KEY.One and KEY.Other, resolved by L.Plural(KEY, count, count), which picks .One for exactly one and .Other otherwise and falls back to the base key when the plural key is missing (MMCA.Common/Source/Presentation/MMCA.Common.UI/Globalization/StringLocalizerPluralExtensions.cs:23,:26,:40-:51; ADR-027). This test turns that convention into a build failure whose message is the instruction: split the key and use L.Plural (PluralSentenceResourceTests.cs:37-:38). [Rubric §27, i18n] assesses whether text adapts correctly to language and quantity; this fixes pluralization at the resource layer, for the English and Spanish sets the framework ships. [Rubric §14, Testability] covers the detector self-tests, and [Rubric §34, Architecture Governance & Documentation] the choice of an executable convention over a written one.
  • Walkthrough
    • FrameworkResources_HaveNoCountInAFixedPluralSentence (:17-:39): enumerates *.resx under the repo's Source folder, skipping MudTranslations* files and anything under bin or obj (:20-:27), then asserts a non-vacuous floor of at least four files (SharedResource and ErrorResources, base plus es) so a wrong scan root cannot pass having read nothing (:29-:30). Every violation is reported as file: key = "value" (count followed by 'word') (:32-:33,:94).
    • Detector_Flags_ACountInAFixedPluralSentence (:41-:48): four inline cases, including the (s) hedge ("{0} item(s) in your cart"), a Spanish sentence, and a count in parentheses, each expected to produce exactly one violation.
    • Detector_Accepts_AValueThatIsNotAFixedPluralSentence (:50-:58): five cases that must produce none: a .Other key, a singular noun after the placeholder, a verb ("{0} with Id {1} was not found."), a preposition, and a .One key.
    • Detector_Accepts_TheBaseKeyOfAPluralPair_AsItsFallback (:60-:67): a base key whose .Other sibling exists is skipped, because it is only the fallback for a resource set not yet split.
    • FixedPluralSentences (:85-:95) is the detector: it drops .Other keys and any key with a .Other sibling (:89-:90), then matches each value against CountFollowedByPlural, a placeholder ({0}, {1:N0}) followed by whitespace and a word with an optional (s) / (es) suffix (:110-:112, 1000 ms match timeout). IsPluralMarked (:97-:102) counts a word as plural when it ends in (s) or (es), or is four or more letters ending in s and not in NonNounsEndingInS, a fixed English and Spanish stop list (does, this, antes, entonces and so on, :105-:108).
  • Why it's built this way - the detector is a heuristic, so it is proven from both sides with inline data rather than trusted: the flag cases pin its recall and the accept cases pin the false positives it must not raise. The resource scan is a plain XML read of data elements (:72-:77) rather than a ResourceManager load, so it needs no culture setup and sees every key in every satellite file.
  • Where it's used - an independent class in the Common architecture suite; nothing references it outside its own file. The framework call site it protects is NotificationSend, which resolves L.Plural("Notif.Send.SentTo", ...) (MMCA.Common/Source/Presentation/MMCA.Common.UI/Pages/Notifications/NotificationSend.razor.cs:137).
  • Caveats / not-in-source - the four-letter, ends-in-s rule is English-shaped. A plural of three letters or fewer ("{0} ids") or an irregular plural not ending in s ("{0} children") passes unflagged, and a non-noun ending in s that is missing from the stop list would be a false positive. The scan covers MMCA.Common's own Source tree only; consumer repos are not scanned by this class.

FixtureEntity

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.HardDeleteFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:11 · Level 4 · class

  • What it is - the entity type the hard-delete fixtures erase, keep, and soft-delete. It is the T of the DbSet<T>, IQueryable<T> and List<T> those fixtures operate on.
  • Depends on - AuditableBaseEntity<TIdentifierType> closed over int (HardDeleteFixtures.cs:11, import at :2).
  • Concept introduced - soft-delete as the framework's deletion model, and the EF members that bypass it. An entity sets IsDeleted = true and the global query filter hides it, so the row survives for audit, restore and erasure accounting; a hard delete bypasses all of that (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:26-:30). HardDeletesOnlyInAllowedTypes (:62) reads every scannable assembly's IL and reports any call to one of four EF Core member names, Remove, RemoveRange, ExecuteDelete and ExecuteDeleteAsync (:10-:11), from a type outside the repo's allowlist. The interesting half is the disambiguation. Remove is an ordinary collection method name, so it only counts when the declaring type is one of four EF entity-set types, DbContext, DbSet<T>, InternalDbSet<T,T> or LocalView<T> (:18-:24,:138-:139); ExecuteDelete and ExecuteDeleteAsync are unambiguous wherever EF declares them, so any declaring type under the Microsoft.EntityFrameworkCore prefix counts (:130,:138). Without that scoping the rule would fire on every dictionary and cache in the repo, which is the version of this idea that gets deleted after a week. [Rubric §8 - Data Architecture] assesses whether the storage model's deletion semantics hold everywhere; [Rubric §30 - Compliance/Privacy/Data Governance] is why it matters, since an erased row silently drops out of audit and erasure accounting (ADR-005); [Rubric §14 - Testability] covers the fixtures.
  • Walkthrough - one member, public string Name { get; set; } = string.Empty; (:13). It is public (unlike the fixtures around it) because it appears in the public signatures of the static fixture methods and is the anchor type the self-test's map resolves the assembly from (SoftDeleteEnforcementFitnessTests.cs:83).
  • Where it's used - the entity parameter of all five members across DbSetRemovingFixture and family, and the map anchor of SoftDeleteEnforcementFitnessTests.

FixtureCompliantV1ToV2Upcaster, FixtureCompliantV2ToV3Upcaster, FixtureContestedClaimUpcaster, FixtureRivalClaimUpcaster, FixtureBackwardsVersionUpcaster

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.UpcasterFixtures.IntegrationEvents · (see per-type table) · Level 4 · class

Five internal sealed upcasters over the eight fixture contracts, each a single expression-bodied Upcast that copies the one field across. Between them they are the truth table for the two event-upcaster fitness rules (ADR-090): two clean rungs of a chain, two rivals claiming one source, and one pointing backwards down the version ladder.

  • Depends on - IEventUpcaster in its two-parameter form (EventUpcasterFixtures.cs:1) and the fixture event contracts they are generic over.
  • Concept introduced - the upcast chain must be a function, and it must run forwards. When a retired contract arrives from the outbox, the consumer resolves the one upcaster registered for that type and replays the message as its successor. Two properties make that mechanical rather than lucky. First, at most one upcaster may read a given source contract: with two claimants, which one runs would depend on DI registration order, so EventUpcastersHaveUniqueSourceTypes groups by source and reports any group with more than one entry (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:14-:17). Second, the target must declare a strictly higher SchemaVersion than the source, because an upcaster that moves sideways or down is not producing a successor at all and the chain stops being a ladder anyone can reason about; EventUpcastersIncreaseSchemaVersion compares the two declared versions and flags targetVersion <= sourceVersion (:44-:48). Note what the second rule deliberately does not do: a missing or non-int SchemaVersion is skipped rather than reported (:37-:42), because that is the business of the separate IntegrationEventsDeclareSchemaVersion rule. One rule, one judgement. Both rules find their subjects by matching the interface on name and arity, "IEventUpcaster`2" (:81-:83), which is how the rule library stays free of a compile dependency on the framework's own Application package. [Rubric §6 - CQRS & Event-Driven] assesses whether asynchronous contracts evolve safely; [Rubric §9 - API & Contract Design] covers the versioning discipline; [Rubric §29 - Resilience & Business Continuity] is the operational consequence, since an outbox row written against a retired contract has to be replayable weeks later.
  • Walkthrough - every one of the five has the same body shape, public {Target} Upcast({Source} integrationEvent) => new(integrationEvent.Sku);, which is the typed overload declared by IEventUpcaster (MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/Events/IEventUpcaster.cs:82); SourceType and TargetType come free as default interface implementations off the generic arguments (:72,:75), which is exactly what the rules read.
Type File:Line The case it models
FixtureCompliantV1ToV2Upcaster MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:54 Clean: the only claimant on FixtureCompliantV1, and its target declares version 2. Must be absent from both rules' reports.
FixtureCompliantV2ToV3Upcaster MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:60 The second rung, and the sharper of the two clean cases: its source is the previous upcaster's target. That is a chain, not a duplicate claim, and a naive implementation that grouped on "types that appear in more than one upcaster" would wrongly flag it. It gets its own dedicated fact.
FixtureContestedClaimUpcaster MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:66 Offender: reads FixtureContestedV1 and produces V2. Legal in isolation.
FixtureRivalClaimUpcaster MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:72 The second claimant on the same FixtureContestedV1, producing V3 instead. The offence is the pair, so the rule's message must name the contested contract and both upcasters.
FixtureBackwardsVersionUpcaster MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:78 Offender for the other rule: source FixtureBackwardsV2 (version 2), target FixtureBackwardsV1 (version 1). It is the only claimant on its source, so it passes the unique-source rule and fails the version rule, which is what keeps the two rules' proofs independent.
  • Why they're built this way - the two rules judge different things about the same set of types, so the fixture set is designed so that each offender trips exactly one of them. If the backwards upcaster also shared a source, a failure in the unique-source rule would mask a regression in the version rule. Being internal keeps them off the assembly's public surface while still visible to ConcreteClasses, which is what the rule enumerates (ArchitectureRules.Upcasters.cs:86).
  • Where they're used - reflected over by EventUpcasterFitnessTests through UpcasterTestMap, and named by nameof in its assertions.

FixtureDoubleHopUpcaster

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.UpcasterFixtures.IntegrationEvents · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/DoubleHopUpcasterFixtures.cs:29 · Level 4 · class

  • What it is - an internal sealed offender fixture: one class that declares two upcaster contracts at once, IEventUpcaster<FixtureHopV1, FixtureHopV2> and IEventUpcaster<FixtureHopV2, FixtureHopV3> (DoubleHopUpcasterFixtures.cs:29-:30). It exists so the unique-source rule can be proven to reject a multi-hop upcaster class, not only two classes contesting one source.
  • Depends on - IEventUpcaster in both its non-generic and two-parameter forms, and three internal sealed rung contracts declared in the same file: FixtureHopV1 (default schema version, :8), FixtureHopV2 (SchemaVersion => 2, :11-:14) and FixtureHopV3 (SchemaVersion => 3, :17-:20), each a positional record over a single Sku and derived from BaseIntegrationEvent.
  • Concept - cross-references the upcast-chain-as-a-function concept taught at the upcaster fixture family. The extra point this fixture carries is that an upcaster class must be exactly one hop. The runtime EventUpcasterRegistry indexes each instance by its single SourceType (MMCA.Common/Source/Core/MMCA.Common.Application/Services/EventUpcasterRegistry.cs:65,:71). Declaring two generic contracts makes the two default SourceType / TargetType implementations (MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/Events/IEventUpcaster.cs:72) conflict, so the compiler forces explicit non-generic members, and those can name only one hop. The class therefore reports V1 to V2 (DoubleHopUpcasterFixtures.cs:32-:34), routes the non-generic Upcast to the V1 overload (:40-:41), and the V2 to V3 rung silently disappears at runtime (fixture doc, :22-:28). Nothing about that shape fails to compile, which is why it needs a fitness rule. [Rubric §6, CQRS & Event-Driven] covers safe contract evolution; [Rubric §14, Testability] covers the adversarial fixture.
  • Walkthrough - two public typed Upcast overloads, FixtureHopV1 -> FixtureHopV2 (:36) and FixtureHopV2 -> FixtureHopV3 (:38), each copying Sku across, plus the three explicit IEventUpcaster members described above. The rule finds it because EventUpcastersHaveUniqueSourceTypes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:14) counts each class's "IEventUpcaster`2" interfaces by name and arity (:96-:98) and reports any class with more than one, with a message ending "an upcaster class must be exactly one hop" (:21-:23). That multi-hop check sits beside the contested-source check (:16-:19) and both feed the same violation list (:25).
  • Where it's used - named by nameof in EventUpcasterFitnessTests.UniqueSourceRule_FlagsAClassDeclaringTwoUpcasterContracts, which asserts the rule's message contains the class name and the phrase "exactly one hop" (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventUpcasterFitnessTests.cs:37-:44); see EventUpcasterFitnessTests.
  • Caveats / not-in-source - the fixture sits in the same UpcasterFixtures namespace the other upcaster facts read, so it is also present in the unique-source rule's output for the contested-contract fact; that fact asserts only Contain / NotContain on specific names (EventUpcasterFitnessTests.cs:19-:24) and is unaffected.

FixtureLeakedPayload

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.PayloadPurityFixtures.IntegrationEvents · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/PayloadPurityFixtures/IntegrationEvents/PayloadPurityFixtures.cs:20 · Level 4 · record

  • What it is - the domain-typed payload the payload-purity rule is built to catch: a nested record carrying an AuditableBaseEntity<int>? reference, one level down from the event that holds it.
  • Depends on - AuditableBaseEntity<TIdentifierType> closed over int (internal sealed record FixtureLeakedPayload(AuditableBaseEntity<int>? Entity);, PayloadPurityFixtures.cs:20).
  • Concept - the framework's payload-purity rule (ADR-010) requires an integration event crossing a transport boundary to carry primitives and DTOs only, because a subscriber on the far side has no shared DbContext to resolve a leaked entity reference against. This fixture is nested one level inside its event on purpose: PayloadRule_FlagsADomainTypeReachedThroughANestedPayload asserts the failure message names the full path FixtureLeakingEvent.Payload.Entity, not just Payload, proving the rule walks nested payload shapes rather than stopping at the event's own properties (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs:55-:57).
  • Where it's used - the sole property of the fixture event's Payload, read by IntegrationEventPayloadPurityRuleTests.PayloadRule_FlagsADomainTypeReachedThroughANestedPayload (IntegrationEventPayloadPurityTests.cs:48-:59).

FixtureMap (Cqrs), FixtureMap (Governance)

MMCA.Common.Architecture.Tests.Cqrs and MMCA.Common.Architecture.Tests.Governance · (see per-type table) · Level 4 · class

Two unrelated one-entry maps that happen to share the plain name FixtureMap, both private sealed and nested in a different outer test class, so neither is visible outside its own file.

  • Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
  • Concept introduced - each proves a different rule against a deliberately scoped fixture set, the same pattern taught at FixtureAssemblyMap: register only the layer(s) the rule under test reads, so the assertion is provable against a known shape rather than the real codebase. The Cqrs instance is a dual-layer map, Module("Fixtures", Layer.Api, ...) plus Module("Fixtures", Layer.Application, ...) over the same assembly (ConstructorDependencyCountTestsBaseTests.cs:127-:82), because ConstructorDependencyCountTestsBase reads both Map.Api() and Map.ModuleApplication() to bound controller and handler constructors independently. The Governance instance is a single Shared-layer Framework(Layer.Shared, ...) map (FeatureFlagLifecycleTests.cs:108-:109) that stands in for the deliberately bad fixtures FeatureFlagsDeclareLifetime must flag: an undeclared flag, a Permanent flag that also sets RemoveBy, a Temporary flag with no RemoveBy, and one whose RemoveBy does not parse as an ISO yyyy-MM-dd date. [Rubric §14 - Testability] assesses whether both rules are provable with focused inputs; [Rubric §34 - Architecture Governance and Documentation] is why FeatureFlagsDeclareLifetime exists (ADR-031's dead-toggle detector).
  • Walkthrough - both declare RepoToken => "MMCA.Common" and a single-entry DefineLayers(); see the per-type table for the exact layer call each makes.
Type File:Line Layer(s) The rule it scopes
FixtureMap (in ConstructorDependencyCountTestsBaseTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:121 Module("Fixtures", Layer.Api, ...) and Module("Fixtures", Layer.Application, ...), both anchored on typeof(FatFixtureController).Assembly (:81-:82) ConstructorDependencyCountTestsBase's controller and handler ceiling checks, via the TightCeilingTests/ConformantTests subclasses (:98,:110).
FixtureMap (in FeatureFlagLifecycleRuleTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:104 Framework(Layer.Shared, typeof(FixtureBadFeatures).Assembly) (:109) ArchitectureRules.FeatureFlagsDeclareLifetime, via the private DeclarationFailure() helper (:96-:101), and TemporaryFeatureFlagsAreNotPastRemoveBy for the expiry facts (:72,:86-:87).
  • Why they're built this way - each is nested private inside the single test class that consumes it, keeping an eight-to-ten-line fixture map beside the assertions it serves rather than in a shared file whose scope a reader would have to look up. It is the same trade-off FixtureModuleMap and FixtureAssemblyMap make.
  • Where they're used - constructed inline where each fact needs it: new FixtureMap() inside TightCeilingTests/ConformantTests (ConstructorDependencyCountTestsBaseTests.cs:146,:112), and new FixtureMap() inline at each ArchitectureRules call in FeatureFlagLifecycleRuleTests (FeatureFlagLifecycleTests.cs:72,:86,:98).

StaleAllowListTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:171 · Level 4 · class

  • What it is - the adversarial fixture for the stale-entry half of the anonymous-endpoint gate: a subclass whose allow-list names an endpoint that does not exist, so the base's AllowList_HasNoStaleEntries fact must report it rather than shrug.
  • Depends on - AnonymousEndpointTestsBase (private sealed class StaleAllowListTests : AnonymousEndpointTestsBase, AnonymousEndpointTestsBaseTests.cs:171).
  • Concept introduced - an allow-list rots in two directions, and only one of them is obvious. A missing entry fails loudly the moment a new [AllowAnonymous] lands. A stale entry fails silently and permanently: the endpoint gets renamed, re-gated with [Authorize], or deleted, and the list keeps granting a permission that is no longer being requested. Nothing breaks, so nobody looks, and the next endpoint that happens to match that identifier inherits an approval nobody granted it. The base therefore runs the comparison both ways, computing stale as the allow-list entries with no match in the scanned set (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:109-:89), with a because that spells out the consequence: an entry that no longer matches hides a renamed or re-gated endpoint behind a permission that is no longer being granted (:88). [Rubric §11 - Security] is the property; [Rubric §15 - Best Practices & Code Quality] is the mechanism, since a self-pruning list is one that stays readable.
  • Walkthrough - TargetAssemblies is this test assembly (:110-:111), so the fixture controllers are discovered normally. AllowedAnonymousEndpoints holds exactly one entry, "MMCA.Common.Architecture.Tests.NoLongerAnonymousController.ReadAsync" (:113-:114), naming a controller that has never existed. That makes the fixture fail both facts at once (every real fixture endpoint is now an unlisted offender as well), which is harmless because the fact under proof invokes only AllowList_HasNoStaleEntries as a delegate.
  • Where it's used - the subject of Base_Fails_WhenTheAllowListHasAStaleEntry (AnonymousEndpointTestsBaseTests.cs:26-:35), which asserts the message contains "NoLongerAnonymous" (:33), the distinctive fragment of the phantom identifier.

StaleUndecoratedAllowListTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:199 · Level 4 · class

  • What it is - the adversarial fixture for the stale-entry half of the base's second override list, EndpointsWithoutAuthorizationAttribute: a subclass that hand-names an endpoint that has never existed, so the staleness check on that list, distinct from the allow-list's own, is what must catch it.
  • Depends on - AnonymousEndpointTestsBase (private sealed class StaleUndecoratedAllowListTests : AnonymousEndpointTestsBase, AnonymousEndpointTestsBaseTests.cs:199).
  • Concept - cross-references the two-directions-of-rot concept introduced at StaleAllowListTests, applied here to the strict-mode override list rather than the allow-list. TargetAssemblies is this test assembly (:199-:200), AllowedAnonymousEndpoints is empty (:202), and EndpointsWithoutAuthorizationAttribute holds the one phantom entry, "MMCA.Common.Architecture.Tests.NoLongerUndecoratedController" (:204-:205).
  • Where it's used - the subject of Base_Fails_WhenTheUndecoratedAllowListHasAStaleEntry (AnonymousEndpointTestsBaseTests.cs:107-:114), which asserts UndecoratedAllowList_HasNoStaleEntries throws with a message containing "NoLongerUndecorated" (:113).

StrictConformantTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:210 · Level 4 · class

  • What it is - the compliant fixture for the base's opt-in strict mode: a subclass with RequireExplicitAuthorizationDecision => true (:215) whose EndpointsWithoutAuthorizationAttribute is computed from the live scan itself, [.. UndecoratedEndpoints()] (:217-:218), so it always names exactly what the scan would otherwise flag and never goes stale.
  • Depends on - AnonymousEndpointTestsBase (private sealed class StrictConformantTests : AnonymousEndpointTestsBase, AnonymousEndpointTestsBaseTests.cs:210).
  • Concept - cross-references the strict-mode concept introduced at StrictDriftedTests. Where that fixture proves strict mode fails a genuinely undecorated controller, this one proves the same mode accepts an endpoint once it is reported and declared, and that inheritance still resolves correctly: InheritingFixtureController and its abstract base are both absent from UndecoratedEndpointsForTest()'s result (:91-:96), because the authorization decision was made once on the abstract base's action, exactly as ASP.NET Core resolves it.
  • Walkthrough - the internal UndecoratedEndpointsForTest() helper (:220) exposes the protected base scan to the test class. Base_DoesNotReport_AControllerThatInheritsItsDecisionFromAnAbstractBase (AnonymousEndpointTestsBaseTests.cs:86-:97) calls it directly; Base_Accepts_AnUndecoratedEndpointThatIsAllowListed (:99-:104) instead invokes the inherited Endpoints_DeclareAnAuthorizationDecision fact as a delegate and asserts it does not throw.
  • Where it's used - the subject of two facts in AnonymousEndpointTestsBaseTests: Base_DoesNotReport_AControllerThatInheritsItsDecisionFromAnAbstractBase and Base_Accepts_AnUndecoratedEndpointThatIsAllowListed.

StrictDriftedTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:189 · Level 4 · class

  • What it is - the adversarial fixture for the base's opt-in strict mode: a subclass with RequireExplicitAuthorizationDecision => true (:194) that declares nothing in either override list, so a controller with no [Authorize] and no [AllowAnonymous] is invisible to neither list and the strict check must still catch it.
  • Depends on - AnonymousEndpointTestsBase (private sealed class StrictDriftedTests : AnonymousEndpointTestsBase, AnonymousEndpointTestsBaseTests.cs:189).
  • Concept introduced - an opt-in strict mode for the gap the base allow-list alone cannot close. [AllowAnonymous]'s absence is not the same claim as [Authorize]'s presence: a controller action that carries neither attribute is invisible to the plain allow-list scan, because nothing on it looks anonymous. RequireExplicitAuthorizationDecision is the base's answer, a second scan that demands every action declare one or the other. [Rubric §11 - Security] is the property under test; [Rubric §14 - Testability] is why the fixture proves the failure path and not just the passing one.
  • Walkthrough - TargetAssemblies is this test assembly (:189-:190), AllowedAnonymousEndpoints is empty (:192), and RequireExplicitAuthorizationDecision is the only override beyond that (:194).
  • Where it's used - the subject of Base_Fails_WhenAControllerCarriesNoAuthorizationDecisionAtAll (AnonymousEndpointTestsBaseTests.cs:74-:84), which invokes Endpoints_DeclareAnAuthorizationDecision as a delegate and asserts it throws with a message naming UndecoratedFixtureController (:81).

RouteAuthorizationTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:14 · Level 4 · class

  • What it is - the meta-test for the shipped route-authorization base: one seven-case theory proving the role matcher reads every [Authorize] attribute on a page, plus two facts proving the governed-page assertion passes when the role is present among stacked or inherited attributes and still fails when it is absent.
  • Depends on - RouteAuthorizationTestsBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:22), through three private probe subclasses declared in the same file: RoleProbe (RouteAuthorizationTestsBaseTests.cs:76), StackedAttributeTests (:87) and MissingRoleTests (:99). Externals: xUnit [Theory]/[Fact] and AwesomeAssertions.
  • Concept introduced - an attribute can be stacked or inherited, so a role gate must be matched across all of them. [Authorize] allows multiple instances, and reflection does not promise which comes first, so a matcher that read only the first would see a bare [Authorize] and report a regression that is not one. The base's RequiresRole checks whether any authorize attribute carries the exact role (RouteAuthorizationTestsBase.cs:86-:91), and GovernedPages_RequireDeclaredRole (:50) fails the build for governed pages that lack it. [Rubric §11, Security] is the property (an admin route must not regress to one any signed-in user can reach); [Rubric §14, Testability] covers proving the matcher from both sides.
  • Walkthrough
    • RequiresRole_ReadsEveryAuthorizeAttribute (RouteAuthorizationTestsBaseTests.cs:56-:65): four pages expected true (a plain role gate, policy then role, role then policy, and a bare [Authorize] subclass over a role-gated base) and three expected false (bare only, a different role plus a bare attribute, and no authorize at all). RoleProbe.Requires exposes the protected matcher (:120).
    • GovernedPages_WithStackedOrInheritedAttributes_KeepTheirRoleGate (:67-:73): invokes the inherited fact as a delegate on StackedAttributeTests, which governs the three stacked or inherited pages (:131-:134), and asserts it does not throw.
    • GovernedPages_WithoutTheRole_StillFail (:75-:82): MissingRoleTests governs OtherRolePlusBarePage (:143) and the delegate must throw with that type name in the message, so the gate is not vacuous.
  • Why it's built this way - the page fixtures are nested public types in the test class itself, each isolating one attribute order, so the theory data reads as a truth table. All three probes set RequiredRole to "Admin" (:54).
  • Where it's used - an independent class in the Common architecture suite; nothing references it outside its own file. Consumers subclass the base it protects.

UnmarkedOverwriteSubject

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.AnonymizationFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AnonymizationFixtures/AnonymizationFixtures.cs:16 · Level 4 · class

  • What it is - an internal sealed IAnonymizable fixture whose Anonymize() nulls Nickname and sets IsErased but carries no [Pii] marker, so it is the deliberately bad subject the privacy rule must flag (AnonymizationFixtures.cs:16-:34).
  • Depends on - IAnonymizable and Result (Anonymize() returns Result.Success()).
  • Concept introduced - the erasure method is the source of truth for what is personal data. PropertiesOverwrittenByAnonymizeCarryPii samples an instance, runs Anonymize() and reports any property it changed that is not marked [Pii]. The subject carries four decoys that pin the rule's scope: Nickname is overwritten and unmarked (must be flagged), IsErased is flipped by the erasure but is a flag, not personal data, Tags is a collection (out of scope) and City is never touched. AnonymizationRulesTests asserts the message names UnmarkedOverwriteSubject.Nickname and does not name IsErased, Tags or City (AnonymizationRulesTests.cs:26,:38-:40). [Rubric §11, Security] and [Rubric §14, Testability] apply.
  • Walkthrough - Nickname has a private setter (:21), City is init-only (:23), Tags exposes a read-only view of a private list (:27), and Anonymize() touches only Nickname and IsErased (:29-:34).
  • Where it's used - registered through the sample factory static () => new UnmarkedOverwriteSubject() throughout MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/AnonymizationRulesTests.cs (15 references there plus 14 more elsewhere per the usage index).

UnsampledSubject

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.AnonymizationFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AnonymizationFixtures/AnonymizationFixtures.cs:46 · Level 4 · class

  • What it is - an internal sealed IAnonymizable with an empty-success Anonymize() and no properties (AnonymizationFixtures.cs:46-:49). It exists to be an IAnonymizable type that has no sample factory registered.
  • Depends on - IAnonymizable and Result.
  • Concept - cross-references the erasure-as-source-of-truth concept at UnmarkedOverwriteSubject, covering the two coverage checks around it. The rule must report an IAnonymizable type missing from the sample set, and the assertion names UnsampledSubject by full name while not naming UnmarkedOverwriteSubject, which has a sample (AnonymizationRulesTests.cs:116-:122). The reverse rule reports a factory that returns a different type than the one it was registered for, with the message UnmarkedOverwriteSubject: the sample factory returned UnsampledSubject (:93-:99).
  • Where it's used - AnonymizationRulesTests.cs (6 references there plus 5 more elsewhere per the usage index), as the wrong-type factory result and as the deliberately unsampled type.

SingleLayerMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering.GovernedAi · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/GovernedAi/AiBoundaryRulesTests.cs:36 · Level 4 · class

  • What it is - a private one-layer architecture map nested in AiBoundaryRulesTests: it takes any assembly through its primary constructor and registers it as the map's single Application layer, so the governed-AI rule scans exactly that assembly and nothing else.
  • Depends on - ArchitectureMapBase (private sealed class SingleLayerMap(Assembly assembly) : ArchitectureMapBase, AiBoundaryRulesTests.cs:36), the Layer enum and LayerRef; System.Reflection.Assembly from the BCL.
  • Concept - cross-references the narrowed-map technique taught at CancellationTestMap. The difference is that this map is parameterized: where CancellationTestMap hard-codes its own test assembly, SingleLayerMap takes the assembly as a constructor argument, so one map type serves both the failing and the passing fact. Registering the assembly as Application is what puts it in scope at all, because GovernedAiPackageStaysBehindInfrastructure skips any assembly whose name contains .Infrastructure or names the AI package itself (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Ai.cs:79, :99-:107), and neither test assembly does.
  • Walkthrough - RepoToken => "MMCA.Common" (AiBoundaryRulesTests.cs:38) and a one-entry DefineLayers() returning Framework(Layer.Application, assembly) (:40-:41).
  • Where it's used - constructed twice in its declaring class, once over typeof(FixtureAiConsumer).Assembly (:19) and once over typeof(ApiControllerBase).Assembly (:30); nothing outside the defining file references it.

SpanishAccentFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance.Localization · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Localization/SpanishAccentFitnessTests.cs:12 · Level 4 · class

  • What it is - the meta-test for SpanishAccentTestsBase: two facts that run the base's Spanish_resources_keep_their_accents fact over two fixture folders, proving the gate accepts correct Spanish that merely looks unaccented and still rejects a word that is missing its accent.
  • Depends on - SpanishAccentTestsBase through a private nested subclass FixtureSpanishTests (SpanishAccentFitnessTests.cs:37-:40), ArchitectureMapBase for FindRepoRoot("MMCA.Common.slnx") (:14-:19), the two *.es.resx files under Tests/Architecture/MMCA.Common.Architecture.Tests/SpanishFixtures/ (Compliant/Compliant.es.resx, Offending/Offending.es.resx), and externals xUnit [Fact], Xunit.Sdk.XunitException and AwesomeAssertions.
  • Concept - cross-references the thin-subclass gate taught at SpanishAccentTests; this class adds the false-positive side of the proof. The base deliberately leaves out words that are correct both with and without the accent depending on meaning, naming aun ("even" unaccented, "still" accented) among them (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SpanishAccentTestsBase.cs:20-:24), while todavia stays in the default list (:34-:39). A gate that flagged correct Spanish would teach developers to bypass it, so the acceptance case is pinned as firmly as the rejection case. [Rubric §27, i18n] assesses whether localized text is correct; [Rubric §14, Testability] applies because the gate is proven from both sides against fixtures rather than against live resources.
  • Walkthrough
    • FixtureRoot (SpanishAccentFitnessTests.cs:14-:19) resolves the repo root and descends to Tests/Architecture/MMCA.Common.Architecture.Tests/SpanishFixtures.
    • UnaccentedAun_MeaningEven_IsAccepted (:21-:27) points the subclass at Compliant, whose single value is Aun as&#237;, puede continuar. (Compliant.es.resx:4, the accented i stored as a character reference), and asserts the inherited fact does not throw.
    • AWordMissingItsAccent_IsStillRejected (:29-:35) points it at Offending, whose value is La solicitud todavia esta pendiente. (Offending.es.resx:4), and asserts an XunitException whose message contains "todavia". The same sentence carries esta, which the base excludes on purpose, so only the one genuine mistake is required in the message.
    • FixtureSpanishTests(string resourceRoot) (:37-:40) overrides only ResourceRoot, so both facts run the base's defaults unchanged.
  • Why it's built this way - the file's doc comment records that the nested subclass is private so xUnit does not discover it as a test class, and its fact is invoked directly over the fixture folders instead (:6-:11). Feeding fixtures through the real base keeps the proof honest: the word list, the whole-word match and the <value>-only scan are all exercised exactly as consumers run them. See ADR-015.
  • Where it's used - an independent class in the Common architecture suite; nothing outside the defining file references it.

SpanishAccentTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/SpanishAccentTests.cs:11 · Level 4 · class

  • What it is - the framework's binding of the Spanish-accent fitness function: a five-line subclass that points the shared scan at MMCA.Common's own Source folder, so no *.es.resx string ships a common word with its accent or n-tilde missing (codigo, sesion, contrasena).
  • Depends on - SpanishAccentTestsBase (public sealed class SpanishAccentTests : SpanishAccentTestsBase, SpanishAccentTests.cs:11) and ArchitectureMapBase for FindRepoRoot("MMCA.Common.slnx") (:13-:14).
  • Concept introduced - a thin subclass of an authored-once gate. Those strings compile, render and pass every functional test, so a Spanish-speaking user is the only detector until a rule exists; the base supplies the scan and the subclass supplies only what differs per repo. The one abstract member is ResourceRoot (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SpanishAccentTestsBase.cs:46); this class overrides nothing else, so it takes the default word list (:32-:39), no AllowedEntries (:58) and a MinimumResourceFileCount of 1 (:64). The base reads every *.es.resx under the root (:72-:76) and checks only each <value> text, matching whole words case-insensitively with a one-second regex timeout (:82-:86). [Rubric §27, i18n] assesses whether localized text is correct; [Rubric §34, Architecture Governance & Documentation] applies because the convention is executable.
  • Walkthrough - ResourceRoot is Path.Combine(ArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx"), "Source") (SpanishAccentTests.cs:13-:14), a property initializer, so it resolves once per instance. The base's floor assertion fails the fact if that root holds no Spanish resources (SpanishAccentTestsBase.cs:79-:80).
  • Where it's used - collected and run by xUnit in the Common architecture suite; MMCA.ADC declares its own SpanishAccentTests subclass (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/SpanishAccentTests.cs), which is where repo-specific vocabulary or exceptions would go.

AiBoundaryRulesTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering.GovernedAi · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/GovernedAi/AiBoundaryRulesTests.cs:13 · Level 5 · class

  • What it is - the meta-test for the type-level governed-AI rule in ArchitectureRules: two facts proving GovernedAiPackageStaysBehindInfrastructure really fails a non-Infrastructure layer that names an MMCA.Common.AI type, and does not mistake MMCA.Common.API for that package.
  • Depends on - ArchitectureRules (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Ai.cs:75), its own nested SingleLayerMap (AiBoundaryRulesTests.cs:36), the fixture FixtureAiConsumer (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AiBoundaryFixtures/FixtureAiConsumer.cs:9), ApiControllerBase as the anchor for the API assembly (AiBoundaryRulesTests.cs:30), and externals xUnit [Fact] plus AwesomeAssertions.
  • Concept introduced - a fitness rule can pass vacuously, so the rule itself needs a test that it can fail. The class doc records the defect it pins (M257): the rule once matched the dotted entry MMCA.Common.AI., which NetArchTest never matches because it compares whole namespace segments, so the gate passed in every subclass while checking nothing (AiBoundaryRulesTests.cs:7-:12). The rule now matches the undotted segment, GovernedAiPackageNamespace = "MMCA.Common.AI" (ArchitectureRules.Ai.cs:25, used at :83). Undotted raises the opposite risk, a prefix match catching MMCA.Common.API, so the class pins both directions. [Rubric §16, AI-Native Application Architecture] assesses whether model access is confined behind a governed boundary; this class is what keeps that confinement check from silently going dead. [Rubric §34, Architecture Governance & Documentation] applies because a governance rule is only as good as its proof that it fires.
  • Walkthrough
    • ALayerNamingAGovernedAiType_FailsTheRule (AiBoundaryRulesTests.cs:15-:24): maps the test assembly as Application through SingleLayerMap(typeof(FixtureAiConsumer).Assembly). FixtureAiConsumer.NameOf(PromptContract) takes an MMCA.Common.AI type as a parameter (FixtureAiConsumer.cs:14), which is all it takes to depend on the package. The fact asserts the message contains "must not reference MMCA.Common.AI" (the rule's text at ArchitectureRules.Ai.cs:87) and names FixtureAiConsumer.
    • TheApiAssembly_IsNotMistakenForTheAiPackage (AiBoundaryRulesTests.cs:26-:33): maps the MMCA.Common.API assembly as Application and asserts the rule does not throw, with the reason "API and AI are different namespace segments" (:32).
  • Why it's built this way - both facts call the static rule directly over a one-assembly map instead of subclassing AiDependencyIsolationTestsBase, which invokes the same rule over a repo's full map (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/AiDependencyIsolationTestsBase.cs:29). A single controlled assembly makes the expected outcome unambiguous. See ADR-120 and ADR-015.
  • Where it's used - an independent class in the Common architecture suite; nothing outside the defining file references it.

AnonymizationRulesTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance.Privacy · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/AnonymizationRulesTests.cs:15 · Level 5 · class

  • What it is - the meta-test for the two anonymization rules in ArchitectureRules: eleven facts that prove the reverse rule (PropertiesOverwrittenByAnonymizeCarryPii) reports exactly the unmarked overwrites and the coverage rule (AnonymizableEntitiesAreCoveredBySamples) reports exactly the unsampled entities.
  • Depends on - ArchitectureRules (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Anonymization.cs:12,:49), the shipped RefreshSession entity (built by BuildRefreshSession, AnonymizationRulesTests.cs:162-:169), the fixture classes in AnonymizationFixtures/AnonymizationFixtures.cs (UnmarkedOverwriteSubject :16, FailingAnonymizeSubject :37, UnsampledSubject :46, AbstractAnonymizableSubject :52, NotAnonymizableSubject :58), and Xunit.Sdk.XunitException, which each failing fact asserts by exact type.
  • Concept introduced - testing a behavioral privacy rule against a sample table. The reverse rule builds each sample, snapshots the text and reference properties declared on the type, runs Anonymize(), and reports every property whose value moved but carries no [Pii] (rule :49-:63, :65-:97); an erased value in an unmarked property would otherwise survive in the audit trail's OldValue for the retention window (rule doc :35-:38). Value types and collections are deliberately out of scope (:40-:46), which ReverseRule_IgnoresValueTypesCollectionsAndUntouchedProperties pins (AnonymizationRulesTests.cs:41-:51). [Rubric §30 - Compliance/Privacy/Data Governance] applies, and [Rubric §14 - Testability] for the failure messages that name what to mark.
  • Walkthrough - a private Samples(...) helper (:171-:172) turns tuples into the rule's Dictionary<Type, Func<object>>.
    • Reverse rule, eight facts (:29-:110): an unmarked overwrite is reported with a message that says to add [Pii] (:29-:38); a marked overwrite on RefreshSession (IpAddress, UserAgent) passes (:53-:61); a mixed table reports only the unmarked one (:63-:75); an empty table passes (:77-:80); and three malformed-sample facts name an Anonymize() that fails (:82-:90), a key that is not anonymizable (:92-:100), and a factory returning another type (:102-:110).
    • Coverage rule, three facts (:113-:160): a concrete anonymizable type with no sample is named along with the AnonymizableSamples override to extend, while an abstract type, a non-anonymizable type and a sampled type are not (:113-:135); full coverage passes (:137-:155); no anonymizable entity plus an empty table passes (:157-:160).
  • Why it's built this way - the pair closes the gap in the forward [Pii] => IAnonymizable scan: the reverse rule only checks entities someone sampled, so the coverage rule is what stops a new anonymizable entity from silently escaping it (rule doc :5-:9). Proving the NotThrow cases as deliberately as the failing ones keeps a repo with no anonymizable entity green. See ADR-005.
  • Where it's used - an independent class in the Common architecture suite; nothing outside the defining file references it.

AnonymousEndpointTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTests.cs:14 · Level 5 · class

  • What it is - the framework's own anonymous-endpoint allow-list: the live, reviewed statement of every controller action and routable page MMCA.Common ships without an authorization gate. Twenty-one entries today: the four credential-exchange endpoints, two password-recovery endpoints, two email-confirmation endpoints, four OAuth challenge/callback endpoints, six credential UI pages, and three landing/outcome pages that render nothing caller-dependent.
  • Depends on - AnonymousEndpointTestsBase (public sealed class AnonymousEndpointTests : AnonymousEndpointTestsBase, AnonymousEndpointTests.cs:14), ApiControllerBase as the anchor for the API assembly (:18), and UISharedAssemblyReference as the anchor for the shared UI assembly (:19).
  • Concept introduced - an allow-list as a review artifact, not a suppression list. The gate's value is not that it blocks [AllowAnonymous]; it is that adding one becomes a line in a test file with a comment next to it, landing in a diff that a human reads. Every entry here carries its justification inline. Login, register and refresh mint or rotate the token pair, so requiring one would be circular (:24-:28); the OAuth completion code is the caller's only credential at that point and is single-use, burned on first exchange (:29-:31); forgot-password and reset-password are for a caller who has lost the credential, throttled by the same auth-ip rate-limit policy, and forgot-password always answers 202 so it reveals nothing about which addresses hold accounts (:32-:34); the two email-confirmation endpoints are followed from a mail client with no session, an unconfirmed account may be unable to sign in at all when confirmation is required, both are throttled by the auth-ip policy, and send always answers 202 on a well-formed request (ADR-116, :39-:41). The three OAuth challenge endpoints and the provider-callback completion run before any local token exists, so they declare their anonymity rather than relying on the absence of an attribute (:44-:46); the six credential UI pages (the confirm-email landing joins the five earlier ones) cannot already hold a token if the caller has to sign in, register or recover, and AuthorizeRouteView reads attributes and ignores the fallback policy so these must declare themselves (:51-:55); the three landing and outcome pages render nothing that depends on the caller (:62). [Rubric §11 - Security] assesses the deliberateness of the authorization posture; note that the compensating control for every credential-exchange entry is rate limiting rather than authentication (ADR-029). [Rubric §9 - API & Contract Design] applies because the anonymous surface is part of the published contract, and [Rubric §34 - Architecture Governance & Documentation] because the reasoning lives in compiled code rather than a wiki page.
  • Walkthrough
    • TargetAssemblies (:16-:20) names two assemblies by anchor type, so the scan covers both the controllers and the routable Blazor pages the framework ships.
    • AllowedAnonymousEndpoints (:22-:66) is the twenty-one-entry list. Two of them are worth reading closely: PasswordResetAuthControllerBase`2.ForgotPasswordAsync and its reset sibling (:37-:38) carry a backtick-2 arity suffix, because PasswordResetAuthControllerBase<TForgotPasswordCommand, TResetPasswordCommand> is generic over the app's command records and reflection renders a generic type's FullName that way. The comment above them says exactly that (:35-:36), which is the kind of note that saves the next person an hour.
    • RequireExplicitAuthorizationDecision => true (:71) opts the framework's own controllers and pages into the StrictConformantTests/StrictDriftedTests strict gate: every concrete controller and every routable page in MMCA.Common must declare its authorization decision explicitly, so none of them depends on a reader noticing a missing attribute (:68-:70).
    • MinimumScannedTypes => 21 (:75) raises the base's default floor of 1 to this repo's known count, described in the comment as a floor and not an equality: 12 API controller types plus the routable UI pages (:73-:74). Removing a scanned type is therefore a failure rather than a quietly smaller scan.
  • Why it's built this way - the endpoints that cannot require a token are exactly the ones an attacker reaches first, so the framework's position is that they must be enumerated, justified, and rate-limited rather than merely working. Opting into RequireExplicitAuthorizationDecision is the framework holding itself to the stricter of the two gates it ships, rather than only proving the gate works against fixtures. The base's own doc records the one thing this gate cannot see: minimal-API endpoints opt out through the .AllowAnonymous() builder call, which is endpoint metadata produced at map time and invisible to static reflection (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:18-:24). The framework's minimal-API anonymous surface (JWKS, OIDC discovery, app-association, session-cookie refresh, health) is deliberate and small, but it is not covered here.
  • Where it's used - collected and run by xUnit in the Common architecture suite; its facts come entirely from the base. Consumers write their own subclass with their own list.
  • Caveats / not-in-source - the minimal-API blind spot above is stated in the base's documentation, and closing it would need an endpoint-metadata check over a built host. Nothing in this class compensates for it.

CancellationTestMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenFitnessTests.cs:63 · Level 5 · class

  • What it is - a one-layer architecture map used only by CancellationTokenFitnessTests: it registers this test assembly as the map's single Application layer so the cancellation-token rule scans the fixture services and nothing else.
  • Depends on - ArchitectureMapBase (private sealed class CancellationTestMap : ArchitectureMapBase, CancellationTokenFitnessTests.cs:63), the Layer enum, and LayerRef.
  • Concept - cross-references the map concept from CommonArchitectureMap. The narrowing matters twice over here. First, the cancellation rule only visits the Application and Infrastructure layers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CancellationTokens.cs:45-:47), so registering the test assembly as Application is what puts the fixtures in scope at all. Second, the rule's automatic exemption compares against map.Layers.Select(l => l.Assembly) (:42), so a map holding only this assembly is what makes IAsyncEnumerator<T> (declared in the BCL, outside the map) count as an externally-fixed contract. A wider map would not change that result here, but the single entry keeps the fixture's meaning unambiguous.
  • Walkthrough - RepoToken => "MMCA.Common" (:65) and a one-entry DefineLayers() returning Framework(Layer.Application, typeof(CancellationTokenFitnessTests).Assembly) (:67-:68).
  • Where it's used - constructed on every call of the test class's private RunRule helper (:56).

CancellationTokenFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenFitnessTests.cs:12 · Level 5 · class

  • What it is - the meta-test for the trailing-CancellationToken rule: three facts that prove it flags a missing, misplaced, or misnamed token, leaves compliant and out-of-scope members alone, honors the caller-supplied exemption list, and never flags a signature the repo does not own.
  • Depends on - ArchitectureRules (the rule under test, CancellationTokenFitnessTests.cs:55), its own CancellationTestMap (:56), the six fixture services in MMCA.Common.Architecture.Tests.CancellationFixtures (:1), and externals xUnit [Fact] plus AwesomeAssertions' Should().Throw<Exception>().Which (:59).
  • Concept introduced - asserting on a rule's failure message rather than on whether it threw. Every fixture in this file is deliberately in scope at once, so the rule always throws; a bare "it threw" assertion would prove nothing. Instead the private RunRule helper (:53-:60) captures the exception's message once and each fact makes positive and negative Contain assertions against that single string. The result is one rule execution that proves six independent behaviors, and every assertion carries a because clause spelling out the ruling it pins (:19-:24). [Rubric §14 - Testability] assesses whether guardrails are themselves trustworthy; this is the technique that makes a multi-branch classifier provable in three facts. [Rubric §12 - Performance & Scalability] is the property being defended, as taught in the fixture family section.
  • Walkthrough
    • Rule_FlagsMissingToken_ButNotCompliantOrNonAsyncMembers (:14-:25): asserts both MissingTokenFixtureService.RunAsync and .PingAsync appear (:19-:22), and that neither CompliantFixtureService nor the internal HiddenAsync does (:23-:24).
    • Rule_FlagsMisplacedAndMisnamedTokens (:27-:36): asserts the two offender method names appear together with the rule's exact classifier strings, "must be the LAST parameter" (:33) and "must be named 'cancellationToken'" (:35), so the message a developer will actually read is pinned, not just the fact of a failure.
    • Rule_HonorsExemptions_AndSignaturesTheRepoDoesNotOwn (:38-:51): runs the rule twice. Without exemptions, ExemptableFixtureService.LegacyAsync is reported (:41-:43); with that one entry passed in (:45), it disappears (:47). The same run also asserts MoveNextAsync never appears (:48-:50), which is the automatic exemption rather than the explicit one.
  • Why it's built this way - the rule is one of the framework's broadest (it judges every public awaitable method in two layers), so its false-positive behavior matters as much as its true-positive behavior. Pinning the message text also protects the developer experience: the classifier's wording is what tells someone how to fix the build. See ADR-015.
  • Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the same rule is CancellationTokenConventionTests.

EmptyScanTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:178 · Level 5 · class

  • What it is - the adversarial fixture for the constructor-dependency gate's non-vacuity guards: a subclass whose map's Api and Application layers hold neither a controller nor a handler, so both NotBeEmpty guards inside the base's facts must fire.
  • Depends on - ConstructorDependencyCountTestsBase (private sealed class EmptyScanTests : ConstructorDependencyCountTestsBase, ConstructorDependencyCountTestsBaseTests.cs:178) and its sibling BareMap (:87-:96), whose two layers both point at typeof(Common.Shared.Abstractions.Result).Assembly (:93-:94), a package with no controllers and no CQRS handlers.
  • Concept - cross-references the vacuity-guard concept the base class states directly in each fact: Controllers_DoNotExceedConstructorDependencyCeiling asserts controllers.Should().NotBeEmpty("the guard must scan at least one API controller (otherwise it passes vacuously)") (ConstructorDependencyCountTestsBase.cs:123-:95), and Handlers_DoNotExceedConstructorDependencyCeiling asserts the equivalent for handlers (:118-:119). A scan of nothing would otherwise report zero offenders and pass, which is indistinguishable from a healthy repo. [Rubric §14 - Testability] is the theme: the guard that proves the guard is not vacuous.
  • Walkthrough - Map is BareMap (:123), and all three ceilings are left at their ConformantTests-matching values (int.MaxValue, 3, 3, :125,:127,:129) because the ceilings are irrelevant here: the fact must throw before ever comparing a count against them, on the NotBeEmpty guard alone.
  • Where it's used - BothPopulations_FailVacuousScans (ConstructorDependencyCountTestsBaseTests.cs:60-:72) constructs one EmptyScanTests instance and asserts both Controllers_DoNotExceedConstructorDependencyCeiling and Handlers_DoNotExceedConstructorDependencyCeiling throw, each with a because clause stating what the empty scan would otherwise hide: "a map whose API assemblies hold no controller would pass while checking nothing" (:68-:69) and the handler equivalent (:70-:71).

ConformantTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:156 · Level 5 · class

  • What it is - the positive fixture for the constructor-dependency ceiling gate over controllers and handlers: a subclass whose ceilings sit exactly at the fixture population's widest constructor, so both facts must pass.
  • Depends on - ConstructorDependencyCountTestsBase (private sealed class ConformantTests : ConstructorDependencyCountTestsBase, ConstructorDependencyCountTestsBaseTests.cs:156, base at MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:29) and its sibling FixtureMap (:75-:84), which points both the Api and Application layers of the map at this test assembly (Module("Fixtures", Layer.Api, ...) and Module("Fixtures", Layer.Application, ...), :81-:82).
  • Concept introduced - a ceiling is a ratchet, not a budget. The base class doc states the rule directly: a consumer sets its high-water mark at the current widest constructor so the next class cannot silently grow past it (ConstructorDependencyCountTestsBase.cs:13-:17). ConformantTests is the fixture that proves a ceiling set at that mark does not itself trip the gate, which is what distinguishes a ratchet from an arbitrary cap: MaxControllerConstructorDependencies => 3 and MaxHandlerConstructorDependencies => 3 (:116,:118) equal the exact dependency counts the sibling TightCeilingTests fixture proves are offenders one below (TightCeilingTests ceilings of 2, :105,:107, flag FatFixtureController (3 ctor dependencies) and the two handler fixtures at the same count, ConstructorDependencyCountTestsBaseTests.cs:23,:44,:47). [Rubric §1 - Single Responsibility] is the property under test; [Rubric §14 - Testability] is why the fixture proves the boundary case rather than only the offending one.
  • Walkthrough - Map is the shared FixtureMap (:112), MaxConstructorDependencies is left at int.MaxValue (:114) because neither test that constructs this fixture exercises the Application-*Service fact, only the controller and handler delegates read directly off the instance. MaxControllerConstructorDependencies => 3 (:116) and MaxHandlerConstructorDependencies => 3 (:118) are the two ceilings under proof.
  • Where it's used - Controllers_AtTheCeiling_AreNotFlagged (ConstructorDependencyCountTestsBaseTests.cs:28-:34) reads new ConformantTests().Controllers_DoNotExceedConstructorDependencyCeiling and asserts NotThrow; Handlers_AtTheCeiling_AreNotFlagged (:52-:58) does the same against Handlers_DoNotExceedConstructorDependencyCeiling. Both assertions state the same ruling in their because clause: "the ceiling is a high-water mark: the widest controller/handler sits exactly on it" (:33,:57).

FitnessDependent

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:48 · Level 5 · class

  • What it is - the other half of the specification-navigation fixture: an entity that holds a foreign-key scalar and a navigation to a FitnessPrincipal, so both a safe (scalar-only) and an unsafe (navigating) specification can be written over it.
  • Depends on - AuditableBaseEntity<TIdentifierType> (public sealed class FitnessDependent : AuditableBaseEntity<int>, SpecificationFitnessTests.cs:48) and FitnessPrincipal (the Principal? navigation, :52).
  • Concept introduced - cross-references the fixture concept introduced by FitnessPrincipal. This type adds the parts a specification can filter on: PrincipalId (scalar FK, :50), a nullable Principal navigation (:52), and a Flag scalar (:54). The scalar-versus-navigation split is the whole point: it lets one fixture support both the pattern the rule must flag and the pattern it must leave alone.
  • Walkthrough - three auto-properties: int PrincipalId (:50), FitnessPrincipal? Principal (:52), bool Flag (:54). ScalarOnlySpec filters on PrincipalId and Flag; NavigatingSpec reaches through Principal.
  • Where it's used - the entity type parameter for all four fixture specifications in SpecificationFitnessTests: NavigatingSpec, ScalarOnlySpec, and the two query variants (NavigatingQuerySpec, ScalarOnlyQuerySpec).

DbSetRemovingFixture, ExecuteDeletingFixture, SoftDeletingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.HardDeleteFixtures · (see per-type table) · Level 5 · class

Three internal static classes holding five one-line methods between them: three that erase rows through EF Core and two that do not. They are the compiled call sites the hard-delete rule has to tell apart.

  • Depends on - Microsoft.EntityFrameworkCore for DbSet<T> and the ExecuteDeleteAsync queryable extension (HardDeleteFixtures.cs:1) and FixtureEntity as the element type.
  • Concept - cross-references the hard-delete concept introduced by FixtureEntity. What this set proves is the disambiguation, which is the whole engineering content of the rule. SoftDeletingFixture.Forget calls List<FixtureEntity>.Remove (:41), which is the same method name the rule bans, on a type the rule must ignore. Its own doc says why that matters (:32-:36): Remove on a List<T> is what makes the naive "ban the name Remove" version of this rule useless. Having a genuine List<T>.Remove compiled next to a genuine DbSet<T>.Remove is what turns the declaring-type check from an implementation detail into a proven behavior.
  • Walkthrough - the rule reports each site as {type.FullName}.{method.Name} calls {DeclaringType.Name}.{callee.Name} (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:115), so the failure text names both ends. The allowlist is checked at the type level before any method is walked (:99-:102), which is why an entry silences a whole type rather than one call.
Type File:Line What it models
DbSetRemovingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:17 Two erasing members in one type: Purge calls DbSet<T>.Remove (:19) and PurgeMany calls DbSet<T>.RemoveRange (:21-:22). Both must be reported, which is how the test proves the rule walks every method rather than stopping at the first hit.
ExecuteDeletingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:26 The server-side delete: PurgeAsync calls IQueryable<T>.ExecuteDeleteAsync (:28-:29). Kept in a separate type so the type-level allowlist can silence the DbSet fixture while this one still fails.
SoftDeletingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:37 Two near-misses: Deactivate calls the framework's own entity.Delete() (:39), and Forget calls List<T>.Remove (:41). Neither is a hard delete, so the whole type name must be absent from the report.
  • Why they're built this way - two erasing types rather than one is what makes the type-level allowlist fact meaningful: exempting DbSetRemovingFixture must leave ExecuteDeletingFixture reported. Keeping all five methods to a single expression means the compiled IL contains the call under test and essentially nothing else.
  • Where they're used - scanned by SoftDeleteEnforcementFitnessTests through its FixtureAssemblyMap, which registers this assembly as the map's single Infrastructure layer.

DbSetRemovingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.HardDeleteFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:17 · Level 5 · class

  • What it is - the primary offender for the hard-delete rule: a static helper that erases rows through the DbSet API's two removing members, Remove and RemoveRange.
  • Depends on - Microsoft.EntityFrameworkCore.DbSet<TEntity> (HardDeleteFixtures.cs:1) and its own FixtureEntity (:11).
  • Concept introduced - reading IL for the exact erasing member names, not for a type-level "calls DbSet somewhere" heuristic. Purge calls set.Remove(entity) (:19); PurgeMany calls set.RemoveRange(entities) (:21-:22). The rule must name both members independently in its failure message, which is what HardDelete_ReportsEveryErasingMember in SoftDeleteEnforcementFitnessTests pins (SoftDeleteEnforcementFitnessTests.cs:37-:38).
  • Walkthrough - internal static class DbSetRemovingFixture with two static methods, Purge(DbSet<FixtureEntity>, FixtureEntity) (:19) and PurgeMany(DbSet<FixtureEntity>, IEnumerable<FixtureEntity>) (:21-:22), each a one-line call into the EF member under test.
  • Where it's used - the subject of HardDelete_OutsideTheAllowlist_IsFlagged, HardDelete_ReportsEveryErasingMember, and half of AllowlistedType_SilencesOnlyThatType in SoftDeleteEnforcementFitnessTests (SoftDeleteEnforcementFitnessTests.cs:21-:28,:30-:40,:62-:74); the last asserts an allowlist entry naming this type silences it while ExecuteDeletingFixture is still reported (:72-:73).

ErrorCatalogFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:15 · Level 5 · class

  • What it is - the meta-test for the two error-catalog rules: eight facts that pin the cross-type collision, the two-branch near-miss, the shared-code exemption on both rules, the unprefixed offender, the Error.TooManyRequests factory read into the catalog, the UNVERIFIABLE report for a dynamically-built code, and the distinct-code inventory.
  • Depends on - ArchitectureRules (ErrorCodesAreUnique, ErrorCodesUseAnAllowedPrefix and DistinctErrorCodeCount, ErrorCatalogFitnessTests.cs:24,:58,:105), its nested FixtureModuleMap, the six fixture catalogs (:1), and Xunit.Sdk.XunitException (:3).
  • Concept introduced - the third verdict: UNVERIFIABLE, and why a static analyser should have one. Most fitness functions are binary. These are not, because a code assembled at run time genuinely cannot be read out of the instruction stream, and both available binary answers are wrong: passing it hides a possible collision, failing it bans a legitimate pattern the framework never decided to ban. The rules take the third option and surface those sites in the failure message under an UNVERIFIABLE heading (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.ErrorCatalog.cs:155), so the reader learns the catalog has a blind spot and where it is. DomainThrowFitnessTests pins the same verdict for the same reason on a different rule. [Rubric §14 - Testability] assesses whether a guardrail is honest about its own limits, which is exactly what a named third verdict is; [Rubric §9 - API & Contract Design] is the property.
  • Walkthrough - a private static readonly string[] SharedCodes = ["Error.NotFound"] (:17) stands in for a repo's allowed-shared-codes list, and IsTicketsCode (:113) is the prefix delegate, closing over the fixture module's name exactly as a consumer's would close over its real module names.
    • DuplicateCode_AcrossTwoTypes_IsFlagged (:21-:31): asserts the message names the code and both owning types (:28-:30), which is what makes a collision actionable.
    • CodeReusedAcrossBranchesOfOneType_IsNotADuplicate (:33-:42): the near-miss, absent from the report because uniqueness is measured per declaring type.
    • SharedCode_OnTheAllowList_IsExemptFromUniqueness (:44-:53) and PrefixedCodes_AndAllowedSharedCodes_PassThePrefixRule (:79-:89): the shared-code exemption proven against both rules, not just the one it was written for.
    • UnprefixedCode_IsFlagged (:55-:64): asserts both the code and the owning type appear (:62-:63).
    • TooManyRequestsCode_IsReadIntoTheCatalog (:68-:77): Error.TooManyRequests is a code-first factory like its siblings, so its code (TooFast) and owning type (ThrottledErrors) must appear in the prefix report rather than escaping both gates unreported (:75-:76).
    • DynamicCode_IsReportedAsUnverifiable (:91-:100): asserts the literal string "UNVERIFIABLE" and the owning type name are both present (:98-:99).
    • DistinctCodeCount_CountsTheLiteralCatalog (:102-:110): the non-vacuity guard, asserted as a floor of 5 with the five literal codes enumerated in the because (:107-:109). A floor rather than an equality, because the count is over literals the scan can read and a future fixture addition should not be a test failure.
  • Why it's built this way - both rules read the same collected sites, so proving them in one class against one fixture catalog keeps the two verdicts comparable, and each fact makes exactly one claim about the shared message. See ADR-015.
  • Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of both rules is ErrorCatalogTestsBase, whose default prefix delegate closes over the repo's own module names.

ExecuteDeletingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.HardDeleteFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:26 · Level 5 · class

  • What it is - the second hard-delete offender: a static helper that erases rows with EF Core's server-side ExecuteDeleteAsync, a distinct erasing path from DbSet.Remove because it never materializes or tracks the rows it deletes.
  • Depends on - IQueryable<TEntity>.ExecuteDeleteAsync (HardDeleteFixtures.cs:1,:29) and its own FixtureEntity (:11).
  • Concept - cross-references the erasing-member concept introduced by DbSetRemovingFixture. This fixture proves the rule's IL scan catches a second, unrelated erasing API rather than one it happened to be written against: PurgeAsync calls query.ExecuteDeleteAsync(cancellationToken) (:28-:29), which compiles to a different call site than DbSet.Remove but must still be flagged by name.
  • Walkthrough - internal static class ExecuteDeletingFixture with one static method, PurgeAsync(IQueryable<FixtureEntity>, CancellationToken) (:28-:29), a one-line pass-through into ExecuteDeleteAsync.
  • Where it's used - named in HardDelete_ReportsEveryErasingMember's "PurgeAsync" assertion (SoftDeleteEnforcementFitnessTests.cs:39) and in AllowlistedType_SilencesOnlyThatType, where it is the offender still reported after DbSetRemovingFixture alone is allowlisted (:72-:73).

SoftDeletingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.HardDeleteFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:37 · Level 5 · class

  • What it is - the negative fixture for HardDeletesOnlyInAllowedTypes: it deletes the framework's way and separately removes from an ordinary in-memory List<T>, neither of which is a hard delete, so the rule must stay silent about both calls.
  • Depends on - FixtureEntity (HardDeleteFixtures.cs:11), whose soft-delete Delete() it calls (:39).
  • Concept introduced - List<T>.Remove is what makes a naive "ban the method name Remove" version of this rule useless. The class doc states the point directly: Remove on a List<T> must not be confused with DbSet<T>.Remove, which DbSetRemovingFixture exercises as the positive case the rule must flag (:33-:36).
  • Walkthrough - two static methods: Deactivate(FixtureEntity entity) calls entity.Delete(), the framework's soft-delete (:39); Forget(List<FixtureEntity> cache, FixtureEntity entity) calls cache.Remove(entity) on the in-memory list, not a DbSet (:41).
  • Where it's used - registered as part of the Infrastructure layer FixtureAssemblyMap points at for SoftDeleteEnforcementFitnessTests, where HardDeletesOnlyInAllowedTypes must not flag either call.

ChildlessFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CascadeFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:99 · Level 5 · class

  • What it is - an aggregate with a collection, but a collection of strings rather than child entities, so the cascade rule has nothing to cascade to even though the type never overrides Delete().
  • Depends on - AuditableAggregateRootEntity<TIdentifierType> (internal sealed class ChildlessFixture : AuditableAggregateRootEntity<int>, CascadeFixtures.cs:99).
  • Concept - cross-references the cascade-detection concept introduced by MissingOverrideFixture. Where that fixture is the true offender (children present, Delete() not overridden), this one proves the rule reads the collection's element type and not merely its presence: Tags is a List<string> (:101), so it is not a child-entity collection at all, and a naive "any collection plus no override" check would wrongly flag it.
  • Walkthrough - one private field, private readonly List<string> _tags = [] (:101), exposed as IReadOnlyCollection<string> Tags (:103). No Delete() override.
  • Where it's used - the subject of AggregateWithNoChildEntities_IsIgnored in CascadeSoftDeleteFitnessTests (CascadeSoftDeleteFitnessTests.cs:62-:71), which asserts the rule's failure message does not name it (:68-:70).

ChildlessFixture, ExemptedOffenderFixture, HelperCascadingFixture, LoopCascadingFixture, MissingOverrideFixture, SelfOnlyDeleteFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CascadeFixtures · (see per-type table) · Level 5 · class

Six aggregate roots in one file, every one an internal sealed class deriving AuditableAggregateRootEntity<TIdentifierType> over int, differing only in what (if anything) their Delete() override does with their child collection. They are the six-way truth table for the cascade-soft-delete rule.

  • Depends on - AuditableAggregateRootEntity<TIdentifierType>, CascadeChildFixture, and Result (CascadeFixtures.cs:1-:2).
  • Concept - cross-references the cascade concept introduced by CascadeChildFixture. What this set adds is the body half of the rule, and it is the sharper half. The rule reads the IL of the Delete() override and accepts exactly two shapes: a direct call to DeleteChildren (the helper on the aggregate base, MMCA.Common/Source/Core/MMCA.Common.Domain/Entities/AuditableAggregateRootEntity.cs:273), or a zero-argument Delete reached through the callvirt opcode, which is what a hand-rolled foreach (var line in _lines) line.Delete(); compiles to (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.CascadeSoftDelete.cs:172-:186). What it deliberately refuses to accept is base.Delete(), and the reason is a small piece of C# trivia doing real work: base.X() on a virtual member is the one thing C# emits as a non-virtual call, and nothing else in the language produces that pairing, so excluding it separates "delete myself" from "delete my children" without guessing (:178-:182). That single discrimination is what makes SelfOnlyDeleteFixture catchable at all.
  • Walkthrough - the rule reports two distinct reasons and names the offending collection in both, so the failure text tells a developer which member to fix: "no Delete() override, so its children ({names}) stay active" (:160) and "Delete() never deletes a child, so its children ({names}) stay active" (:165). Allowlist entries are type full names or namespace prefixes, matched ordinally exactly as the hard-delete rule matches (:79-:83).
Type File:Line What it models
HelperCascadingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:23 The framework-idiomatic cascade: Result.Combine(DeleteChildren<CascadeChildFixture, int>(_children), base.Delete()) (:29-:30). Must pass.
LoopCascadingFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:37 The hand-rolled cascade every aggregate used before the helper existed: a foreach calling each child's Delete() (:45-:48) then base.Delete(). Must also pass, which is what stops the rule from mandating one spelling.
MissingOverrideFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:58 Owns _orphans and never overrides Delete() at all (:60-:62). The first violation reason, and the report must name _orphans.
SelfOnlyDeleteFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:70 The subtle offender: it does override Delete(), and even touches the collection with _ignored.Clear() before calling base.Delete() (:76-:81). Detaching children from an in-memory list is not a soft delete, so every row stays active. Its doc names the point: this is the shape a naive "does it override Delete" check would pass, which is why the rule reads the body (:65-:69).
ExemptedOffenderFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:88 Structurally identical to MissingOverrideFixture, kept separate so one fact can allowlist it and prove an exemption silences exactly the type it names while the other offender still fails (:84-:87).
ChildlessFixture MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:99 The out-of-scope case: an aggregate with no Delete() override whose only collection is a List<string> (:101-:103). A list of strings is not a child-entity collection, so the rule must ignore the type entirely rather than report it.
  • Why they're built this way - three passing and three interesting failing cases, with the two offenders made structurally identical so the allowlist fact has an unambiguous control. Every fixture is internal and namespaced, which is what keeps them invisible to the framework rules that run over CommonArchitectureMap and to the other self-tests in this assembly (file doc, :10-:15). See ADR-005 and ADR-015.
  • Caveats / not-in-source - the rule's documented limits apply to these fixtures too (ArchitectureRules.CascadeSoftDelete.cs:84-:93): only direct calls in the override are read, so an aggregate that delegates the loop to a private helper is reported; the rule proves a cascade exists, not that it covers every collection; only fields declared on the aggregate are inspected; and grandchildren are the child's own cascade to own. No fixture here exercises those boundaries.
  • Where they're used - scanned by CascadeSoftDeleteFitnessTests through its FixtureAssemblyMap, and named by nameof in all six of its facts.

ExemptedOffenderFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CascadeFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:88 · Level 5 · class

  • What it is - the same shape as MissingOverrideFixture (children present, Delete() never overridden), kept as a separate type so one self-test can allowlist it by name and prove the exemption silences exactly that type and no other offender.
  • Depends on - AuditableAggregateRootEntity<TIdentifierType> (internal sealed class ExemptedOffenderFixture : AuditableAggregateRootEntity<int>, CascadeFixtures.cs:88) and CascadeChildFixture (the child collection, :90).
  • Concept - cross-references the allowlist-precision concept introduced at ChildlessFixture's sibling assertions. Being a genuine offender with the identical shape as MissingOverrideFixture is the point: if the rule's allowlist matched by shape rather than by fully-qualified type name, allowlisting this type would silence both, and AllowlistedType_SilencesOnlyThatType would have nothing to distinguish.
  • Walkthrough - private readonly List<CascadeChildFixture> _exempt = [] (:90), exposed as IReadOnlyCollection<CascadeChildFixture> Exempt (:92). No Delete() override.
  • Where it's used - the subject of AllowlistedType_SilencesOnlyThatType in CascadeSoftDeleteFitnessTests (CascadeSoftDeleteFitnessTests.cs:73-:85): passing {FixtureNamespace}.{nameof(ExemptedOffenderFixture)} to the rule removes it from the failure message (:76,:83) while MissingOverrideFixture is still reported (:84).

HelperCascadingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CascadeFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:23 · Level 5 · class

  • What it is - the positive fixture for the cascade-soft-delete rule AggregatesCascadeSoftDeleteToChildren: an aggregate whose Delete() override cascades through the framework's own DeleteChildren<TChild, TId>() helper, so the rule must stay silent about it.
  • Depends on - AuditableAggregateRootEntity<TIdentifierType> (internal sealed class HelperCascadingFixture : AuditableAggregateRootEntity<int>, CascadeFixtures.cs:23) and CascadeChildFixture (:25).
  • Concept introduced - cross-references the fixture concept in FixtureAssemblyMap: the rule reads the IL of Delete(), so proving it accepts a shape means compiling a real override that calls the shared helper, not asserting against a description of one.
  • Walkthrough - a private List<CascadeChildFixture> _children (:25) exposed read-only through Children (:27); Delete() combines DeleteChildren<CascadeChildFixture, int>(_children) with base.Delete() (:29-:30).
  • Where it's used - registered by FixtureAssemblyMap as part of the Domain layer for CascadeSoftDeleteFitnessTests, where AggregatesCascadeSoftDeleteToChildren must not flag it.

LoopCascadingFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CascadeFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:37 · Level 5 · class

  • What it is - a second positive fixture for AggregatesCascadeSoftDeleteToChildren, cascading through a hand-rolled foreach loop instead of the framework helper: the shape every aggregate used before DeleteChildren<TChild, TId>() existed, which the rule must still accept.
  • Depends on - AuditableAggregateRootEntity<TIdentifierType> (internal sealed class LoopCascadingFixture : AuditableAggregateRootEntity<int>, CascadeFixtures.cs:37) and CascadeChildFixture (:39).
  • Concept introduced - cross-references HelperCascadingFixture: together the two prove the rule reads the effect of Delete() (every child's Delete() gets called) rather than pattern-matching one specific call shape.
  • Walkthrough - a private List<CascadeChildFixture> _items (:39) exposed through Items (:41); Delete() iterates _items, calling item.Delete() on each and discarding the result before returning base.Delete() (:43-:51).
  • Where it's used - registered by FixtureAssemblyMap alongside HelperCascadingFixture for CascadeSoftDeleteFitnessTests.

MissingOverrideFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CascadeFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:58 · Level 5 · class

  • What it is - the negative fixture for AggregatesCascadeSoftDeleteToChildren: an aggregate that owns a child collection but never overrides Delete() at all, so deleting the root would leave every child row active. The rule must report it by name.
  • Depends on - AuditableAggregateRootEntity<TIdentifierType> (internal sealed class MissingOverrideFixture : AuditableAggregateRootEntity<int>, CascadeFixtures.cs:58) and CascadeChildFixture (:60).
  • Concept introduced - cross-references FixtureAssemblyMap: the absence of an override is itself the offending shape, so this fixture has to compile with no Delete() member at all rather than an empty one.
  • Walkthrough - a private List<CascadeChildFixture> _orphans (:60) exposed through Orphans (:62); no Delete() override anywhere in the type.
  • Where it's used - registered by FixtureAssemblyMap for CascadeSoftDeleteFitnessTests, where the rule's failing fact names it.

SelfOnlyDeleteFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.CascadeFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:70 · Level 5 · class

  • What it is - the sharpest negative fixture for AggregatesCascadeSoftDeleteToChildren: it overrides Delete(), even touches its child collection, and still leaves every child row active, because clearing an in-memory list is not a soft-delete.
  • Depends on - AuditableAggregateRootEntity<TIdentifierType> (internal sealed class SelfOnlyDeleteFixture : AuditableAggregateRootEntity<int>, CascadeFixtures.cs:70) and CascadeChildFixture (:72).
  • Concept introduced - reading the override's body, not just its existence, is what the rule buys over a naive check. The class doc states this directly: "a naive 'does it override Delete' check would pass" this shape, which is why the rule reads the body (:66-:68).
  • Walkthrough - a private List<CascadeChildFixture> _ignored (:72) exposed through Ignored (:74); Delete() calls _ignored.Clear() then base.Delete() (:76-:81), never calling Delete() on any child.
  • Where it's used - registered by FixtureAssemblyMap for CascadeSoftDeleteFitnessTests, where the rule's failing fact names it alongside MissingOverrideFixture.

CommandValidatorCoverageFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:16 · Level 5 · class

  • What it is - the meta-test for the command-validation coverage rule: eight facts that pin each coverage shape, both escape hatches, and the inventory count that keeps the whole gate from passing vacuously.
  • Depends on - ArchitectureRules (CommandsHaveValidators and HandledCommandCount, CommandValidatorCoverageFitnessTests.cs:25,:103), its nested FixtureModuleMap, the thirteen types in MMCA.Common.Architecture.Tests.CommandValidatorFixtures (:1), and Xunit.Sdk.XunitException (:3), which every fact asserts on by exact type rather than as a bare Exception.
  • Concept introduced - proving a rule and proving it is not vacuous, in the same class. Seven of the eight facts read the failure message of a rule that always throws, the same technique CancellationTokenFitnessTests uses. The eighth is different in kind: CommandInventory_CountsTheDataCarryingCommands calls HandledCommandCount and pins it at exactly 5 (:100-:108). That number exists because a map that resolved to no module Application assemblies would make every other fact in the class pass while reading nothing, and the same counter is what CommandValidatorCoverageTestsBase exposes to consumers as their own non-vacuity guard (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:73-:77). Pinning an exact count rather than a floor is affordable here because the fixture set is closed; a consumer uses a floor. [Rubric §14 - Testability] and [Rubric §24 - Forms/Validation/UX Safety] apply.
  • Walkthrough - one private const string FixtureNamespace (:18) and one readonly FixtureModuleMap (:20) are shared by every fact.
    • CommandWithNoValidation_IsFlagged (:22-:31) and CommandWithItsOwnValidator_IsCovered (:33-:42): the offender and the direct-coverage case, asserted by Contain and NotContain on the one message.
    • CommandCoveredThroughTheRequestBridge_IsCovered (:44-:53) and BridgeWithNoRequestValidator_IsNotCoverage (:55-:64): the two halves of the bridge, with the second because stating exactly why a registered CommandRequestValidator is not coverage on its own (:63).
    • PayloadFreeCommand_IsSkipped (:66-:75): the marker command, absent from the report because it has no settable property to validate.
    • AllowlistedCommand_IsExempt (:77-:89): passes one fully-qualified command name and asserts that command disappears while the other offender is still reported (:87-:88). That second assertion is what distinguishes a working exemption from a rule that stopped firing.
    • AllowlistedNamespace_SilencesTheRule (:91-:98): passes the fixture namespace and asserts NotThrow, the only fact in the class where the rule is quiet.
    • CommandInventory_CountsTheDataCarryingCommands (:100-:108): the count, with the because enumerating the five commands by name.
  • Why it's built this way - the escape hatch has two granularities (a type and a namespace) and both need proof, because a repo records "this command genuinely has nothing to check" at whichever level fits. Asserting on XunitException specifically rather than Exception means an unexpected failure inside the rule (a null reference, a bad cast) fails the fact instead of being mistaken for the rule firing. See ADR-015.
  • Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the same rule is CommandValidatorCoverageTestsBase, which MMCA.Common itself does not subclass because it declares no modules.

CycleTestMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleFitnessTests.cs:52 · Level 5 · class

  • What it is - a one-layer map for the namespace-cycle fitness test, rooted at the CycleFixtures namespace so the rule sees only the three fixture namespaces and none of the real test code.
  • Depends on - ArchitectureMapBase (private sealed class CycleTestMap : ArchitectureMapBase, NamespaceCycleFitnessTests.cs:52), LayerRef, and the Layer enum.
  • Concept introduced - constructing a LayerRef directly to override the derived root namespace. Every other map in this chapter uses the Framework(layer, assembly) helper, which computes the layer's root namespace as MMCA.Common.{segment} (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:94-:95). That is wrong here, because the cycle rule groups types by the namespace segment directly beneath LayerRef.RootNamespace (ArchitectureRules.Cycles.cs:12-:16) and skips types outside it entirely: with a derived root the fixtures would not be grouped into Left / Right / Acyclic nodes at all. So this map builds the LayerRef by hand with FixtureRoot as the fourth argument (:58-:62), which is the one place in the suite where the record's positional constructor (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:31) is used directly. [Rubric §14 - Testability] is the theme: scoping the map is what keeps the assertion about the fixtures instead of about whatever else the test assembly happens to contain.
  • Walkthrough - RepoToken => "MMCA.Common" (:54) and a DefineLayers() returning a single new LayerRef(string.Empty, Layer.Application, typeof(NamespaceCycleFitnessTests).Assembly, FixtureRoot) (:56-:63), where FixtureRoot is the class constant "MMCA.Common.Architecture.Tests.CycleFixtures" (:13). The empty first argument is the module name, which is what marks the layer as framework-owned rather than module-owned.
  • Where it's used - constructed once per fact in NamespaceCycleFitnessTests (:18,:32,:42).

DataSubjectSample

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiErasureContractFitnessTests.cs:79 · Level 5 · class

  • What it is - a representative "data subject" fixture: a single object that carries [Pii] members alongside non-PII fields and implements an in-place erasure path, so the framework's privacy machinery can be exercised end to end against a realistic shape.
  • Depends on - IAnonymizable (it is private sealed class DataSubjectSample : IAnonymizable, PiiErasureContractFitnessTests.cs:79), PiiAttribute (marks Email and FullName, :91,:94), and Result (returned from Anonymize, :99).
  • Concept introduced - closing a vacuous fitness function with a stand-in data subject. The framework's [Pii] => IAnonymizable scan (PiiConventionTests) has nothing to assert because MMCA.Common ships no PII-bearing domain entity of its own. Rather than invent a fake aggregate in the Domain layer, this fixture models the exact contract a consumer PII aggregate (for example MMCA.ADC's User) must satisfy, and lets PiiErasureContractFitnessTests prove the three §30 mechanisms compose. [Rubric §30 - Compliance/Privacy/Data Governance] assesses whether erasure, redaction, and masking actually work together; this sample is the vehicle that keeps the framework's proof of that non-vacuous.
  • Walkthrough - public constants publish the expected before and after values so the tests can assert without magic literals: SampleId = 7, PublicCity = "Atlanta", OriginalEmail, OriginalFullName (:81-:84), plus private anonymized placeholders (:86-:87). Id (:89) and City (:97) are non-PII init pass-through fields; Email and FullName are [Pii] with private setters (:91-:95), which is what lets Anonymize rewrite them in place while no caller outside the type can. Anonymize() (:99-:105) overwrites both PII fields with the fixed placeholders and returns Result.Success(); because it re-applies constants, calling it twice is idempotent by construction (:101).
  • Why it's built this way - ADR-005 (soft-delete versus erasure) requires that a right-to-erasure path be idempotent and leave no clear text behind. The fixture encodes that contract in the smallest object that can be pushed through PiiRedactor and Anonymize together.
  • Where it's used - the sole fixture for PiiErasureContractFitnessTests.

FixtureAssemblyMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · (see per-type table) · Level 5 · class

The name is used seven times in this assembly, once per IL-reading rule under proof, and all seven are the same one-entry map: this test assembly registered as a single framework layer, so the rule's whole-layer scan lands on the fixtures. They are nested in different outer classes, so their full names differ and none is visible outside its own file.

  • Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
  • Concept introduced - the layer a fixture map declares is not decoration, it is the scope selector. Each of these seven rules starts from a different part of the map, so each map has to name a layer the rule reads or the rule reads nothing and passes vacuously. The cascade and hard-delete rules walk every assembly the map registers, through ScannableAssemblyLocations (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:88-:92), so their layer choice is nominal; the domain-throw rule reads map.OfLayer(Layer.Domain) and nothing else (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:112), so registering Domain is mandatory there. The clock-read rule reads the Domain and Application layers together (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.ClockReads.cs:68-:69), so either would do and its map picks Domain; the read-repository rule reads Application only (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.QueryHandlerRepositories.cs:87), so that entry is mandatory too. The substitutability rule scans every registered assembly when the map names no modules, which this module-less map does, so its Domain entry is nominal (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Substitutability.cs:95-:102). All seven use Framework(...) rather than Module(...), which is the opposite choice from FixtureModuleMap and correct for the same reason: these rules do not filter on module ownership, so the simplest entry is the honest one. Reading assemblies off disk by Assembly.Location also explains a shared limitation: a rule in this family cannot inspect a dynamic or single-file assembly, and the helper silently skips any location that does not exist on disk.
  • Walkthrough - each declares RepoToken => "MMCA.Common" and a single-entry DefineLayers() naming one layer and one anchor type from the fixture namespace it serves. Anchoring by typeof(SomeFixture).Assembly rather than by Assembly.GetExecutingAssembly() is deliberate: it ties the map to a type a compiler would fail on if the fixtures moved.
Type File:Line Layer and anchor The rule it scopes
FixtureAssemblyMap (in CascadeSoftDeleteFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:97 Framework(Layer.Domain, typeof(CascadeChildFixture).Assembly) (:103) AggregatesCascadeSoftDeleteToChildren.
FixtureAssemblyMap (in SoftDeleteEnforcementFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementFitnessTests.cs:77 Framework(Layer.Infrastructure, typeof(FixtureEntity).Assembly) (:83) HardDeletesOnlyInAllowedTypes.
FixtureAssemblyMap (in DomainEventHandlerSaveFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:110 Framework(Layer.Application, typeof(FixtureDomainEvent).Assembly) (:116) DomainEventHandlersDoNotSave.
FixtureAssemblyMap (in DomainThrowFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:150 Framework(Layer.Domain, typeof(NonThrowingFixture).Assembly) (:156) DomainThrowsOnlyArgumentGuards, which reads the Domain layer only, so this entry is load-bearing.
FixtureAssemblyMap (in ClockReadTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:98 Framework(Layer.Domain, typeof(InjectedClockFixture).Assembly) (:104) DomainAndApplicationDoNotReadTheClock.
FixtureAssemblyMap (in QueryHandlerReadRepositoryTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/Repositories/QueryHandlerReadRepositoryTests.cs:58 Framework(Layer.Application, typeof(ReadRepositoryQueryHandlerFixture).Assembly) (:64) QueryHandlersUseReadRepositories, which reads the Application layer only, so this entry is load-bearing.
FixtureAssemblyMap (in SubstitutabilityRuleTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/SubstitutabilityRuleTests.cs:73 Framework(Layer.Domain, typeof(FixtureRefusingShape).Assembly) (:79) SubstitutableMembersDoNotRefuseTheContract, a module-less map so the rule scans the fixtures.
  • Why they're built this way - seven nested six-line classes rather than one shared fixture map, so each test file is readable end to end and no test can change another's scope by editing a shared file. It is the same trade-off FixtureModuleMap makes.
  • Caveats / not-in-source - because each map registers the whole test assembly, a rule in this family sees every type in it, not just the fixtures in its own namespace. Three of the original six are unaffected because their fixtures are the only matching shapes present. The domain-throw rule is not, which is why it carries an explicit allowlist of the assembly's non-fixture throws (DomainThrowFitnessTests.cs:29-:34), including the substitutability fixtures that deliberately throw NotSupportedException. The clock-read and read-repository tests take the other way out: they assert on what the failure report names rather than on the report being otherwise empty, and say so in their class docs (ClockReadTests.cs:10-:12, QueryHandlerReadRepositoryTests.cs:10-:12).
  • Where they're used - constructed once as a readonly field per test class (CascadeSoftDeleteFitnessTests.cs:21, SoftDeleteEnforcementFitnessTests.cs:19, DomainEventHandlerSaveFitnessTests.cs:21, DomainThrowFitnessTests.cs:36, ClockReadTests.cs:18, QueryHandlerReadRepositoryTests.cs:18, SubstitutabilityRuleTests.cs:20). The first four and the seventh pass it to every rule call; the last two pass it through a single Report helper that turns the rule's failure into a message string (ClockReadTests.cs:88, QueryHandlerReadRepositoryTests.cs:48), and keep the real CommonArchitectureMap as their base class's Map.

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/NavigationContractTests.cs:19 · Level 1 · class

  • What it is - a documentation-drift gate for navigation (rubric §25): it asserts that the "Routes shipped by the framework" table in NavigationFlow.md stays in lockstep with the routable pages the MMCA.Common.UI assembly actually ships, that each route's documented auth posture matches the [Authorize] reality on the page, and that every route parameter carries a type constraint.
  • Depends on - UISharedAssemblyReference (the reflection anchor for the shared UI assembly, NavigationContractTests.cs:105), plus externals: ASP.NET Core RouteAttribute and AuthorizeAttribute (the route and guard metadata reflected over, :107,:113), two source-generated regexes via [GeneratedRegex] partial properties (:139,:142), the embedded NavigationFlow.md manifest resource (:125; wired by the csproj at MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/MMCA.Common.Architecture.Tests.csproj:11-:13), xUnit [Fact], and AwesomeAssertions.
  • Concept introduced - documentation as an executable contract. This is the first place a test asserts a hand-authored markdown doc against live code reality rather than the reverse. NavigationFlow.md lives next to the framework code (it is an embedded resource of this test project, per MMCA.Common/CLAUDE.md) precisely so this gate can parse it; a page added, removed, or re-routed without the doc moving in the same change fails the build, and so does an auth-posture lie in either direction. [Rubric §25 - Navigation & IA] assesses whether routing and information architecture are deliberate and documented; this test turns the route table into a build-enforced invariant instead of review discipline, and its fourth fact holds the typed-deep-link half of the same category. [Rubric §11 - Security] and [Rubric §26 - Front-End Security] also apply: a route the doc calls Authenticated must carry [Authorize], and a route it calls Anonymous must not, so a mis-documented guard cannot pass silently. [Rubric §34 - Architecture Governance & Documentation] is the umbrella: the documentation is proven current by CI.
  • Walkthrough - two constants pin the contract: MinimumRoutes = 8 (:21) and DocResource = "NavigationFlow.md" (:22). Four [Fact]s enforce it.
    • RoutablePages_AreDiscovered_GateIsNotVacuous (:24-:28) asserts reflection finds at least eight routed pages, so a broken anchor cannot let the whole gate pass having scanned nothing.
    • EveryRoutablePage_IsDocumented_AndEveryDocumentedRoute_Exists (:30-:43) computes the set difference both ways: undocumented routes (real but missing from the doc, :36) and phantom routes (documented but no longer real, :37) must both be empty.
    • EveryDocumentedAuthPosture_MatchesTheRouteAttributeReality (:45-:79) walks each documented route, skips ones the set-equality fact already reports as phantoms (:54-:57), classifies the auth cell as Authenticated..., Anonymous, or Any (:59-:61), and flags three violation kinds: an unrecognized posture string (:63-:66), a doc that promises authentication where the page carries no [Authorize] (:67-:69), and a doc that promises an open route where the page is actually guarded (:71-:73).
    • RouteTemplateParameters_AllCarryTypeConstraints (:81-:100) matches every {...} segment of every discovered template with RouteParameterRegex and fails any whose body carries no : (:90-:94). The violation text states the ruling: an unconstrained parameter accepts arbitrary strings, so the page rather than the router becomes the validation boundary, where a constrained one renders NotFound before the component is reached.
    • Two private helpers supply the two sides. DiscoverRoutedPages (:102-:121) reflects over the shared-UI assembly, collecting each RouteAttribute.Template and whether the type carries [Authorize] (inherited attributes included, :113); DiscoverDocumentedRoutes (:123-:137) reads the embedded doc, throwing a clear InvalidOperationException when the resource is missing (:125-:126), and extracts route rows via the source-generated RouteRowRegex (:139-:140, a 2000ms-timeout [GeneratedRegex] partial property).
  • Why it's built this way - NavigationFlow.md is deliberately kept in MMCA.Common next to the routed pages (not in the Website docs library) so this gate can embed and parse it; making route and auth-posture documentation a compiled assertion is what keeps a public navigation contract from rotting. ADR-015 establishes the fitness-function approach this class applies to documentation.
  • Where it's used - an independent class in the MMCA.Common.Architecture.Tests suite; it runs in CI's build-and-test job (fast, no database) and has no Store or ADC counterpart because it guards the framework's own shared-UI route table. It is also one of the two entries DomainThrowFitnessTests allowlists, because its missing-resource guard is a real InvalidOperationException throw inside this assembly (DomainThrowFitnessTests.cs:31).

SortableColumnFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/SortableColumnFitnessTests.cs:15 · Level 5 · class

  • What it is - the meta-test for the MudDataGrid sortable-column rule: four facts proving it reports a sortable TemplateColumn with its file and line, catches the same defect written as a Razor expression, stays silent on every near-miss, and fails loudly on a missing scan root.
  • Depends on - ArchitectureRules (SortableGridColumnsUsePropertyColumn, SortableColumnFitnessTests.cs:31), ArchitectureMapBase (FindRepoRoot, :18), four real .razor fixture files under RazorFixtures/, and Xunit.Sdk.XunitException (:2).
  • Concept introduced - scanning markup text, and the near-misses that decide whether a text scan is usable. MudBlazor's server-side sort reads the bound property off a PropertyColumn; a TemplateColumn has no bound property, so marking one Sortable="true" produces a clickable header that sorts nothing, or sorts only the page's local slice. The failure is silent: the grid renders, the arrow toggles, and the order is wrong (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.Markup.cs:10-:15). The defect lives in markup that compiles perfectly, so this is the one rule in the suite that reads .razor text rather than IL or reflection (:16-:19). That choice imports every classic text-scanning hazard, and the rule addresses each one: the tag is read to its closing > through quoted attribute values, so attribute order, wrapped lines and a generic T="Foo<Bar>" argument all work (:22-:29); the Sortable value is matched quoted or unquoted, with or without an @ expression prefix and parentheses, while a value bound to a field is left alone because its value is not knowable from markup; and @* *@ comments are blanked with spaces before the scan, newlines preserved, so a commented example neither fails the gate nor shifts the reported line numbers (:31-:34,:82-:108). [Rubric §18 - UI Architecture] and [Rubric §23 - Front-End Performance] both apply, since server-side sorting is what keeps a grid from paging in memory; [Rubric §14 - Testability] is the technique.
  • Walkthrough - FixtureRoot is built from ArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx") plus the four path segments down to RazorFixtures (:17-:22), with Offending and Clean as expression-bodied properties over it (:24,:26).
    • SortableTemplateColumn_IsFlaggedWithFileAndLine (:28-:37): asserts the message contains SortableTemplateColumnGrid.razor:7. That exact number is the point: in the fixture the element opens on line 7 and the Sortable="true" attribute sits on line 8 (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/RazorFixtures/Offending/SortableTemplateColumnGrid.razor:7-:9), and the rule must report where the element starts, not where the attribute happens to land.
    • ExpressionBoundLiteralTrue_IsAlsoFlagged (:39-:48): pins ExpressionSortableTemplateColumnGrid.razor:5, a one-line element with Sortable="@(true)" written first (RazorFixtures/Offending/ExpressionSortableTemplateColumnGrid.razor:5).
    • ConformingMarkup_Passes (:50-:57): the discrimination fact, and the one carrying the most fixture. ConformingGrid.razor packs seven near-misses into one file: a sortable PropertyColumn (:6), a TemplateColumn with no Sortable at all (:9), one explicitly Sortable="false" (:16), one bound to a field (:19), a longer attribute name IsSortable="true" (:22), a generic argument carrying an angle bracket inside a quoted value with a real sortable PropertyColumn on the line below it (:26-:27), and a longer element name TemplateColumnGroup (:30). CommentedOutGrid.razor holds the eighth: the whole defect inside a @* *@ block (:3-:9).
    • MissingRoot_FailsRatherThanScanningNothing (:59-:70): points the rule at a folder that does not exist and asserts the message contains "markup root not found" (:67), which is this rule's version of the non-vacuity floor. A path typo must fail the gate rather than quietly making it pass.
  • Why it's built this way - the fixtures are real .razor files rather than inline strings because the rule takes directory roots and enumerates files itself, excluding bin and obj (ArchitectureRules.Markup.cs:71-:76); testing it through anything but the filesystem would be testing a different method. The generic-argument case (ConformingGrid.razor:24-:27) is the sharpest of the seven: its own comment records the failure being prevented, a tag scan that runs past the element and swallows the sortable PropertyColumn below it.
  • Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the rule is SortableColumnConventionTestsBase, which supplies the repo's real markup roots.
  • Caveats / not-in-source - a Sortable value bound to a field or property is deliberately not judged, so a TemplateColumn whose binding evaluates to true at run time passes this gate. ConformingGrid.razor:19 is that shape, listed as conforming.

NamespaceCycleFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleFitnessTests.cs:11 · Level 5 · class

  • What it is - the meta-test for the namespace-acyclicity rule: three facts that prove it reports the two-namespace fixture cycle, leaves the acyclic fixture namespace out of the report, accepts the cycle when the allowance covers every namespace on it, and still fails when the allowance covers only part of it.
  • Depends on - ArchitectureRules (ArchitectureRules.NamespacesHaveNoDependencyCycles, NamespaceCycleFitnessTests.cs:18), its nested CycleTestMap, the three fixture namespaces under CycleFixtures (LeftModelBase, LeftService, RightModel, AcyclicConsumer), and externals xUnit plus AwesomeAssertions.
  • Concept introduced - why a cycle allowance must cover the whole component, not just a name on the path. The rule takes an optional allowedCycleNamespaces list and skips a cycle only when every namespace on it is listed (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:39-:44,:61). The reason is subtle and worth internalizing: if a partial allowance sufficed, a fourth namespace joining an already-accepted tangle would slip in silently, because the reported shortest path might not even mention it (:57-:60). The third fact in this class is the executable proof of exactly that policy. [Rubric §15 - Best Practices & Code Quality] and [Rubric §7 - Microservices Readiness] are the properties defended; [Rubric §34 - Architecture Governance & Documentation] covers the allowance mechanism, which forces an accepted tangle to be written down in code rather than tolerated in review.
  • Walkthrough - a class constant FixtureRoot = "MMCA.Common.Architecture.Tests.CycleFixtures" (:13) is shared by the map and every assertion, so the namespace strings cannot drift apart.
    • Rule_FlagsTwoNamespaceCycle_ButNotAcyclicNamespaces (:15-:26): asserts the message contains both ...CycleFixtures.Left and ...CycleFixtures.Right (:21-:22) and not ...CycleFixtures.Acyclic (:23-:25), each with a because naming the edge that put it there.
    • Rule_Passes_WhenTheWholeCycleIsAllowed (:28-:36): passes both namespaces as the allowance and asserts NotThrow() (:31-:35). This is the only fact in the class where the rule does not throw.
    • Rule_StillFails_WhenOnlyPartOfTheCycleIsAllowed (:38-:46): passes only Left and asserts it still throws, with the reason stated inline as "a partial allowance must never hide a cycle" (:45).
  • Why it's built this way - the allowance is the rule's only escape hatch, and an escape hatch that can be misused silently is worse than none. Proving both its accepting and its refusing behavior is what lets the framework's own NamespaceCycleTests carry a real three-namespace allowance without weakening the gate.
  • Where it's used - an independent class in the Common architecture suite.

ProtoContractFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ProtoContractFitnessTests.cs:14 · Level 5 · class

  • What it is - the meta-test for the frozen-.proto contract gate: seven facts that prove a pinned proto matches its committed snapshot, that a renumbered field and an added rpc are both reported, that a missing file fails loudly, that the parser ignores the lines that are not part of the wire contract, that block comments in any position do not hide declarations, and that members behind an rpc body or inside one-line message and enum bodies are still pinned.
  • Depends on - ArchitectureRules (ProtoContractsMatchFrozenList, ProtoContractFitnessTests.cs:47, and BuildProtoContract, :93), ArchitectureMapBase (FindRepoRoot, :92), three fixture .proto files under TestData/, and externals xUnit plus AwesomeAssertions.
  • Concept introduced - a wire contract as a committed, sorted string snapshot. A .proto file is a published contract: a renumbered field silently breaks every deployed peer, because protobuf keys on the number and not the name. The rule renders each file into a canonical, ordered list of lines (one per rpc with its streaming flag, one per message field with its label, type and NUMBER, one per enum value) and compares that list to a frozen array checked into the test (:28-:42). Regenerating the snapshot is a deliberate act: you print BuildProtoContract and paste the result, which is what makes a contract change a reviewable diff rather than a silent one. [Rubric §9 - API & Contract Design] assesses whether published contracts are versioned and change-controlled; this is the gRPC half of that discipline, and it is the same technique the integration-event contract snapshot uses (IntegrationEventContractTestsBaseTests proves that side). [Rubric §7 - Microservices Readiness] applies because these are exactly the contracts that survive extraction (ADR-007).
  • Walkthrough - three constants locate the fixtures relative to the repo root: SolutionFileName = "MMCA.Common.slnx" (:16), the TestData directory (:18), and the pinned and drifted file arrays (:20,:22). The 12-line FrozenContract (:28-:42) is the snapshot, sorted so the comparison is order-independent for the author and deterministic for the diff.
    • Rule_PassesWhenTheProtoMatchesItsFrozenSnapshot (:44-:50): the positive case.
    • Rule_FlagsARenumberedField (:52-:64): asserts both sides of the change are reported, the new product_id = 7 line as unexpected (:58-:60) and the frozen product_id = 1 line as missing (:61-:63), which is what makes a renumbering legible in the failure output.
    • Rule_FlagsAnAddedRpc (:66-:74): asserts the added DeleteProduct rpc is named, because an addition peers have not been told about is still a contract change.
    • Rule_ReportsAMissingProtoFileExplicitly (:76-:87): a path typo must produce <missing proto file> does-not-exist.proto rather than silently pinning an empty contract, the classic way a snapshot gate rots into a no-op.
    • BuildProtoContract_IgnoresSyntaxImportAndOptionLines (:89-:98): resolves the repo root, builds the contract from the pinned file, and asserts it equals FrozenContract exactly and contains neither the csharp_namespace option nor the timestamp.proto import (:95-:97). Those lines are code-generation details, not wire contract, so churning them must not fail anyone's build.
    • BuildProtoContract_SurvivesBlockCommentsInEveryPosition (:100-:110): builds the contract from fitness-catalog-commented.proto (:104-:105) and asserts it equals the same FrozenContract (:107-:109). That fixture carries a block comment closed on its own line, a trailing one after a field, a /* inside a line comment, and a block spanning two lines (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/TestData/fitness-catalog-commented.proto:17, :19, :20, :24-:25). A comment stripper that miscounts any of them swallows the declarations after it, which would shrink the contract and surface as a spurious "missing" diff, so comments are proven inert the same way the option and import lines are.
    • Parser_PinsMembersBehindAnRpcBodyAndInsideOneLineBodies (:112-:128): builds the contract from fitness-shapes.proto (:117-:118) and asserts exactly six lines: two Kind enum values, Reply.kind and Req.id, and both ShapeService rpcs (:121-:127). The closing brace of an rpc option body must not pop the service (otherwise Second would vanish), and a one-line message or enum body must still pin its members (comment at :112-:113).
  • Why it's built this way - MMCA.Common ships no protos of its own, so this is the framework's only exercise of a consumer-facing rule (class doc, :9-:12); consumers subclass ProtoContractTestsBase. The pinned and drifted fixture files differ by exactly the two breaking changes under test (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/TestData/fitness-catalog.proto versus fitness-catalog-drifted.proto), and the pinned one deliberately includes a nested message, a nested enum, a repeated field, an optional field, and a streaming rpc so the parser is exercised on every shape it claims to handle. The commented file renders to the identical snapshot, so the one frozen array serves all three.
  • Where it's used - an independent class in the Common architecture suite. None of the three fixture protos is compiled by any project.

ReadRepositoryFixtureAggregate

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ReadRepositoryFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ReadRepositoryFixtures/ReadRepositoryFixtures.cs:10 · Level 5 · class

  • What it is - the compiled-in aggregate QueryHandlerReadRepositoryTests runs its handler fixtures against: an entity with no members of its own, present only so IUnitOfWork.GetRepository<T, int>() and GetReadRepository<T, int>() have a real type parameter to close over.
  • Depends on - AuditableAggregateRootEntity<TIdentifierType> (internal sealed class ReadRepositoryFixtureAggregate : AuditableAggregateRootEntity<int>, ReadRepositoryFixtures.cs:10).
  • Concept introduced - the rule under test reads which repository call a handler's IL makes, not what the repository returns. None of the three handler fixtures ever executes; the file's own header states it (:8-:9). WriteRepositoryQueryHandlerFixture calls unitOfWork.GetRepository<ReadRepositoryFixtureAggregate, int>() after an await Task.Yield() (:22-:23), which is the shape the read-repository rule must flag in a query handler even once the method has become a compiler-generated state machine; the sibling read-repository fixture calls GetReadRepository<ReadRepositoryFixtureAggregate, int>() instead (:34), the shape the rule must accept.
  • Walkthrough - a single-line sealed class with no members: internal sealed class ReadRepositoryFixtureAggregate : AuditableAggregateRootEntity<int>; (:10).
  • Where it's used - the entity type parameter for every repository call in WriteRepositoryQueryHandlerFixture, ReadRepositoryQueryHandlerFixture and WriteRepositoryCommandHandlerFixture (:23,:34,:45), all in the same file; not referenced outside it.

SliceCohesionFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:17 · Level 5 · class

  • What it is - the meta-test for the vertical-slice-cohesion rule ArchitectureRules.HandlersAreCoLocatedWithTheirContracts: four facts that prove an abstract handler stranded away from its contract's namespace is flagged, the same shape sitting beside its contract is not, a handler over a generic type-parameter contract is skipped outright, and a handler serving two stranded contracts is reported for both.
  • Depends on - ArchitectureRules.HandlersAreCoLocatedWithTheirContracts (SliceCohesionFitnessTests.cs:24,:35,:46,:59), its nested FixtureApplicationMap held in the _map field (:19), and the MMCA.Common.Architecture.Tests.SliceFixtures fixture handlers (StrandedFixtureHandlerBase, GetFixturePreferencesHandlerBase, GetFixtureEntityHandlerBase<TQuery>, FixtureTwoContractHandlerBase) and their contracts GetFixturePreferencesQuery, GetFixtureFarQuery and GetFixtureSecondFarQuery, plus externals xUnit and AwesomeAssertions.
  • Concept introduced - an abstract base is scanned on the same terms as a concrete handler, but only when its contract is a concrete same-assembly type, and every contract a handler serves is judged on its own. The rule this class exercises checks the command/query and handler for a shared namespace and exempts a contract that is a generic type parameter, since ICommandHandler<TCommand, ...> and IQueryHandler<TQuery, ...> bases are the framework's own generic handlers and cannot be co-located with every contract they might someday serve (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Slices.cs:27-:33). Because registration wires a class for every closed handler interface it implements, the rule loops over every contract of each handler and reports each stranded one (ArchitectureRules.Slices.cs:35-:37, the inner loop at :46-:57). This class proves all three properties: an abstract base whose contract IS a concrete same-assembly type is checked like any handler (StrandedFixtureHandlerBase), one whose contract is a type parameter (GetFixtureEntityHandlerBase<TQuery>) is exempt regardless of where it lives, and a class implementing two handler interfaces (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/FixtureTwoContractHandlerBase.cs:12-:13) is flagged once per stranded contract, not only for whichever interface reflection lists first. [Rubric §5 - Vertical Slice Architecture] is the property; [Rubric §14 - Testability] is the technique, an IL/namespace scan that only compiling the shapes side by side can prove.
  • Walkthrough - all four facts build the rule call as an act delegate over the same _map and read the thrown message, so none of them needs a passing map of its own.
    • AnAbstractHandlerStrandedFromItsContract_IsFlagged (:21-:30): asserts the message contains nameof(StrandedFixtureHandlerBase), with the reason stated inline, an abstract base is the shape consumers derive from, so a base stranded away from its contract strands every handler built on it.
    • AnAbstractHandlerBesideItsContract_IsNotFlagged (:32-:41): asserts the message does NOT contain nameof(GetFixturePreferencesHandlerBase), because the base and the query it serves share one namespace, exactly what the rule asks for.
    • AnAbstractHandlerOverAGenericContract_IsSkipped (:43-:52): asserts the message does NOT contain nameof(GetFixtureEntityHandlerBase<>), since a contract that is a type parameter is not a type any folder can hold, so the framework's own generic handler bases stay exempt.
    • AHandlerServingTwoStrandedContracts_IsFlaggedForBoth (:54-:64, tagged L242 in its comment): captures the thrown message once and asserts it contains both nameof(GetFixtureFarQuery) and nameof(GetFixtureSecondFarQuery) (:62-:63), the two contracts FixtureTwoContractHandlerBase serves from one namespace away (both declared under SliceFixtures/Stranded/).
    • FixtureApplicationMap (:66-:78): a private nested ArchitectureMapBase with RepoToken => "MMCA.Common" and a single Framework(Layer.Application, typeof(GetFixturePreferencesQuery).Assembly) layer (:74-:77), registering this test assembly as the Application layer the rule reads its handlers from.
  • Why it's built this way - the rule always throws against this map, so all four facts read the same message shape without any of them constructing a passing map, the same discipline IdempotencyFitnessTests uses for the same reason: one deliberately-offending fixture set keeps the assertions honest about which handler or contract is named.
  • Where it's used - an independent class in the Common architecture suite; its fixtures live in MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/ (SliceFixtures.cs, FixtureTwoContractHandlerBase.cs and the Stranded/ subfolder). The shipped binding of the same rule over real code is SliceCohesionTestsBase.

FormsConventionTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/FormsConventionTestsBaseTests.cs:11 · Level 5 · class

  • What it is - the self-test for the password-form rule inside FormsConventionTestsBase: it feeds the rule's two public static helpers, MissingPasswordFormMarkers and RendersChangePasswordCard, plus the shared MissingMarkers they build on (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:138; the two helpers at :102, :248), hand-built markup so each marker can be shown to fire on its own.
  • Depends on - FormsConventionTestsBase and ArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx") (FormsConventionTestsBaseTests.cs:45), see ArchitectureMapBase.
  • Concept - prove each marker by deleting exactly one. CompleteForm (:13-:19) is a markup string with an ErrorSummary bound to _passwordForm?.Errors, three required fields and both password validators. CompletePasswordForm_HasNoMissingMarkers (:22) asserts it is clean; every other fact removes one ingredient with Replace and asserts the helper names the loss: no summary (:26), a field no longer required, which drops both the requiredness and the required message below three (:32), and no match validation, which leaves the single entry ValidateConfirmPassword (:53). [Rubric §14 - Testability] is the theme: a markup-scanning rule that could not be seen to fail would pass vacuously.
  • Walkthrough - SharedChangePasswordCard_KeepsEveryPasswordFormMarker (:42) reads the real Source/Presentation/MMCA.Common.UI/Components/Auth/ChangePasswordCard.razor and requires an empty marker list, because a consumer Profile page that renders the shared card passes the base's Profile fact on that card's strength (:37-:40). The commented-out facts (:58-:85, tagged M238) pin that a guard wrapped in a Razor @* *@ or HTML <!-- --> comment no longer renders, so it must not count: a commented ErrorSummary (:63-:67) and a commented required field (:69-:73) are flagged on the password form, and a commented UnsavedChangesGuard is flagged by MissingMarkers while an uncommented one after a note passes (:75-:85). The theory RendersChangePasswordCard_IgnoresCommentedOutTags (:87-:94) pins that a @* *@ or <!-- --> comment around the tag does not count as rendering it, while a tag after a comment does.
  • Where it's used - run by the test assembly only; nothing outside the defining file references it.

ForwardedJwtAudienceFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Api · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/ForwardedJwtAudienceFitnessTests.cs:15 · Level 5 · class

  • What it is - the self-test for the fail-closed audience rule ArchitectureRules.ForwardedJwtBearerAudienceIsFailClosed(sourceRoots, minimumCalls) (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Api/ArchitectureRules.ForwardedJwt.cs:25), run over two on-disk fixture trees instead of the real hosts. The shipped-host run is the abstract ForwardedJwtAudienceTestsBase (.../Bases/Api/ForwardedJwtAudienceTestsBase.cs:29).
  • Depends on - ArchitectureRules, ArchitectureMapBase.FindRepoRoot for the JwtAudienceFixtures root with its Clean and Offending folders (ForwardedJwtAudienceFitnessTests.cs:17-:22), and JwtAudience.RequireConfigured (MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/Auth/JwtAudience.cs:28).
  • Concept - a text-matching rule needs both a counted positive and named negatives. ConformingCall_Passes_AndNearMissesAreNotCounted (:29) passes with minimumCalls: 1 and throws with 2, proving the clean fixture holds exactly one real call because its comment, documentation mention and declaration do not count. FallbackInsteadOfTheGuard_IsFlaggedWithFileAndLine (:40) and FallbackMaskedInsideTheGuard_IsFlagged (:49) require the failure to name FallbackProgram.cs:4 and MaskedProgram.cs:5 (a ?? fallback inside the guard). MissingRoot_FailsRatherThanScanningNothing (:58) requires "source root not found" so a mistyped path cannot pass by scanning nothing.
  • Walkthrough - RuleLiteral_MatchesTheShippedGuard (:66) compares nameof(JwtAudience.RequireConfigured) to the string "RequireConfigured", because the rule matches JwtAudience.RequireConfigured( by text: renaming the guard fails this fact until the rule moves with it.
  • Where it's used - run by the test assembly; the fixture programs are JwtAudienceFixtures/Clean/Program.cs, Offending/FallbackProgram.cs and Offending/MaskedProgram.cs.

FixtureLeakingEvent

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.PayloadPurityFixtures.IntegrationEvents · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/PayloadPurityFixtures/IntegrationEvents/PayloadPurityFixtures.cs:12 · Level 5 · record

  • What it is - the deliberately bad fixture for the integration-event payload-purity rule: an event whose payload reaches a domain entity one level down, through a nested record, so the rule can be proven to fire rather than merely asserted to pass (PayloadPurityFixtures.cs:6-:10).
  • Depends on - BaseIntegrationEvent (:12) and its nested payload FixtureLeakedPayload, which is itself typed on AuditableBaseEntity<int> (:20).
  • Concept introduced - a leak one level down is the one a reviewer misses. The payload rule's assertion in IntegrationEventPayloadPurityRuleTests.PayloadRule_FlagsADomainTypeReachedThroughANestedPayload checks the full dotted path FixtureLeakingEvent.Payload.Entity in the failure message, not just the event's name (IntegrationEventPayloadPurityTests.cs:55-:57), and separately asserts the leaked assembly (MMCA.Common.Domain) appears too (:58). Nesting the domain type inside Payload rather than putting it directly on the event is what makes that path assertion meaningful: a shallow leak would pass a rule that only reads top-level properties.
  • Walkthrough - a single nullable FixtureLeakedPayload? Payload init property (:15); the type carries no other members. FixtureLeakedPayload(AuditableBaseEntity<int>? Entity) (:20) is the domain-typed nested record that completes the leak.
  • Where it's used - both FrameworkProbeMap and ModuleProbeMap in IntegrationEventPayloadPurityRuleTests register it as the sole type in their Shared layer (IntegrationEventPayloadPurityTests.cs:83,:92), and PayloadRule_LeavesACleanEventAlone asserts the sibling FixtureCleanEvent is absent from the same failure message (:66-:69), which is what proves the rule names only the offender.

IdempotencyFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:12 · Level 5 · class

  • What it is - the meta-test for the POST idempotency-intent gate: three facts that prove the rule names the undeclared controller, stays silent about the three declared ones, and skips abstract controllers entirely.
  • Depends on - ArchitectureRules (ArchitectureRules.PostActionsDeclareIdempotencyIntent, IdempotencyFitnessTests.cs:17), its nested IdempotencyTestMap and four controller fixtures, IdempotentAttribute and NonIdempotentAttribute (:2), plus Microsoft.AspNetCore.Mvc (:1), xUnit, and AwesomeAssertions.
  • Concept introduced - nameof instead of string literals in message assertions. Every assertion here is built from nameof(UndeclaredFitnessController) and friends (:21,:32,:35,:38,:48), so renaming a fixture is a compile-time event rather than a silently-passing test. Combined with the always-throwing design (CancellationTokenFitnessTests uses the same shape), it means the suite fails loudly on refactors instead of drifting. [Rubric §14 - Testability] and [Rubric §15 - Best Practices & Code Quality] both apply.
  • Walkthrough
    • Rule_FlagsThePostThatDeclaresNothing (:14-:23): calls the rule through an act delegate, captures the thrown exception, and asserts the message contains UndeclaredFitnessController.CreateThingAsync (:20-:22). Note it asserts the qualified member, so the fact fails if the rule reports a type without its offending action.
    • Rule_AcceptsDirectAndInheritedAndOptedOutDeclarations (:25-:40): three NotContain assertions covering the direct declaration (:31-:33), the inherited one (:34-:36), and the justified opt-out (:37-:39), each with a because clause stating the ruling.
    • Rule_SkipsAbstractControllers (:42-:50): asserts AbstractFitnessControllerBase is absent, which is the ConcreteClasses filter's doing, not an attribute's.
  • Why it's built this way - one deliberately-offending fixture means the rule always throws, so all three facts can read the same message shape without any of them having to construct a passing map. The gate itself exists because an undeclared POST is a duplicate-write incident waiting to happen (ADR-015 for the enforcement approach).
  • Where it's used - an independent class in the Common architecture suite; the shipped binding of the same rule over real code is IdempotencyConventionTests.

IdempotencyTestMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:52 · Level 5 · class

  • What it is - a one-layer map that registers this test assembly as the Api layer, which is the only layer the POST idempotency rule scans.
  • Depends on - ArchitectureMapBase (private sealed class IdempotencyTestMap : ArchitectureMapBase, IdempotencyFitnessTests.cs:52), the Layer enum, and LayerRef.
  • Concept - cross-references the map concept from CommonArchitectureMap. The layer choice is the whole configuration: PostActionsDeclareIdempotencyIntent starts from map.OfLayer(Layer.Api) (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:48), so pointing the Api layer at the test assembly is what substitutes the fixture controllers for the framework's real ones.
  • Walkthrough - RepoToken => "MMCA.Common" (:54) and a one-entry DefineLayers() returning Framework(Layer.Api, typeof(IdempotencyFitnessTests).Assembly) (:56-:57). The root namespace the helper derives (MMCA.Common.API) is unused by this rule, which scans whole assemblies rather than namespace-scoped subsets.
  • Where it's used - constructed once per fact in IdempotencyFitnessTests (:17,:28,:45).

IdentifierFixtureMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/StronglyTypedIdFitnessTests.cs:53 · Level 5 · class

  • What it is - a one-layer map that registers this test assembly as the Shared layer, which is the only layer StronglyTypedIdsAreReadonlyRecordStructs scans.
  • Depends on - ArchitectureMapBase (private sealed class IdentifierFixtureMap : ArchitectureMapBase, StronglyTypedIdFitnessTests.cs:53), the Layer enum, and LayerRef.
  • Concept - cross-references the map concept from CommonArchitectureMap: the layer choice is the whole configuration, so pointing the Shared layer at the test assembly is what substitutes the fixture id wrappers for the framework's own strongly typed ids.
  • Walkthrough - RepoToken => "MMCA.Common" (:55) and a one-entry DefineLayers() returning Framework(Layer.Shared, typeof(StronglyTypedIdFitnessTests).Assembly) (:57-:58).
  • Where it's used - constructed once per fact in StronglyTypedIdFitnessTests: the two facts that flag a non-record and a mutable wrapper (:16,:26), the fact that flags extra state (:36), and the fact that pins the two compliant shapes are silent (:46).

LayerDependencyOverrideTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyOverrideTests.cs:18 · Level 5 · class

  • What it is - six facts pinning the per-module escape on the map-completeness rule ModulesDeclareLayers: a thin module fails under the strict default, passes when the override names its own layers, and every other module stays strict while one is overridden.
  • Depends on - ArchitectureRules (ModulesDeclareLayers in both overloads, LayerDependencyOverrideTests.cs:30,:43), its nested FakeArchitectureMap, LayerRef and the Layer enum.
  • Concept introduced - why an escape hatch is per-module rather than a trimmed default. ModulesDeclareLayers is the rule that stops every other per-module layer rule from passing vacuously: it fails when a repo forgets to register a module's assembly in its map (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Layers.cs:82-:88). That creates a real problem for a deliberately thin module, one that owns no aggregate and no persistence, so no Domain and no Infrastructure assembly exists to register. Before the override existed there was exactly one way to express that: trim the default list, which silently stops enforcing those layers for every other module in the repo. One thin module would buy blanket permission to forget an assembly anywhere (rule doc, :96-:104). The three-argument overload (:113) instead takes a per-module dictionary and replaces the required list for the modules it names, leaving everything else at the strict default (:131-:137). Naming the exception keeps the weakening exactly where it is true. [Rubric §3 - Clean Architecture] assesses whether layer boundaries are declared and enforced; [Rubric §34 - Architecture Governance & Documentation] is the mechanism, since the exception ends up written in code with a reason next to it rather than silently absent; [Rubric §14 - Testability] is why an override needs its own test at all, since an escape hatch that is too wide is indistinguishable from a disabled rule.
  • Walkthrough - a static FiveLayers list (:20-:21) is the required set every fact starts from, and two helpers build maps from it: Full(module) yields all five layers, Thin(module) yields only Shared, Application and Api (:117-:121).
    • ModulesDeclareLayers_FailsForAThinModule_WhenEveryModuleIsHeldToTheFullList (:23-:34): the baseline, asserting the exact message "*module 'Notification' declares no Layer.Domain*" (:33).
    • ModulesDeclareLayers_PassesForAThinModule_WhenTheOverrideNamesItsOwnLayers (:36-:52): the same map with an override naming Notification's three real layers, asserted NotThrow.
    • ModulesDeclareLayers_KeepsEveryOtherModuleStrict_WhileOneIsOverridden (:54-:73): the load-bearing fact. Both modules are thin and only one is overridden, so the other must still fail (:71-:72). Its inline comment states the whole reason the override exists rather than a trimmed default list (:57-:58).
    • ModulesDeclareLayers_OverrideCanAlsoDemandMore_NotJustLess (:75-:92): overrides a full module with the five layers plus Layer.Ui and asserts it now fails (:87,:91). Pins that the override replaces rather than subtracts, so a repo can hold one module to a stricter bar than the rest.
    • ModulesDeclareLayers_TwoArgumentOverloadIsUnchanged (:94-:104) and ModulesDeclareLayers_NullOverrides_HoldEveryModuleToTheDefaultList (:106-:115): the two backward-compatibility facts, since the two-argument overload is what every current consumer calls (comment, :96-:98) and it forwards overrides: null (ArchitectureRules.Layers.cs:89-:90).
  • Why it's built this way - the map is a fixture rather than any real repo's, because the point under test is the rule's arithmetic over module names and layers, not any particular assembly (class doc, :14-:16). Asserting on the exact message with WithMessage rather than on a bare throw is what pins which module and which layer got reported, which is the only part of the output a developer acts on.
  • Where it's used - an independent class in the Common architecture suite. The rule it guards is called by LayerDependencyTestsBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/LayerDependencyTestsBase.cs:57), which passes the subclass's ModuleRequiredLayerOverrides; MMCA.Common's own LayerDependencyTests declares no modules, so the rule is vacuous there and the consumers are the real users.

LooseCeilingTests

MMCA.Common.Architecture.Tests | MMCA.Common.Architecture.Tests.Cqrs | MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:167 | Level 5 | class

  • What it is - a private nested fixture inside ConstructorDependencyCountTestsBaseTests: a ConstructorDependencyCountTestsBase subclass whose controller and handler ceilings are set one ABOVE the widest fixture class (4 each, against three declared dependencies), so the tightness facts must flag both populations.
  • Depends on - ConstructorDependencyCountTestsBase (the abstract base it extends, ConstructorDependencyCountTestsBaseTests.cs:167), the nested FixtureMap (:121-:130, built as Map at :169), and int.MaxValue for MaxConstructorDependencies (:171) so the Application *Service fact stays out of play.
  • Concept introduced - a ceiling that is merely not exceeded is a budget; a tight ceiling is a ratchet. MaxControllerConstructorDependencies => 4 (:173) and MaxHandlerConstructorDependencies => 4 (:175) pass the exceed-the-ceiling facts trivially, yet leave one dependency of slack. The *_ConstructorDependencyCeilingIsTight facts on the base (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:181,:194) fail it and tell the author to lower the ceiling "from 4 to 3" (:226), so the next class cannot grow into the slack. [Rubric section 15 - Best Practices & Code Quality] is the property; [Rubric section 14 - Testability] is the technique.
  • Walkthrough - four overrides: Map (:169), MaxConstructorDependencies (:171), and the two ceilings (:173,:175).
  • Why it's built this way - a third ceiling value beside the tight (2) and exact-fit (3) subclasses completes the set of cases: below, at, and above the observed maximum.
  • Where it's used - two facts in ConstructorDependencyCountTestsBaseTests: LooseControllerCeiling_IsFlagged_NamingPopulationCeilingAndObservedMax and LooseHandlerCeiling_IsFlagged. Private to that file, not used elsewhere.

InlineStyleFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui.ComponentConventions · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/ComponentConventions/InlineStyleFitnessTests.cs:12 · Level 5 · class

  • What it is - the meta-test for the no-inline-styles design-system rule: it pins the line detector spelling by spelling, then proves the directory scan reports file, line and attribute, passes clean markup, honors prefix allowances, and fails rather than passing vacuously when it scans too few files or a missing root.
  • Depends on - ArchitectureRules (InlineStyleViolations and RazorMarkupCarriesNoInlineStyles, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.InlineStyles.cs:84,:37), ArchitectureMapBase (FindRepoRoot, used to build FixtureRoot, InlineStyleFitnessTests.cs:14-:20), the Offending and Clean Razor fixtures under RazorFixtures/InlineStyles, and externals xUnit plus AwesomeAssertions.
  • Concept introduced - a markup rule is pinned at two levels: the one-line detector, and the scan around it. The detector is a regex for an attribute or parameter whose name ends in style or Style (optionally StyleFunc) assigned with = followed by a double quote, a single quote or @ (so a single-quoted value and a bare Razor expression count), behind a lookbehind that keeps data-style and CSS text such as list-style out (ArchitectureRules.InlineStyles.cs:127). Lines inside an @code or @functions block are C#, not markup, so a second regex skips them by brace depth and scanning resumes with the markup after the block (:131, :93). The scan adds the guards that stop a moved root from making the gate vacuous: a missing root is a violation (:52), fewer than minimumRazorFiles files is a violation (:66-:68), and each allowedViolations entry exempts every violation that starts with it, file or file:line (:61). [Rubric section 20] is the property; [Rubric section 14 - Testability] is the technique.
  • Walkthrough - FixtureRoot is resolved from the repo root (:14-:20).
    • Detector_FlagsEveryInlineStyleSpelling (:22-:31): a six-row theory (style=, Style=, CellStyle=, HeaderStyle=, RowStyleFunc=, and Style = with spaces) each yielding exactly one finding that names the fixture file Sample.razor, line 1 and the matched attribute.
    • Detector_FlagsSingleQuotedAndExpressionValuedStyles (:33-:42): a five-row theory (style='...', Style=@_style, Style = @(CardStyle(n)), CellStyle='...', RowStyleFunc=@RowStyle), each yielding one finding, so quote style and Razor expressions cannot dodge the rule.
    • Detector_SkipsTheCodeBlock_AndKeepsScanningMarkupAfterIt (:44-:65): C# variables named style, rowStyle and cellStyle inside two @code blocks (brace-on-same-line and brace-on-next-line forms) are ignored, while the markup <div style=...> between them is the only finding, a style= hit on line 7 of the Sample.razor fixture file.
    • Detector_LeavesNearMissesAlone (:67-:73): class=, data-style=, list-style-none and CellClass= produce nothing.
    • OffendingMarkup_IsFlaggedWithFileLineAndAttribute (:75-:86): the Offending fixture is reported as InlineStyled.razor:2 style= through :5 RowStyleFunc=.
    • ConformingMarkup_Passes (:88-:94): the Clean fixture does not throw.
    • AllowListEntry_ExemptsOnlyThePrefixItNames (:96-:106): InlineStyled.razor exempts the whole file, InlineStyled.razor:2 leaves the other lines flagged.
    • TooFewFiles_FailsRatherThanPassingVacuously (:108-:114): demanding 2 files from a 1-file root fails with "scanned 1 .razor file(s), expected at least 2".
    • MissingRoot_FailsRatherThanScanningNothing (:116-:122): a nonexistent folder fails with "markup root not found".
  • Why it's built this way - the detector is public (ArchitectureRules.InlineStyles.cs:84) precisely so each spelling can be pinned line by line without fixture files; the file-based facts then only need to prove the plumbing.
  • Where it's used - an independent class in the Common architecture suite; no usage outside its defining file.

StronglyTypedIdFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/StronglyTypedIdFitnessTests.cs:11 · Level 5 · class

  • What it is - the meta-test for the strongly-typed-id fitness rule: four facts that prove it flags a wrapper that is not a record, one that is not readonly, and one carrying extra state beyond its wrapped value, while staying silent about the two compliant declarations.
  • Depends on - ArchitectureRules.StronglyTypedIdsAreReadonlyRecordStructs, its nested IdentifierFixtureMap, five fixture wrapper types under StronglyTypedIdFixtures (NotARecordFixtureId, MutableFixtureId, ExtraStateFixtureId, CompliantFixtureId, CompliantStringFixtureId), and externals xUnit plus AwesomeAssertions.
  • Concept introduced - the opt-in strongly-typed-id shape is a structural contract, not a naming convention. ADR-115 makes a wrapper two lines, public readonly record struct OrderId(int Value) : IStronglyTypedId<OrderId, int> plus its From factory, and this rule is what enforces the shape rather than trusting it by convention: the wrapper must be a record (so equality is structural), must be readonly (so it behaves like the primitive it wraps), and must carry no member beyond the wrapped value (so nothing hides state EF and JSON binding do not know about). [Rubric §4 - DDD] is the property, a strongly typed id is a value object at the identifier boundary; [Rubric §15 - Best Practices & Code Quality] is the discipline of failing the build rather than the code review when the shape drifts.
  • Walkthrough - all four facts call the same rule against the same map, so the class carries no shared fixture beyond that map.
    • Rule_FlagsAWrapperThatIsNotARecord (:27-:35): asserts the message names NotARecordFixtureId and contains "record".
    • Rule_FlagsAWrapperThatIsNotReadonly (:37-:45): asserts MutableFixtureId and "readonly".
    • Rule_FlagsAWrapperCarryingExtraState (:47-:55): asserts ExtraStateFixtureId and "Label", the name of the offending extra member.
    • Rule_SaysNothingAboutTheCanonicalDeclaration (:57-:65): the discrimination fact. It asserts the message does NOT contain CompliantFixtureId or CompliantStringFixtureId, so the other three facts are proven against a rule that can also stay silent, not one that always throws.
    • IdentifierFixtureMap (:67-:73): a private nested ArchitectureMapBase with RepoToken => "MMCA.Common" and a single Framework(Layer.Shared, typeof(StronglyTypedIdFitnessTests).Assembly) layer, so the rule scans this test assembly's own fixture types.
  • Why it's built this way - three distinct violation shapes plus one compliant-and-silent control is what proves the rule reads structure (record-ness, readonly-ness, member count) rather than matching on a name pattern; none of that is verifiable by reading the rule's own source, only by compiling both shapes and pointing a map at them, the same discipline CascadeSoftDeleteFitnessTests states explicitly for IL-reading rules.
  • Where it's used - an independent class in the Common architecture suite; the fixture wrapper types live in MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/StronglyTypedIdFixtures/StronglyTypedIdFixtures.cs.

TightCeilingTests

MMCA.Common.Architecture.Tests | MMCA.Common.Architecture.Tests.Cqrs | MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:144 | Level 5 | class

  • What it is - a private nested fixture inside ConstructorDependencyCountTestsBaseTests: a ConstructorDependencyCountTestsBase subclass whose controller and handler ceilings are set tight (2 each) against a nested FixtureMap that scans a three-dependency controller and two three-dependency handlers, so both populations are guaranteed to be flagged.
  • Depends on - ConstructorDependencyCountTestsBase (the abstract base it extends, ConstructorDependencyCountTestsBaseTests.cs:144), the nested FixtureMap (:116-:125) pointing both the Api and Application layers at this test assembly, and int.MaxValue for MaxConstructorDependencies, since the Application *Service ceiling is not exercised by this fixture (:101-:104).
  • Concept introduced - a ceiling paired with fixtures that deliberately exceed it, the standard shape for proving a counting rule fires. The two ceilings, MaxControllerConstructorDependencies => 2 (:106) and MaxHandlerConstructorDependencies => 2 (:108), sit one below the three dependencies the fixture controller and fixture handlers actually declare, so Controllers_DoNotExceedConstructorDependencyCeiling and Handlers_DoNotExceedConstructorDependencyCeiling both throw when run against this map. [Rubric section 14 - Testability] is the technique.
  • Walkthrough - three overrides only: Map returns the nested FixtureMap (:100), MaxConstructorDependencies is left at int.MaxValue with a comment stating the Application *Service fact is not exercised here (:102-:104), and the two ceilings (:106,:108) are both set to 2.
  • Why it's built this way - one map with deliberately-noncompliant fixtures lets ConstructorDependencyCountTestsBaseTests prove the offender-naming behavior (type name plus dependency count) for both the controller and handler populations from a single nested type, rather than duplicating the fixture assembly per population.
  • Where it's used - two facts in ConstructorDependencyCountTestsBaseTests: Controllers_AreFlagged_WhenAControllerExceedsTheCeiling and Handlers_AreFlagged_WhenEitherHandlerContractExceedsTheCeiling. Private to that file, not used elsewhere.

SubstitutabilityRuleTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance.Substitutability · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/SubstitutabilityRuleTests.cs:16 · Level 5 · class (sealed)

  • What it is - the meta-test for the Liskov substitutability rule: four facts that pin a refusing override, an unwritten interface implementation, a plain throwing method that must be left alone, and a by-name exclusion.
  • Depends on - ArchitectureRules (SubstitutableMembersDoNotRefuseTheContract, SubstitutabilityRuleTests.cs:25), its nested FixtureAssemblyMap : ArchitectureMapBase (:73, see ArchitectureMapBase), the fixture types in MMCA.Common.Architecture.Tests.SubstitutabilityFixtures (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:9-:53) and Xunit.Sdk.XunitException.
  • Concept introduced - a rule that reads IL cannot be verified by reading it, the same principle taught at CascadeSoftDeleteFitnessTests (class doc, SubstitutabilityRuleTests.cs:10-:14). The rule (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Substitutability.cs:50) decides from the IL of each override or interface implementation whether it throws System.NotSupportedException or System.NotImplementedException (:12-:15), so the only proof is to compile the offending and innocent members side by side and point a map at them. [Rubric §14 - Testability] is the technique; [Rubric §34 - Architecture Governance & Documentation] is the enforced-not-trusted property.
  • Walkthrough - every fact calls the rule against a shared _map (:20) and reads the thrown XunitException message.
    • OverrideThrowingNotSupported_IsFlagged (:22-:31): the message names FixtureRefusingShape.Describe and the exception type System.NotSupportedException, so the report says what was thrown, not only where.
    • InterfaceImplementationThrowingNotImplemented_IsFlagged (:33-:42): FixtureUnfinishedExporter.Export and System.NotImplementedException, the interface-implementation half of the rule.
    • NonOverrideMethodThatThrows_IsNotFlagged (:44-:53): FixtureStandaloneThrower (SubstitutabilityFixtures.cs:40) overrides and implements nothing, so no caller was promised the operation and it must not appear in the message.
    • ExcludedMember_IsNotFlagged (:55-:70): first proves the member IS reported with an empty exclusion set (a control), then excludes FixtureExcludedShape.Corners by its Namespace.Type.Method name (:18) and asserts it vanishes while FixtureRefusingShape is still reported, so an exclusion silences only the member it names.
  • Why it's built this way - the fixture map is module-less (Framework(Layer.Domain, typeof(FixtureRefusingShape).Assembly), :79), and a module-less map makes the rule scan every layer (ArchitectureRules.Substitutability.cs:97), which is what lets it reach this test assembly. Each negative case has a positive control beside it, so a rule that silently flagged nothing would fail the first two facts. See ADR-015.
  • Where it's used - an independent class in the Common architecture suite; the shipped binding of the rule over real code is SubstitutabilityTestsBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SubstitutabilityTestsBase.cs:35).

IntegrationEventContractTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventContractTestsBaseTests.cs:13 · Level 6 · class

  • What it is - the meta-test for the frozen integration-event contract base: thirteen facts proving that a reordered member list is not a break, that everything a consumer can actually observe (a missing member, an extra member, a retyped member, a retyped generic argument, a nullability change, a missing event) still is, that the rendered contract is precise enough to see those changes at all, and that the snapshot-regeneration switch writes paste-ready literals.
  • Depends on - IntegrationEventContractTestsBase (the type under test, through the nested ProbeTests), ArchitectureRules (BuildIntegrationEventContract, :199), its nested FixtureMap, the fixture events in the same test assembly (keyed by .FixtureTallyEvent, .FixtureCountEvent, .FixtureNullableLabelEvent and the like), and externals xUnit plus AwesomeAssertions.
  • Concept introduced - a snapshot gate has to be insensitive to everything the wire is insensitive to, and sensitive to everything it is not, or it trains people to update it by rote. The base compares each event's member list as a SET rather than a sequence, and the reason is stated plainly in its own doc (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventContractTestsBase.cs:10-:16): JSON carries no member order, so reordering two properties in a record declaration changes nothing a consumer can observe, and failing the build for it would teach a team to regenerate the one gate that guards real breakage without reading it. The comparison therefore reports five kinds of difference and no others: MISSING EVENT and NEW EVENT (:69-:73), and per event MISSING member, changed type and EXTRA member (:90-:114). The retype is reported as a retype rather than as an unrelated add and remove, because that is the failure a consumer actually sees: the property still deserializes, into the wrong shape (:82-:84). The member list is split on commas outside angle brackets (SplitTopLevel, :163-:180), so a multi-argument generic type stays one member, and each line is keyed on the event's [EventName] value when it declares one, its full type name otherwise (:29-:31). [Rubric §6 - CQRS & Event-Driven] and [Rubric §9 - API & Contract Design] are the properties; [Rubric §33 - Developer Experience] is the reason for the set comparison, since a gate that cries wolf stops being read.
  • Walkthrough - the class never hard-codes a whole expected contract. It builds the live one from FixtureMap through LiveContract() (:198-:199) and then either mutates it or asserts on specific lines of it, so every case stays honest as the fixture types change (class doc, :8-:10).
    • Base_Passes_WhenTheCommittedLiteralsAreAlphabetical (:15-:23): the unmutated baseline, which is the shape every consumer's committed literal is written in today.
    • Base_Passes_WhenMembersAreListedInADifferentOrder (:25-:34): reverses each line's member list through ReverseMembers (:202-:206) and asserts NotThrow, with the because stating why order is not observable.
    • Base_Fails_WhenTheCodeDeclaresAMemberTheContractDoesNot (:36-:47) and Base_Fails_WhenTheContractDeclaresAMemberTheCodeDropped (:49-:60): drop the first member and append a GhostProperty:String, asserting "EXTRA member" and "MISSING member GhostProperty:String" respectively.
    • Base_Fails_WhenAMembersTypeChanges (:62-:74): rewrites the first member's type to Guid and asserts "changed type".
    • Base_Fails_WhenAnEventIsMissingFromTheSnapshot (:76-:83): drops the first line entirely and asserts "NEW EVENT", which is the direction that matters most, an event shipped without a consumer rollout.
    • Contract_SpellsOutGenericArguments_AndPinsAnIntermediateBase (:85-:98): asserts the exact FixtureShapedEvent line, { Count:Nullable<Int32>, Origin:String, Tags:IReadOnlyList<String> }, so generic arguments and a member inherited from an intermediate consumer base are both rendered, and asserts no fixture line carries SchemaVersion: or DateOccurred:, because the framework envelope is the framework's contract, not the event's.
    • Base_Fails_WhenAGenericArgumentOfAMultiArgumentMemberChanges (:100-:114): rewrites <String, Int32> to <String, Int64> on the FixtureTallyEvent line and asserts "member Counts changed type", proving the comma inside the angle brackets belongs to the member type and not to the member list.
    • Contract_Differs_WhenTheOnlyChangeIsIntToNullableInt (:116-:124): the member blocks of FixtureNullableCountEvent and FixtureCountEvent must differ, since int to int? changes what a consumer must accept.
    • Contract_Differs_WhenTheOnlyChangeIsANullableReferenceAnnotation (:126-:137): pins { Label:String?, Notes:IReadOnlyList<String?> } against its non-nullable twin { Label:String, Notes:IReadOnlyList<String> }, so both a nullable member and a nullable generic argument are part of the rendered promise.
    • Base_Fails_WhenOnlyAMembersNullabilityChanges (:139-:153): strips the ? from Label:String? and asserts "member Label changed type".
    • Contract_KeysAnEventOnItsEventName_WhenItDeclaresOne (:155-:166): the contract contains Fixture.Named.v1 { Value:Int32 } and no line mentions FixtureNamedEvent, so a CLR rename does not churn the snapshot of an event whose [EventName] is the wire identity the outbox stores.
    • Base_WritesTheLiveContractAsLiterals_WhenTheSnapshotOutputVariableIsSet (:168-:188): sets IntegrationEventContractTestsBase.SnapshotOutputVariable (MMCA_CONTRACT_SNAPSHOT_OUT, base :27) to a temp path, runs the probe fact, and asserts the file holds each live line as a quoted, comma-terminated C# literal (the base writes it at :40-:44); a finally clears the variable and deletes the file.
    • MutateFirstMultiMemberEvent (:212-:221) is the helper that keeps the four generic failure cases robust: it finds the first event declaring more than one member and asserts that such an event exists (:216), so a fixture reshaped down to single-member events fails loudly instead of making the cases vacuous. Split (:223-:228) parses one line back into a name and a member array through SplitTopLevel (:231-:248), which mirrors the base's angle-bracket-aware splitter; MembersOf (:191-:195) returns the member block of the single line containing a marker.
  • Why it's built this way - deriving the expectations from the live contract and mutating them is what stops this meta-test from becoming a second snapshot that also needs maintaining. The few literal lines it does pin are the rendering rules themselves (generic arguments, nullability, the [EventName] key), where the exact text is the point. The probe subclasses are private so xUnit does not collect their inherited facts as tests of their own (class doc, :10-:11), the same discipline DriftedTests follows. The snapshot-output fact mutates a process-wide environment variable; its own comment (:171-:172) relies on it being the only fact that invokes the probe this way and on xUnit running one class's tests sequentially.
  • Where it's used - an independent class in the Common architecture suite. The base it protects is subclassed per repo by the consumers, whose committed ExpectedContract literals are the real subject of the gate.
  • Caveats / not-in-source - the base's Parse keeps a malformed committed line, braces stripped, as an event with no members (IntegrationEventContractTestsBase.cs:131-:138) so it surfaces as a mismatch rather than being silently dropped. No fact here exercises that path.

LocalizationResourceTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizationResourceTests.cs:12 · Level 6 · class

  • What it is - the MMCA.Common binding of the resource-completeness rule: it asserts the framework's own .resx files fully translate every supported non-default culture, deriving the required-culture set from the live allowlist rather than restating it.
  • Depends on - LocalizationResourceTestsBase (base rule, LocalizationResourceTests.cs:12) and SupportedCultures (source of the required cultures, :15).
  • Concept introduced - deriving a gate's expectations from the same allowlist production uses. Instead of hardcoding "translate Spanish," the override computes RequiredCultures as SupportedCultures.All minus SupportedCultures.Default (:14-:17), so adding a locale to the app automatically extends the coverage requirement. [Rubric §27 - i18n] assesses whether localization is complete and enforced; this gate makes a missing translation a build failure and self-updates when the supported set grows.
  • Walkthrough - two members. RequiredCultures (:14-:17) filters SupportedCultures.All to the non-default entries; the allowlist is [Default, "es"] with Default = "en-US" (MMCA.Common/Source/Core/MMCA.Common.Shared/Globalization/SupportedCultures.cs:12,:18), so the required set is today exactly es. MinimumBaseResources => 3 (:21) sets a non-vacuous floor: the scan must find at least three base resources (ErrorResources for the API, plus SharedResource and MudTranslations for the UI), so a wrong scan root or repo re-layout cannot let the gate pass having checked nothing. The base's default for that floor is zero, which skips the guard entirely (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizationResourceTestsBase.cs:21), so overriding it here is what makes the gate honest. The single inherited [Fact] is Translations_AreComplete_ForEveryRequiredCulture (LocalizationResourceTestsBase.cs:23-:25).
  • Why it's built this way - ADR-027 (localization) requires supported cultures to be fully translated; the derived allowlist plus the minimum-count floor turn that into a self-maintaining CI gate. The pseudo-locale qps-Ploc is deliberately kept out of SupportedCultures.All (SupportedCultures.cs:20-:28) precisely so this gate does not demand a .qps-Ploc.resx sibling.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite.

ModuleProbeMap

MMCA.Common.Architecture.Tests.Contracts · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs:87 · Level 6 · class

  • What it is - the module-bearing twin of FrameworkProbeMap: the identical test assembly, declared under one "Probe" module's Shared layer instead of a framework layer, so the residency rule has a module boundary to check against.
  • Depends on - ArchitectureMapBase, Layer and LayerRef (the Module() helper), and FixtureLeakingEvent as the assembly anchor.
  • Concept - pairing this map with FrameworkProbeMap over the SAME assembly changes exactly one variable, module presence, so ResidencyRule_FlagsAnEventOutsideASharedAssembly (IntegrationEventPayloadPurityTests.cs:30-:39) can prove the rule reacts to the module boundary specifically and not to anything else about the assembly's shape.
  • Walkthrough - RepoToken => "MMCA.Common" (:89) and a single Module("Probe", Layer.Shared, typeof(FixtureLeakingEvent).Assembly) layer (:91-:92). The test assembly's real name does not end in .Shared, so declaring it Shared under a module is what gives the residency rule something to flag.
  • Why it's built this way - the class doc above IntegrationEventPayloadPurityRuleTests states the intent directly (:23-:26): "a rule that only ever passes proves nothing", both rules are pointed at this assembly whose fixtures include a deliberately bad event, and both must name it. FrameworkProbeMap and ModuleProbeMap are the two probes that make that adversarial coverage possible without touching the rule under test.
  • Where it's used - ResidencyRule_FlagsAnEventOutsideASharedAssembly (:30-:39), asserting the message contains FixtureCleanEvent and "is not a *.Shared assembly". Private to IntegrationEventPayloadPurityRuleTests; not used outside this file.

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · (see per-type table) · Level 6 · class

The same unsafe and safe pair as NavigatingSpec and ScalarOnlySpec, rebuilt one level further down the specification hierarchy as QuerySpecification<TEntity, TIdentifierType> subclasses. They exist to pin a single ruling: adding includes, ordering, and paging to a specification must not take it out of the navigation rule's reach.

  • Depends on - QuerySpecification<TEntity, TIdentifierType> (both, SpecificationFitnessTests.cs:76,:89), FitnessDependent and FitnessPrincipal (the entities), and System.Linq.Expressions (:1).
  • Concept introduced - walking the whole base chain instead of matching one type. The rule selects candidates with HasBaseTypeStartingWith("MMCA.Common.Domain.Specifications.Specification") (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:7,:33), and that helper climbs BaseType all the way up (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/RuleHelpers.cs:88-:91). Because QuerySpecification itself derives from Specification, the prefix still matches two levels down, and the string prefix match means both Specification and QuerySpecification are caught by the one condition. That is the kind of thing that is true today and could quietly stop being true after a refactor of the specification hierarchy, which is exactly why it gets its own fact rather than being left as an assumption. [Rubric §7 - Microservices Readiness] and [Rubric §8 - Data Architecture] are the underlying properties (a navigating predicate cannot cross a data-source boundary, ADR-006); [Rubric §14 - Testability] is why the pair exists.
  • Walkthrough - both declare an explicit parameterless constructor, which is load-bearing: the rule instantiates candidates to read Criteria and skips any type without one (ArchitectureRules.Specifications.cs:37-:41). Each constructor also exercises a different part of the query surface, so the fixtures prove that the extra machinery does not interfere with the analysis.
Type File:Line What differs
NavigatingQuerySpec MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:76 The offender. Its constructor adds ordering and paging, AddOrderBy(d => d.PrincipalId) and ApplyPaging(skip: 0, take: 10) (:79-:83), and its Criteria dereferences the navigation, d => d.Principal!.IsActive (:86). Must be reported.
ScalarOnlyQuerySpec MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:89 The safe counterpart, and a deliberately awkward one: its constructor calls AddInclude(nameof(FitnessDependent.Principal)) (:92), so it does pull in the related entity, while its Criteria filters only on own columns (:95). Must not be reported. Includes are an eager-loading instruction, not a filter predicate, and the rule analyzes the predicate only.
  • Why they're built this way - ScalarOnlyQuerySpec is the sharper of the two. Without it, a future rule implementation that (reasonably but wrongly) also inspected Includes would still pass every other fixture in the file. Both are declared public while their plain-Specification siblings are private, which makes no difference to the rule (it scans ConcreteClasses across the assembly, not by visibility).
  • Where they're used - the two inputs to Rule_AlsoAnalyzesQuerySpecifications in SpecificationFitnessTests (:26-:38).

LifetimeTokenConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui.ComponentConventions · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/ComponentConventions/LifetimeTokenConventionTests.cs:12 · Level 6 · class (sealed)

  • What it is - Common's binding of the lifetime-token gate: no component code reads _cts.Token directly, it goes through a LifetimeToken property, plus a detector theory and two guards proving the scan cannot pass vacuously.
  • Depends on - LifetimeTokenConventionTestsBase (LifetimeTokenConventionTests.cs:12; base at MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LifetimeTokenConventionTestsBase.cs:13), ArchitectureMapBase.FindRepoRoot (:14-:15), and ArchitectureRules (ComponentsReadTheirTokenThroughLifetimeToken, ArchitectureRules.LifetimeTokens.cs:28; DirectTokenReads, :75).
  • Concept introduced, reading Token off a disposed source is the hazard. The base doc (LifetimeTokenConventionTestsBase.cs:6) names it: a component that reads _cts.Token after its source is disposed throws, so reads go through LifetimeToken, which hands back an already-canceled token instead. The inherited fact Components_ReadTheirTokenOnlyThroughLifetimeToken (LifetimeTokenConventionTestsBase.cs:27-:29) runs the rule over ScanRoot with MinimumCodeBehindFiles and ScanAllCodeFiles. [Rubric §34 - Architecture Governance & Documentation] covers the executable convention.
  • Walkthrough - three overrides bind it: ScanRoot is Source/Presentation/MMCA.Common.UI (:14-:15), MinimumCodeBehindFiles => 15 (:17, a floor; 17 .razor.cs files when the gate landed, :16), and ScanAllCodeFiles => true (:19) so plain .cs files are scanned too (the base default is false, LifetimeTokenConventionTestsBase.cs:25). Three members then test the rule itself:
    • Detector_FlagsADirectReadOutsideTheLifetimeTokenProperty (:74-:83): a seven-row theory over DirectTokenReads; _cts.Token and _cts!.Token are flagged, the LifetimeToken property bodies (both the plain and the null-guarded form), a different field name and _cts.LifetimeToken() are not.
    • DirectRead_IsReportedWithFileLineAndTheFix (:85-:88): the report reads Pages/Sample.razor.cs:2 reads _cts.Token directly; use LifetimeToken, so the message is the instruction.
    • TooFewCodeBehindFiles_FailsRatherThanPassingVacuously (:90-:96) and MissingRoot_FailsRatherThanScanningNothing (:98-:104): a floor of 10,000 throws "expected at least 10000" (ArchitectureRules.LifetimeTokens.cs:51), and a nonexistent folder throws "scan root not found" (:36).
  • Where it's used - MMCA.ADC binds the same base (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/LifetimeTokenConventionTests.cs).

FixtureMap

MMCA.Common.Architecture.Tests | MMCA.Common.Architecture.Tests.Governance and MMCA.Common.Architecture.Tests.Cqrs | (see per-type table) | Level 4 | class

The name is reused three times in this assembly, one per outer test class that needs its own narrowly-scoped map; two are in this group's scope and documented here, and a third, nested inside IntegrationEventContractTestsBaseTests (Contracts namespace), scopes the integration-event contract rule instead. All three are private sealed, nested in different outer classes, which is what keeps them from colliding.

  • Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
  • Concept - cross-references the module-versus-framework distinction taught at FixtureModuleMap. The Governance instance is built with Framework(...) (FeatureFlagLifecycleTests.cs:108-:109), because the rules it feeds, FeatureFlagsDeclareLifetime and TemporaryFeatureFlagsAreNotPastRemoveBy, read whatever assemblies the map lists without a module-scoping distinction. The Cqrs instance is built with two Module("Fixtures", ...) entries instead, one per scanned layer (ConstructorDependencyCountTestsBaseTests.cs:126-:83), because ConstructorDependencyCountTestsBase's ceiling check is layer-specific: API controllers off Map.Api(), CQRS handlers off Map.ModuleApplication().
  • Walkthrough - both declare RepoToken => "MMCA.Common" (FeatureFlagLifecycleTests.cs:106; ConstructorDependencyCountTestsBaseTests.cs:123). The Governance instance's DefineLayers() returns a single Framework(Layer.Shared, typeof(FixtureBadFeatures).Assembly) (:108-:109). The Cqrs instance's DefineLayers() returns two entries, Module("Fixtures", Layer.Api, typeof(FatFixtureController).Assembly) and Module("Fixtures", Layer.Application, typeof(FatFixtureController).Assembly) (ConstructorDependencyCountTestsBaseTests.cs:126-:83), so the same fixture assembly, anchored on FatFixtureController, answers both the API and Application scans the ceiling rule runs.
Type File:Line The rule it scopes
FixtureMap (in FeatureFlagLifecycleRuleTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:104 Constructed fresh at each call site: the private DeclarationFailure() helper for ArchitectureRules.FeatureFlagsDeclareLifetime (:98), and directly by the two ExpiryRule_* facts for ArchitectureRules.TemporaryFeatureFlagsAreNotPastRemoveBy (:72,:87).
FixtureMap (in ConstructorDependencyCountTestsBaseTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:121 Both Map-defining fixture subclasses point at a fresh FixtureMap(): TightCeilingTests (:100), whose tight ceilings make the offender assertions in Controllers_AreFlagged_WhenAControllerExceedsTheCeiling and Handlers_AreFlagged_WhenEitherHandlerContractExceedsTheCeiling fire, and ConformantTests (:112), whose wider ceilings back the two *_AreNotFlagged facts.
  • Why it's built this way - each is nested private sealed inside the test class that uses it, so neither is visible to the other and neither can be mistaken for a repo map. Duplicating a handful of lines per outer class is cheaper than a shared fixture map whose meaning would have to be read from another file.
  • Where they're used - see the table above; each instance is constructed only inside the outer test class that declares it.

ConstructorDependencyCountTestsBaseTests

MMCA.Common.Architecture.Tests | MMCA.Common.Architecture.Tests.Cqrs | MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:14 | Level 6 | class

  • What it is - the meta-test for ConstructorDependencyCountTestsBase: nine facts proving the controller and handler constructor-dependency ceilings each flag an offender by name and dependency count, both pass a fixture sitting exactly on the ceiling, both fail against a map whose layers hold no controller or handler at all, and the companion tightness facts flag a ceiling set above the observed maximum while accepting one set exactly on it.
  • Depends on - ConstructorDependencyCountTestsBase (Controllers_DoNotExceedConstructorDependencyCeiling, Handlers_DoNotExceedConstructorDependencyCeiling), its own nested TightCeilingTests, ConformantTests and EmptyScanTests subclasses (:139-:171), the nested FixtureMap and BareMap (:116-:137), and the fixture population in MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs (FatFixtureController, RebuildFixtureProjectionHandler, GetFixtureProjectionHandler).
  • Concept introduced - a ceiling is a high-water mark, not a strict less-than. ConformantTests sets both ceilings to 3, exactly the dependency count the fixture controller and fixture handlers declare, and Controllers_AtTheCeiling_AreNotFlagged / Handlers_AtTheCeiling_AreNotFlagged assert NotThrow against it (:68-:75,:92-:99), proving the rule accepts equality rather than only strictly-fewer. [Rubric section 15 - Best Practices & Code Quality] is the property; [Rubric section 14 - Testability] is the technique, an IL/reflection-driven count that only compiling both a tight and an exact-fit fixture population can prove either way.
  • Walkthrough - all five facts build a subclass instance and read its two assertion delegates.
    • Controllers_AreFlagged_WhenAControllerExceedsTheCeiling (:57-:66): against TightCeilingTests, asserts the thrown message names FatFixtureController with "(3 ctor dependencies)" (:63-:65).
    • Controllers_AtTheCeiling_AreNotFlagged (:68-:75): against ConformantTests, asserts NotThrow.
    • Handlers_AreFlagged_WhenEitherHandlerContractExceedsTheCeiling (:77-:90): asserts the message names both RebuildFixtureProjectionHandler and GetFixtureProjectionHandler, each with "(3 ctor dependencies)", proving ICommandHandler and IQueryHandler implementations are scanned as one population (:85-:89).
    • Handlers_AtTheCeiling_AreNotFlagged (:92-:99): the handler-side equality control.
    • BothPopulations_FailVacuousScans (:101-:113): against EmptyScanTests (Map is BareMap, pointing both layers at Common.Shared.Abstractions.Result's assembly, which holds neither a controller nor a handler), asserts BOTH assertion delegates throw, one because the API assembly holds no controller to check and one because the Application assembly holds no handler, the non-vacuity floor for this rule.
    • LooseControllerCeiling_IsFlagged_NamingPopulationCeilingAndObservedMax (:74-:88): against LooseCeilingTests (ceiling 4, widest controller takes 3), asserts Controllers_ConstructorDependencyCeilingIsTight throws a message naming the population ("API controllers"), the ceiling property and value ("MaxControllerConstructorDependencies is 4"), the observed maximum ("takes 3"), the FatFixtureController at that maximum, and the fix ("from 4 to 3").
    • LooseHandlerCeiling_IsFlagged (:90-:97): the handler-side check, the message names "CQRS handlers".
    • CeilingsAtTheObservedMaximum_AreTight (:99-:109): against ConformantTests, both tightness facts NotThrow.
    • TightnessCheck_FailsVacuousScans (:111-:118): against EmptyScanTests, a population with nothing to measure cannot prove its ceiling tight, so the fact throws.
    • FixtureMap (:121-:130) and BareMap (:133-:142): private nested ArchitectureMapBases, RepoToken => "MMCA.Common", each declaring one Api-layer and one Application-layer Module entry over this test assembly (FixtureMap) or over Common.Shared.Abstractions.Result's assembly (BareMap).
  • Why it's built this way - three subclasses (TightCeilingTests, ConformantTests, EmptyScanTests) sharing one fixture map versus a bare one is what proves the ceiling comparison accepts equality but not excess, and that a scan finding no controller or handler still fails loudly rather than passing by omission.
  • Where it's used - an independent class in the Common architecture suite; its usage site is the fixture population file MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs.

DependencyVersionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DependencyVersionTests.cs:9 · Level 6 · class

  • What it is - the MMCA.Common binding of the shared dependency-pin rule: a one-line sealed subclass that turns on the checks enforcing this repo's two commercial-license package ceilings, MassTransit below major 9 and SixLabors.ImageSharp below major 4.
  • Depends on - DependencyVersionTestsBase (public sealed class DependencyVersionTests : DependencyVersionTestsBase;, DependencyVersionTests.cs:9), which in turn calls ArchitectureRules.PinnedPackageMajorBelow against this repo's Directory.Packages.props.
  • Concept introduced - the thin-subclass fitness pattern. Almost every rule in this project lives once in the reusable MMCA.Common.Testing.Architecture package as an abstract *TestsBase, and each repo activates it with a near-empty subclass. This is the first of many such subclasses; the body-less form here is the extreme case (no configuration at all: the declaration ends at its semicolon, DependencyVersionTests.cs:9). [Rubric §32 - Dependency & Supply-Chain] assesses guarding against risky dependency drift; the base parses the pinned versions and fails the build before either ceiling is crossed, so a "just bump the version" edit cannot slip through.
  • Walkthrough - no members; the entire behavior is inherited. The base contributes two [Fact]s. MassTransit_MustNotExceed_MajorVersion8 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DependencyVersionTestsBase.cs:27-:37) walks the three MassTransit package ids MassTransit, MassTransit.RabbitMQ, and MassTransit.Azure.ServiceBus.Core (:17-:22) with an exclusiveMajorCeiling of 9, because v9 requires a commercial license (MT_LICENSE) and every broker-enabled host fails its startup license check without one, while CI never starts a broker so the build would otherwise stay green (:32-:35). ImageSharp_MustNotExceed_MajorVersion3 (:47-:60) does the same for SixLabors.ImageSharp (:45) with a ceiling of 4, because ImageSharp v4's MSBuild targets fail at build time without a $(SixLaborsLicenseKey) (:55-:58). Both id lists are protected virtual, so a repo that pins neither package can override them to an empty list and make the rule vacuous.
  • Why it's built this way - the pins are real only in MMCA.Common (where these packages are actually declared); ADC and Store inherit MassTransit transitively through MMCA.Common.Infrastructure and deliberately do not subclass the base, because the default list would assert a Directory.Packages.props entry they do not declare (DependencyVersionTestsBase.cs:8-:13). See ADR-016, which records the lockstep-versioning policy and the license-ceiling pattern these two facts implement.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI's build-and-test job.
  • Caveats / not-in-source - the class's own doc comment (DependencyVersionTests.cs:5-:8) still describes only the MassTransit trap; the ImageSharp ceiling arrived later on the base and the subclass doc was not extended. The behavior is the base's two facts, not the comment.

FixtureMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventContractTestsBaseTests.cs:254 · Level 6 · class

  • What it is - a consumer-shaped map over this test assembly, declaring one module Shared layer, so the integration-event contract scan covers the fixture events here and none of the framework's own.
  • Depends on - ArchitectureMapBase (private sealed class FixtureMap : ArchitectureMapBase, IntegrationEventContractTestsBaseTests.cs:254), LayerRef and the Layer enum.
  • Concept - cross-references the module-versus-framework distinction taught at FixtureModuleMap and FakeConsumerMap. The single Module("Fixture", Layer.Shared, ...) entry (:130) does two things at once. It makes the map module-bearing, so IntegrationEvents excludes framework layers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:157-:73) and no framework-shipped event can leak into the contract the probes compare; and it names Shared, which is where a consumer's integration events are required to live, so the fixture events do not simultaneously trip the residency rule. RepoToken => "MMCA.FixtureRepo" (:126) is deliberately unlike MMCA.Common, so nothing derived from the token can collide with the real framework namespaces.
  • Walkthrough - DefineLayers() is a yield-based iterator with one entry (:128-:131). The events it brings into scope are the eight upcaster fixture contracts, which are the only IIntegrationEvent implementations this assembly declares. That reuse is what makes the class doc's promise hold, that the mutation cases stay honest as the fixture types change (:9-:10): the baseline is rebuilt from live types on every call rather than hard-coded.
  • Where it's used - built fresh by LiveContract() (:86-:87) on every mutation, and supplied as the Map of every ProbeTests instance (:136).

FolderWidthTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FolderWidthTests.cs:16 · Level 6 · class

  • What it is - the MMCA.Common binding of the folder-width rule: a sealed subclass that points the shared rule at this repo's root and declares the three folders this repo keeps deliberately flat.
  • Depends on - FolderWidthTestsBase (public sealed class FolderWidthTests : FolderWidthTestsBase, FolderWidthTests.cs:16) and ArchitectureMapBase for FindRepoRoot("MMCA.Common.slnx") (FolderWidthTests.cs:18), which walks upward until it finds the solution file so the test does not depend on the working directory.
  • Concept - the same thin-subclass fitness pattern taught at DependencyVersionTests, here with configuration rather than none: the repo supplies RepoRoot and, because it has documented exceptions, ExemptFolderSuffixes. [Rubric §5 - Modularity & Structure] is the property. The rule enforces "feature by folder, use case by leaf": no folder under Source/ or Tests/ may hold more than MaxDirectFiles direct code units, defaulting to 12 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FolderWidthTestsBase.cs:23), so a folder keeps naming a feature instead of drifting into a technical bucket (base doc, FolderWidthTestsBase.cs:3-:13).
  • Walkthrough - two overrides and no facts of its own.
    • RepoRoot (FolderWidthTests.cs:18) is an initialized get-only property, so the upward search for MMCA.Common.slnx runs once per test instance.
    • ExemptFolderSuffixes (FolderWidthTests.cs:26-:33) returns three repo-relative suffixes: MMCA.Common.Application/UseCases/Decorators, MMCA.Common.Application.Tests/Decorators, and MMCA.Common.Domain/Interfaces. The doc above it (:22-:27) gives the reason for the first pair: the decorator pipeline is nine cross-cutting concerns times command and query, so splitting it by concern would yield nine two-file folders, and the test twin mirrors it. The third is the entity marker interfaces.
    • The single inherited [Fact], Folders_stay_narrow (FolderWidthTestsBase.cs:31-:33), delegates to ArchitectureRules.FoldersStayNarrow (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FolderWidth.cs:34), which walks Source and Tests recursively (:42-:50), skips the tool-owned and build-output segments bin, obj, Migrations, Platforms, Resources, node_modules, wwwroot and .git (:82), and counts a .razor component and its co-located X.razor.cs code-behind as one unit (:105-:119).
  • Why it's built this way - counting code units rather than files is what keeps the cap honest for a Blazor project, where a component is three files that describe one thing; skipping Migrations/ and Platforms/ is what keeps generated and SDK-owned trees from failing a rule about human-authored layout. Exemptions are folder-path suffixes rather than a blanket switch, so each accepted flat namespace is a line someone had to write and can be read back. See ADR-109 for the convention itself and ADR-015 for the fitness-function approach.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI. The same base is activated once per repo; the ADC binding is the identical shape with FindRepoRoot("MMCA.ADC.slnx") and no exemptions at all (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/FolderWidthTests.cs:10-:12).
  • Caveats / not-in-source - the class-level doc comment (FolderWidthTests.cs:5-:17) describes a wider exemption set than the class configures, naming the application contracts, the shared auth contracts, the API startup extensions and the integration-test package root. The behavior is the three suffixes actually listed at :30-:32; the property's own doc (:22-:27) records the current rationale and points at UPGRADING.md.

CascadeSoftDeleteFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:17 · Level 6 · class

  • What it is - the meta-test for the cascade-soft-delete rule: six facts that pin both accepted cascade forms, both violation reasons with their named child collection, the out-of-scope aggregate, and both granularities of the exemption list.
  • Depends on - ArchitectureRules (AggregatesCascadeSoftDeleteToChildren, CascadeSoftDeleteFitnessTests.cs:26), its nested FixtureAssemblyMap, the seven types in MMCA.Common.Architecture.Tests.CascadeFixtures (:1), and Xunit.Sdk.XunitException (:3).
  • Concept introduced - a rule that reads IL cannot be verified by reading it. The class doc states the principle in those words (:7-:16), and it is the organizing idea behind this whole family of self-tests. AggregatesCascadeSoftDeleteToChildren decides its verdict from the instruction stream of a Delete() override, so no amount of reviewing the rule's source proves it distinguishes base.Delete() from child.Delete(). The only proof is to compile both shapes side by side and point a map at them. [Rubric §14 - Testability] is the technique; [Rubric §8 - Data Architecture] and [Rubric §30 - Compliance/Privacy/Data Governance] are the properties, taught at CascadeChildFixture.
  • Walkthrough - a private const string FixtureNamespace (:19) and a readonly FixtureAssemblyMap (:21) are shared by every fact, and five of the six read the message of a rule call that always throws.
    • AggregateWithoutDeleteOverride_IsFlagged_WithItsChildCollection (:23-:33): asserts three things about one line, the type name, the reason "no Delete() override", and the collection name "_orphans" (:30-:32). Pinning the collection name is what makes the failure actionable rather than merely true.
    • DeleteOverrideThatOnlyDeletesItself_IsFlagged_WithItsChildCollection (:35-:45): the same three assertions for the other reason, "Delete() never deletes a child" and "_ignored". The two distinct reason strings are the rule distinguishing "you forgot" from "you tried and it does not work".
    • BothCascadeForms_AreAccepted (:47-:60): asserts both the helper form and the hand-rolled loop are absent, each with a because naming the form.
    • AggregateWithNoChildEntities_IsIgnored (:62-:71): the scope fact, with the ruling stated inline, a List<string> is not a child-entity collection.
    • AllowlistedType_SilencesOnlyThatType (:73-:85) and AllowlistedNamespace_SilencesTheRule (:87-:94): the two exemption granularities. The type-level fact carries the control assertion, that the other offender is still reported (:83-:84).
  • Why it's built this way - two structurally identical offenders is what makes the type-level exemption fact meaningful; two distinct violation reasons is what keeps a regression in the body reader from hiding behind the simpler missing-override check. See ADR-005 and ADR-015.
  • Where it's used - an independent class in the Common architecture suite; the shipped binding of the same rule over real code is CascadeSoftDeleteConventionTestsBase, activated here by CascadeSoftDeleteConventionTests and in both consumer repos over their own modules.

FrameworkProbeMap

MMCA.Common.Architecture.Tests.Contracts · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs:78 · Level 6 · class

  • What it is - a private nested ArchitectureMapBase over the payload-purity test assembly with no module boundary declared, one of a matched pair with ModuleProbeMap that isolates whether a rule's behavior comes from module presence or from the assembly itself.
  • Depends on - ArchitectureMapBase, Layer and LayerRef (the Framework() helper), and FixtureLeakingEvent (only as the assembly anchor typeof(FixtureLeakingEvent).Assembly).
  • Concept introduced - a moduleless map answers to a different baseline. IntegrationEventsLiveInSharedAssemblies only checks residency for module-bearing maps, so pointing it at FrameworkProbeMap is deliberately vacuous: ResidencyRule_IsVacuousForAModuleLessMap (IntegrationEventPayloadPurityTests.cs:41-:46) asserts NotThrow, "a framework map has no module boundary, so its events answer to its public API baseline rather than to a module's Shared assembly". The same map, unchanged, then IS used to prove the payload-purity rule (a check that does not care about modules), which is what keeps the two rules' scopes distinct. [Rubric §7 - Microservices Readiness] is the property the residency rule defends; [Rubric §30 - Compliance/Privacy/Data Governance] is what the payload-purity rule defends, since a domain type reached through a nested payload is exactly how a PII field leaks onto the wire.
  • Walkthrough - RepoToken => "MMCA.Common" (:96) and a single Framework(Layer.Shared, typeof(FixtureLeakingEvent).Assembly) layer (:98-:99); no module entry.
  • Where it's used - three facts in IntegrationEventPayloadPurityRuleTests: ResidencyRule_IsVacuousForAModuleLessMap (:41-:46), PayloadRule_FlagsADomainTypeReachedThroughANestedPayload (:48-:59, asserting the message names the path FixtureLeakingEvent.Payload.Entity and the leaking assembly MMCA.Common.Domain), and PayloadRule_LeavesACleanEventAlone (:61-:70, asserting FixtureCleanEvent is absent from the failure). Private to that class; not used outside this file.

InlineStyleTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui.ComponentConventions · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/ComponentConventions/InlineStyleTests.cs:12 · Level 6 · class (sealed)

  • What it is - Common's binding of the no-inline-style gate: a four-line subclass that points the shared base at this repo's Source/ tree and sets the floor on how many .razor files the scan must reach.
  • Depends on - InlineStyleTestsBase (class InlineStyleTests : InlineStyleTestsBase, InlineStyleTests.cs:12; base at MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/InlineStyleTestsBase.cs:12), ArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx") (:14), and ArchitectureRules.
  • Concept introduced, a base-class binding supplies only the repo-specific facts. The inherited fact RazorMarkup_CarriesNoInlineStyles (InlineStyleTestsBase.cs:29-:31) calls ArchitectureRules.RazorMarkupCarriesNoInlineStyles(MarkupRoots, MinimumRazorFiles, AllowedInlineStyles) (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.InlineStyles.cs:37). The subclass overrides only the two abstract members, RepoRoot and MinimumRazorFiles; MarkupRoots defaults to Source under the repo root (InlineStyleTestsBase.cs:21) and AllowedInlineStyles to empty (:27), so Common carries no reviewed exemption. [Rubric §20 - Design System & Theming] is the property protected (styling lives in stylesheets, not markup), and [Rubric §34 - Architecture Governance & Documentation] the enforced-not-trusted half.
  • Walkthrough - RepoRoot (:14) resolves the repo from MMCA.Common.slnx. MinimumRazorFiles => 50 (:18) is a floor, not an equality: the comment records 56 .razor files when the gate landed (53 in UI, 2 in UI.Web, 1 in UI.Maui), so fewer than 50 means the scan root moved, and the rule fails with "expected at least" instead of passing on an empty scan (ArchitectureRules.InlineStyles.cs:68).
  • Where it's used - the sibling in this repo is InlineStyleFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/ComponentConventions/InlineStyleFitnessTests.cs:12), and MMCA.ADC binds the same base with its own floor (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/InlineStyleTests.cs).

FixtureAssemblyMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs.Repositories · (see per-type table) · Level 9 · class

The name is used five times in this assembly, once per IL-reading rule under proof, and all five are the same one-entry map: this test assembly registered as a single framework layer, so the rule's whole-layer scan lands on the fixtures. They are nested in different outer classes, so their full names differ and none is visible outside its own file.

  • Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
  • Concept introduced - the layer a fixture map declares is not decoration, it is the scope selector. Each of these five rules starts from a different part of the map, so each map has to name the matching layer or the rule reads nothing and passes vacuously. The cascade and hard-delete rules walk every assembly the map registers, through ScannableAssemblyLocations (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:88-:92), so their layer choice is nominal; the domain-throw rule reads map.OfLayer(Layer.Domain) and nothing else (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:111-:111), so registering Domain is mandatory there, and the read-repository rule reads map.OfLayer(Layer.Application) only (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.QueryHandlerRepositories.cs:86-:89), so Application is mandatory there. All five use Framework(...) rather than Module(...), which is the opposite choice from FixtureModuleMap and correct for the same reason: these rules do not filter on module ownership, so the simplest entry is the honest one. Reading assemblies off disk by Assembly.Location also explains a shared limitation: a rule in this family cannot inspect a dynamic or single-file assembly, and the helper silently skips any location that does not exist on disk. [Rubric §14 - Testability] assesses whether a guardrail is provable with focused inputs, and a map that scopes the scan to fixtures is what makes each of these five rules provable at all.
  • Walkthrough - each declares RepoToken => "MMCA.Common" and a single-entry DefineLayers() naming one layer and one anchor type from the fixture namespace it serves. Anchoring by typeof(SomeFixture).Assembly rather than by Assembly.GetExecutingAssembly() is deliberate: it ties the map to a type a compiler would fail on if the fixtures moved.
Type File:Line Layer and anchor The rule it scopes
FixtureAssemblyMap (in CascadeSoftDeleteFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:97 Framework(Layer.Domain, typeof(CascadeChildFixture).Assembly) (:103) AggregatesCascadeSoftDeleteToChildren.
FixtureAssemblyMap (in SoftDeleteEnforcementFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementFitnessTests.cs:77 Framework(Layer.Infrastructure, typeof(FixtureEntity).Assembly) (:83) HardDeletesOnlyInAllowedTypes.
FixtureAssemblyMap (in DomainEventHandlerSaveFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:110 Framework(Layer.Application, typeof(FixtureDomainEvent).Assembly) (:116) DomainEventHandlersDoNotSave.
FixtureAssemblyMap (in DomainThrowFitnessTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:150 Framework(Layer.Domain, typeof(NonThrowingFixture).Assembly) (:144) DomainThrowsOnlyArgumentGuards, which reads the Domain layer only, so this entry is load-bearing.
FixtureAssemblyMap (in QueryHandlerReadRepositoryTests) MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/Repositories/QueryHandlerReadRepositoryTests.cs:58 Framework(Layer.Application, typeof(ReadRepositoryQueryHandlerFixture).Assembly) (:64) QueryHandlersUseReadRepositories, which reads the Application layer only.
  • Why they're built this way - five nested six-line classes rather than one shared fixture map, so each test file is readable end to end and no test can change another's scope by editing a shared file. It is the same trade-off FixtureModuleMap makes.
  • Caveats / not-in-source - because each map registers the whole test assembly, a rule in this family sees every type in it, not just the fixtures in its own namespace. Four of the five are unaffected because their fixtures are the only matching shapes present; the domain-throw rule is not, which is why it carries an explicit allowlist of the assembly's non-fixture throws (DomainThrowFitnessTests.cs:29-:33).
  • Where they're used - constructed once as a readonly field per test class (CascadeSoftDeleteFitnessTests.cs:21, SoftDeleteEnforcementFitnessTests.cs:19, DomainEventHandlerSaveFitnessTests.cs:21, DomainThrowFitnessTests.cs:36, QueryHandlerReadRepositoryTests.cs:18) and passed to every rule call in each.

ProbeTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventContractTestsBaseTests.cs:264 · Level 6 · class

  • What it is - a parameterized negative fixture: a private sealed subclass of the frozen-contract base whose ExpectedContract is whatever the enclosing test hands its constructor.
  • Depends on - IntegrationEventContractTestsBase (private sealed class ProbeTests(IReadOnlyList<string> expected) : IntegrationEventContractTestsBase, IntegrationEventContractTestsBaseTests.cs:264) and FixtureMap (:136).
  • Concept introduced - parameterizing the drift instead of hard-coding it. DriftedTests is the same technique with fixed wrong values, which works when a base has three assertions and each needs one wrong constant. Here the base has one assertion with six interesting failure modes, so a fixed drifted subclass would mean six near-identical nested classes. A primary-constructor parameter collapses them into one type instantiated six times with different data (:20,:30,:41,:54,:68,:79). The trade-off is worth naming: the parameterized form is only safe because the expected contract is derived from the live one and then mutated, so a probe can never assert against a stale hand-written literal.
  • Walkthrough - two overrides, both initialized rather than computed: Map { get; } = new FixtureMap() (:136) and ExpectedContract { get; } = expected (:138). Being private keeps xUnit from collecting the inherited IntegrationEventContracts_ShouldMatch_TheFrozenSnapshot as a test of its own, and the enclosing facts invoke it as a delegate instead.
  • Where it's used - constructed six times across the facts of IntegrationEventContractTestsBaseTests, once per contract mutation.

ScalarOnlySpec

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:69 · Level 6 · class

  • What it is - the deliberately-safe counterpart to NavigatingSpec: its Criteria filters only on the entity's own scalar columns, the pattern the fitness function must leave alone.
  • Depends on - Specification<TEntity, TIdentifierType> (private sealed class ScalarOnlySpec : Specification<FitnessDependent, int>, SpecificationFitnessTests.cs:69) and FitnessDependent.
  • Concept introduced - cross-references the navigation-safety concept from NavigatingSpec; this is the positive example. A predicate over the entity's own scalars (d => d.PrincipalId == 1 && d.Flag, :71) translates to SQL on any engine and survives extraction, so the rule must not flag it. Having both a flagged and an un-flagged fixture is what makes the test prove the rule discriminates rather than just always throwing.
  • Walkthrough - one overridden Criteria filtering on PrincipalId and Flag (:71). The test asserts the rule's exception message does not contain this type's name.
  • Where it's used - the "should not be flagged" input to SpecificationFitnessTests.

SoftDeleteEnforcementFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementFitnessTests.cs:15 · Level 6 · class

  • What it is - the meta-test for the hard-delete rule: five facts proving EF's erasing members are caught by name and declaring type, that every erasing member in a type is reported rather than just the first, that an ordinary collection Remove is never mistaken for one, and that both allowlist granularities work.
  • Depends on - ArchitectureRules (HardDeletesOnlyInAllowedTypes, SoftDeleteEnforcementFitnessTests.cs:24), its nested FixtureAssemblyMap, the four types in MMCA.Common.Architecture.Tests.HardDeleteFixtures (:1), and Xunit.Sdk.XunitException (:3).
  • Concept - cross-references the hard-delete concept taught at FixtureEntity. The fact worth dwelling on is HardDelete_ReportsEveryErasingMember (:30-:40), which asserts all three method names Purge, PurgeMany and PurgeAsync appear in one message. A rule that stopped at the first hit per type would still pass every other fact in the class, and a partial report is worse than none in a codebase-wide sweep: a developer fixes the one named site, the build goes green on the next run only because the next offender is now first, and the sweep takes as many rounds as there are call sites. The rule's collector is a nested yield return over every method and every instruction (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:104-:118), and this fact is what pins that.
  • Walkthrough - a private const string FixtureNamespace (:17) and a readonly FixtureAssemblyMap (:19) are shared by every fact.
    • HardDelete_OutsideTheAllowlist_IsFlagged (:21-:28): the baseline, asserted with WithMessage("*DbSetRemovingFixture*").
    • HardDelete_ReportsEveryErasingMember (:30-:40): the completeness fact described above, with a because naming which EF member each one maps to.
    • OrdinaryCollectionRemove_IsNotAHardDelete (:42-:51): the discrimination fact, asserting the whole SoftDeletingFixture type name is absent even though it contains a literal Remove call.
    • AllowlistedNamespace_SilencesTheRule (:53-:60) and AllowlistedType_SilencesOnlyThatType (:62-:74): the two granularities, the second carrying the control assertion that ExecuteDeletingFixture is still reported (:72-:73).
  • Why it's built this way - the class doc names the principle again (:7-:14): a rule that reads IL cannot be verified by reading it, so the offending and the innocent call sites are compiled side by side and a map is pointed at them. See ADR-005 and ADR-015.
  • Where it's used - an independent class in the Common architecture suite; the shipped binding of the same rule over real code is SoftDeleteEnforcementTestsBase, activated here by SoftDeleteEnforcementTests with the framework's four reviewed eraser types.

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:63 · Level 6 · class

  • What it is - the deliberately-unsafe fixture specification: its Criteria navigates from the dependent into a related entity, the exact pattern the fitness function must flag.
  • Depends on - Specification<TEntity, TIdentifierType> (private sealed class NavigatingSpec : Specification<FitnessDependent, int>, SpecificationFitnessTests.cs:63) and FitnessDependent / FitnessPrincipal (the entities it filters over).
  • Concept introduced - why cross-entity navigation in a specification is unsafe across data sources. Under database-per-service (ADR-006), a d => d.Principal!.IsActive predicate (:65) assumes the related entity lives in the same queryable model; once the principal is extracted to another physical source, that navigation cannot translate to SQL (and on Cosmos the cross-source navigation is degraded out of the model entirely, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:9-:15). The fitness function SpecificationsDoNotNavigateToOtherEntities treats it as a violation and points at the fix: resolve the related keys first and filter on the foreign-key column instead. [Rubric §7 - Microservices Readiness] assesses whether the code stays extractable; this fixture is the negative example that proves the readiness guard fires.
  • Walkthrough - one member, an overridden Criteria expression that dereferences the Principal navigation (:65). Being parameterless matters: the rule only instantiates and inspects specifications that expose a parameterless constructor (ArchitectureRules.Specifications.cs:37-:41), so a fixture with constructor dependencies would be skipped and prove nothing. The test asserts the rule's exception message contains this type's name.
  • Where it's used - the "should be flagged" input to SpecificationFitnessTests; paired with ScalarOnlySpec.

PiiErasureContractFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiErasureContractFitnessTests.cs:19 · Level 6 · class

  • What it is - a non-vacuous §30 fitness test that pushes a representative [Pii] data subject through the framework's own privacy machinery, proving that PII detection, redaction and masking, and in-place erasure compose end to end rather than each being verified in isolation.
  • Depends on - DataSubjectSample (the fixture), PiiRedactor, IAnonymizable, and Result. Externals: xUnit [Fact], AwesomeAssertions.
  • Concept introduced - a contract-composition fitness function. Where the plain [Pii] => IAnonymizable scan is structural (does every marked type also expose an erasure path), this test exercises the behavior the scan presumes. It is the pattern for proving a cross-cutting compliance contract actually holds when its parts run together. [Rubric §30 - Compliance/Privacy/Data Governance] assesses that erasure and log-masking genuinely protect subject data; this test is the framework's executable evidence.
  • Walkthrough - four [Fact]s, each isolating one link in the contract. DataSubject_DeclaresPii_SoTheContractIsNotVacuous (:21-:24) asserts PiiRedactor.HasPii recognizes the sample, so the later guards assert against something real. PiiRedactor_MasksEveryPiiMember_AndPassesThroughNonPii (:26-:35) redacts an instance and checks Email and FullName become PiiRedactor.RedactedToken while Id and City pass through unchanged (:31-:34). PiiRedactor_LeaksNoClearTextPii_ToLogsOrTelemetry (:37-:50) verifies neither the redacted dictionary values (:42-:44) nor RedactToString output (:46-:49) contain the original email or name, covering both the structured-log and the flat-string rendering paths. The fourth fact (:52-:72) asserts the sample is IAnonymizable (:56), that Anonymize() succeeds and changes the PII fields (:59-:62), that a second call also succeeds and leaves the fields erased (idempotence, :64-:66, ADR-005), and finally that an anonymized subject still leaks no original clear text when redacted (:69-:71), proving erasure and redaction compose.
  • Why it's built this way - the plain [Pii] => IAnonymizable scan is vacuous in the framework (no PII entity in Common's Domain); this test closes that gap by forcing the machinery through a stand-in subject, so the §30 guarantee is proven, not merely assumed. Consumers (MMCA.ADC's User) run the same contract against their real aggregates.
  • Where it's used - an independent test class in the Common architecture suite; its structural counterpart is PiiConventionTests, whose own doc names this class as the non-vacuous half of the pair (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiConventionTests.cs:10-:11).

PiiInventoryTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance.Privacy · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/PiiInventoryTestsBaseTests.cs:14 · Level 6 · class

  • What it is - a self-test for the shared PiiInventoryTestsBase rule: it subclasses the base against a fixture assembly and a fixture privacy policy, then proves the two inventory rules actually fail on a bad policy instead of passing vacuously.
  • Depends on - PiiInventoryTestsBase (the base it derives from), FixturePiiSubject (the fixture [Pii] type), and ArchitectureRules (PiiMembersHaveInventoryRows, PiiInventoryRowsMapToPiiMembers). Externals: xUnit [Fact], AwesomeAssertions.
  • Concept introduced - testing the fitness rule, not the code. A fitness rule that has only ever seen a clean policy cannot be trusted. The class overrides Assemblies (:18) and PrivacyPolicyPath (:20) to point at the fixture, then feeds the rule deliberately broken markdown files from a PiiInventoryFixtures folder (:67-:68) and asserts the exact violation text. It is the same discipline as IntegrationEventContractTestsBaseTests, applied to the §30 privacy inventory.
  • Walkthrough - four [Fact]s. MissingRow_Fails (:33-:40) runs PiiMembersHaveInventoryRows on a policy lacking the Phone row and expects a message containing FixturePiiSubject.Phone has no row. RowWithEmptyLawfulBasis_Fails (:42-:51) expects the empty Lawful basis cell to be reported for Phone while Email, whose row is complete, is not named. OrphanRow_Fails (:53-:60) runs the reverse rule PiiInventoryRowsMapToPiiMembers on a policy listing Fax and expects names no live [Pii] member. OrphanRowFixture_StillCoversEveryMember (:62-:65) guards the orphan fixture itself: the forward rule must not throw on it, so only the reverse direction can fail.
  • Why it's built this way - the two rules are bidirectional (every [Pii] member has a policy row, every row maps to a live member), so each direction needs its own failing fixture, and each fixture must isolate a single defect. Asserting on message fragments with a reason string keeps a failure self-explanatory. See ADR-015.
  • Where it's used - an independent test class in the Common architecture suite; no other file references it.

SpecificationFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:13 · Level 7 · class

  • What it is - the test that verifies the SpecificationsDoNotNavigateToOtherEntities fitness function actually discriminates: it must flag a specification that navigates into another entity, must leave a scalar-only specification alone, and must reach both shapes through the QuerySpecification subclass as well.
  • Depends on - ArchitectureRules (the rule under test), and its own nested fixtures SpecTestMap, FitnessDependent, FitnessPrincipal, NavigatingSpec, ScalarOnlySpec, and the two query variants (NavigatingQuerySpec, ScalarOnlyQuerySpec).
  • Concept introduced - testing the test: verifying a fitness function is neither vacuous nor over-broad. A rule that never fires is useless; a rule that flags everything is worse. This class proves the specification-navigation guard does exactly one thing by feeding it both a positive and a negative fixture in a single run. [Rubric §14 - Testability] assesses whether the guardrails themselves are trustworthy; this is the meta-test that earns that trust, and ADR-015 is the decision that makes such guardrails build-gating in the first place. Compare ModuleConformanceTestsBaseTests, which applies the same discipline to the module-conformance base.
  • Walkthrough - two [Fact]s, both driving the same single rule execution shape. Rule_FlagsNavigatingSpecification_ButNotScalarSpecification (:15-:24) wraps the rule call in an act delegate, ArchitectureRules.SpecificationsDoNotNavigateToOtherEntities(new SpecTestMap()) (:18), captures the thrown exception through Should().Throw<Exception>().Which (:20), and asserts its message contains NavigatingSpec and the word "navigates" while not containing ScalarOnlySpec (:21-:23). Both halves matter: the first two assertions prove the rule fires, the third proves it does not over-report. Rule_AlsoAnalyzesQuerySpecifications (:26-:38) repeats the pair one level down the hierarchy, asserting NavigatingQuerySpec is reported and ScalarOnlyQuerySpec is not (:32-:37). The nested types below the facts supply the model: SpecTestMap (:40), the two entities (:48,:57), and the four specifications (:63,:69,:76,:89).
  • Why it's built this way - the navigation rule protects future extraction (a navigating specification cannot cross a data-source boundary, ADR-006); a discriminating test keeps the rule honest as it evolves, and the second fact keeps it honest as the specification hierarchy evolves.
  • Where it's used - an independent class in the Common architecture suite.

SpecTestMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:40 · Level 7 · class

  • What it is - a minimal architecture map used only by SpecificationFitnessTests: it registers this test assembly as the single Application layer so the specification-navigation rule has a model to scan.
  • Depends on - ArchitectureMapBase (private sealed class SpecTestMap : ArchitectureMapBase, SpecificationFitnessTests.cs:40), the Layer enum, and LayerRef.
  • Concept introduced - cross-references the map concept from CommonArchitectureMap. Where the real map spans seven packages, this one collapses to a single self-referential Application layer (Framework(Layer.Application, typeof(SpecificationFitnessTests).Assembly), :45) because the fixtures (the four specifications and two entities) live in the test assembly itself. It is the smallest map that lets a fitness function run against hand-crafted types; the rule scans Application and Domain layers for specification subclasses (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:30-:33), so one Application entry is enough.
  • Walkthrough - RepoToken => "MMCA.Common" (:42) and a one-entry DefineLayers() (:44-:45) pointing at this assembly.
  • Where it's used - instantiated once per fact inside SpecificationFitnessTests (:18,:29).

BadgeGranter, DirectSavingHandler, PointsWriter

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · (see per-type table) · Level 8 · class

The three fixture types that actually call IUnitOfWork.SaveChangesAsync. Each is an internal sealed class with one injected IUnitOfWork, one constructor, and one method whose whole body is the save. They are the terminals of every call chain the domain-event handler save rule has to find.

  • Depends on - IUnitOfWork (DomainEventSaveFixtures.cs:2), plus the handler interface and event for the one that is itself a handler.
  • Concept - cross-references the handler-save concept from IBadgeGranter. What this trio pins is how the rule recognizes a save at all, which is broader than a single interface. IsSaveCall accepts a callee named SaveChanges or SaveChangesAsync (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:12) whose declaring type is either under the Microsoft.EntityFrameworkCore namespace prefix, or whose simple name ends with one of four suffixes: DbContext, DbContextFactory, UnitOfWork or Repository (:20-:21,:235-:255). Matching by suffix rather than by a full type name is what lets the rule see EF's own contexts, the framework's DbContextFactory save surface, a consumer's SQLServerDbContext, and any IRepository that forwards to one, without the rule package taking a compile dependency on any of them. These three fixtures all reach it through IUnitOfWork, the suffix case.
  • Walkthrough - all three share the same shape: a private readonly IUnitOfWork _unitOfWork, an internal constructor assigning it, and one method returning or awaiting _unitOfWork.SaveChangesAsync(cancellationToken). What differs is where they sit in a chain.
Type File:Line Its position in a chain
DirectSavingHandler MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:16 Depth zero: the handler saves in its own HandleAsync (:22-:23). Caught at the call site with no traversal needed, which is what makes it the control for the depth-bound and allowlist facts, since neither can hide it.
PointsWriter MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:53 Depth two: the second hop of the transitive chain, reached only through PointsAwarder. Its WriteAsync is async and awaits with ConfigureAwait(false) (:59-:60), so the call the walk must find lives inside a compiler-generated state machine rather than in the method body.
BadgeGranter MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:70 Behind an abstraction: the sole implementation of IBadgeGranter, reached from InterfaceDispatchSavingHandler only after the walk expands the interface call to its targets. Also async (:76-:77).
  • Why they're built this way - three different routes to the same terminal is the design. If all three saves were direct, the walk's traversal, its state-machine handling and its interface resolution would all be untested; if the direct one did not exist, nothing would prove that a save at depth zero is caught independently of the walk at all, which is the assertion the allowlist and depth-bound facts rest on.
  • Where they're used - DirectSavingHandler is named by three facts of DomainEventHandlerSaveFitnessTests (:30,:92,:103); PointsWriter is asserted to appear in the reported chain (:43); BadgeGranter is reached transitively and is the reason InterfaceDispatchSavingHandler is reported at all.

ReadRepositoryQueryHandlerFixture, WriteRepositoryCommandHandlerFixture, WriteRepositoryQueryHandlerFixture

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.ReadRepositoryFixtures · (see per-type table) · Level 8 · class

Three compiled handler fixtures proving QueryHandlersUseReadRepositories, the rule that a query handler must call IUnitOfWork.GetReadRepository, never GetRepository (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.QueryHandlerRepositories.cs:12-:16,:37). One is the compliant query handler, one is a command handler that is legitimately out of the rule's scope, and one is the violation: a query handler that reaches for the write repository from behind an await.

  • Depends on - IUnitOfWork, IQueryHandler<in TQuery, TResult> and ICommandHandler<in TCommand, TResult>, and ReadRepositoryFixtureAggregate/ReadRepositoryFixtureQuery/ReadRepositoryFixtureCommand (ReadRepositoryFixtures.cs).
  • Concept - cross-references the call-graph-walk concept taught at FixtureAssemblyMap. This trio pins two things about the rule at once: that it discriminates by handler kind (a query handler is scanned, a command handler with the identical GetRepository call is not, because the rule only walks types implementing IQueryHandler<,>) and that it sees through an async body. WriteRepositoryQueryHandlerFixture puts its offending call after await Task.Yield() (:26-:29) specifically so the violation sits inside the compiler-generated state machine rather than the declared method body, which is what the rule's IL reader has to resolve to catch it at all.
  • Walkthrough - each is an internal sealed class with a primary-constructor-injected IUnitOfWork and a single HandleAsync.
    • ReadRepositoryQueryHandlerFixture (ReadRepositoryFixtures.cs:29-:33): IQueryHandler<ReadRepositoryFixtureQuery, Result<int>> calling unitOfWork.GetReadRepository<ReadRepositoryFixtureAggregate, int>() (:30) then CountAsync (:31). The compliant shape.
    • WriteRepositoryCommandHandlerFixture (:40-:49): ICommandHandler<ReadRepositoryFixtureCommand, Result> calling unitOfWork.GetRepository<ReadRepositoryFixtureAggregate, int>() (:59) then discarding a CountAsync (:60). Legitimate, because a command handler is the write side.
    • WriteRepositoryQueryHandlerFixture (:17-:26): IQueryHandler<ReadRepositoryFixtureQuery, Result<int>> that awaits Task.Yield() (:20) before calling unitOfWork.GetRepository<ReadRepositoryFixtureAggregate, int>() (:21), the violation the rule must still catch post-await.
  • Why they're built this way - three fixtures rather than one prove the rule fires on the true violation, stays silent on the two shapes that must not be flagged (a compliant query handler and any command handler at all), and does so even when the call is compiler-relocated into a state machine.
  • Where they're used - the compiled input to QueryHandlerReadRepositoryTests: QueryHandlerAskingForTheWriteRepository_IsFlagged_EvenInsideAnAsyncBody and AllowlistedHandler_IsSilenced assert on WriteRepositoryQueryHandlerFixture (QueryHandlerReadRepositoryTests.cs:23-:26,:38-:41), QueryHandlerAskingForTheReadRepository_IsNotFlagged asserts ReadRepositoryQueryHandlerFixture is absent (:28-:30), and CommandHandlerAskingForTheWriteRepository_IsOutOfScope asserts WriteRepositoryCommandHandlerFixture is absent (:32-:36).

PointsAwarder

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:42 · Level 9 · class

  • What it is - hop one of the transitive save chain: a service that saves nothing itself and delegates to PointsWriter, which does.
  • Depends on - PointsWriter (constructor-injected, DomainEventSaveFixtures.cs:44,:46).
  • Concept - cross-references IBadgeGranter. Its distinctive job is to be the middle of a chain, which is what makes it the natural subject of the allowlist-pruning fact. Allowlist entries in this rule do two things at once: a matching type is neither reported nor walked into (rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:72-:74), because EnqueueTargets filters targets by the same allowlist before queueing them (:170-:172). Allowlisting a collaborator therefore stops the walk at that node and the handler behind it disappears from the report, while a handler that saves directly is unaffected because its save is found at the call site rather than through a target expansion. That distinction is exactly what one fact pins.
  • Walkthrough - one field, one internal constructor, and internal Task AwardAsync(CancellationToken cancellationToken) => _writer.WriteAsync(cancellationToken); (:48-:49). It is a plain task-returning pass-through, not an async method, so it generates no state machine of its own. Being internal rather than public is irrelevant to the rule, which reads Cecil type definitions rather than reflected public surface.
  • Where it's used - asserted to appear in the reported chain as the first hop (DomainEventHandlerSaveFitnessTests.cs:42), and used as the allowlist entry in AllowlistedCollaborator_PrunesTheWalkWithoutHidingDirectSaves (:81).

TransitiveSavingHandler

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.DomainEventSaveFixtures · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:31 · Level 10 · class

  • What it is - the real-world offender shape: a handler that delegates to a service, which delegates to a writer, which saves. Two hops, of which only the last (PointsWriter.WriteAsync) is an async method.
  • Depends on - IDomainEventHandler<in TDomainEvent> closed over FixtureDomainEvent, and PointsAwarder (DomainEventSaveFixtures.cs:31,:33).
  • Concept - cross-references IBadgeGranter. This is the fixture that forces the two hardest parts of the walk. First, the last hop (PointsWriter.WriteAsync, DomainEventSaveFixtures.cs:59) is async, so the save call the walk needs is not in the declared method body: the C# compiler moves it into a generated state-machine type. The handler and PointsAwarder hops are plain task-returning pass-throughs (:37-:38, :48-:49), so their calls sit in the declared bodies. The rule handles that by resolving each method's AsyncStateMachineAttribute to its generated type and reading that type's methods too (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:200-:231), then collapsing those generated types back onto their declaring type for the report so the printed chain reads like source (:265-:277). The fixture doc (:26-:29) still words it as both hops being async, but the point stands for the writer hop: the walk must follow the compiler-generated state machines to see the save. Second, the walk is breadth-first with a visited set and a depth bound (:120-:158), which is what makes it cycle-safe and what makes the depth-bound fact possible.
  • Walkthrough - one field, one internal constructor, and public Task HandleAsync(...) => _awarder.AwardAsync(cancellationToken); (:37-:38), a non-async pass-through. The reported chain for it names three nodes and the save: the handler, PointsAwarder, PointsWriter, and SaveChangesAsync.
  • Where it's used - the subject of TransitiveSave_TwoHopsAway_IsFlagged (DomainEventHandlerSaveFitnessTests.cs:34-:45), which asserts all four names appear, and the control in both AllowlistedCollaborator_PrunesTheWalkWithoutHidingDirectSaves (:88-:90) and DepthBound_StopsTheWalk (:104-:106), where it must not appear.

DomainEventHandlerSaveFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:17 · Level 11 · class

  • What it is - the meta-test for the domain-event-handler save rule: seven facts pinning a direct save, a two-hop transitive save with its full chain, an interface call resolved to its implementation, a clean handler, both allowlist behaviors, and the depth bound.
  • Depends on - ArchitectureRules (DomainEventHandlersDoNotSave, DomainEventHandlerSaveFitnessTests.cs:26), its nested FixtureAssemblyMap, the eight types in MMCA.Common.Architecture.Tests.DomainEventSaveFixtures (:1), and Xunit.Sdk.XunitException (:3).
  • Concept introduced - a call-graph walk, and the three things a test of one has to pin. The rule is the most machinery-heavy in the suite: it reads every scannable assembly into Mono.Cecil, builds a CallGraphIndex over them, finds every type implementing the domain-event-handler interface, and breadth-first searches outward from each of its methods for a save, reporting the shortest chain it finds (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:93-:109,:125-:158). Three properties of such a walk need proof and none is visible by reading it. That it reaches far enough: the transitive fact. That it resolves indirection: the interface fact. That it stops: the depth-bound fact. The last is the one teams usually skip, and it matters twice over here, because maxCallDepth defaults to 6 (:79) and a walk with no bound over a real repo's assemblies would not terminate usefully. The reported chain is itself part of the contract, which is why the transitive fact asserts on every node rather than on the handler alone: a report that named only the handler would leave a developer to rediscover the path by hand. [Rubric §6 - CQRS & Event-Driven] is the property; [Rubric §12 - Performance & Scalability] is why the bound exists; [Rubric §14 - Testability] is the technique.
  • Walkthrough - a private const string FixtureNamespace (:19) and a readonly FixtureAssemblyMap (:21) are shared by every fact.
    • DirectSave_InTheHandler_IsFlagged (:23-:32) and TransitiveSave_TwoHopsAway_IsFlagged (:34-:45): the two reach cases, the second asserting the handler, both hops and SaveChangesAsync all appear, each with a because naming what that node proves.
    • SaveBehindAnInterface_IsResolvedToItsImplementation (:47-:56): the indirection case.
    • HandlerThatOnlyMutates_IsNotFlagged (:58-:67): the discrimination case.
    • AllowlistedNamespace_SilencesTheRule (:69-:76): the whole fixture namespace exempted, asserted NotThrow, with the because naming the real use, a cascade a repo accepts while migrating.
    • AllowlistedCollaborator_PrunesTheWalkWithoutHidingDirectSaves (:78-:94): the sharpest fact in the class. Allowlisting PointsAwarder removes TransitiveSavingHandler from the report because the walk stops at the pruned node, and leaves DirectSavingHandler reported because a direct save is detected at the call site. Both halves are asserted, which is what distinguishes a working prune from a broken rule.
    • DepthBound_StopsTheWalk (:96-:107): calls the rule with maxCallDepth: 1 and asserts the direct save is still found while the two-hop save is not, with the because calling the bound the documented limit of the walk.
  • Why it's built this way - every fixture shape maps to one branch of the walk, and the allowlist and depth facts each carry a control assertion so a rule that simply stopped working could not pass them. The rule exists because a handler that saves opens a second write inside the first: it re-enters the change tracker, can raise a fresh event cascade, and persists work the outer transaction may still roll back (rule doc, :23-:29). Handlers mutate state and let the owning unit of work flush it; anything needing an independent write belongs on the outbox (ADR-003).
  • Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the rule is DomainEventHandlerSaveTestsBase.

CommonArchitectureMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommonArchitectureMap.cs:15 · Level 12 · class

  • What it is - the architecture map for the MMCA.Common framework: it names each package's layer and pins the layer to a concrete assembly, so the shared rule library knows which assembly is Shared, Domain, Application, and so on for this repo.
  • Depends on - ArchitectureMapBase (internal sealed class CommonArchitectureMap : ArchitectureMapBase, CommonArchitectureMap.cs:15), the Layer enum, LayerRef, and one anchor type per package (Result, BaseEntity<>, DomainEventDispatcher, ApplicationDbContext, ApiControllerBase, ResultGrpcExtensions, UISharedAssemblyReference, :21-:27).
  • Concept introduced - the map as the single point of repo-specific truth for architecture rules. The rule bodies live once in MMCA.Common.Testing.Architecture and are parameterized by an IArchitectureMap; each repo supplies exactly one map so the same rules run identically across Common, Store, and ADC. Because Common is a module-less framework, every layer is registered as a framework layer via the Framework(...) helper rather than a module layer, and that distinction is load-bearing well beyond bookkeeping: several rules branch on map.ModuleNames.Count to decide whether they are judging a framework or a consumer (see EventScopeFitnessTests), and several others read only the per-module accessors, which is why the self-tests for those use FixtureModuleMap instead of this one. [Rubric §3 - Clean Architecture] assesses whether layer boundaries are explicit and enforced; this map is the machine-readable statement of those boundaries.
  • Walkthrough - RepoToken => "MMCA.Common" (:17) identifies the repo and is what the source-scanning rules use to locate the repo root (they look for {RepoToken}.slnx). DefineLayers() (:19-:28) returns one Framework(Layer.X, anchorType.Assembly) entry per package, using a single anchor type to resolve each assembly (mirrors the old PackageAssemblies helper): Shared, Domain, Application, Infrastructure, Api, Grpc, and Ui (:21-:27). The doc comment (:8-:13) records a deliberate omission: MMCA.Common.UI.Maui (ADR-042) is absent because its four MAUI TFM assemblies cannot load in the ubuntu net10.0 test process, so its UI-plus-Shared boundary is enforced at compile time by EnforceUIMauiLayerBoundary in Source/Build/MMCA.Common.LayerEnforcement.targets and the windows build-maui CI job instead.
  • Why it's built this way - one map per repo keeps the rule bodies DRY and identical everywhere (see the "Architecture Enforcement" section in MMCA.Common/CLAUDE.md); anchoring by type keeps the assembly reference refactor-safe.
  • Where it's used - supplied as Map by every thin *ConventionTests subclass in this unit, used directly by EventScopeFitnessTests as the module-less contrast case (EventScopeFitnessTests.cs:39), by EventUpcasterFitnessTests as the map that owns no upcasters (EventUpcasterFitnessTests.cs:62) and by DomainThrowFitnessTests as the framework's own ADR-013 reference run (DomainThrowFitnessTests.cs:143). It is also the pattern the single-layer fitness maps (SpecTestMap, IdempotencyTestMap, CancellationTestMap, CycleTestMap, FixtureAssemblyMap) collapse.

FrameworkModuleArchitectureMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain.EntityModel · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/EntityModel/FrameworkModuleArchitectureMap.cs:16 · Level 12 · class

  • What it is - a second architecture map for the framework repo that registers MMCA.Common's own Shared, Domain and Application assemblies as one business module named "Framework", so the module-scoped DDD rules have real subjects in a repo that otherwise declares no modules.
  • Depends on - ArchitectureMapBase (internal sealed class FrameworkModuleArchitectureMap : ArchitectureMapBase, FrameworkModuleArchitectureMap.cs:16), the Layer enum, LayerRef, and four anchor types: Result (:25), BaseEntity<> (:26), DomainEventDispatcher (:27) and ApplicationDbContext (:28).
  • Concept introduced - the same assemblies, declared twice, because the declaration decides which rules can see them. CommonArchitectureMap registers every package as a framework layer, which is the correct shape for layering and purity rules but leaves any rule that reads only IArchitectureMap's ModuleDomain, ModuleShared and ModuleApplication accessors with nothing to judge. The doc comment names those rules (sealed entities, no public setters, no public aggregate constructors, immutable DTOs, commands, queries and domain events) and the aggregates they then run over: the Notifications family, PushNotification and UserNotification (:5-:15). Without this map those facts would pass vacuously in the framework repo, and the framework would ship DDD rules it does not itself obey. [Rubric §4 - DDD] is the property; [Rubric §34 - Architecture Governance & Documentation] covers the framework holding itself to the bar it exports.
  • Walkthrough - public const string ModuleName = "Framework" (:19) is the module label. RepoToken => "MMCA.Common" (:21) matches the primary map. DefineLayers() (:23-:29) returns three Module(ModuleName, Layer.X, ...) entries for Shared, Domain and Application (:25-:27) and one Framework(Layer.Infrastructure, ...) entry (:28). Infrastructure stays a framework layer on purpose: the rules only read it to prove entities and DTOs do not live there (:13-:14). Api, Grpc and Ui are not registered at all, unlike CommonArchitectureMap.
  • Why it's built this way - adding module entries to CommonArchitectureMap would flip every rule that branches on map.ModuleNames.Count into judging the framework as a consumer; a separate map scoped to the two DDD bindings that need it keeps that switch untouched.
  • Where it's used - supplied as Map by exactly two classes in the same folder: EntityConventionTests (EntityConventionTests.cs:14) and ImmutabilityTests (ImmutabilityTests.cs:13).

FrameworkSanityTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FrameworkSanityTests.cs:13 · Level 12 · class

  • What it is - the home for the few architecture checks that are Common-only and do not generalize into the shared rule library: the MMCA.Common.Grpc transport boundary and the placement of the IMessageBus, IJwksProvider, and ILiveChannelPublisher abstractions.
  • Depends on - IMessageBus, IJwksProvider, ILiveChannelPublisher, and the NetArchTest Types query API routed through ArchitectureAssert.
  • Concept introduced - repo-specific sanity next to the shared library. Not every rule fits the parameterized base classes; some assert facts true only of the framework repo. Keeping them in one explicitly-named class documents the boundary between "shared rule applied here" and "Common-only invariant." [Rubric §7 - Microservices Readiness] (transport isolation) and [Rubric §3 - Clean Architecture] (abstraction placement) both apply: gRPC is pure transport and must not couple to Domain, Application, or Infrastructure, and the cross-cutting abstractions must sit in the layer their consumers depend on.
  • Walkthrough - three private static Assembly accessors anchor the Grpc, Application, and Infrastructure assemblies by an anchor type each (:15-:19). Three [Fact]s assert MMCA.Common.Grpc has no dependency on Domain, Application, or Infrastructure (:21-:34) via the AssertNoDependency helper (:51-:59), which runs a Types.InAssembly(...).ShouldNot().HaveDependencyOnAny(...) NetArchTest query and routes the result through ArchitectureAssert.NoViolations (:58). Three more [Fact]s assert placement by comparing the abstraction's declaring assembly against the anchored layer assembly: IMessageBus lives in Application (:36-:39), IJwksProvider in Infrastructure because it handles crypto and PEM material (:41-:44), and ILiveChannelPublisher in Application beside IPushNotificationSender (:46-:49).
  • Why it's built this way - the message-bus abstraction must stay in Application so application code depends on transport through it (extraction boundary, ADR-007); the JWKS provider is crypto and belongs in Infrastructure (ADR-004). These are load-bearing placements, so they get their own asserted facts.
  • Where it's used - an independent class in the Common architecture suite; it has no counterpart in Store or ADC because only Common owns the Grpc package and defines these abstractions.

ModuleIsolationTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleIsolationTestsBaseTests.cs:16 · Level 12 · class

  • What it is - coverage of the module-isolation rule set itself, and in particular of the one rule that closes a gap the six named rules leave open: a module's Domain reaching another module's Application.
  • Depends on - ArchitectureRules (seven isolation rules called by name, ModuleIsolationTestsBaseTests.cs:30,:48,:60-:65,:83), its nested StubMap, and the framework's Infrastructure and Domain assemblies as the two real subjects (:25,:44,:81).
  • Concept introduced - a cross product beats a hand-written pair list, and a test can document why. The six named isolation rules cover Domain, Application, Infrastructure and Api each against their own layer in another module, plus Domain and Application against another module's Infrastructure (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Modules.cs:7-:29). That leaves ten pairs unchecked, and one of them matters: a Sales.Domain -> Catalog.Application project reference passed every gate while compile-coupling two modules that ADR-007 and ADR-008 promise can be extracted separately. The per-module layer rules do not catch it because they forbid only the same module's higher layers, and the compile-time layer guard does not because it only knows MMCA.Common.* references (rule doc, :34-:41; class doc, :8-:14). ModuleInternalLayersAreIsolated (:47) closes it by iterating the full four-by-four product, with Layer.Ui deliberately excluded because a module's UI composing another module's UI is a real, intended arrangement in the shipped apps (:42-:45). MMCA.Common declares no modules, so every one of these rules is vacuous against the real map, which is exactly why a stub map is what makes the pairs assertable at all. [Rubric §7 - Microservices Readiness] is the property (ADR-059); [Rubric §34 - Architecture Governance & Documentation] is the second fact's contribution; [Rubric §14 - Testability] is the technique.
  • Walkthrough - a static readonly StubMap named CrossLayerViolation (:21-:25) declares module "Beta" owning one Domain assembly, and names MMCA.Common.Application as another module's Application layer. The assembly it registers is the framework's Infrastructure assembly, which really does depend on that namespace, so the violation is genuine rather than simulated (comment, :18-:20).
    • ModuleInternalLayersAreIsolated_CatchesADomainReachingAnotherModulesApplication (:27-:33): asserts the cross-product rule throws.
    • ModuleInternalLayersAreIsolated_CatchesAUiOrSharedReachingAnotherModulesApplication (:35-:51, M194): a [Theory] over Layer.Ui and Layer.Shared (:37-:39). It builds a StubMap whose source layer is that layer, with the same forbidden MMCA.Common.Application namespace, and asserts the cross-product rule throws. A module's UI or Shared reaching another module's Application is the same breach as Domain doing it, so both are sources the product must cover (as targets they stay legal, per the test comment at :35-:36); the failure message says a module must reach another only through its Shared contracts.
    • TheSixNamedRules_DoNotCoverThatPair (:53-:72): the unusual one. It calls all six named rules against the same violating map and asserts each one does not throw. Its inline comment states the purpose (:56-:57): it documents exactly why the cross-product rule exists, and if one of the six ever does start covering the pair, this test says so and the new rule can be re-scoped. That is a test whose failure is a prompt to think rather than a defect report.
    • ModuleInternalLayersAreIsolated_IsSatisfiedWhenNothingCrossesAModuleBoundary (:74-:86): a second stub map naming a namespace nothing references, over the framework's Domain assembly, asserted NotThrow. The discrimination half.
  • Why it's built this way - proving a gap requires asserting six negatives, which is only legible because they are listed as an array of Action and looped (:58-:71). Writing them as six separate facts would have buried the point. See ADR-015.
  • Where it's used - an independent class in the Common architecture suite. The rules it covers are bound for real by ModuleIsolationTestsBase in the two module-bearing consumer repos; MMCA.Common has no modules, so it runs none of them outside this file.

FrameworkModels

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:55 · Level 13 · class

  • What it is - a private static helper nested inside DeleteBehaviorConventionTests that builds a real, compiled EF Core model for each of the framework's two production engines, so the delete-behavior convention rule can inspect the SQLite model and the SQL Server model rather than reason about attributes in the abstract.
  • Depends on - SqliteDbContext and SQLServerDbContext (:63,:78), PhysicalDataSource and DataSourceKey.Default (:67,:82), AuditSaveChangesInterceptor and DomainEventSaveChangesInterceptor (:91-:95), IEntityDataSourceRegistry (:96), and Microsoft.EntityFrameworkCore.DbContextOptionsBuilder from the BCL.
  • Concept introduced - standing up the minimum viable ApplicationDbContext for a model-only assertion. Reading OnModelCreating output requires a constructed context, but constructing one drags in every service OnConfiguring demands. Services() (:88-:98) registers only what that base method actually resolves: an AuditSaveChangesInterceptor and a DomainEventSaveChangesInterceptor, both handed an explicit TimeProvider.System, the latter wired to a NoDomainEventDispatcher and a NoOutboxSignal, plus a NoEntityDataSources registry, each a do-nothing fake bound through the real interfaces. Nothing here touches a database; Sqlite() opens DataSource=:memory: and SqlServer() points at an unreachable connection string, because building the model does not execute a connection. [Rubric §34 - Architecture Governance & Documentation] is the property: a convention test that asserts against context.Model is asserting against the same object EF Core migrations generate from, which is a stronger guarantee than pattern-matching source text.
  • Walkthrough - two public factory methods and one private helper. Sqlite() (:57-:70) and SqlServer() (:72-:85) each build a DbContextOptionsBuilder<T>, construct the matching sealed context (SqliteDbContext or SQLServerDbContext) with a PhysicalDataSource keyed by DataSourceKey.Default(DataSource.Sqlite|SQLServer), and return context.Model after disposing the context. Services() (:88-:98) builds the shared ServiceProvider both factories pass in, passing timeProvider: TimeProvider.System explicitly to DomainEventSaveChangesInterceptor (:91-:95) rather than relying on its default.
  • Why it's built this way - engine parity is the point: the same convention rule runs against both models from the same fixture setup, so a delete-behavior default that only SQLite or only SQL Server gets right would show up as a two-model mismatch rather than pass silently on whichever engine the CI runner happened to exercise.
  • Where it's used - called from DeleteBehaviorConventionTests's [Theory] cases to source the model each case asserts against; not used outside that file.

EventVersioningConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventVersioningConventionTests.cs:12 · Level 13 · class

  • What it is - the two-line binding of the integration-event convention rules (SchemaVersion, BaseIntegrationEvent inheritance, namespace residency; ADR-010) to MMCA.Common's own map.
  • Depends on - EventConventionTestsBase (public sealed class EventVersioningConventionTests : EventConventionTestsBase, EventVersioningConventionTests.cs:12) and CommonArchitectureMap (:14).
  • Concept introduced - none new; the same shipped-binding shape FeatureFlagLifecycleTests below and every other *TestsBase subclass in this suite follow. The class doc names the one concrete event this binding proves against today: OutputCacheEvictionRequested in MMCA.Common.Domain.IntegrationEvents (:8-:10); EventUpcasterFitnessTests above proves the upcaster half of the same rule family against fixtures because the framework ships no upcaster of its own. [Rubric §6 - CQRS & Event-Driven] and [Rubric §9 - API & Contract Design] are the properties; [Rubric §34 - Architecture Governance & Documentation] is the shipped-binding discipline (ADR-010).
  • Walkthrough - one member, Map (:14), overridden to new CommonArchitectureMap(); the base class's facts run against MMCA.Common.Domain.IntegrationEvents.
  • Why it's built this way - the class doc states its own failure mode directly, that the rules fail the build the moment a new integration event ships that breaks the convention, rather than only surfacing when an old outbox row is replayed (:9-:10).
  • Where it's used - an independent class in the Common architecture suite; the same base is subclassed by MMCA.ADC and MMCA.Store with their own maps.

HandlerResultConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:12 · Level 13 · class

  • What it is - the two-line binding of the handler-result convention to MMCA.Common's own Application layer: every concrete command/query handler's TResult must be Result or Result<T>.
  • Depends on - HandlerResultConventionTestsBase (public sealed class HandlerResultConventionTests : HandlerResultConventionTestsBase, HandlerResultConventionTests.cs:12) and CommonArchitectureMap (:14). The class doc names ResultFailureFactory's InvalidOperationException as the runtime-only enforcement of the same constraint (:9-:10).
  • Concept introduced - catching at build time what would otherwise only surface as a runtime exception. Without this rule, a handler declared with a non-Result TResult is invisible until the decorator pipeline's ResultFailureFactory throws against it in production, which is what the class doc calls out directly. Running the check over the framework's own Notifications command/query handlers is the same self-application pattern DomainThrowFitnessTests uses for the Domain layer. [Rubric §6 - CQRS & Event-Driven] is the property (a uniform Result-based handler contract the decorator pipeline and error catalog both assume); [Rubric §34 - Architecture Governance & Documentation] is catching the drift in CI rather than at first request.
  • Walkthrough - one member, Map (:14), overridden to new CommonArchitectureMap(); the [Fact]s that scan every concrete ICommandHandler/IQueryHandler implementation for a non-conforming TResult live in the base.
  • Why it's built this way - the same one-class-per-repo shape every *TestsBase subclass in this suite follows (ADR-015); the constraint itself is ADR-013's Result pattern applied at the handler boundary.
  • Where it's used - an independent class in the Common architecture suite; the rule and its fixtures live entirely in the base, exercised for real against MMCA.Common's own Notifications handlers.

IdempotencyConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:10 · Level 13 · class

  • What it is - the two-line binding of the idempotency convention gate to MMCA.Common's own API layer: every POST it ships must either replay on an Idempotency-Key or state in code why it must not.
  • Depends on - IdempotencyConventionTestsBase (public sealed class IdempotencyConventionTests : IdempotencyConventionTestsBase, IdempotencyConventionTests.cs:10) and CommonArchitectureMap (:12).
  • Concept introduced - none new; the class doc frames the rule as a gate rather than a blanket requirement, "either replays ... or says in code why it must not" (:7-:8), which matches the [Idempotent] attribute mechanics described in MMCA.Common/CLAUDE.md (Idempotency: Idempotency-Key header, 24h replay cache, BuildCacheKey joining subject/method/route/client key, IDistributedLock mutual exclusion). [Rubric §11 - Security] and [Rubric §9 - API & Contract Design] are the properties (duplicate-request safety documented per endpoint); [Rubric §34 - Architecture Governance & Documentation] is the escape-hatch-with-reasons discipline the same doc comment style enforces elsewhere in this suite.
  • Walkthrough - one member, Map (:12); the base class's [Fact]s enumerate every POST action in the assemblies the map registers and check for [Idempotent] or a documented exemption.
  • Why it's built this way - the same shipped-binding shape as every other *TestsBase subclass in this suite (ADR-015); the idempotency mechanics themselves are ADR-017.
  • Where it's used - an independent class in the Common architecture suite; MMCA.ADC and MMCA.Store subclass the same base with their own maps.

LayerDependencyTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyTests.cs:9 · Level 13 · class

  • What it is - the two-line binding of the Clean Architecture layer-flow rules to MMCA.Common's own packages: API/Grpc -> Infrastructure -> Application -> Domain -> Shared, with UI/Grpc restricted to Shared only and UI.Web sitting above both (MMCA.Common/CLAUDE.md, Architecture).
  • Depends on - LayerDependencyTestsBase (public sealed class LayerDependencyTests : LayerDependencyTestsBase, LayerDependencyTests.cs:9) and CommonArchitectureMap (:11).
  • Concept introduced - the runtime half of a two-gate enforcement scheme. MMCA.Common/CLAUDE.md documents two enforcement gates for the same layer rule: a compile-time one, Source/Build/MMCA.Common.LayerEnforcement.targets, which fails the build on a forbidden ProjectReference, and a runtime one, this NetArchTest suite, which fails the build on a forbidden namespace dependency the compile-time gate cannot see (a reflection-based call, a string-typed reference). The two are meant to move together; the doc states new rules go in both. [Rubric §3 - Clean Architecture] is the property directly asserted; [Rubric §34 - Architecture Governance & Documentation] is the two-gate discipline.
  • Walkthrough - one member, Map (:11); the base class's facts assert the forward layer flow and the UI/Grpc/UI.Web exceptions over the assemblies CommonArchitectureMap registers.
  • Why it's built this way - the same one-class-per-repo shape every *TestsBase subclass in this suite follows (ADR-015).
  • Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps.

LocalizedTextConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:11 · Level 13 · class

  • What it is - the binding of the hard-coded-literal rule to MMCA.Common's own UI package: snackbar messages, page titles, <PageTitle> markup and breadcrumb labels in MMCA.Common.UI must resolve through IStringLocalizer (ADR-027), not a raw string, and a <DeleteConfirmation> must receive a localized EntityType noun. It also carries its own fixture-driven facts that pin how ArchitectureRules judges that DeleteConfirmation case.
  • Depends on - LocalizedTextConventionTestsBase (public sealed class LocalizedTextConventionTests : LocalizedTextConventionTestsBase, LocalizedTextConventionTests.cs:11), CommonArchitectureMap (:15), and ArchitectureRules.UserVisibleTextIsLocalized (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.LocalizedText.cs:62), which the fixture helper calls directly (LocalizedTextConventionTests.cs:136).
  • Concept introduced - a floor count as a scan-root safety valve, plus rule-behavior facts against a throwaway fixture. The class overrides MinimumScannedFiles => 20 (:18); the inline comment gives the reason: MMCA.Common.UI alone carries about 30 razor files, so a floor of 20 catches a wrong scan root, where a rule that scans zero files would otherwise report a false pass (no violations because nothing was checked). The rule enforces that floor with razorFiles.Count >= minimumScannedFiles (ArchitectureRules.LocalizedText.cs:76). Separately, the private helper ScanOneRazorFile (LocalizedTextConventionTests.cs:128) writes one Probe.razor file (FixtureFileName, :13) into a fresh temp directory, runs the rule over it with no allowed suffixes, and deletes the directory in a finally, so the facts test the rule's line-level judgment rather than the repo's current markup. [Rubric §27 - i18n] is the property; [Rubric §34 - Architecture Governance & Documentation] is the false-pass guard.
  • Walkthrough - Map (:15) and MinimumScannedFiles (:18) bind the inherited scan to the Common source tree. The nine DeleteConfirmation_* facts and theories pin the EntityType check:
    • a literal EntityType="Product" fails, and the message names the fixture file and its offending line (it must contain {FixtureFileName}:1, :31) and the attribute EntityType (:26); an empty literal EntityType="" also fails (:53);
    • a razor expression (including the nested-quote shape @L["Entity.Product"].Value), a variable, or an omitted attribute passes (:44);
    • a tag split across lines is judged as one element and the violation is reported on line 3, where the attribute sits, not the line that opens the tag (:65); the same shape with an expression passes (:80);
    • the check is scoped to the <DeleteConfirmation> element, so a literal EntityType on <EntityBadge>, alone or after a closed DeleteConfirmation tag, passes (:101);
    • a namespace-qualified tag (MMCA.Common.UI.Components.Forms.DeleteConfirmation) still fails (:110);
    • a line carrying the i18n: allow marker passes (:120). The rule side matches: the opening-tag regex accepts an optional dotted prefix (ArchitectureRules.LocalizedText.cs:35), ScanFile skips any i18n: allow line (:98), and it tracks the tag from its opening line until the start tag ends, yielding {relative}:{i + 1} passes a literal EntityType to DeleteConfirmation (:136-151).
  • Why it's built this way - the same one-class-per-repo shape every *TestsBase subclass follows, plus the floor count because a scan rule that silently scans nothing is a worse failure mode than one that reports a real violation. The DeleteConfirmation facts live here because the shared dialog renders EntityType in its title and prompt (the rule's own comment cites "Eliminar Product" for a Spanish user, ArchitectureRules.LocalizedText.cs:133-135), and a multi-line, scoped tag check is easy to regress into either a false pass or a false positive on an unrelated component.
  • Where it's used - an independent class in the Common architecture suite; ADC carries its own LocalizedTextConventionTests subclass of the same base (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:15). The shared chrome MMCA.Common.UI ships is what every consumer's UI is built on top of, so this rule is what keeps that chrome localizable.

MicroserviceExtractionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:11 · Level 13 · class

  • What it is - the two-line binding of the transport-boundary rule to MMCA.Common's own packages: Domain, Application and Shared must stay free of MassTransit, gRPC and Protobuf types, so application and domain code talks only to abstractions while transport choices live at the edges (MMCA.Common/CLAUDE.md, Microservices Extraction Boundaries).
  • Depends on - MicroserviceExtractionTestsBase (public sealed class MicroserviceExtractionTests : MicroserviceExtractionTestsBase, MicroserviceExtractionTests.cs:11) and CommonArchitectureMap (:13); the class doc points to its Common-specific sibling, FrameworkSanityTests, for the Grpc package's own boundary and abstraction-placement checks that the shared base does not cover.
  • Concept introduced - none new; MMCA.Common/CLAUDE.md names the concrete instance this rule enforces: IMessageBus lives in Application, while InProcessMessageBus and BrokerMessageBus (MassTransit) live in Infrastructure, selected by MessageBusSettings. This class is what turns that prose boundary into a CI failure the moment Application, Domain or Shared references MassTransit directly. [Rubric §7 - Microservices Readiness] is the property being defended: the monolith-to-service Strangler Fig move (ADR-008) depends on transport being swappable at the edge without touching business logic.
  • Walkthrough - one member, Map (:13); the base class's facts scan the Domain, Application and Shared assemblies the map registers for a forbidden reference to MassTransit, Grpc.Core/Grpc.Net.Client, or generated Protobuf types.
  • Why it's built this way - the same one-class-per-repo shape every *TestsBase subclass follows; keeping the framework-specific extras in a sibling class (FrameworkSanityTests) rather than widening this one keeps the shared rule identical to what Store and ADC subclass.
  • Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps.

NamespaceCycleTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleTests.cs:9 · Level 13 · class

  • What it is - the MMCA.Common binding of the namespace-acyclicity rule: a thin subclass that supplies the repo's map and then spends twenty-nine lines arguing, edge by edge, for the one namespace cycle the framework accepts.
  • Depends on - NamespaceCycleTestsBase (public sealed class NamespaceCycleTests : NamespaceCycleTestsBase, NamespaceCycleTests.cs:9) and CommonArchitectureMap (:11); the rule body and its fact counts live in the base, and the rule's behavior is proven against fixtures by NamespaceCycleFitnessTests.
  • Concept introduced - an allowlist that must name a whole strongly connected component, not an edge. A cycle-breaking exemption is the easiest place in an architecture suite to hide a design failure, so this override is written to make hiding expensive. AllowedCycleNamespaces lists three namespaces, not three edges, because the allowance covers the entire SCC: a fourth namespace joining the tangle changes the component and still fails the test (:37-:38). And the justification sits in the compiled file next to the list, so a diff that adds a fourth entry is a diff that has to argue against three paragraphs. [Rubric §15 - Best Practices & Code Quality] is the property being defended; [Rubric §34 - Architecture Governance & Documentation] is the escape-hatch-with-reasons discipline (ADR-015).
  • Walkthrough - two members. Map (:11) is the usual override. AllowedCycleNamespaces (:42-:47) returns MMCA.Common.Infrastructure, MMCA.Common.Infrastructure.Persistence and MMCA.Common.Infrastructure.Messaging (:44-:46), the accepted root -> Messaging -> Persistence -> root tangle inside a single assembly and a single package (:14-:16). Its doc comment (:13-:41) takes the three edges one at a time:
    • root -> Messaging (:19-:20): DependencyInjection lives in the root namespace and binds the buses and their settings, which is what a composition root is for.
    • Messaging -> Persistence (:23-:27): the buses are the outbox transport. InProcessEventBus and BrokerEventBus enqueue OutboxMessage rows and wake the OutboxProcessor through IOutboxSignal, the consumers resolve the physical source through IDataSourceResolver, and the outbox reports through BrokerMetrics and reads MessageBusSettings. Splitting the two would put half of one delivery guarantee on each side of a package boundary.
    • Persistence -> root (:30-:34): the EntityTypeConfiguration* shims carry [UseDataSource] and [UseDatabase], marker attributes that stay in the root namespace because consumers annotate their own configurations with them. Pushing them down into Persistence would deepen the public annotation surface for every consumer in order to fix an internal graph edge.
  • Why it's built this way - a comment closes with the history of the exemption itself (:38-:40): before the 2026-09 feature-by-folder reorganization the third node was MMCA.Common.Infrastructure.Settings, that folder dissolved into the features it configured, and TenancySettingsValidator, which carried the old Settings -> Persistence edge, now lives under Persistence/Tenancy. The list moved because the code moved, which is the argument for keeping the allowlist in a compiled file rather than a wiki page nobody re-reads.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI's build-and-test job (fast, no database). Store and ADC subclass the same base with their own maps and their own allowlists.

AggregateConventionTests, CancellationTokenConventionTests, DomainPurityTests, EventVersioningConventionTests, HandlerResultConventionTests, IdempotencyConventionTests, LayerDependencyTests, LocalizedTextConventionTests, MicroserviceExtractionTests, NamespaceCycleTests, PiiConventionTests, RawQueryableConventionTests, SliceCohesionTests, StateManagementConventionTests, UIArchitectureConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · (see per-type table) · Level 13 · class

These fifteen sealed classes share one shape: each is a thin subclass of a shared *TestsBase rule from the MMCA.Common.Testing.Architecture package, supplying the repo's CommonArchitectureMap (and, for a few, one extra override) so the same rule body runs identically across MMCA.Common, MMCA.Store, and MMCA.ADC. This is the [Rubric §34 - Architecture Governance & Documentation] and [Rubric §14 - Testability] story: architecture conventions are executable and enforced in CI rather than left to review, and the rule logic lives in exactly one place (ADR-015). See the thin-subclass pattern introduced by DependencyVersionTests. The canonical body of each rule is the corresponding *TestsBase; these subclasses only wire in the map and any repo-specific floor or allowlist. Each fails the build-and-test CI job on violation, and a couple are deliberately vacuous today (they assert nothing until the framework grows a type that could break the convention, at which point they fire).

Where a subclass carries an override beyond Map, that override is itself a documented architectural decision, not configuration: the exemption lists below (NamespaceCycleTests, CancellationTokenConventionTests, StateManagementConventionTests, RawQueryableConventionTests) all carry their justification in code, which is the point of putting the escape hatch in a compiled file rather than a wiki.

Type File:Line Base rule What it enforces / what differs
AggregateConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/AggregateConventionTests.cs:9 AggregateConventionTestsBase DDD aggregate-root factory rules for the framework's own aggregates: Domain exposes aggregate roots, each has a Result<T>-returning static Create factory and no public constructor. The minimal variant for repos with no business modules. Supplies only Map (:11). [Rubric §4 - DDD.]
CancellationTokenConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenConventionTests.cs:10 CancellationTokenConventionTestsBase The trailing-token convention taught above, applied to the real Application and Infrastructure packages. Supplies Map (:12) and overrides CancellationTokenExemptMethods with the two SignalR hub methods NotificationHub.JoinChannelAsync and NotificationHub.LeaveChannelAsync (:23-:27). The justification (:14-:22) is worth reading: a hub method signature is the client-visible RPC contract, bound by name and argument list by SignalR's dispatcher, so adding a parameter would break every shipped consumer's client for a token the hub already has (both methods pass Context.ConnectionAborted straight into the group calls). [Rubric §12 - Performance & Scalability.]
DomainPurityTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/DomainPurityTests.cs:9 DomainPurityTestsBase Domain and Shared stay framework-free, and Application stays host-agnostic (no EF Core, no ASP.NET Core). Supplies only Map (:11); the base's extra-forbidden-dependency hook (used by Store for "Stripe", ADC for "RabbitMQ") stays empty here. [Rubric §3 - Clean Architecture.]
EventVersioningConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventVersioningConventionTests.cs:12 EventConventionTestsBase Every integration event declares a SchemaVersion, inherits BaseIntegrationEvent, and lives in an *.IntegrationEvents namespace (ADR-010). Supplies only Map (:14). No longer vacuous: the framework ships one concrete integration event today, OutputCacheEvictionRequested in MMCA.Common.Domain.IntegrationEvents (class doc, :7-:10). [Rubric §6 - CQRS & Event-Driven.]
HandlerResultConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:12 HandlerResultConventionTestsBase Every concrete command/query handler's TResult must be Result or Result<T>, the constraint the decorator pipeline otherwise only enforces at runtime when ResultFailureFactory throws on a short-circuit (:9-:10); scans the framework's Notifications handlers. Supplies only Map (:14). [Rubric §6 - CQRS & Event-Driven.]
IdempotencyConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:10 IdempotencyConventionTestsBase The POST idempotency-intent gate proven by IdempotencyFitnessTests, applied to the framework's own API layer: every POST it ships either replays on an Idempotency-Key or says in code why it must not (:6-:8). Supplies only Map (:12). [Rubric §9 - API & Contract Design, Rubric §29 - Resilience.]
LayerDependencyTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyTests.cs:9 LayerDependencyTestsBase Clean Architecture layer-flow for the framework packages: Domain, Application, Infrastructure, Shared, and Ui each reference only what they may, plus the map-completeness facts LayerMapDeclaresLayers and ModulesDeclareLayers whose override is proven by LayerDependencyOverrideTests. Supplies only Map (:11), so the base's default required-layer set applies and the per-module half is vacuous here. [Rubric §3 - Clean Architecture.]
LocalizedTextConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:11 LocalizedTextConventionTestsBase Shared MMCA.Common.UI ships no hard-coded user-visible literals: snackbar messages, page titles, <PageTitle> markup, and breadcrumb labels must resolve through IStringLocalizer (ADR-027). Supplies Map (:13) and overrides MinimumScannedFiles => 20 (:16), a floor that catches a wrong scan root (the comment notes MMCA.Common.UI alone carries about 30 razor files, :15). [Rubric §27 - i18n.]
MicroserviceExtractionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:11 MicroserviceExtractionTestsBase Domain, Application, and Shared stay free of MassTransit, Grpc, and Protobuf, so a module behaves identically in-process or extracted (ADR-007, ADR-008). Supplies only Map (:12); Common-only transport sanity lives in FrameworkSanityTests instead (:7-:8). [Rubric §7 - Microservices Readiness.]
NamespaceCycleTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleTests.cs:9 NamespaceCycleTestsBase The acyclicity rule proven by NamespaceCycleFitnessTests, applied to the real framework packages. Supplies Map (:11) and overrides AllowedCycleNamespaces with the one accepted tangle inside MMCA.Common.Infrastructure: root, .Persistence, .Settings (:39-:44). The 26-line justification (:13-:38) argues each of the three edges individually (composition root binds settings; TenancySettingsValidator takes an optional IDataSourceResolver so a bad tenant override fails the boot instead of resolving cross-tenant at runtime; the [UseDataSource] and [UseDatabase] marker attributes stay in the root namespace because consumers annotate with them) and notes that none of the three is separately extractable anyway, being one assembly and one package. Because the allowance must cover the whole strongly connected component, a fourth namespace joining the tangle still fails. [Rubric §15 - Best Practices & Code Quality.]
PiiConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiConventionTests.cs:25 PiiConventionTestsBase Every domain entity declaring a [Pii] property implements IAnonymizable (ADR-005). Supplies Map (:98) and one AnonymizableSamples entry for RefreshSession (:100-:112), and adds a name-based marking rule (:114-:134), so it is no longer structurally vacuous in the framework (class doc, :6-:24); the machinery is proven non-vacuously by PiiErasureContractFitnessTests. [Rubric §30 - Compliance/Privacy.]
RawQueryableConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:13 RawQueryableConventionTestsBase Bans the raw IQueryable repository surfaces in Application code via a textual scan; a raw-queryable handler is EF-coupled and cannot move behind a gRPC boundary. Because Common declares no modules, it overrides ApplicationSourceDirectories() (:18-:22) to scan the framework's own Source/Core/MMCA.Common.Application project (resolving the repo root through ArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx"), :20), and overrides AllowedFiles (:25-:37) to whitelist the deliberate composition root EntityQueryService.cs and the five Notifications handlers whose cross-entity joins are the documented exception. Supplies Map at :15. [Rubric §8 - Data Architecture.]
SliceCohesionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionTests.cs:10 SliceCohesionTestsBase Handlers and validators each sit in the same namespace as the command or query they serve, so a Notifications use-case slice stays one cohesive unit. Supplies only Map (:12). [Rubric §5 - Vertical Slice.]
StateManagementConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/StateManagementConventionTests.cs:11 StateManagementConventionTestsBase The shared MMCA.Common.UI assembly carries no mutable static state (a static is shared across every Blazor Server circuit) and its stateful services stay scoped rather than singleton. Supplies Map (:13), carries facts proving the base's singleton scanner (:24-:100), and overrides AllowedStaticMembers to whitelist MMCA.Common.UI.Pages.Common.ErrorMessages._localizer (:21-:22), a write-once wiring point configured idempotently by the root layout, not per-user state (:15-:20). [Rubric §19 - State Management.]
UIArchitectureConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:11 UIArchitectureConventionTestsBase Holds the container/presentational split with mechanical caps: *.razor.cs code-behind files and inline @code blocks each stay within the base's convention limit. Supplies only Map (:13), so the base's defaults apply. [Rubric §18 - UI Architecture.]
  • Why they're built this way - see the two-layer "Architecture Enforcement" model in MMCA.Common/CLAUDE.md: rules are enforced at compile time (Source/Build/MMCA.Common.LayerEnforcement.targets) and at runtime here, with the runtime bodies factored into one shared package so Common, Store, and ADC stay identical. Each subclass exists only so xUnit discovers the rule in this repo's assembly with this repo's map.
  • Where they're used - all fifteen run in the MMCA.Common.Architecture.Tests project during CI's build-and-test job (fast, no database).
  • Caveats / not-in-source - the per-rule fact counts live in each *TestsBase, not in these subclasses; the base sections elsewhere in this chapter are the authority on exactly what each rule asserts.

AggregateConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/AggregateConventionTests.cs:9 · Level 13 · class

  • What it is - the two-line binding of the DDD aggregate-root factory rules to MMCA.Common's own map (AggregateConventionTests.cs:5-:9).
  • Depends on - AggregateConventionTestsBase (:9) and CommonArchitectureMap (:11).
  • Concept - none new; the same shipped-binding shape as AiDependencyIsolationTests and FeatureFlagLifecycleTests: the rule body and its facts live once in AggregateConventionTestsBase, and each repo supplies only its own IArchitectureMap.
  • Walkthrough - public sealed class AggregateConventionTests : AggregateConventionTestsBase (:9) overrides Map with new CommonArchitectureMap() (:11); it declares no members of its own.
  • Why it's built this way - the same one-class-per-repo pattern every other *TestsBase subclass in this suite uses (ADR-015).
  • Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps.

AiDependencyIsolationTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/AiDependencyIsolationTests.cs:14 · Level 13 · class

  • What it is - the two-line binding of the model-boundary fitness rules to MMCA.Common's own map: a language-model dependency stays confined to Infrastructure and to the governed MMCA.Common.AI package, checked in both directions.
  • Depends on - AiDependencyIsolationTestsBase (AiDependencyIsolationTests.cs:14) and CommonArchitectureMap (:16).
  • Concept introduced - none new; this is the shipped binding of the base class's two rules for the framework's own assemblies, the same shape as FeatureFlagLifecycleTests two sections below. AiDependencyIsolationTestsBase documents the concept: an AI SDK is one of the two ways the model boundary leaks (naming the vendor SDK directly, or taking MMCA.Common.AI as a convenience and passing a PromptContract around as a domain type), and either turns "the app calls a model at one place" into a dependency spread through the app that cannot be versioned, evaluated, capped or swapped. [Rubric §16 - AI-Native Application Architecture] is the property this class defends for MMCA.Common itself.
  • Walkthrough - overrides Map with new CommonArchitectureMap() (:16); the two [Fact]s live entirely in the base class and run ArchitectureRules.LanguageModelSdksStayAtTheModelBoundary and ArchitectureRules.GovernedAiPackageStaysBehindInfrastructure against it.
  • Why it's built this way - the same one-class-per-repo pattern every other *TestsBase subclass in this suite uses (ADR-015): the rule body lives once in the shared package, and each repo supplies only its own IArchitectureMap.
  • Where it's used - an independent class in the Common architecture suite; MMCA.ADC subclasses the same base with its own map (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/AiDependencyIsolationTests.cs).

CancellationTokenConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenConventionTests.cs:10 · Level 13 · class

  • What it is - the trailing-CancellationToken convention binding for MMCA.Common's own Application and Infrastructure packages, plus an override list for the one signature shape the rule cannot see through.
  • Depends on - CancellationTokenConventionTestsBase (CancellationTokenConventionTests.cs:10) and CommonArchitectureMap (:12).
  • Concept - a SignalR hub method is not an ordinary public API: it IS the client-visible RPC contract, bound by name and argument list by SignalR's dispatcher, and every shipped consumer's JavaScript/Blazor client already invokes it with a fixed argument list. Adding a trailing token parameter would change that wire contract for a token the hub already has some other way. CancellationTokenExemptMethods is the class's one override (:23-:27), naming NotificationHub.JoinChannelAsync and NotificationHub.LeaveChannelAsync: both already pass Context.ConnectionAborted into their group calls, so the work is cancellable without a parameter the rule can find.
  • Walkthrough - public sealed class CancellationTokenConventionTests : CancellationTokenConventionTestsBase (:10) overrides Map with new CommonArchitectureMap() (:12) and overrides CancellationTokenExemptMethods to return the two hub method names (:23-:27).
  • Why it's built this way - the exemption list documents, at the point the rule would otherwise fail, exactly why a hub method is the one legitimate exception to the trailing-token convention, rather than leaving a future reader to rediscover it.
  • Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps and (where needed) their own exemption lists.

DomainPurityTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/DomainPurityTests.cs:9 · Level 13 · class

  • What it is - the framework-independence rule binding for MMCA.Common's own map: Domain and Shared stay free of framework dependencies, and Application stays host-agnostic.
  • Depends on - DomainPurityTestsBase (DomainPurityTests.cs:9) and CommonArchitectureMap (:11).
  • Concept - none new; the same shipped-binding shape as AggregateConventionTests and AiDependencyIsolationTests: the rule body and its facts live once in DomainPurityTestsBase, and each repo supplies only its own IArchitectureMap. This is the class that pins Clean Architecture's inward-only dependency direction for the framework itself, since MMCA.Common has no modules whose Domain/Application layers could be checked any other way.
  • Walkthrough - public sealed class DomainPurityTests : DomainPurityTestsBase (:9) overrides Map with new CommonArchitectureMap() (:11); it declares no other members.
  • Why it's built this way - the same one-class-per-repo pattern every other *TestsBase subclass in this suite uses (ADR-015).
  • Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps.

PiiConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiConventionTests.cs:25 · Level 13 · class

  • What it is - the framework repo's binding of the PII rules (ADR-005, rubric section 30): a sealed subclass that inherits the [Pii]-implies-IAnonymizable rule, adds a second, framework-only rule of its own (every string property whose name says it holds a person's email, name, phone number or address carries [Pii]), and supplies one anonymization sample so the inherited reverse rule runs against a real framework entity (class doc, PiiConventionTests.cs:6-:24).
  • Depends on - PiiConventionTestsBase (public sealed class PiiConventionTests : PiiConventionTestsBase, :25), CommonArchitectureMap and the IArchitectureMap abstraction it satisfies (:98), the Layer enum it queries the map with (:142-:143), RefreshSession (the MMCA.Common.Domain.Auth entity used as the sample, :103), and, by name only, PiiAttribute, IAnonymizable and PiiRedactor.
  • Concept introduced - a marking rule that gives a vacuous erasure rule something to check. The inherited fact, EntitiesWithPiiProperties_ShouldImplement_IAnonymizable (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/PiiConventionTestsBase.cs:34), only fires on a property that already carries [Pii]; a property nobody marked is invisible to it, and PiiRedactor logs that value in clear text (class doc, :14-:15). This subclass closes that gap by deciding from the property name which members hold personal data and requiring the marker on each. The name rule is deliberately narrow: a contact detail is any name containing Email, Phone or Address (ContactFragments, :88), and a person's name is a property ending in one of nine names such as FirstName, DisplayName or UserName (PersonNames, :92-:96). A bare Name is excluded on purpose, because in the framework it names a role, permission, event or file, never a person (:90-:91). [Rubric §30 - Compliance/Privacy/Data Governance] is the property (ADR-005).
  • Walkthrough - Map (:98) is the usual override. AnonymizableSamples (:100-:112) overrides the base's empty default (PiiConventionTestsBase.cs:31) with one entry keyed by typeof(RefreshSession): a static factory that calls RefreshSession.Create(...) with fixed fixture values (user id 1, a refresh token, UTC created and expiry dates in October 2026, a documentation-range IP address and a user agent, :103-:110) and returns .Value! (:111). The framework has no module Domain assembly, so the base's coverage fact scans nothing here; this sample is what keeps the reverse rule PropertiesOverwrittenByAnonymize_MustCarryPii (PiiConventionTestsBase.cs:43) exercised against a real entity in this repo (class doc, :16-:19). PersonalDataMembers (:140-:153) builds the scanned population: every type of the map's Domain layer (:142), plus the Application types whose namespace is, or sits under, one of MMCA.Common.Application.Auth, .Users or .Notifications (PersonalDataApplicationNamespaces, :80-:85; filter :143-:147); from those it keeps non-compiler-generated classes (:150), takes their public instance properties declared on the type itself (:151), and keeps the string-typed ones whose name passes IsPersonalDataName (:152, :155-:157). Two facts then run over it. PersonalDataMembers_AreDiscovered_GateIsNotVacuous (:114-:118) asserts the population is non-empty, so a drifted namespace list or name rule fails rather than leaving a gate that checks nothing. PersonalDataMembers_ShouldCarry_PiiAttribute (:120-:134) collects every member with no attribute named PiiAttribute (:27, matched by type name with inherit: true, :124-:125) and fails with the sorted DeclaringType.Property list in the message (:126-:133).
  • Why it's built this way - the Shared assembly is out of scope by construction, not by omission: it does not reference Domain, where [Pii] lives, so a Shared DTO cannot carry the marker (class doc, :21-:22). Matching the attribute by type name rather than by typeof keeps the check independent of which assembly declared the attribute. The sample is built with a literal factory call rather than reflection so the test stays a statement of what Anonymize() must overwrite. The redaction and erasure machinery the rules feed is proven end to end separately by PiiErasureContractFitnessTests (class doc, :22-:23).
  • Where it's used - discovered and run by xUnit as part of MMCA.Common.Architecture.Tests in CI's build-and-test job; nothing constructs it. MMCA.ADC subclasses the same base against its own map (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Privacy/PiiConventionTests.cs), without the framework-only marking rule.

SliceCohesionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionTests.cs:10 · Level 13 · class

  • What it is - the framework repo's binding of the vertical-slice cohesion fitness rules (rubric section 5): a three-line sealed subclass that supplies the map and inherits both [Fact]s, so the framework's own Notifications use-case slices are held to the co-location rule.
  • Depends on - SliceCohesionTestsBase (public sealed class SliceCohesionTests : SliceCohesionTestsBase, SliceCohesionTests.cs:10), CommonArchitectureMap and the IArchitectureMap abstraction it satisfies (SliceCohesionTests.cs:12).
  • Concept - the same base-plus-thin-subclass shape as StronglyTypedIdConventionTests and StateManagementConventionTests: the rule lives once in MMCA.Common.Testing.Architecture and is re-run per repo by a subclass whose only job is to name the repo. A use-case slice keeps its command/query, its handler, and its validator together in one namespace, so a feature stays a cohesive unit rather than spreading across horizontal Handlers//Validators/ folders (SliceCohesionTestsBase.cs:4-:8).
  • Walkthrough - one member: protected override IArchitectureMap Map { get; } = new CommonArchitectureMap(); (SliceCohesionTests.cs:12). The two inherited facts, Handlers_ShouldBeCoLocatedWith_TheirContracts and Validators_ShouldBeCoLocatedWith_TheirContracts, call ArchitectureRules.HandlersAreCoLocatedWithTheirContracts(Map) and ArchitectureRules.ValidatorsAreCoLocatedWithTheirContracts(Map) respectively (SliceCohesionTestsBase.cs:14-:20).
  • Why it's built this way - MMCA.Common scopes the rule to its Notifications slices, the one place the framework repo itself has command/query/handler/validator trios; ADC and Store subclass the same base scoped to their module Application layers instead (SliceCohesionTestsBase.cs:7-:8).
  • Where it's used - discovered and run by xUnit as part of MMCA.Common.Architecture.Tests; nothing constructs it.

UIArchitectureConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:11 · Level 13 · class

  • What it is - the framework repo's binding of the shared UI-architecture fitness rules: a three-line sealed subclass that supplies the map and inherits both [Fact]s, so MMCA.Common's own shared pages and primitives are held to the code-behind and inline-@code caps.
  • Depends on - UIArchitectureConventionTestsBase (public sealed class UIArchitectureConventionTests : UIArchitectureConventionTestsBase, UIArchitectureConventionTests.cs:11), CommonArchitectureMap and the IArchitectureMap abstraction it satisfies (UIArchitectureConventionTests.cs:13).
  • Concept - cross-references the base-plus-thin-subclass shape of the shared fitness bases (see CancellationTokenConventionTestsBase and AggregateConventionTestsBase for the same arrangement): the rule is authored once in MMCA.Common.Testing.Architecture and re-run per repo by a subclass whose only job is to name the repo. [Rubric §18] is the category the rule's own doc comment cites (UIArchitectureConventionTests.cs:6): it asks whether the container/presentational split is actually held, and this class is the mechanical answer for the framework repo. The caps are convention rather than measurement, so the base marks them virtual and the doc comment says they should move only with a recorded decision (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/UIArchitectureConventionTestsBase.cs:11-:12); this subclass overrides none of them.
  • Walkthrough - one member: protected override IArchitectureMap Map { get; } = new CommonArchitectureMap(); (UIArchitectureConventionTests.cs:13). Everything else is inherited. The map is the only thing the base needs from a repo: it takes Map.RepoToken, finds the repo root by walking up to {RepoToken}.slnx, and scans that repo's Source/ tree (UIArchitectureConventionTestsBase.cs:89, :91-:92), skipping obj/ and bin/ plus any ExcludedPathFragments (:97-:99, empty here since the subclass does not override :41). The two inherited facts then run: CodeBehinds_StayWithinTheLineCap globs *.razor.cs and fails any file over MaxCodeBehindLines = 400 (:44, :46, :22), first asserting at least MinimumCodeBehindFiles = 1 file was discovered so a glob that matched nothing cannot pass vacuously (:35); RazorFiles_KeepInlineCodeBlocksSmall globs *.razor, measures each file's inline block from the first line starting with @code to end of file, and fails anything over MaxInlineCodeLines = 120 (:63, :65, :29).
  • Why it's built this way - the caps catch the failure mode directly (a ballooning code-behind is the visible symptom of page logic that belongs in an injected UI service or an extracted sub-component, UIArchitectureConventionTestsBase.cs:5-:10), and putting them in CI makes the split enforced rather than review-enforced. Measuring the @code block as "the tail of the file" is a deliberate convention shortcut (:24-:27) rather than a Razor parse.
  • Where it's used - discovered and run by xUnit as part of MMCA.Common.Architecture.Tests; nothing constructs it. Its two sibling bindings are the identical subclasses in the consumer repos, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:10 (map: AdcArchitectureMap) and MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:9 (map: StoreArchitectureMap), neither of which overrides a cap either.

StateManagementConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/StateManagementConventionTests.cs:11 · Level 13 · class

  • What it is - the framework repo's binding of the state-management convention fitness functions (rubric section 19): a sealed subclass that supplies the map and one recorded exemption, so MMCA.Common.UI itself is held to the per-circuit state model Blazor Server requires, and that also carries the facts proving the base's singleton source scanner.
  • Depends on - StateManagementConventionTestsBase (public sealed class StateManagementConventionTests : StateManagementConventionTestsBase, StateManagementConventionTests.cs:11), CommonArchitectureMap and the IArchitectureMap abstraction it satisfies (StateManagementConventionTests.cs:13).
  • Concept - the same base-plus-thin-subclass shape as SliceCohesionTests, StronglyTypedIdConventionTests and UIArchitectureConventionTests: the rule is authored once in MMCA.Common.Testing.Architecture and re-run per repo by a subclass whose only job is to name the repo (and, here, to record one exemption). Blazor Server shares one process across every circuit, so a mutable static member silently leaks one user's state into another's; the base holds two rules against that (StateManagementConventionTestsBase.cs:7-:15): a reflection scan of the repo's Layer.Ui assemblies fails on any mutable static field or settable static property, and a source scan fails the build if a production UI project registers a stateful *StateService/*StateContainer as a singleton rather than scoped.
  • Walkthrough - Map (:13) is the usual override. AllowedStaticMembers (:21-:22) overrides the base's empty default with one entry, MMCA.Common.UI.Pages.Common.ErrorMessages._localizer, with the reason recorded next to it in the doc comment (:15-:20): the root layout configures the shared IStringLocalizer exactly once, idempotently, and the localizer itself resolves against the ambient UI culture per call, so no user's state leaks to another; the static API is kept because every consumer call site depends on it (ADR-027). The rest are facts of this subclass's own, which prove the base's source scanner rather than the repo; each calls the base's protected static FindSingletonStateRegistrations (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:92). SingletonScan_JudgesPathsBelowTheSourceRoot_AndCatchesAWrappedRegistration (:24-:47) builds a throwaway tree under the temp directory whose path deliberately contains a Testing segment above the Source root (:29), writes the same line-wrapped services.AddSingleton<\n ProbeStateService>(); registration into an MMCA.Probe.UI project and an MMCA.Probe.Domain project (:30-:36), and asserts exactly one offender (the UI project's Registrations.cs, line 1) from exactly one scanned file (:38-:41), deleting the tree in a finally (:43-:46). SingletonScan_DoesNotReachAcrossAFluentChain (:51-:57) is a theory over three chained shapes, an AddSingleton of a non-state service followed by .AddScoped<IWizardStateService, ...>() on one line, across a line break, and in typeof(...) form, and expects no offender, because only the scoped call names the state service. SingletonScan_StillCatchesEverySingletonStateRegistrationShape (:60-:71) is the converse, nine inline registrations (generic with one or two type arguments, nested generic, TryAddSingleton, typeof, instance, factory lambda, and a singleton chained after a scoped call) that must each yield exactly one finding, line 1 of the Registrations.cs fixture file. SingletonScan_SkipsOnlyAWholeTestingSegment (:75-:82) pins the folder rule: MMCA.Probe.UI/TestingHelpers and MMCA.Probe.UI/Pages/LoadTesting are scanned (one offender each), while MMCA.Probe.Testing.UI and MMCA.Probe.UI/Testing are skipped (zero). All three share the private helper ScanOneUiFile (:84-:100), which writes one Registrations.cs under a temp Source tree and returns the scanner's offenders. The two inherited facts, UiAssemblies_CarryNoMutableStaticState and UiProjects_RegisterStatefulServicesScoped, run against MMCA.Common.UI unchanged (StateManagementConventionTestsBase.cs:29,:67).
  • Why it's built this way - exempting the member individually rather than excluding the whole ErrorMessages type keeps the rule honest about exactly what is write-once versus per-request: a future mutable field added to that same class would still fail. The base's first fact asserts its UI-assembly set is non-empty before scanning (StateManagementConventionTestsBase.cs:33-:34), and its second asserts at least one UI source file was scanned (:74-:75), so a map that forgot to register Layer.Ui or a scan root that matched nothing fails loudly instead of passing vacuously. The extra facts pin scanner properties a real-repo run cannot: paths are judged relative to the source root, segment by segment, so a checkout cloned under a directory named Testing is still scanned (StateManagementConventionTestsBase.cs:83-:86, :100-:103, and the comment at :27-:28 here); the match is bounded by AddSingleton's own type-argument and argument lists, so a registration wrapped across lines is one hit and a call chained after it is out of reach (:86-:87, :131-:139); and only a whole dot-separated Testing part marks test-support code, so a production folder that merely contains the word is still scanned (:123-:129).
  • Where it's used - discovered and run by xUnit as part of MMCA.Common.Architecture.Tests; nothing constructs it. MMCA.ADC subclasses the same base against its own map (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/StateManagementConventionTests.cs).

CascadeSoftDeleteConventionTests, ConcurrencyConventionTests, ContractImplementationTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · (see per-type table) · Level 13 · class

Three more thin subclasses of the same shape as the fifteen above, each supplying only Map. They are grouped separately because they share a second property that is worth understanding on its own: all three are ratchets rather than assertions in this repo. MMCA.Common has nothing today that could violate them, so a green run proves nothing about the framework's current code, and each subclass's own doc says so plainly.

  • Depends on - the corresponding *TestsBase and CommonArchitectureMap, and nothing else.
  • Concept introduced - a deliberately vacuous gate, kept because of when it will first fire. The instinct is to delete a test that asserts nothing, and for a unit test that instinct is right. For a convention gate it is wrong, and the reason is timing. A convention is cheapest to hold on the first type that could break it, when the shape is still a draft and nobody depends on it; it is most expensive to retrofit later, across every type that has since copied the first one. Running the rule from the start means the invariant is enforced from that first type onward with no test for anyone to remember to write. The cost is that the suite carries three facts that currently read nothing, and the mitigation is that each carries its own note saying so, so a reader is never misled into thinking the framework has been checked. Compare PiiConventionTests, which is vacuous in the same way and is paired with a non-vacuous behavioral test, and ServiceContractPurityTests, which spells the ratchet argument out at length. [Rubric §34 - Architecture Governance & Documentation] is the category: a ratchet is governance expressed as code with a known, dated cost.
Type File:Line Base rule What it will enforce, and why it is vacuous today
CascadeSoftDeleteConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:14 CascadeSoftDeleteConventionTestsBase An aggregate root that owns children deletes them in its own Delete() override. Supplies only Map (:16). The framework's Source/ declares no child-bearing aggregate today, so this run is the ratchet; it fires the moment the framework grows one whose children a delete would leave active. Its doc points at CascadeSoftDeleteFitnessTests as where the rule's own behavior is proven (:5-:12). [Rubric §8 - Data Architecture, Rubric §30 - Compliance/Privacy.]
ConcurrencyConventionTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:13 ConcurrencyConventionTestsBase No *UpdateRequest carries a concurrency token in its body: the token belongs in the If-Match header, not in the payload, so a client cannot forge or omit it silently. Supplies only Map (:14). MMCA.Common is module-less, so there is no update request to judge; the rule fires if the framework itself grows one that reintroduces a body token (:5-:10). [Rubric §9 - API & Contract Design.]
ContractImplementationTests MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ContractImplementationTests.cs:11 ContractImplementationTestsBase The class serving a published [ServiceContract] interface must not itself be public, so consumers bind to the contract rather than to the implementation. Supplies only Map (:13). MMCA.Common marks no interface with the attribute today, so the rule is attribute-driven and currently selects nothing (:5-:9). Its purity sibling ServiceContractPurityTests is in the same position for the same reason. [Rubric §7 - Microservices Readiness, Rubric §1 - SOLID.]
  • Why they're built this way - each is a two-line subclass with a doc comment that is longer than its body, which is the correct ratio for a ratchet: the code says nothing surprising and the comment carries the whole decision. Keeping them in this repo also means the framework is held to the same bar it exports, so a new framework type that would break a consumer's gate fails here first rather than in the consumers after a release.
  • Where they're used - all three run in the MMCA.Common.Architecture.Tests project during CI's build-and-test job. The same three bases are subclassed non-vacuously by MMCA.Store and MMCA.ADC over their own modules.
  • Caveats / not-in-source - a green run of any of these three says nothing about the framework's code today. Only CascadeSoftDeleteConventionTests has a companion that proves its rule fires at all (CascadeSoftDeleteFitnessTests); the other two rules have no fixture-backed proof in this repo.

DomainThrowFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:17 · Level 13 · class

  • What it is - the meta-test for the domain-throw rule: ten facts pinning both offender kinds, the three permitted argument guards, the compiler-written switch-expression throw, the bare rethrow, the non-throwing control, the UNVERIFIABLE verdict, both allowlist granularities, and a final run of the rule against MMCA.Common's own Domain.
  • Depends on - ArchitectureRules (DomainThrowsOnlyArgumentGuards, DomainThrowFitnessTests.cs:41), its nested FixtureAssemblyMap (:150-:158), CommonArchitectureMap (:143), the fixture types in MMCA.Common.Architecture.Tests.DomainThrowFixtures (:1), including SwitchExpressionFixture (DomainThrowFixtures.cs:115), and Xunit.Sdk.XunitException (:3).
  • Concept introduced - the allowlist doing its day job, in the test's own setup. Every other self-test in this assembly points its map at a namespace whose only inhabitants are its fixtures. This one cannot: the rule reads the whole Domain-layer assembly, and this assembly contains throws that are not this file's fixtures. NavigationContractTests guards its embedded resource with an InvalidOperationException, the OpenAPI XML-comment source generator emits a transformer into every assembly that references the API layer, and the substitutability rule's own fixtures (see CommonSubstitutabilityTests) throw on purpose. All three are allowlisted in a named static array with a doc comment explaining each (:21-:34), and the comment makes the wider point: generated plumbing is exactly what an allowlist is for. The third entry, the namespace MMCA.Common.Architecture.Tests.SubstitutabilityFixtures (:33), is what keeps a sibling rule's deliberate offenders from leaking into this rule's report. The first entry is a fully qualified type name, MMCA.Common.Architecture.Tests.Ui.NavigationContractTests (:31), so the allowlist tracks the test's folder: moving that class under Ui/ moved the string with it. Reading that array teaches more about how the escape hatch is meant to be used than any of the escape-hatch facts do. The final fact is a different kind of assertion again: MMCACommonDomain_HoldsOnlyArgumentGuards (:140-:147) runs the rule against the real CommonArchitectureMap with an empty allowlist and asserts NotThrow, with the because recording the fact that makes it interesting, that MMCA.Common's own Domain is the reference implementation of ADR-013 and its single throw is the ArgumentNullException guard in Specification.cs. That is the framework holding itself to the rule it exports, with no exemptions at all. [Rubric §4 - DDD] and [Rubric §15 - Best Practices & Code Quality] are the properties; [Rubric §34 - Architecture Governance & Documentation] covers the allowlist-with-reasons discipline; [Rubric §14 - Testability] is the technique.
  • Walkthrough - a private const string FixtureNamespace (:19), the NonFixtureThrows array (:29-:34) and a readonly FixtureAssemblyMap (:36) are shared; every fixture fact passes NonFixtureThrows so the fixtures are the only subject.
    • BusinessException_IsFlagged (:38-:47): asserts both the owning type and the exception's full name System.InvalidOperationException appear (:45-:46). Naming the exception is what tells a developer which throw to convert.
    • CustomDomainException_IsAlsoFlagged (:49-:58): the custom-exception case.
    • ArgumentGuards_AreNotFlagged (:60-:69): the permitted-guard case, with a because listing all three exception types and stating why they are exempt, a caller bug rather than a business outcome.
    • CompilerSwitchExpressionThrow_IsNotFlagged (:71-:80): asserts the report never names SwitchExpressionException. Its subject is SwitchExpressionFixture, a switch expression with no discard arm (DomainThrowFixtures.cs:111-:118), whose default-arm throw is written by the compiler rather than the developer, so the rule must not charge it to the domain method.
    • BareRethrow_IsNotFlagged (:82-:91) and NonThrowingCode_IsNotFlagged (:93-:102): the two silent cases.
    • ThrowOfAValueBuiltElsewhere_IsReportedAsUnverifiable (:104-:113): asserts the literal "UNVERIFIABLE" and the owning method's type name, the same third-verdict discipline ErrorCatalogFitnessTests pins for the catalog rules.
    • AllowlistedNamespace_SilencesTheRule (:115-:124) and AllowlistedType_SilencesOnlyThatType (:126-:138): the two granularities, each built by spreading NonFixtureThrows and appending one entry, with the type-level fact carrying the control assertion that the other offender is still reported (:136-:137).
    • MMCACommonDomain_HoldsOnlyArgumentGuards (:140-:147): the self-application described above.
  • Why it's built this way - ten facts for one rule, and the reason is that this rule has the widest blast radius: it judges every throw in every Domain assembly of every repo that adopts it, so both its false positives and its blind spots are expensive. Asserting the exception's full name rather than just the type's is what makes the report usable. See ADR-013 and ADR-015.
  • Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the rule is DomainThrowTestsBase.
  • Caveats / not-in-source - the Microsoft.AspNetCore.OpenApi.Generated allowlist entry (:32) silences a whole generated namespace. If the generator ever emitted a type a developer also wrote into that namespace, its throws would be silenced too; nothing in the class detects that. The sibling entry is a string, not a typeof, so renaming or relocating NavigationContractTests without editing :31 would quietly re-arm the rule against it. The SubstitutabilityFixtures entry (:33) is also a string and silences everything under that namespace, so a non-fixture type added there would be exempt from the rule too.

CascadeSoftDeleteConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:14 · Level 13 · class

  • What it is - the cascade-soft-delete convention binding for MMCA.Common's own map: an aggregate root that owns children must delete them in its own Delete() override.
  • Depends on - CascadeSoftDeleteConventionTestsBase (CascadeSoftDeleteConventionTests.cs:14) and CommonArchitectureMap (:16).
  • Concept - MMCA.Common's Source/ declares no child-bearing aggregate today, so this run is a ratchet rather than a live assertion: it does nothing until the framework itself grows an aggregate whose children a delete would leave active, at which point it starts failing. It is the same rule Store and ADC subclass over their own modules, where it does have live subjects. The rule's own detection logic is proven separately, against compiled fixtures, by CascadeSoftDeleteFitnessTests.
  • Walkthrough - public sealed class CascadeSoftDeleteConventionTests : CascadeSoftDeleteConventionTestsBase (:14) overrides Map with new CommonArchitectureMap() (:16); it declares no other members.
  • Why it's built this way - a module-less framework has no positive case to assert, so shipping the binding anyway (rather than skipping it) is what turns "grows an aggregate with children" into a build failure instead of a silent gap.
  • Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps, where the rule has real aggregates to check.

ClockReadTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:14 · Level 13 · class

  • What it is - both the framework's own binding of the ambient-clock rule and its meta-test: it subclasses ClockReadTestsBase over CommonArchitectureMap, so the inherited fact runs the rule against MMCA.Common's own Domain and Application, and it adds seven fixture facts pinning that a direct read of either clock type is flagged, that DateTime.Today is flagged, that a read inside a lambda or an async state machine is still attributed to the member that wrote it, that a read inside an async lambda is attributed to its source member and exempted by that member's allowlist entry, that an injected TimeProvider is left alone, and both allowlist granularities (class doc, ClockReadTests.cs:7-:13).
  • Depends on - ClockReadTestsBase (public sealed class ClockReadTests : ClockReadTestsBase, :14), CommonArchitectureMap (:20), ArchitectureRules (DomainAndApplicationDoNotReadTheClock, :88), its nested FixtureAssemblyMap (:97-:106), and the eight fixtures in the sibling MMCA.Common.Architecture.Tests.ClockReadFixtures namespace (:16): UtcNowReadingFixture, OffsetNowReadingFixture, LambdaClockReadingFixture, AsyncClockReadingFixture, AsyncLambdaClockReadingFixture, InjectedClockFixture, TwoMemberClockFixture and TodayReadingFixture (ClockReadFixtures.cs:48).
  • Concept introduced - a compiler-generated body still belongs to the member that wrote it. A lambda closure and an async state machine each compile into their own nested type, so a naive IL scan would attribute a clock read inside either to that generated type rather than to the source. ReadsInsideLambdaAndAsyncBodies_AreFlaggedUnderTheirSourceMember (:37-:48) is the fact that pins the rule does not make that mistake: LambdaClockReadingFixture.Clock and AsyncClockReadingFixture.StampAsync both appear in the report under their declaring member's name, not a compiler-generated one. [Rubric §14 - Testability] is the technique, a fitness function proven against fixtures before it is trusted against real code; [Rubric §4 - DDD] is the property, since the rule is what lets a domain method take time as an input instead of reading it.
  • Walkthrough - Map is overridden with new CommonArchitectureMap() (:20), which is all the inherited DomainAndApplication_ShouldNotReadTheAmbientClock fact needs (ClockReadTestsBase.cs:26-:28). For the self-tests, a private const string FixtureNamespace (:16) and a readonly nested FixtureAssemblyMap instance (:18) point the rule at this test assembly's own Domain layer, anchored on InjectedClockFixture (:104); a private Report helper (:83-:95) runs the rule against that map and returns the caught XunitException.Message, or an empty string when the rule passes. Because the fixture map registers the whole test assembly, every self-test asserts on what the report names rather than on the report being empty (:10-:12).
    • DirectReadsOfEitherClockType_AreFlagged (:22-:29): asserts the report names both UtcNowReadingFixture.Stamp (DateTime.UtcNow) and OffsetNowReadingFixture.Stamp (DateTimeOffset.Now).
    • DateTimeToday_IsFlagged (:31-:35): asserts TodayReadingFixture.Stamp reads DateTime.Today, with the because that DateTime.Today reads the ambient clock exactly as DateTime.Now does.
    • ReadsInsideLambdaAndAsyncBodies_AreFlaggedUnderTheirSourceMember (:37-:48): the attribution case above.
    • ReadsInsideAnAsyncLambda_AreAttributedToTheSourceMemberAndExemptedWithIt (:50-:62): an async lambda compiles to a state machine nested in the closure's display class, so the member name must be read past both leading brackets; asserts AsyncLambdaClockReadingFixture.Clock reads DateTime.UtcNow is reported, and that allowlisting {FixtureNamespace}.AsyncLambdaClockReadingFixture.Clock removes the fixture from the report, as the rule's doc promises.
    • InjectedTimeProvider_IsNotFlagged (:64-:68): the control, asserting InjectedClockFixture never appears.
    • MemberAllowlistEntry_ExemptsOnlyThatMember (:70-:77): allowlists TwoMemberClockFixture.Exempted by its fully qualified member name and asserts the sibling StillReported member on the same type is still flagged, proving the allowlist grain works at member level, not type level.
    • NamespaceAllowlistEntry_ExemptsEveryTypeUnderIt (:79-:81): allowlists the whole fixture namespace and asserts nothing from it appears.
  • Why it's built this way - the rule inspects IL rather than source, so its riskiest cases, a closure and a state machine, need their own fixtures and their own fact; without ReadsInsideLambdaAndAsyncBodies_AreFlaggedUnderTheirSourceMember a regression that misattributed either would pass every other fact silently. DateTime.Today gets its own fixture for the same reason: it is a third property name on the clock type, and a rule that matched only Now and UtcNow would miss it. The two allowlist facts follow the same member-vs-namespace split DomainThrowFitnessTests uses for its own allowlist. Folding the binding and the meta-test into one class means the framework runs the exact rule it ships against its own code in the same file that proves the rule works.
  • Where it's used - discovered and run by xUnit in the Common architecture suite. ADC's sibling binding of ClockReadTestsBase is MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ClockReadTests.cs. See ADR-128.

CloudVendorIsolationTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/CloudVendorIsolationTests.cs:17 · Level 13 · class

  • What it is - the framework's own binding of the cloud-vendor boundary: every layer of CommonArchitectureMap, the service defaults (MMCA.Common.Aspire) and the test infrastructure packages must name no Azure, AWS or Google Cloud SDK type and reference no vendor SDK or vendor adapter assembly (class doc, CloudVendorIsolationTests.cs:5-:16; rule doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/CloudVendorIsolationTestsBase.cs:3-:14).
  • Depends on - CloudVendorIsolationTestsBase (public sealed class CloudVendorIsolationTests : CloudVendorIsolationTestsBase, :17; see the rollup), CommonArchitectureMap (:21), and four assembly anchors: Aspire.Extensions (:22), CrossServiceFixtureBase (:23), AppHostFixtureBase from MMCA.Common.Testing.Aspire (:24) and the base type itself, which stands for MMCA.Common.Testing.Architecture (:25).
  • Concept introduced - the runtime half of a two-gate boundary. The compile-time guard, EnforceCloudVendorBoundary in Source/Build/MMCA.Common.LayerEnforcement.targets, judges declared package references; this class judges the compiled assemblies, so a vendor type that arrives through a transitive path is still caught (CloudVendorIsolationTestsBase.cs:9-:12). Unlike most bindings in this suite it does not take an IArchitectureMap: the base asks for a flat CloudNeutralAssemblies list (CloudVendorIsolationTestsBase.cs:18), because the boundary covers packages (Aspire, the testing packages) that are not layers of any map. [Rubric §34 - Architecture Governance & Documentation] is the category; [Rubric §14 - Testability] the technique.
  • Walkthrough - two overrides, no facts of its own.
    • CloudNeutralAssemblies (:19-:26): a collection expression spreading new CommonArchitectureMap().Layers.Select(layer => layer.Assembly) (:21) and appending the four anchor assemblies above (:22-:25), which the class doc calls the places the Azure dependencies used to live (:8).
    • AllowedVendorPrefixes (:28-:32): an ordinal-keyed dictionary with one entry, MMCA.Common.Infrastructure may name Microsoft.Azure.Cosmos (:31), because Cosmos DB is one of the four EF engines rather than a cloud service a host opts into (:12-:14). The base default is empty (CloudVendorIsolationTestsBase.cs:24-:25).
    • The two inherited facts, CloudNeutralAssemblies_ShouldNotName_ACloudVendorType and CloudNeutralAssemblies_ShouldNotReference_ACloudVendorAssembly (CloudVendorIsolationTestsBase.cs:27-:33), call ArchitectureRules.CloudNeutralAssembliesNameNoVendorType and ArchitectureRules.CloudNeutralAssembliesReferenceNoVendorAssembly (see ArchitectureRules) with both overrides.
  • Why it's built this way - the allowance is scoped per assembly name, not global, so the Cosmos exemption cannot leak into any other cloud-neutral assembly. Keeping the rule body in the shipped base means a consumer subclasses it with its own assembly list. See ADR-134.
  • Where it's used - discovered and run by xUnit in the Common architecture suite; nothing constructs it. MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/CloudVendorRulesTests.cs:13 names it in a doc comment as the positive half of the vendor rules, whose negative half that class proves against Google SDK fixtures.

CommonSubstitutabilityTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance.Substitutability · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/CommonSubstitutabilityTests.cs:10 · Level 13 · class

  • What it is - the substitutability binding for MMCA.Common's own map: no override or interface implementation may refuse the contract it fulfils by throwing NotSupportedException or NotImplementedException.
  • Depends on - SubstitutabilityTestsBase (CommonSubstitutabilityTests.cs:10; its one fact is OverridesAndInterfaceImplementations_ShouldNotThrow_NotSupportedOrNotImplemented, SubstitutabilityTestsBase.cs:34), IArchitectureMap and CommonArchitectureMap (:12).
  • Concept introduced - Liskov substitution as a build gate. A subtype that refuses an operation its base or interface promises breaks every caller that substitutes it in, and only at run time. The base reads IL with the same throw scan as DomainThrowTestsBase, so only an exception constructed in place and thrown is judged (SubstitutabilityTestsBase.cs:10-:14). The subclass supplies only Map (:12); the base's protected ExcludedMembers (SubstitutabilityTestsBase.cs:31, empty by default) is the reviewed-refusal escape hatch and this binding does not override it, so the framework runs with no exemptions. [Rubric §1 - SOLID] is the property and [Rubric §34 - Architecture Governance & Documentation] the category.
  • Why it's built this way - a one-line binding keeps the rule body once in the shipped package, so Store and ADC subclass the same base over their module assemblies. The rule's own fixtures live under MMCA.Common.Architecture.Tests.SubstitutabilityFixtures and throw deliberately, which is why DomainThrowFitnessTests allowlists that namespace (DomainThrowFitnessTests.cs:33). See ADR-015.
  • Where it's used - run by the MMCA.Common.Architecture.Tests project in CI's build-and-test job; nothing references it from code.
  • Caveats / not-in-source - the fixtures that prove the rule fires are not part of this unit's source slice, so whether a companion self-test exists is Not determinable from the brief.

ConcurrencyConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:13 · Level 13 · class

  • What it is - the optimistic-concurrency convention binding for MMCA.Common's own map: no *UpdateRequest carries a concurrency token in its request body.
  • Depends on - ConcurrencyConventionTestsBase (ConcurrencyConventionTests.cs:13) and CommonArchitectureMap (:14).
  • Concept - MMCA.Common is module-less, so this run is a ratchet rather than a live assertion, the same shape as CascadeSoftDeleteConventionTests: it does nothing until the framework itself grows an update request that reintroduces a body token, and it is the same rule Store and ADC subclass over their own modules.
  • Walkthrough - public sealed class ConcurrencyConventionTests : ConcurrencyConventionTestsBase (:12) overrides Map with new CommonArchitectureMap() (:14); it declares no other members.
  • Why it's built this way - keeping the binding shipped even with no live subject means the convention starts failing the moment it is violated, rather than only after someone remembers to add a check for it.
  • Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps.

ContractImplementationTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ContractImplementationTests.cs:11 · Level 13 · class

  • What it is - the [ServiceContract] encapsulation rule binding for MMCA.Common's own map: the class serving a published contract interface must not itself be public.
  • Depends on - ContractImplementationTestsBase (ContractImplementationTests.cs:11) and CommonArchitectureMap (:13).
  • Concept - MMCA.Common marks no interface [ServiceContract] today, so this run is a ratchet rather than a live assertion, the same shape as CascadeSoftDeleteConventionTests and ConcurrencyConventionTests. The rule is attribute-driven rather than name-pattern-driven; the reasoning for that choice lives on the shared base rather than being repeated here.
  • Walkthrough - public sealed class ContractImplementationTests : ContractImplementationTestsBase (:11) overrides Map with new CommonArchitectureMap() (:13); it declares no other members.
  • Why it's built this way - the same one-class-per-repo pattern every other *TestsBase subclass in this suite uses (ADR-015).
  • Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps.

EntityConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain.EntityModel · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/EntityModel/EntityConventionTests.cs:12 · Level 13 · class

  • What it is - the framework repo's binding of the DDD entity-shape rules: MMCA.Common's own Notifications aggregates are sealed, built through a Result-returning Create factory with no public constructor, and expose no public setter (class doc, EntityConventionTests.cs:5-:11).
  • Depends on - EntityConventionTestsBase (:12) and FrameworkModuleArchitectureMap (:14), not CommonArchitectureMap.
  • Concept - the map choice is the whole point. The base's facts read the module-scoped accessors, so over CommonArchitectureMap they would have nothing to judge and pass vacuously; FrameworkModuleArchitectureMap registers the framework assemblies as a module so the rules land on PushNotification and UserNotification (:9-:10). [Rubric §4 - DDD] assesses whether the tactical patterns are actually applied; this is what makes the framework obey the factory-plus-Result construction contract it asks consumers to follow (ADR-129).
  • Walkthrough - one member, Map (:14). Eight inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/EntityConventionTestsBase.cs:15, :18, :21, :24, :27, :30, :33, :36) cover aggregate-root exposure, the Result-returning Create factory, no public aggregate constructors, factory return types, sealing, Domain-layer placement, non-public property setters, and the DTO/request exclusion from Domain and Infrastructure.
  • Why it's built this way - the same one-class-per-repo pattern every other *TestsBase subclass in this suite uses (ADR-015), with the map swapped so the rules are live rather than ratchets.
  • Where it's used - discovered and run by xUnit in the Common architecture suite; its sibling ImmutabilityTests uses the same map. ADC subclasses the same base with its own map (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/EntityConventionTests.cs).

DeleteBehaviorConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:28 · Level 14 · class

  • What it is - the framework repo's binding of the delete-behavior fitness functions (ADR-119): a sealed subclass that builds the framework's own two finalized EF Core models (SQLite and SQL Server) with no database connection opened, and asserts each against the RestrictDeleteByDefaultConvention stamp the model already carries.
  • Depends on - DeleteBehaviorConventionTestsBase (public sealed class DeleteBehaviorConventionTests : DeleteBehaviorConventionTestsBase, DeleteBehaviorConventionTests.cs:28), ArchitectureRules (CascadingForeignKeysAreExplicitlyOptedIn, ConventionForeignKeysRestrictDeletes, DeleteBehaviorConventionTests.cs:57,:66), SqliteDbContext and SQLServerDbContext (:79,:94-:98), and a set of private no-op fakes (NoModuleAssemblies, NoEntityDataSources, NoDomainEventDispatcher, NoOutboxSignal, :122-:163) that satisfy the DI graph ApplicationDbContext.OnConfiguring demands without any real registrations.
  • Concept - the base class's own Model property only exercises one engine, so this subclass adds a second [Fact] (SqlServerModel_CascadingDeletes_AreExplicitlyOptedIn / SqlServerModel_ConventionDeletes_AreRestricted, :53-:69) that builds a second model against SQLServerDbContext and asserts both rules against it too, covering both relational engines the framework maps entities onto today. The nested FrameworkModels helper documents the trick that makes this possible without infrastructure: only the model is asserted, so neither context ever opens its connection, and EF builds the model from the configurations alone (:71-:74). NoEntityDataSources answers TryGetDataSourceKey with false for everything so the cross-source degrade convention becomes a no-op, and its two other lookups return a default key rather than throwing so a throwing fixture does not trip the repo's own domain-throw rule (:127-:131).
  • Walkthrough - Model (inherited) builds the SQLite model via DbContextOptionsBuilder<SqliteDbContext>().UseSqlite("DataSource=:memory:") and FrameworkModels.Sqlite() (:77-:90); the two added facts build the SQL Server model the same way via FrameworkModels.SqlServer() (:92-:105). Both paths construct their context with the same four dependencies: Services() (an AuditSaveChangesInterceptor, a DomainEventSaveChangesInterceptor, and a no-op IEntityDataSourceRegistry, :108-:118), a NoModuleAssemblies provider that returns an empty assembly list so the model holds only the framework's own tables (:121-:125), and a PhysicalDataSource describing the (unused) connection. Both inherited facts then read context.Model and assert the convention's stamps against it.
  • Why it's built this way - PostgreSQL takes the SQL Server mapping unchanged for delete-behavior purposes, so asserting SQLite plus SQL Server covers the framework's relational engines without a third context class; Cosmos is a deliberate no-op for this convention since it has no foreign key constraints to restrict (DeleteBehaviorConventionTestsBase.cs:16-:18). Building the model with in-memory/unopened connections keeps the fitness test fast and database-free, consistent with the rest of MMCA.Common.Architecture.Tests.
  • Where it's used - discovered and run by xUnit as part of MMCA.Common.Architecture.Tests; nothing constructs it. MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs is the sibling binding in the ADC repo, built the same way against ADC's own module assemblies.

EventNameConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventNameConventionTests.cs:13 · Level 13 · class

  • What it is - the meta-test for the [EventName] rule: three facts pinning the offender, the compliant control and the framework's own events.
  • Depends on - ArchitectureRules (IntegrationEventsDeclareEventName, ArchitectureRules.Events.cs:33), CommonArchitectureMap (EventNameConventionTests.cs:37-:40), its nested FixtureProbeMap (:43-:49, see the rollup), the fixture events in MMCA.Common.Architecture.Tests.EventNameFixtures.IntegrationEvents (FixtureEventNameMissingEvent, FixtureEventNameDeclaredEvent; EventNameFixtures.cs:1) and System.Exception.
  • Concept introduced - pinning a persistence-safety rule to its reason. The rule requires every concrete integration event to declare [EventName], so outbox and inbox rows store a stable wire identity rather than a CLR type name that a rename, namespace move or assembly move would orphan (ArchitectureRules.Events.cs:27-:41). The test's because strings repeat that reason (:22), and the report assertion "[EventName" (:23) checks the message tells the developer what to add. [Rubric §9 - API & Contract Design] and [Rubric §14 - Testability] apply.
  • Walkthrough - FixtureProbeMap is a module-less map over this test assembly (:43-:49), its single Shared layer built from typeof(FixtureEventNameMissingEvent).Assembly (:48), so every fixture event is scanned.
    • EventWithoutEventName_IsFlagged (:16-:24): the report names FixtureEventNameMissingEvent and contains "[EventName".
    • EventWithEventName_IsNotFlagged (:27-:35): the compliant fixture is absent from the report.
    • FrameworkEvents_AllDeclareEventName (:38-:40): runs the rule against the real CommonArchitectureMap and asserts NotThrow, the framework holding itself to the rule it ships.
  • Why it's built this way - the first two facts assert on the failure message because the rule reports by throwing; a flagged and an unflagged control in one assembly is what proves the rule discriminates. The consumer-facing binding is the IntegrationEvents_ShouldDeclare_EventName fact in EventConventionTestsBase (EventConventionTestsBase.cs:21). See ADR-015.
  • Where it's used - an independent class in the Common architecture suite; fixtures sit in EventNameFixtures.cs.
  • Caveats / not-in-source - the attribute is matched by full name and must sit on the event itself (it is not inherited, ArchitectureRules.Events.cs:29-:31), so a derived event relying on its base's [EventName] is flagged.

EventScopeFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventScopeFitnessTests.cs:13 · Level 13 · class

  • What it is - the ownership-scoping guard for the integration-event rules: three facts pinning that a consumer-shaped map neither snapshots nor polices the framework's own events, while the framework's module-less map still covers them at the source.
  • Depends on - ArchitectureRules (BuildIntegrationEventContract and IntegrationEventsResideInSharedIntegrationEventsNamespace, EventScopeFitnessTests.cs:18,:30), CommonArchitectureMap (:39), its own FakeConsumerMap, and BaseIntegrationEvent as the Domain-assembly anchor (:1,:56).
  • Concept introduced - a rule's scope is part of its contract, and ownership decides it. Both event rules ask the same question of a map, and both answer differently depending on whether the map declares modules. IntegrationEvents includes framework layers only when map.ModuleNames.Count == 0 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:155-:73), and the residency rule enforces the Shared-layer requirement only when the map is module-bearing (:31,:33). The reasoning is ownership: a framework-shipped event is the framework's contract, gated by the framework's own conventions and public-API baseline, so a consumer's frozen snapshot must neither churn on it nor claim jurisdiction over where it lives. This class is a regression guard, and the class doc names the incident that motivated it (:6-:12): when the framework shipped its first concrete integration event, OutputCacheEvictionRequested, the Helpdesk canary broke. [Rubric §6 - CQRS & Event-Driven] assesses whether event contracts are governed; [Rubric §9 - API & Contract Design] covers the frozen-snapshot mechanism; [Rubric §33 - Developer Experience] is the failure mode being prevented, a framework release that reds every consumer's architecture suite for a change they did not make.
  • Walkthrough
    • ModuleBearingMap_ExcludesFrameworkEvents_FromContractSnapshot (:15-:23): builds the contract from FakeConsumerMap and asserts no line mentions OutputCacheEvictionRequested.
    • ModuleBearingMap_PassesResidencyRule_DespiteFrameworkEvents (:26-:34): asserts the residency rule does not throw for the consumer map, even though the framework's Domain assembly hosts that event outside any Shared assembly (inline comment, :28-:29).
    • ModuleLessMap_StillCoversFrameworkEvents (:36-:44): the other half of the proof, using the real CommonArchitectureMap and asserting the same event is in the contract. Without this third fact the scoping change would be indistinguishable from simply having disabled the rule.
  • Why it's built this way - a scoping fix is exactly the kind of change that silently over-corrects. Pinning both the exclusion and the retention, against a real framework event rather than a fixture one, is what keeps the fix from becoming a hole.
  • Where it's used - an independent class in the Common architecture suite; the rules it scopes are bound for real by EventVersioningConventionTests here and by the per-repo IntegrationEventContractTestsBase subclasses in the consumers.

EventUpcasterFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventUpcasterFitnessTests.cs:12 · Level 13 · class

  • What it is - the meta-test for the two event-upcaster fitness rules: five facts proving the unique-source rule reports a contract claimed twice, that it leaves a legitimate chain alone, that it rejects one class declaring two upcaster contracts, that the version rule reports an upcaster pointing at a lower SchemaVersion, and that both rules pass on a map owning no upcasters at all.
  • Depends on - ArchitectureRules (EventUpcastersHaveUniqueSourceTypes and EventUpcastersIncreaseSchemaVersion, EventUpcasterFitnessTests.cs:64,:66,:72,:79), the fixture contracts and fixture upcasters plus FixtureDoubleHopUpcaster (see the rollup) (:1), its nested UpcasterTestMap (:85-:91), CommonArchitectureMap (:62), plus xUnit and AwesomeAssertions.
  • Concept introduced - proving a rule is not vacuous when the framework itself has nothing to judge. MMCA.Common ships no upcaster of its own, so running either rule over the real CommonArchitectureMap proves only that it does not crash. That is a real property worth pinning (a rule that threw on an empty set would red every consumer that has not adopted upcasting yet), and the last fact pins exactly it. But the other four facts have to manufacture their subjects, which is what the UpcasterFixtures namespace and the private map are for. The pattern to take away: a fitness function for a consumer-facing convention is proven in the framework repo against fixtures, and only smoke-tested against the framework's own code. ProtoContractFitnessTests, CommandValidatorCoverageFitnessTests and ServiceContractPurityTests sit in the same position for their rules. [Rubric §6 - CQRS & Event-Driven] and [Rubric §9 - API & Contract Design] are the properties defended; [Rubric §14 - Testability] is the technique.
  • Walkthrough - two private helpers each run one rule against a fresh UpcasterTestMap and return the thrown message: RunUniqueSourceRule (:70-:75) and RunSchemaVersionRule (:77-:82). Because the fixtures always contain an offender for each rule, both helpers can assert Should().Throw<Exception>().Which.Message unconditionally and let the facts make positive and negative Contain assertions against the one string.
    • UniqueSourceRule_FlagsTheContestedContract_ButNotTheCompliantLadder (:14-:25): asserts the message names the contested contract and both rival upcasters (:19-:21), which is what makes the failure actionable, and that the compliant first rung is absent (:22-:24).
    • UniqueSourceRule_DoesNotFlag_AnUpcasterWhoseSourceIsAnotherUpcastersTarget (:31-:33): the sharp one, given its own fact and its own doc comment (:27-:30). The compliant ladder's middle contract is both a target and a source; that is a chain, not a duplicate claim, and the rule must leave it alone.
    • UniqueSourceRule_FlagsAClassDeclaringTwoUpcasterContracts (:37-:44): asserts the message names FixtureDoubleHopUpcaster (:42) and contains "exactly one hop" (:43). The comment above it gives the reason (:35-:36): the registry indexes one SourceType per upcaster instance, so a class declaring two IEventUpcaster<,> contracts is one hop at runtime and must be rejected outright; the rule's report line spells out the same reason (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:23).
    • SchemaVersionRule_FlagsTheBackwardsUpcaster_ButNotTheCompliantLadder (:46-:57): names the offender (:51) and, notably, pins the message text "must declare a HIGHER SchemaVersion" (:52-:54), because the wording is what tells a developer which direction is allowed. Both clean rungs are asserted absent (:55-:56).
    • BothRules_Pass_OnAMapThatOwnsNoUpcasters (:59-:68): runs both rules against the real framework map and asserts NotThrow, with the because stating that the framework ships no upcaster so the rule passes vacuously (:65).
  • Why it's built this way - the two rules exist because an upcast chain that is not a function, or that runs backwards, produces a bug that only appears when an old outbox row is replayed, potentially long after the change that caused it (ADR-090, building on ADR-010). Catching it at build time is worth a fixture namespace.
  • Where it's used - an independent class in the Common architecture suite. The shipped binding of both rules is EventConventionTestsBase, whose two facts call them for every repo (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/EventConventionTestsBase.cs:27,:30), and which MMCA.Common activates through EventVersioningConventionTests.

FakeConsumerMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventScopeFitnessTests.cs:50 · Level 13 · class

  • What it is - a consumer-shaped architecture map: the framework's Domain assembly registered as a framework layer plus one module layer, which is the minimum that makes a map module-bearing.
  • Depends on - ArchitectureMapBase (private sealed class FakeConsumerMap : ArchitectureMapBase, EventScopeFitnessTests.cs:50), LayerRef, the Layer enum, and BaseIntegrationEvent as the anchor for the framework Domain assembly (:56).
  • Concept introduced - the Module(...) factory, and what a single module entry changes. Every other map in this chapter uses only Framework(...), whose LayerRef.Module is the empty string (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:94-:95). Module(name, layer, assembly) sets it to the module name and derives the root namespace as {RepoToken}.{module}.{segment} (:98-:99), and ModuleNames is computed from exactly those non-empty entries (:27-:32). One module layer is therefore all it takes to flip a map from "framework" to "consumer" for every rule that branches on ownership. Building that shape from the framework's own assemblies, rather than referencing a real consumer, keeps the regression guard inside MMCA.Common's CI where it can run before a release.
  • Walkthrough - RepoToken => "MMCA.FakeConsumer" (:52), deliberately not MMCA.Common, so the derived module namespace looks like a consumer's. DefineLayers() (:54-:58) is a yield-based iterator returning two entries: Framework(Layer.Domain, typeof(BaseIntegrationEvent).Assembly) (:56), which brings the framework's real integration event into the map, and Module("Fake", Layer.Shared, typeof(EventScopeFitnessTests).Assembly) (:57), a stand-in module Shared layer that ships no integration events of its own (class doc, :46-:49). That combination is the exact situation the guard exists for: a consumer whose map can see a framework event but does not own it.
  • Where it's used - the input to the first two facts of EventScopeFitnessTests (:18,:31).
  • Caveats / not-in-source - the class doc says the stand-in module Shared layer ships no integration events (:46-:49), but the assembly it points at is this whole test assembly, which does declare the eight upcaster fixture contracts. Those fixtures live in a *.IntegrationEvents namespace precisely so the residency fact still passes (EventUpcasterFixtures.cs:9-:13), and the contract fact only asserts the absence of the framework's event, so neither fact is affected. The doc comment is narrower than the code.

FeatureFlagLifecycleRuleTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:23 · Level 13 · class

  • What it is - the meta-test for the two feature-flag lifecycle rules: seven facts pinning the four ways a flag's declaration can be wrong, that a correctly declared flag is left alone, that a temporary flag past its RemoveBy fails naming the date and the owner, and that the rule passes both on the day of expiry and against the framework's own flags.
  • Depends on - ArchitectureRules.FeatureFlagsDeclareLifetime and ArchitectureRules.TemporaryFeatureFlagsAreNotPastRemoveBy (FeatureFlagLifecycleTests.cs:97,:123), FeatureFlagAttribute as the taught convention (ADR-031, ArchitectureMapBase as the private FixtureMap's base, FeatureFlagLifecycleTests.cs:129), CommonArchitectureMap (:118), and the fixtures FixtureBadFeatures, FixtureGoodFeatures, and FixtureExpiredFeatures in the sibling FeatureFlagFixtures namespace (:1).
  • Concept introduced - a lifecycle rule is checked twice: once at declaration time, once at expiry time. ArchitectureRules.FeatureFlagsDeclareLifetime is the shape check: every flag constant must carry a [FeatureFlag] attribute, a Permanent flag must not set RemoveBy, and a Temporary one must set it as a parseable ISO yyyy-MM-dd date. ArchitectureRules.TemporaryFeatureFlagsAreNotPastRemoveBy is the clock check: it takes a DateOnly and fails any temporary flag whose date has passed, naming the flag, the date, and the owner in the message so the failure is actionable rather than just red. Framing Today as a parameter rather than reading DateTime.UtcNow directly is what lets ExpiryRule_PassesOnTheDayOfTheRemoveByDate (:110-:113) pin the boundary exactly: a flag due to be gone BY its RemoveBy date is still inside its window on that date, not already expired. This is the dead-toggle detector the CQRS decorator pipeline's FeatureGate step depends on staying honest (MMCA.Common/CLAUDE.md, Feature Flag Lifecycle). [Rubric §34 - Architecture Governance & Documentation] is the property (a lifecycle contract enforced at build time, not by convention); [Rubric §14 - Testability] is the technique.
  • Walkthrough - a private static readonly DateOnly Today = new(2026, 9, 11) (:50) freezes the clock so the expiry facts are deterministic; a nested FixtureMap (:129-:135) points a Shared-layer map at this test assembly so the rules see the deliberately bad fixtures.
    • DeclarationRule_FlagsAConstantWithNoAttribute (:52-:56): asserts the failure message names FixtureBadFeatures.UnannotatedFlag.
    • DeclarationRule_FlagsAPermanentFlagThatSetsRemoveBy (:58-:65) and DeclarationRule_FlagsATemporaryFlagWithNoRemoveBy (:67-:74): the two ways a flag's lifetime and its RemoveBy disagree, each asserting the flag name plus the specific wording ("is Permanent but sets RemoveBy", "must set RemoveBy").
    • DeclarationRule_FlagsATemporaryFlagWhoseRemoveByDoesNotParse (:76-:83): the malformed-date case, asserting "is not an ISO yyyy-MM-dd date".
    • DeclarationRule_LeavesCorrectlyDeclaredFlagsAlone (:85-:92): the control, asserting FixtureGoodFeatures.PermanentFlag and FixtureGoodFeatures.LiveTemporaryFlag are absent from the message.
    • ExpiryRule_FlagsATemporaryFlagPastItsRemoveBy_NamingTheDateAndTheOwner (:94-:107): runs the expiry rule against FixtureExpiredFeatures.ExpiredFlag, asserting the message names the flag, the date 2020-01-01, and owner=Fixtures, and that the still-live FixtureGoodFeatures.LiveTemporaryFlag is absent.
    • ExpiryRule_PassesOnTheDayOfTheRemoveByDate (:109-:113): the boundary case above.
    • ExpiryRule_PassesOnTheFrameworksOwnFlags (:115-:119): runs the expiry rule against the real CommonArchitectureMap at Today and asserts NotThrow, with the because recording that every framework flag is Permanent so none can expire, the same self-application pattern DomainThrowFitnessTests uses.
  • Why it's built this way - the declaration rule alone would still let a temporary flag rot forever once its rollout finishes; pairing it with an expiry rule that runs against a caller-supplied Today is what turns "flag documented" into "flag that fails the build when it overstays". Freezing Today as a class constant, rather than each fact computing its own offset from DateTime.UtcNow, keeps every fact's expected message exact instead of relative. See ADR-031.
  • Where it's used - an independent class in the Common architecture suite; the shipped binding of both rules for a real repo is FeatureFlagLifecycleTests.

FeatureFlagLifecycleTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:13 · Level 13 · class

  • What it is - the two-line binding of the feature-flag lifecycle rules to MMCA.Common's own map: every flag MMCA.Common declares must carry a lifetime, and no temporary one may be past its RemoveBy.
  • Depends on - FeatureFlagLifecycleTestsBase (FeatureFlagLifecycleTests.cs:13) and CommonArchitectureMap (:15).
  • Concept introduced - none new; the same shipped-binding shape as AiDependencyIsolationTests above and every other *TestsBase subclass in this suite. The rules themselves are taught by FeatureFlagLifecycleRuleTests against fixtures; this class is what proves the framework's own flags actually pass them, not just that the rules can be called.
  • Walkthrough - overrides Map with new CommonArchitectureMap() (:15); the two [Fact]s live entirely in the base class and run ArchitectureRules.FeatureFlagsDeclareLifetime and ArchitectureRules.TemporaryFeatureFlagsAreNotPastRemoveBy (against DateOnly.FromDateTime(DateTime.UtcNow), the base's default Today) over CommonArchitectureMap.
  • Why it's built this way - the same one-class-per-repo pattern every other *TestsBase subclass follows (ADR-015): the rule body and every edge case live once in FeatureFlagLifecycleTestsBase / FeatureFlagLifecycleRuleTests, and each repo supplies only its own IArchitectureMap. Because this fact runs against the live clock rather than a frozen Today, a framework flag that MMCA.Common itself lets overstay its RemoveBy fails this CI job on the day it expires, not only when someone thinks to check.
  • Where it's used - an independent class in the Common architecture suite; MMCA.ADC subclasses the same base with its own map (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs), and the FeatureFlagFixtures.cs file in this assembly feeds FeatureFlagLifecycleRuleTests its deliberately bad and good fixtures.

FixtureFormsTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/FormsConventionRequirednessFitnessTests.cs:38 · Level 13 · class

  • What it is - a private, primary-constructor subclass of FormsConventionTestsBase nested in FormsConventionRequirednessFitnessTests (see the rollup) that points the base's create-form scan at a fixture folder instead of a repo's Source/Modules.
  • Depends on - FormsConventionTestsBase (private sealed class FixtureFormsTests(string modulesRoot) : FormsConventionTestsBase, FormsConventionRequirednessFitnessTests.cs:38), IArchitectureMap and CommonArchitectureMap (:40).
  • Concept introduced - redirecting a file-scanning fitness base at fixtures through one virtual root. The base's ModulesRoot defaults to {repo root}/Source/Modules, located from Map.RepoToken (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:59-:60), and its doc says to override it to point the facts at a fixture tree (:55-:58). This subclass is that override. Because it is private, xUnit does not discover it, so an offending fixture does not fail the run; the outer class invokes the inherited fact directly and asserts on the outcome (class doc, FormsConventionRequirednessFitnessTests.cs:6-:11). [Rubric §14 - Testability] is the technique.
  • Walkthrough - two members. Map is a get-only auto-property initialized to new CommonArchitectureMap() (:40), satisfying the base's abstract Map (FormsConventionTestsBase.cs:46); ModulesRoot returns the constructor's modulesRoot (:42). The outer class builds the fixture root under Tests/Architecture/MMCA.Common.Architecture.Tests/RazorFixtures/Forms (:14-:20) and drives the inherited AdminCreateForms_ReadRequirednessOffTheirModel fact (FormsConventionTestsBase.cs:179-:203) twice:
    • ACommentedOutRequirednessMarker_DoesNotSatisfyTheGate (:22-:28): over the Commented folder, asserts an XunitException whose message contains WidgetCreate.razor, proving a ModelValidation.IsRequired marker inside a comment is stripped before the check (FormsConventionTestsBase.cs:187, :192).
    • ALiveMarkerInTheSiblingFieldComponent_SatisfiesTheGate (:30-:36): over the Clean folder, asserts NotThrow, proving a live marker in a sibling *FormFields.razor component counts (FormsConventionTestsBase.cs:189-:193).
  • Why it's built this way - the requiredness fact scans markup text, so the regression it guards (a commented-out marker satisfying it) can only be proven with real files on disk. Overriding one virtual path, rather than copying the scan into a test, keeps the meta-test running the exact shipped fact.
  • Where it's used - constructed only by the two facts of its enclosing FormsConventionRequirednessFitnessTests (:25, :33); nothing outside the defining file references it.

FrameworkConstructorDependencyTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs.ConstructorDependencies · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencies/FrameworkConstructorDependencyTests.cs:25 · Level 13 · class (public, sealed)

  • What it is - the single-responsibility ceiling applied to the framework itself: the three constructor-dependency facts of ConstructorDependencyCountTestsBase, re-pointed from the (empty) module populations at the framework's own Application, Infrastructure and API assemblies, with a ceiling of 7 for services, 5 for controllers and 7 for handlers (FrameworkConstructorDependencyTests.cs:33, :37, :40).
  • Depends on - ConstructorDependencyCountTestsBase (:25), CommonArchitectureMap (:31), the Layer enum and IArchitectureMap's OfLayer (:43, :46, :49), plus reflection over ControllerBase and the ICommandHandler/IQueryHandler contracts, matched by full name rather than by type reference (:27-:29).
  • Concept introduced - reusing a consumer-shaped rule on a repo with no modules by overriding its populations, not its rule. The base's defaults scan module assemblies (ScannedHandlers => Scan(Map.ModuleApplication(), IsCommandOrQueryHandler), MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:81), which are empty in MMCA.Common, so a plain two-line binding would pass vacuously. The class doc names the fix (FrameworkConstructorDependencyTests.cs:6-:12): override the three virtual populations (ConstructorDependencyCountTestsBase.cs:64, :71, :78) and the measured-constructor selector (:86) so the same facts judge the framework's own classes. Two choices make that honest. Abstract classes are included and protected constructors are measured (FrameworkConstructorDependencyTests.cs:51-:53), because the framework's widest classes are abstract bases such as AuthenticationServiceBase and the *HandlerBase/*ControllerBase families whose only constructors are protected (:10-:12). Optional parameters count, since an optional collaborator is still a collaborator (:22). [Rubric §1 - SOLID] is the property; [Rubric §34 - Architecture Governance & Documentation] is the ratchet discipline the doc states outright: lower a ceiling as remediation lands, never raise one to turn a run green (:23).
  • Walkthrough
    • Ceilings: MaxConstructorDependencies => 7 (:33); MaxControllerConstructorDependencies => 5 (:37), commented as the high-water mark of CrudEntityControllerBase, InboxController and UserAccountAuthControllerBase when the gate landed (:35-:36); MaxHandlerConstructorDependencies => 7 (:40), the mark of ResetPasswordHandlerBase (:39). These override the abstract members at ConstructorDependencyCountTestsBase.cs:43, :47, :55.
    • Populations: ScannedServices is every framework class in the Application and Infrastructure layers (FrameworkConstructorDependencyTests.cs:42-:43); ScannedControllers is the API layer filtered by IsController (:45-:46); ScannedHandlers is the Application layer filtered by IsCommandOrQueryHandler (:48-:49), which per the class doc includes the decorators (:18-:20).
    • MeasuredConstructors (:51-:53): public, protected, and protected-internal instance constructors.
    • FrameworkClasses (:61-:68): distinct assemblies, top-level non-static classes, not compiler-generated, and not records (detected by the compiler-emitted <Clone>$ method), because a positional record's parameters are data fields rather than injected collaborators (:55-:60).
    • IsController (:70-:81) walks the base-type chain for Microsoft.AspNetCore.Mvc.ControllerBase; IsCommandOrQueryHandler (:83-:87) matches any generic interface whose definition's full name is the open generic ICommandHandler<,> or IQueryHandler<,> (arity 2).
    • The three facts themselves live in the base: ApplicationServices_DoNotExceedConstructorDependencyCeiling, Controllers_DoNotExceedConstructorDependencyCeiling and Handlers_DoNotExceedConstructorDependencyCeiling (ConstructorDependencyCountTestsBase.cs:96, :116, :140).
  • Why it's built this way - matching the handler and controller contracts by full-name string keeps the test assembly free of a compile-time reference it would only need for a type check, and keeping the rule body in the base means the framework and every consumer are judged by the same arithmetic; only the populations and the ceilings differ.
  • Where it's used - an independent class in the Common architecture suite (no usage sites outside the defining file); MMCA.ADC binds the same base with its module populations through ConstructorDependencyCountTests.

ImmutabilityTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain.EntityModel · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/EntityModel/ImmutabilityTests.cs:11 · Level 13 · class (public, sealed)

  • What it is - the MMCA.Common binding of the immutability rules: DTOs, command/query messages, domain events, integration events and value objects expose no public mutable setter, run over the framework's own assemblies (class doc, ImmutabilityTests.cs:5-:10).
  • Depends on - ImmutabilityTestsBase (:11) and FrameworkModuleArchitectureMap (:13), deliberately not CommonArchitectureMap.
  • Concept introduced - choosing the map that makes a module-scoped rule bite. The DTO, command/query and domain-event immutability rules scope to the map's module layers (FrameworkModuleArchitectureMap.cs:6-:10), and CommonArchitectureMap registers the framework assemblies as framework layers, where those rules are vacuous. FrameworkModuleArchitectureMap registers the same Shared, Domain and Application assemblies as one module named Framework, leaving Infrastructure a framework layer (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/EntityModel/FrameworkModuleArchitectureMap.cs:19, :25-:28), so the rules actually run over the aggregates MMCA.Common ships, the Notifications family (:6-:11). It is the opposite move to FakeConsumerMap: there a module entry is added to prove a rule stays out of framework code; here it is added to pull framework code in. [Rubric §4 - DDD] is the property (the class doc tags rubric section 4, ImmutabilityTests.cs:6); [Rubric §34 - Architecture Governance & Documentation] is applying a consumer convention to the framework itself.
  • Walkthrough - one member, Map (:13), overridden to new FrameworkModuleArchitectureMap(). The five facts are inherited: Dtos_ShouldBe_Immutable, CommandsAndQueries_ShouldBe_Immutable, DomainEvents_ShouldBe_Immutable, IntegrationEvents_ShouldBe_Immutable and ValueObjects_ShouldBe_ImmutableSealedAndInShared (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ImmutabilityTestsBase.cs:13, :16, :19, :22, :25).
  • Why it's built this way - a two-line binding over the wrong map would be green forever and prove nothing; a dedicated map keeps the shared rule untouched while giving it real subjects in the repo that defines it.
  • Where it's used - an independent class in the Common architecture suite. MMCA.ADC subclasses the same base under the same class name with its own map (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ImmutabilityTests.cs).

IntegrationEventPayloadPurityRuleTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs:28 · Level 13 · class

  • What it is - the direct-fixture test of the two rules IntegrationEventPayloadPurityTests (below) exercises indirectly: it calls ArchitectureRules.IntegrationEventsLiveInSharedAssemblies and ArchitectureRules.IntegrationEventPayloadsAreDomainFree against two small purpose-built maps and a fixture event, and asserts on the exception message the rule throws.
  • Depends on - ArchitectureRules (:33,:43,:51,:64,:74), ArchitectureMapBase (:78,:87), and two fixtures defined in the same file: FixtureCleanEvent, FixtureLeakingEvent/FixtureLeakedPayload, and FixtureLeakingEvent.Payload/.Entity.
  • Concept introduced - proving a rule fires by reading its failure message, not just its pass/fail. Four of the five [Fact]s assert on .Which.Message, not merely Should().Throw(), because an architecture rule that throws for the wrong reason is a false positive that still turns CI red. PayloadRule_FlagsADomainTypeReachedThroughANestedPayload (:49-:59) checks the message names the full PATH (FixtureLeakingEvent.Payload.Entity), not just the event, because a leak one level down is exactly the one a reviewer skims past in a shorter message. Two private nested maps carry the scope split: FrameworkProbeMap (:78-:84) has no module boundary, so the residency rule (which only fires inside a *.Shared module assembly) is vacuous over it and only the payload rule is exercised; ModuleProbeMap (:87-:93) wraps the same assembly in a Module(...) layer so the residency rule has something to react to. [Rubric §7 - Microservices Readiness] is the property under test (a leaked domain type in an integration event payload is a hidden coupling between services); [Rubric §34 - Architecture Governance & Documentation] is the message-quality discipline.
  • Walkthrough - five [Fact]s: ResidencyRule_FlagsAnEventOutsideASharedAssembly (:31-:39) runs the residency rule over ModuleProbeMap and asserts the message names FixtureCleanEvent and says "is not a *.Shared assembly". ResidencyRule_IsVacuousForAModuleLessMap (:42-:46) runs it over FrameworkProbeMap and asserts no throw. PayloadRule_FlagsADomainTypeReachedThroughANestedPayload (:49-:59) and PayloadRule_LeavesACleanEventAlone (:62-:70) run the payload rule over FrameworkProbeMap against the leaking and the clean fixture respectively. PayloadRule_PassesOnTheFrameworksOwnAssemblies (:73-:75) runs the payload rule over CommonArchitectureMap itself and asserts it does not throw, naming OutputCacheEvictionRequested in the assertion message as the concrete proof (a list of strings and nothing else).
  • Why it's built this way - splitting the residency and payload checks into their own maps keeps each [Fact] a single-variable test: a residency failure and a payload failure are different rules with different scopes, and conflating them in one fixture would make a red run ambiguous about which rule broke. See ADR-015.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI's build-and-test job; it is the fixture-level proof behind the subject-level assertion IntegrationEventPayloadPurityTests makes against the framework's own assemblies.

IntegrationEventPayloadPurityTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs:18 · Level 13 · class

  • What it is - the MMCA.Common binding of the integration-event purity rule: a two-line subclass that supplies the repo's map so every integration event the framework defines is checked for a domain-free payload and (where applicable) residency in a *.Shared assembly.
  • Depends on - IntegrationEventPayloadPurityTestsBase (public sealed class IntegrationEventPayloadPurityTests : IntegrationEventPayloadPurityTestsBase, :18) and CommonArchitectureMap (:20, the single override).
  • Concept introduced - cross-references IntegrationEventPayloadPurityRuleTests above for the rule mechanics; this subclass supplies no new behavior, only the map. [Rubric §7 - Microservices Readiness] applies the same way: a leaked domain entity in an event payload couples every consumer of that event to the producer's internals.
  • Walkthrough - one member, protected override IArchitectureMap Map { get; } = new CommonArchitectureMap(); (:20). The base class contributes the [Fact]s that walk every integration event the map's layers expose.
  • Why it's built this way - the same zero-argument subclass shape used throughout this chapter for binding a shared rule base to a repo-specific map, so the rule runs once per repo with that repo's own event set.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI's build-and-test job. Subclassed the same way in MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs.

Probe

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DataResidencyTestsBaseTests.cs:24 · Level 13 · class

  • What it is - a private nested subclass of DataResidencyTestsBase inside DataResidencyTestsBaseTests, whose only job is to expose the base's protected ContainsRegionClaim helper to the test class (DataResidencyTestsBaseTests.cs:24-:31).
  • Depends on - DataResidencyTestsBase (private sealed class Probe : DataResidencyTestsBase, :24), CommonArchitectureMap and the IArchitectureMap abstraction (:26).
  • Concept - a test-only subclass used as a key to a protected member. ContainsRegionClaim is protected static on the base (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DataResidencyTestsBase.cs:57), so only a derived type can call it; Probe derives, forwards it as public static bool Contains(string policy, string claim) (:28), and satisfies the two abstract members with the minimum: Map (:26) and an ExtractDeployedRegion that returns string.Empty (:30). It is private deliberately: the base carries a [Fact] of its own, and a public subclass would be collected by xUnit and run that fact against the real repo root, which the framework repo has no deployed region for (class doc, DataResidencyTestsBaseTests.cs:6-:8).
  • Where it's used - only by ContainsRegionClaim_MatchesWholeRegionTokensOnly in the enclosing class (:20).

QueryHandlerReadRepositoryTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs.Repositories · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/Repositories/QueryHandlerReadRepositoryTests.cs:14 · Level 13 · class

  • What it is - the fixture-level proof of the query-handler-uses-read-repository convention rule: four [Fact]s that run ArchitectureRules.QueryHandlersUseReadRepositories against a small fixture assembly and assert on which handler names its failure message names.
  • Depends on - ArchitectureRules (:141, called as ArchitectureRules.QueryHandlersUseReadRepositories(_fixtureMap, allowed)), QueryHandlerReadRepositoryTestsBase (public sealed class QueryHandlerReadRepositoryTests : QueryHandlerReadRepositoryTestsBase, :14), ArchitectureMapBase (the nested FixtureAssemblyMap, :151-:159), and three fixtures from MMCA.Common.Architecture.Tests.ReadRepositoryFixtures: WriteRepositoryQueryHandlerFixture, ReadRepositoryQueryHandlerFixture, WriteRepositoryCommandHandlerFixture (:118,:123,:128).
  • Concept introduced - finding a repository call after an await, inside the compiler's generated state machine. The first [Fact]'s own assertion message states the point directly: "the GetRepository call sits after an await, inside the compiler's state machine, and must still be found" (:116-:119). A query handler that calls GetRepository for the write side only after awaiting some other call compiles to a state machine type, not the method body as written, so a naive IL scan over the declared method would miss it; this fixture exists specifically to prove the rule still catches that shape. [Rubric §6 - CQRS & Event-Driven] is the property: a query handler that reaches for the write repository is either a misclassified command or a read that will bypass whatever read-side optimization (a cached, projected, or replica-backed repository) the write repository does not offer.
  • Walkthrough - four [Fact]s plus a private Report helper and a nested FixtureAssemblyMap. QueryHandlerAskingForTheWriteRepository_IsFlagged_EvenInsideAnAsyncBody (:116-:119) asserts the report contains WriteRepositoryQueryHandlerFixture. QueryHandlerAskingForTheReadRepository_IsNotFlagged (:122-:123) asserts the opposite for the clean fixture. CommandHandlerAskingForTheWriteRepository_IsOutOfScope (:126-:129) asserts a command handler naming the write repository is never flagged, "a command handler writes, so the write repository is the right one there" (:129). AllowlistedHandler_IsSilenced (:132-:134) passes the write-repository query handler's full name into the rule's allowed collection and asserts it drops out of the report. Report (:137-:148) calls the rule inside a try/catch (XunitException) and returns either the empty string or the caught message, so each fact can assert on message content rather than only on throw/no-throw. FixtureAssemblyMap (:151-:159) is an ArchitectureMapBase whose only registered layer is Framework(Layer.Application, typeof(ReadRepositoryQueryHandlerFixture).Assembly), so the rule scans only this test assembly's fixtures rather than the framework's real Application layer.
  • Why it's built this way - proving the rule by message content, the same idiom IntegrationEventPayloadPurityRuleTests uses, catches a rule that throws for the wrong reason; and routing the scan through a fixture-only map keeps the real CommonArchitectureMap free of deliberately-wrong handlers that would otherwise have to be allowlisted permanently just to keep this test passing. See ADR-015.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI's build-and-test job; its fixtures live in ReadRepositoryFixtures.cs in the same assembly, and MMCA.ADC subclasses the same base against its own map (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/QueryHandlerReadRepositoryTests.cs).

RawSqlConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Cqrs · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/RawSqlConventionTests.cs:12 · Level 13 · class

  • What it is - the MMCA.Common binding of the raw-SQL convention rule: a subclass that scans the framework's own Application and Infrastructure source for interpolated or concatenated SQL text and allowlists the one file that legitimately needs it.
  • Depends on - RawSqlConventionTestsBase (public sealed class RawSqlConventionTests : RawSqlConventionTestsBase, :12) and CommonArchitectureMap (:14); ArchitectureMapBase.FindRepoRoot (:19) locates the solution root the scan walks from.
  • Concept introduced - scanning source text, not the compiled model, because the risk (SQL injection) lives in the string literal, not in a type reference a Mono.Cecil rule can see. ScannedSourceDirectories (:17-:22) points the base's text scan at Source/Core/MMCA.Common.Application and Source/Core/MMCA.Common.Infrastructure, the two layers where a hand-built query could appear. AllowedFiles (:25-:32) allowlists exactly one file, DbContextFactory.cs, for a SET IDENTITY_INSERT [schema].[table] ON/OFF statement whose table and schema names come off EF model metadata (entityType.GetSchema()/GetTableName()) rather than caller input, because T-SQL has no parameterized form for an identifier in that position. [Rubric §29 - Security Practices] is the property: the rule fails CI on any new interpolated SQL string outside this one reviewed exception, catching the injection surface before a query with caller-controlled text merges. [Rubric §34 - Architecture Governance & Documentation] is the allowlist-with-reasons discipline; the comment names the matching S2077 Sonar suppression at the call site itself, so the same exception is argued in two places that have to agree.
  • Walkthrough - two members. ScannedSourceDirectories (:17-:22) yields the two directory paths under the discovered repo root. AllowedFiles (:25-:32) is a one-entry IReadOnlyList<string> with the justification comment (:27-:30) directly above the filename (:31).
  • Why it's built this way - text-scanning rather than model-scanning means the rule also catches raw SQL that never reaches EF Core at all (a SqlCommand built by hand, for instance), which a Cecil-based rule over compiled IL would only see after the fact if it used a parameter, not a literal. See ADR-015.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI's build-and-test job. Subclassed the same way in MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/RawSqlConventionTests.cs.

ServiceContractPurityTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Layering · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ServiceContractPurityTests.cs:11 · Level 13 · class

  • What it is - the MMCA.Common binding of the [ServiceContract] purity rule: a two-line subclass that supplies the repo's map so any type marked as part of a published wire surface is checked for dependencies on the producing service's Domain, Application or Infrastructure.
  • Depends on - ServiceContractPurityTestsBase (public sealed class ServiceContractPurityTests : ServiceContractPurityTestsBase, ServiceContractPurityTests.cs:11), IArchitectureMap and CommonArchitectureMap (the single override, :13), and indirectly ServiceContractAttribute, which the rule matches by full name rather than by reference.
  • Concept introduced - a rule that is deliberately vacuous today, kept as a ratchet. Most gates in this chapter earn their place by failing on real code. This one asserts nothing in MMCA.Common, because the framework marks no type with [ServiceContract], and both the subclass doc (:9-:10) and the base's remarks (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ServiceContractPurityTestsBase.cs:12-:17) say so plainly. The value is in the timing, the same argument the three ratchet subclasses rest on: the invariant is enforced from the first marked type onward, with no test for anyone to remember to write, at the moment when a contract package's shape is still cheap to change. Two design choices follow from that. It is attribute-driven rather than Layer.Contracts-driven, because no repo registers that layer today and a layer-iterating rule would pass vacuously forever (base remarks, :9-:11); and it scans every assembly the map registers, so a marked type is judged wherever it lives. A marked type sitting inside a Domain, Application or Infrastructure assembly then fails by construction, which the rule's own remarks call the intent: a published contract belongs in a *.Contracts or Shared assembly, not inside the service it describes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:26-:29). [Rubric §7 - Microservices Readiness] is the property: a contract that leaks a domain entity, a handler abstraction or a persistence type forces every consumer to take the producer's internals as a package dependency, which is what makes an extraction irreversible (:16-:18). [Rubric §9 - API & Contract Design] covers the wire surface itself, and [Rubric §34 - Architecture Governance & Documentation] the ratchet.
  • Walkthrough - one member, protected override IArchitectureMap Map { get; } = new CommonArchitectureMap(); (:13). Everything else is inherited: the base contributes a single [Fact], ServiceContracts_ShouldNotDependOn_ServiceInternals (ServiceContractPurityTestsBase.cs:24-:26), which calls ArchitectureRules.ServiceContractsDoNotDependOnServiceInternals(Map). The rule first derives the forbidden namespace set from the map's Domain, Application and Infrastructure layers (ArchitectureRules.Contracts.cs:57-:66) and returns immediately when that set is empty (:35-:38), then walks every layer, selects marked types through the Mono.Cecil custom rule CarriesServiceContractAttribute (:44,:69-:74), and asserts ShouldNot().HaveDependencyOnAny(forbidden) per layer with the layer's root namespace in the message (:42-:52).
  • Why it's built this way - matching the marker by its full-name string, "MMCA.Common.Shared.Abstractions.ServiceContractAttribute" (ArchitectureRules.Contracts.cs:10-:11), is the same zero-reference idiom the rest of the rule library uses: the testing package deliberately takes no compile dependency on the framework assemblies it inspects. The rule complements, and does not replace, the transport- and layer-purity rules that guard the same boundary from the layer side (ADR-007, ADR-015).
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI's build-and-test job. Its siblings in this chapter are ProtoContractFitnessTests and ContractImplementationTests, the other consumer-facing contract gates the framework exercises without owning any subject of its own.
  • Caveats / not-in-source - because the framework marks no type, this class asserts nothing today; there is no fixture proving the rule fires. That proof exists only in whichever repo first marks a type.

StronglyTypedIdConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/StronglyTypedIdConventionTests.cs:12 · Level 13 · class

  • What it is - the framework repo's binding of the strongly typed identifier fitness function (ADR-115): a three-line sealed subclass that supplies the map and inherits the one [Fact], so any wrapper the framework repo declares is held to the readonly-record-struct shape.
  • Depends on - StronglyTypedIdTestsBase (public sealed class StronglyTypedIdConventionTests : StronglyTypedIdTestsBase, StronglyTypedIdConventionTests.cs:12), CommonArchitectureMap and the IArchitectureMap abstraction it satisfies (StronglyTypedIdConventionTests.cs:14).
  • Concept - the same base-plus-thin-subclass shape as UIArchitectureConventionTests and CancellationTokenConventionTestsBase: the rule lives once in MMCA.Common.Testing.Architecture and is re-run per repo by a subclass whose only job is to name the repo. The rule is deliberately vacuous today: every repo still defaults to the primitive identifier aliases (ADR-048/ADR-085), so MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.StronglyTypedIds.cs:38-:41 finds zero matching types across the map's layers and the rule passes with nothing asserted. Its purpose is to hold the FIRST wrapper a repo declares to the shape the framework's EF converter, JSON converter, comparer and route binding are written against (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/StronglyTypedIdTestsBase.cs:4-:8).
  • Walkthrough - one member: protected override IArchitectureMap Map { get; } = new CommonArchitectureMap(); (StronglyTypedIdConventionTests.cs:14). The inherited fact, StronglyTypedIds_ShouldBe_ReadonlyRecordStructs, calls ArchitectureRules.StronglyTypedIdsAreReadonlyRecordStructs(Map) (StronglyTypedIdTestsBase.cs:14-:15), which loads every type across the map's distinct layer assemblies, filters to types implementing IStronglyTypedId by full-name prefix match, and asserts three properties on each match: it is a value type (ArchitectureRules.StronglyTypedIds.cs:43-:45), declared readonly (:47-:49), and a record (:51-:53), plus that it declares no state beyond the single Value property.
  • Why it's built this way - the three checked properties are load-bearing rather than stylistic: struct keeps an identifier allocation-free on the hot path a primitive alias occupies today, readonly prevents a tracked entity's key from changing under it, and record supplies the structural equality the wrapper exists for (ArchitectureRules.StronglyTypedIds.cs:14-:22). The rule is kept separate from the value-object immutability rule because an identifier has no validation to fail and no Result-returning factory (:26-:29).
  • Where it's used - discovered and run by xUnit as part of MMCA.Common.Architecture.Tests; nothing constructs it. No other repo subclasses StronglyTypedIdTestsBase today: the aliases remain the default everywhere, so this is currently the only binding.

TenantEntityConventionTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain.EntityModel · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/EntityModel/TenantEntityConventionTests.cs:20 · Level 13 · class

  • What it is - a framework-only tenancy marker rule (rubric section 4): two [Fact]s asserting that every concrete MMCA.Common entity that declares or inherits a TenantId property implements ITenantEntity (class doc, TenantEntityConventionTests.cs:6-:11). Unlike its neighbours in this part it is not a subclass of a shared *TestsBase; the rule exists only in this repo.
  • Depends on - CommonArchitectureMap (constructed directly, :47), the Layer enum (:49), ITenantEntity (:35), and, by full-name prefix only, BaseEntity<TIdentifierType> (BaseEntityFullNamePrefix, :22).
  • Concept introduced - a column that looks scoped but is not. ITenantEntity is the marker that applies the named Tenant query filter and the TenantSaveChangesInterceptor stamp, so an entity carrying a tenant column without it is readable and writable across tenants while appearing tenant-scoped (:9-:11). The rule catches that by structure (a property named TenantId, public or not, :23, :34) rather than by trusting the author to remember the marker. Equally deliberate is what it leaves out: the population is the BaseEntity<TId> hierarchy only, so the outbox, internal-command and audit-trail rows fall outside it. Those rows RECORD the tenant of the scope that wrote them (a nullable ambient-context column restored on delivery) rather than being owned by a tenant, and a read filter on them would hide work from the background drainers (:12-:18). [Rubric §4 - DDD] is the category the doc cites; multi-tenant isolation is the property it protects.
  • Walkthrough - FrameworkEntities (:45-:54) unions the map's Domain layer with its Infrastructure assemblies (:49-:51), de-duplicates, and keeps concrete classes for which DerivesFromBaseEntity is true (:53). DerivesFromBaseEntity (:56-:68) walks the base-type chain, reducing each generic base to its definition, and matches the full name against MMCA.Common.Domain.Entities.BaseEntity` by ordinal prefix (:58-:64), so every arity of BaseEntity and every subclass of it (auditable, aggregate root) qualifies. FrameworkEntities_AreDiscovered_GateIsNotVacuous (:25-:28) asserts the population is non-empty; EntitiesWithTenantId_ShouldImplement_ITenantEntity (:30-:42) selects entities that expose a TenantId instance property but are not assignable to ITenantEntity and fails with their full names (:33-:41).
  • Why it's built this way - the non-vacuous guard sits beside the real assertion because both halves of the population query are name- and layer-driven: a renamed BaseEntity or a map that stopped registering Domain would otherwise leave a rule that passes over an empty set.
  • Where it's used - discovered and run by xUnit as part of MMCA.Common.Architecture.Tests in CI's build-and-test job; nothing constructs it and no consumer repo carries a counterpart.

UpcasterTestMap

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Contracts · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventUpcasterFitnessTests.cs:85 · Level 13 · class

  • What it is - a one-layer architecture map used only by EventUpcasterFitnessTests: it registers this test assembly as the map's single Application layer so the two upcaster rules see the fixture upcasters and nothing else.
  • Depends on - ArchitectureMapBase (private sealed class UpcasterTestMap : ArchitectureMapBase, EventUpcasterFitnessTests.cs:85), LayerRef, and the Layer enum.
  • Concept - cross-references the map concept from CommonArchitectureMap, with one detail specific to these rules. Both of them scope by ownership exactly as the integration-event rules do: EventUpcasters includes framework layers only when the map declares no modules (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:81-:64). Because this map is built entirely from Framework(...) entries, ModuleNames is empty and the single Application layer is scanned, which is what puts the fixtures in scope. Registering a module layer instead would silently exclude them and both rules would pass vacuously, so the choice is load-bearing rather than incidental. Compare FakeConsumerMap and FixtureModuleMap, which flip exactly that switch on purpose for rules that scope the other way.
  • Walkthrough - RepoToken => "MMCA.Common" (:76) and a one-entry DefineLayers() returning Framework(Layer.Application, typeof(EventUpcasterFitnessTests).Assembly) (:78-:79). The doc comment states the intent in one line (:73). The layer's derived root namespace is unused by these rules, which enumerate ConcreteClasses across whole assemblies (ArchitectureRules.Upcasters.cs:86) rather than namespace-scoped subsets.
  • Where it's used - constructed on every call of the test class's two private rule helpers (EventUpcasterFitnessTests.cs:72,:68).

SoftDeleteEnforcementTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Domain · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:19 · Level 13 · class

  • What it is - the MMCA.Common binding of the hard-delete rule, and the one thin subclass in this unit whose override is a substantial, individually-argued document: the six framework types that are allowed to erase rows, each with the reason written next to it.
  • Depends on - SoftDeleteEnforcementTestsBase (public sealed class SoftDeleteEnforcementTests : SoftDeleteEnforcementTestsBase, SoftDeleteEnforcementTests.cs:19) and CommonArchitectureMap (:21).
  • Concept introduced - the rule turned back on its own author, and why that keeps a downstream allowlist honest. Store and ADC subclass the same base and allowlist exactly these six framework types, because no module of theirs erases a row of its own. Running the rule here is what keeps that list correct: a NEW framework eraser would otherwise land in the packages and fail downstream, in a repo whose maintainers did not write it and cannot judge it, rather than in the repo where the change was made (class doc, :10-:17). The second decision in that doc is finer: each entry is named individually rather than exempting the MMCA.Common.Infrastructure.Persistence namespace, so a seventh eraser under that namespace still fails here and gets reviewed. A namespace exemption would have been two characters shorter and would have silently pre-approved every future eraser in the framework's persistence layer. [Rubric §8 - Data Architecture] and [Rubric §30 - Compliance/Privacy/Data Governance] are the properties (ADR-005); [Rubric §34 - Architecture Governance & Documentation] is the allowlist-with-reasons discipline; [Rubric §33 - Developer Experience] is the failure-location argument.
  • Walkthrough - two members. Map (:21) is the usual override. AllowedHardDeleteTypes (:24-:60) is the substance, six fully-qualified entries each preceded by its justification:
    • EFRepository<TEntity, TIdentifierType> (:31, listed by its arity-suffixed metadata name under MMCA.Common.Infrastructure.Persistence.Repositories), the framework's set-based delete escape hatch. The comment states the calling convention that makes it acceptable (:26-:30): erasing is the caller's explicit ask (the method is named ExecuteDeleteAsync), and it targets derived rows the caller is about to rewrite, never user data carrying an audit or erasure obligation. Allowlisting the implementation rather than the abstraction is what leaves IRepository free to be used everywhere else.
    • OutboxCleanupService (:36, now under ...Persistence.Outbox.Administration), for PurgeAsync, PurgeInboxAsync and SweepDeadLettersAsync. These rows are delivery plumbing with a bounded lifetime, and the sweep is the retention policy; soft-deleting them would grow the table the job exists to bound (:33-:35).
    • InternalCommandCleanupService and InternalCommandAdministration (:42-:43), the outbox precedent applied to deferred work: an internal-command queue row is execution plumbing with a bounded lifetime whose payload can carry personal data, so the sweep IS the retention policy, and the operator-facing purge beside it erases only rows that already completed (:38-:41).
    • AuditTrailCleanupJob (:47), for audit-trail retention. The comment inverts the usual instinct (:45-:46): erasing past the retention window IS the requirement, and keeping an audit row forever is the privacy defect rather than the safeguard.
    • RefreshSessionCleanupService (:53), for refresh-session retention. A session row is framework bookkeeping rather than an aggregate: it carries no IsDeleted flag and no audit stamps, and its content is a credential digest plus the IP and user-agent of a device, so flagging it instead of erasing it would keep a growing record of a data subject's devices past any use for it (:49-:52).
    • EFPermissionGrantStore (:59), for stored permission grants. A grant row is a set membership, not an aggregate: revoking a capability means the row is not there, and a soft-deleted grant would have to be filtered out of every authorization read forever, with a missed filter failing OPEN. It carries no audit or erasure obligation either, since its whole content is a role name and a permission name (:55-:58).
  • Why it's built this way - because the entries are fully-qualified type names, a namespace move is a test failure until the list is updated: the 2026-09 folder reorganization that put the outbox cleanup job under Outbox/Administration had to be reflected here (:36). The internal-command and permission-grant entries were added on top of the original four once those stores shipped, following the exact same precedent (the outbox entry) rather than a new argument. Six reasons written in a compiled file are also six reasons that get read in a diff when someone adds a seventh. That is the whole argument for an allowlist over a suppression: the list is where the erasure policy actually lives. See ADR-005 and ADR-015; the rule's own behavior is proven against compiled fixtures by SoftDeleteEnforcementFitnessTests.
  • Where it's used - run by the MMCA.Common.Architecture.Tests suite in CI's build-and-test job. The same entries appear in the Store and ADC subclasses of the same base (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs).

DataResidencyTestsBaseTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Governance · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DataResidencyTestsBaseTests.cs:10 · Level 14 · class

  • What it is - the unit test for the region comparison inside the data-residency gate: one six-case [Theory] that pins DataResidencyTestsBase.ContainsRegionClaim to whole-token matching (DataResidencyTestsBaseTests.cs:12-:21).
  • Depends on - DataResidencyTestsBase, reached through its nested Probe subclass (:24-:31).
  • Concept introduced - testing the helper because the gate itself reads the real repo. The base's one fact, PrivacyPolicy_DataStorageRegion_MatchesDeployedRegion, locates the repo root, parses the deployed region from a repo-specific source and reads PRIVACY.md (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DataResidencyTestsBase.cs:26-:45), so it cannot be fed fixtures; the comparison helper is the testable unit (class doc, DataResidencyTestsBaseTests.cs:6-:8). The failure it guards is a false pass: Azure region names nest (westus is a prefix of westus2, centralus is a suffix of southcentralus), so a naive substring check would let a policy claiming "West US 2" satisfy a deployment in westus. The helper normalizes both sides (whitespace stripped, upper-cased) and accepts an occurrence only when no digit follows it and the text before it does not end in a directional prefix (DataResidencyTestsBase.cs:47-:79). [Rubric §30 - Compliance/Privacy/Data Governance] is the property (ADR-105).
  • Walkthrough - ContainsRegionClaim_MatchesWholeRegionTokensOnly(policy, claim, expected) (:19-:22) calls Probe.Contains and asserts the expected boolean, with the because-text "a region that is a prefix of another region must not satisfy the residency gate" (:22). The six [InlineData] rows (:13-:18) pair the negatives with their positives: westus against "West US 2" is false while westus2 is true; centralus against "South Central US" is false while against "Central US" it is true; and two rows prove markdown emphasis (**West US 2**) and a spelled-out claim ("Central US") still match.
  • Where it's used - discovered and run by xUnit as part of MMCA.Common.Architecture.Tests; the gate it backs runs in consumer repos as thin subclasses such as DataResidencyTests in MMCA.ADC.

FormsConventionRequirednessFitnessTests

MMCA.Common.Architecture.Tests · MMCA.Common.Architecture.Tests.Ui · MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/FormsConventionRequirednessFitnessTests.cs:12 · Level 14 · class (public, sealed)

  • What it is - the fixture-level proof of the create-form requiredness fact in FormsConventionTestsBase: two [Fact]s that run AdminCreateForms_ReadRequirednessOffTheirModel against two small .razor fixture trees and assert it fails on one and passes on the other (FormsConventionRequirednessFitnessTests.cs:22-:36).
  • Depends on - FormsConventionTestsBase (the private nested FixtureFormsTests, :38), CommonArchitectureMap and the IArchitectureMap abstraction (:40), ArchitectureMapBase.FindRepoRoot (:15, see ArchitectureMapBase), and XunitException from Xunit.Sdk (:2, :27). Its subjects are four fixture files under RazorFixtures/Forms (:14-:20): Commented/WidgetCreate.razor and Commented/WidgetFormFields.razor, Clean/GadgetCreate.razor and Clean/GadgetFormFields.razor.
  • Concept introduced - a marker that is present in the text but absent from the page. The requiredness fact is a text scan: it passes a create form when its own markup, or any sibling *FormFields.razor beside it, contains ModelValidation.IsRequired (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:180-:203). A text scan alone would accept a marker that sits inside a comment and renders nothing. The base therefore strips Razor (@* *@) and HTML (<!-- -->) comments from each file before it joins the scan (:187, :192, the stripper at :263-:264), and this class is the regression proof of that step (class doc, FormsConventionRequirednessFitnessTests.cs:6-:11, which tags the backlog item M258). The Commented fixture hides the marker in both possible places: a Razor comment in the page (RazorFixtures/Forms/Commented/WidgetCreate.razor:1-:4) and an HTML comment in the sibling field component (RazorFixtures/Forms/Commented/WidgetFormFields.razor:1). The Clean fixture is the positive control for the extraction the fact follows: the page declares nothing itself (RazorFixtures/Forms/Clean/GadgetCreate.razor:1-:5) and its sibling carries the live marker (RazorFixtures/Forms/Clean/GadgetFormFields.razor:1). [Rubric §24 - UX Safety] is the property the base guards (its failure message names the §24 model-sourced validation bridge, FormsConventionTestsBase.cs:202); [Rubric §34 - Architecture Governance & Documentation] is proving a gate can fail, not only that it passes.
  • Walkthrough
    • FormsFixtureRoot (FormsConventionRequirednessFitnessTests.cs:14-:20): the RazorFixtures/Forms folder resolved from the repo root located by MMCA.Common.slnx.
    • ACommentedOutRequirednessMarker_DoesNotSatisfyTheGate (:22-:28): points the fixture subclass at Commented, invokes the fact directly, and asserts it throws an XunitException whose message contains WidgetCreate.razor (:27), so the failure is pinned to the right form rather than to any throw.
    • ALiveMarkerInTheSiblingFieldComponent_SatisfiesTheGate (:30-:36): points it at Clean and asserts no throw.
    • FixtureFormsTests(string modulesRoot) (:38-:43): a private sealed primary-constructor subclass that overrides Map with new CommonArchitectureMap() (:40) and ModulesRoot with the fixture folder (:42). ModulesRoot is the base's virtual scan root, Source/Modules under the repo root by default (FormsConventionTestsBase.cs:55-:60); the fixture folders each hold one *Create.razor, which meets the base's default MinimumCreateForms of 1 (:53) so the discovery guard in DiscoverCreateForms (:233-:248) does not fire first.
  • Why it's built this way - the subclass is private so xUnit does not discover it and run the base's facts over the fixture trees as real tests; the one fact under test is invoked by hand instead (class doc, FormsConventionRequirednessFitnessTests.cs:9-:10). Real .razor files rather than inline strings keep the fixtures on the same file-reading path the gate uses in production, the same choice SortableColumnFitnessTests and InlineStyleFitnessTests make for their text-scanning rules.
  • Where it's used - discovered and run by xUnit as part of MMCA.Common.Architecture.Tests (no usage sites outside the defining file). The base it proves runs for real in consumer repos as thin subclasses such as FormsConventionTests in MMCA.ADC.

CrossServiceDataSource

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:15 · Level 0 · sealed record

  • What it is: a two-field record naming one logical data source a cross-service fixture routes to its own physical database: the logical name the framework's DataSources configuration section keys on (normally the module name) and the database that name resolves to on the shared SQL Server container (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:8-15).
  • Depends on: nothing. A positional sealed record of two strings, declared above CrossServiceFixtureBase in the same file.
  • Concept: it is the declarative half of database-per-service (ADR-006, taught in primer §2) expressed as test data. The doc's own examples are the shape to hold onto: LogicalName is Conference, DatabaseName is ADC_Conference (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:13-14). [Rubric §8, Data Architecture] assesses whether each service owns its own store; this record is how a test fixture states that ownership once and derives everything else from it.
  • Walkthrough: two positional members, LogicalName and DatabaseName (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:15), so it gets structural equality and immutability for free. The base consumes each instance three ways: the database name drives the pre-create loop (CrossServiceFixtureBase.cs:219), and the logical name drives both environment keys SetNamedDataSource pushes, DataSources__{LogicalName}__SQLServerConnectionString and DataSources__{LogicalName}__SQLServerMigrationsAssembly (CrossServiceFixtureBase.cs:278-281).
  • Where it's used: as the DataSources list a subclass supplies (CrossServiceFixtureBase.cs:66); ADC declares three, Identity, Conference and Engagement (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:62-67), and Store declares two, Catalog and Sales (MMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:56-60).

DependencyInjectionAssert

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:13 · Level 0 · class (static)

  • What it is: a one-method assertion helper for the DI registration extensions every module and layer exposes. It proves a registration extension hands back the very IServiceCollection it was given, so a fluent chain stays intact.
  • Depends on: AwesomeAssertions and Microsoft.Extensions.DependencyInjection (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:1-2). No first-party dependency.
  • Concept introduced, the fluent-contract guard. The framework's registration methods are fluent by convention: hosts chain AddApplication().AddInfrastructure(...).AddAPI(...). An extension that returns a new collection silently drops every registration chained after it, and no other test catches that, because the dropped services are simply absent rather than wrong (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:6-11). [Rubric §14, Testability] assesses whether an invariant can be checked cheaply; this one turns an otherwise invisible composition failure into a one-line test. [Rubric §15, Best Practices & Code Quality] covers the convention itself: the return-the-same-collection contract is what lets host composition stay declarative.
  • Walkthrough
    • ReturnsSameCollection(Func<IServiceCollection, IServiceCollection> register) (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:21-32): null-guards the delegate (:23), creates the ServiceCollection itself so the call site stays one line (:25, the doc shows the shape at :16-18), invokes the registration under test (:27), and asserts result.Should().BeSameAs(services, ...) with a because-reason that spells out the consequence of failing (:29-31).
    • Reference equality is the whole assertion. It deliberately says nothing about what was registered; the per-module tests that call it assert their own service descriptors separately.
  • Why it's built this way: creating the collection inside the helper is what keeps adoption free. A module's DI test adds one line per registration extension rather than three lines of arrange plus an assertion nobody remembers to write.
  • Where it's used: seven module DI test classes across the two apps, for example MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.Infrastructure.Tests/DependencyInjectionTests.cs:29, MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.API.Tests/DependencyInjectionTests.cs:63,68, MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.API.Tests/DependencyInjectionTests.cs:68,73, MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.API.Tests/DependencyInjectionTests.cs:49,54, MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.API.Tests/DependencyInjectionTests.cs:26,31, and MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.Application.Tests/DependencyInjectionTests.cs:35. MMCA.Common self-tests the helper, including that it fails for an extension returning a different collection (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/DependencyInjectionAssertTests.cs:13).

Direction

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:67 · Level 0 · enum (private, nested)

  • What it is: a two-value private enum nested in OpenApiBreakingChangeDetector, naming which way a schema travels: Request or Response (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:67-71).
  • Depends on: nothing.
  • Concept introduced, direction-sensitive compatibility. The same schema edit is breaking in one direction and harmless in the other. [Rubric §14, Testability] is served by making that asymmetry an explicit input rather than two copies of the comparison.
  • Walkthrough
    • Response is passed when walking response bodies (:394); a baseline property missing from the live schema is then reported as RemovedResponseProperty (:477-480).
    • Request is passed for request bodies and parameter schemas (:323, :378); on this side the check is instead about newly required properties (:484).
  • Why it's built this way: a client sends requests and reads responses, so removing a response property breaks readers, while adding a required request property breaks writers. One recursive CompareSchema threads the enum instead of duplicating the walk.
  • Where it's used: only inside the defining file; nothing outside it references the type.

EntityBuilderBase<TBuilder, TEntity>

MMCA.Common.Testing · MMCA.Common.Testing.Builders · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Builders/EntityBuilderBase.cs:9 · Level 0 · class (abstract)

  • What it is: the tiny root of the framework's fluent test-data builders. A subclass fixes sensible defaults for one entity type so a test only has to state the properties it actually cares about, then calls Build() to materialize the entity through its real domain factory.
  • Depends on: nothing first-party, and no BCL surface beyond object. Two type parameters and one abstract method is the whole type (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Builders/EntityBuilderBase.cs:9-18).
  • Concept introduced, the Test Data Builder plus the self-referencing generic (CRTP). [Rubric §14, Testability] assesses how easily the code can be exercised in isolation; a builder base is a textbook §14 affordance, it removes the copy-pasted setup that otherwise bloats every arrange step. The signature EntityBuilderBase<TBuilder, TEntity> where TBuilder : EntityBuilderBase<TBuilder, TEntity> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Builders/EntityBuilderBase.cs:9-10) is the curiously-recurring template pattern: a concrete builder passes itself as TBuilder, so the WithX(...) methods a subclass adds can return the concrete builder type and keep a fluent chain strongly typed without a cast.
  • Walkthrough
    • Build() (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Builders/EntityBuilderBase.cs:17): the single abstract member. The XML doc (EntityBuilderBase.cs:12-15) records the contract, the subclass calls the entity's Result-returning factory (ADR-013) and throws if it failed, so a builder never yields a domain object that violated its invariants. The base deliberately owns no state and no default WithX helpers, those live on each concrete builder because defaults are per-entity.
  • Why it's built this way: keeping the base to one abstract method means it adds zero coupling and zero opinions beyond "a builder produces a TEntity". The CRTP is the only structural rule it enforces, and it exists purely so fluent chaining stays type-safe down in the subclasses.
  • Where it's used: the domain-test builders in both apps subclass it, ten today: MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Domain.Tests/Builders/ActivityBuilder.cs:10, .../Builders/EventBuilder.cs:10, .../Builders/SessionBuilder.cs:10, .../Builders/SpeakerBuilder.cs:10, .../Builders/SponsorBuilder.cs:11, MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Domain.Tests/Builders/UserBuilder.cs:10, MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.Domain.Tests/Builders/CategoryBuilder.cs:10, .../Builders/ProductBuilder.cs:10, MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.Domain.Tests/Builders/CustomerBuilder.cs:11, and MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.Domain.Tests/Builders/OrderBuilder.cs:11.
  • Caveats / not-in-source: the "throws on failure" and "sensible defaults" behavior is documented on the base but implemented only in those subclasses, which live outside this unit.

FeatureManagementTestExtensions

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:10 · Level 0 · class (static)

  • What it is: a one-method helper that lets an integration-test host force feature-flag values, overriding whatever appsettings.json would otherwise resolve, so a test can pin a flag on or off and assert both branches of a feature-gated command or query.
  • Depends on: BCL and NuGet only, IServiceCollection and IConfiguration from Microsoft.Extensions.* plus AddFeatureManagement from Microsoft.FeatureManagement (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:1-3). No first-party dependency.
  • Concept: this is the test-side counterpart to the framework's FeatureGateCommandDecorator<TCommand, TResult>, the outermost link in the CQRS pipeline (taught in primer §2). [Rubric §14, Testability] again: a gated handler is only meaningfully testable if a test can flip its flag deterministically. [Rubric §17, DevOps & Deployment] applies too, feature management (ADR-031) is a cross-cutting concern, and this helper keeps its test-time configuration in one reusable place.
  • Walkthrough
    • The whole class body is a single C# preview extension(IServiceCollection services) block (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:12), the same extension-member style the framework uses for DI registration (see primer §4), not a classic this-parameter extension method.
    • ConfigureTestFeatureFlags(Dictionary<string, bool> features) (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:35-62): null-guards the dictionary (:38), then layers the flags on top of the configuration the host already registered rather than replacing it. It finds the last IConfiguration descriptor and reads its ImplementationInstance (:40-42), seeds a ConfigurationBuilder from it when one was found (:44-48), appends the flags as in-memory FeatureManagement:{Name} keys so they win over any FeatureManagement section the host configured (:50-56), registers the composed root as the IConfiguration singleton (:58), calls AddFeatureManagement against its FeatureManagement section (:59), and returns the collection for chaining (:61).
    • The layering is the load-bearing detail, and the doc says why (:18-25): .NET DI hands a non-collection dependency the last registration, so building a flags-only root here would give every component constructed afterwards a configuration containing nothing but FeatureManagement, silently losing connection strings, authentication settings and the data-source section.
    • The pick-up is instance-only by design (:26-31): a configuration registered behind a factory cannot be read without building the provider, so in that case the flags stand alone.
  • Why it's built this way: pushing overrides through the real IConfiguration plus AddFeatureManagement path (rather than mocking an IFeatureManager) means the test exercises the same feature-evaluation code the production host runs, only the source of the flag value changes.
  • Where it's used: it is intended for a test WebApplicationFactory's ConfigureServices, and the XML doc says exactly that (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:14-17). MMCA.Common covers all three behaviors directly: that the flags layer on top of a host configuration (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/FeatureManagementTestExtensionsTests.cs:20), that they drive a resolved IFeatureManager (:43), and that a collection with no configuration at all still gets the flags (:62).
  • Caveats / not-in-source: as of this pass no application test project calls it. A workspace-wide search finds ConfigureTestFeatureFlags only at its definition (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:35) and in MMCA.Common's own unit tests; neither ADC, Store nor Helpdesk uses it. It ships in the package as available capability, not as a technique any application suite currently uses.

IIntegrationTestFixture

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IIntegrationTestFixture.cs:8 · Level 0 · interface

  • What it is: the contract every integration-test fixture implements, the two capabilities a test base needs from a booted host: hand me an HttpClient, and reset the database to clean between tests.
  • Depends on: BCL only (HttpClient, Task). No first-party dependency, which is what lets it sit at Level 0 and be referenced by everything above it.
  • Concept introduced, the test fixture as an abstraction boundary. [Rubric §14, Testability]: by depending on this interface rather than a concrete WebApplicationFactory, the reusable IntegrationTestBase<TFixture> stays host-agnostic, and each downstream app supplies its own concrete fixture with its own Program, JWT keys, and data sources. This is the boundary that keeps the shared test scaffolding in MMCA.Common.Testing and the app-specific wiring in each repo, which is the whole premise of ADR-058.
  • Walkthrough
    • CreateClient() (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IIntegrationTestFixture.cs:11): returns an HttpClient configured for the in-process test server.
    • ResetDatabaseAsync() (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IIntegrationTestFixture.cs:19): resets the database between tests (the doc names Respawn as the typical mechanism). The doc comment (IIntegrationTestFixture.cs:13-18) records a load-bearing rule for the database-per-service topology (ADR-006): a host with multiple physical data sources must reset every relational source, and can enumerate them by resolving IEntityDataSourceRegistry and IDataSourceResolver from the host's services.
  • Why it's built this way: two members, no state, no host coupling. The interface is deliberately minimal so the reset strategy (single database versus multi-source) is the fixture's problem, not the base's.
  • Where it's used: implemented by SqlServerIntegrationTestFixtureBase<TEntryPoint> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:27) and through it by every per-service fixture in both apps; consumed as the TFixture constraint on IntegrationTestBase<TFixture> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:14) and therefore by all three contract bases in this unit.

JwtTokenGenerator

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:30 · Level 0 · class (static)

  • What it is: a static factory that mints signed JWT bearer tokens for integration tests, plus the matching switch that re-points a test host's Bearer scheme at the same committed key. Together they let a test call an authorized endpoint as any role or user without standing up the real login flow or a reachable JWKS endpoint. Each downstream project wraps the generator with role-specific convenience methods (AdminToken, OrganizerToken, and so on, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:11-12).
  • Depends on: BCL and NuGet only, System.Globalization, System.IdentityModel.Tokens.Jwt, System.Security.Claims, System.Security.Cryptography (RSA), Microsoft.AspNetCore.Authentication.JwtBearer (for the options type the second member configures), and Microsoft.IdentityModel.Tokens (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:1-6). The generated claim layout mirrors the framework's ITokenService so downstream auth middleware cannot tell a test token from a real one (JwtTokenGenerator.cs:99-102). The userId parameter is typed UserIdentifierType (JwtTokenGenerator.cs:114), the solution-wide identifier alias (ADR-048).
  • Concept introduced, exercising the real RS256 path in tests. [Rubric §11, Security] assesses how authentication and key handling are done; the deliberate choice here is that tests sign with RS256 (SecurityAlgorithms.RsaSha256, JwtTokenGenerator.cs:131) using an embedded RSA-2048 dev keypair, the same asymmetric algorithm production uses, so integration tests run the identical validation code path (ADR-004, taught in primer §2) rather than a weaker HMAC shortcut. [Rubric §14, Testability] covers the ergonomics: deterministic tokens with no per-run key generation, and a host that validates them with no network dependency at all.
  • Walkthrough
    • Public constants (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:33-96): DefaultIssuer (https://localhost:6001, line 33), DefaultKeyId (mmca-test-key, line 41, the kid the host advertises on its JWKS document), and the paired DefaultPublicKeyPem (line 49) and DefaultPrivateKeyPem (line 68). The class doc records the wiring contract: test host appsettings set Jwt:SigningAlgorithm=RS256, Jwt:RsaPublicKeyPem, and Jwks:KeyId (JwtTokenGenerator.cs:18-20) so RsaJwksProvider publishes a JWKS entry with the matching kid.
    • GenerateToken(...) (JwtTokenGenerator.cs:112-156): imports the PEM private key into RSAParameters inside a using so the RSA instance can be disposed without invalidating the key held by SigningCredentials (:121-131), assembles the claim set (:137-141) and appends any caller-supplied extras (:143-146), then writes a one-hour token (:148-155). Defaulted parameters mean a caller normally passes only audience, user id, and role (:112-119).
    • The claim set is exactly two claims, ClaimTypes.NameIdentifier carrying the culture-invariant user id and ClaimTypes.Role (:137-141). NameIdentifier is what the JWT bearer handler produces for a real token's sub under its default inbound mapping, so a test token carrying it reaches every reader the way a production one does; the comment above it (:133-136) records that the duplicate custom claim that used to ride alongside it is gone, matching ITokenService, because two claims for one identity can disagree.
    • ConfigureInProcessTokenValidation(JwtBearerOptions options, string audience) (JwtTokenGenerator.cs:170-192): the validation half. It nulls Authority and ConfigurationManager and clears RequireHttpsMetadata (:174-176) to stop OIDC/JWKS discovery outright, imports the public key (:178-183), and pins the token-validation parameters to that static key plus the fixed issuer and the caller's audience (:185-191). The doc explains why it has to exist (:158-167): AddForwardedJwtBearer otherwise fetches the issuer and signing keys from the Identity service's JWKS document through the gateway, which no in-process test topology serves.
  • Why it's built this way: the whole point is fidelity. Tokens are indistinguishable in shape and signing algorithm from production, so auth middleware and role checks are under test rather than stubbed. Splitting the key material into a mint side and a validate side is what lets a single-host fixture and a multi-host cross-service fixture share one committed keypair.
  • Where it's used: tokens are applied to a client through IntegrationTestBase<TFixture>'s SetBearerToken(...) (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:42-44) and wrapped by each app's role-specific token helpers. ConfigureInProcessTokenValidation is called from a PostConfigure<JwtBearerOptions> in the test factories of the non-Identity hosts, in ADC (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Infrastructure/ConferenceTestWebApplicationFactory.cs:49,77, .../MMCA.ADC.Engagement.IntegrationTests/Infrastructure/EngagementTestWebApplicationFactory.cs:56,86, .../MMCA.ADC.Notification.IntegrationTests/Infrastructure/NotificationTestWebApplicationFactory.cs:50,69) and in Store (MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Infrastructure/CatalogTestWebApplicationFactory.cs:40,43, .../MMCA.Store.Sales.IntegrationTests/Infrastructure/SalesTestWebApplicationFactory.cs:47,57), plus the cross-service factories (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/ConferenceCrossServiceFactory.cs:50, .../EngagementCrossServiceFactory.cs:54, MMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/Infrastructure/CatalogCrossServiceFactory.cs:50, .../SalesCrossServiceFactory.cs:52). It is covered directly by MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/JwtTokenGeneratorTests.cs:39-95.
  • Caveats / not-in-source: the class doc (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:22-28) carries an explicit security warning, the embedded keypair is committed to the public git repo and is insecure by design, it exists only to make integration tests deterministic. Production keys are provisioned via user-secrets or Azure Key Vault per JwtSettings.RsaPrivateKeyPem and must never be this keypair.

MmcaGatewayHardeningTestsBase<TEntryPoint>

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MmcaGatewayHardeningTestsBase.cs:39 · Level 0 · class (abstract)

  • What it is: the eight edge-hardening gates a gateway host must pass, authored once and re-run as a thin sealed subclass per gateway. It covers what a host adopts from the shared gateway kit: the per-client-IP rate limiter and its bypass list, an optional tighter named policy on the credential route, the correlation ID stamped and echoed on every response, the per-downstream readiness checks, and the active destination health probe on every cluster (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MmcaGatewayHardeningTestsBase.cs:15-22), which a host may declare off, in which case the same gate asserts it stays off. The subclass supplies only its own route-table facts.
  • Depends on: RecordingHttpForwarder (not referenced in code, but the registration the base's doc expects a subclass to make, :32-35), plus Microsoft.AspNetCore.TestHost's TestServer, WebApplicationFactory, Microsoft.Extensions.Diagnostics.HealthChecks' HealthCheckServiceOptions, YARP's IProxyConfigProvider / IProxyConfigFilter, AwesomeAssertions and Xunit (:1-11). On the host side the behaviors it asserts come from the gateway kit: GatewayRateLimitingExtensions, GatewayCorrelationMiddleware, GatewayHealthCheckExtensions and GatewayHealthCheckDefaultsConfigFilter.
  • Concept introduced, driving the test server directly instead of through an HttpClient. The rate limit assertions go through TestServer.SendAsync(Action<HttpContext>, CancellationToken) rather than an HttpClient (:24-29), and the reason is a chain of two facts: the kit partitions on Connection.RemoteIpAddress, which a TestServer request leaves null, and the kit deliberately fails open on an unresolvable IP rather than collapsing every unattributable request into one shared bucket (MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Gateway/GatewayRateLimitingExtensions.cs:238-244). A client-driven test could therefore never observe a 429. Setting the connection IP per request also gives each gate its own partition, which is what lets eight gates share one class fixture without disturbing each other. [Rubric §11, Security] and [Rubric §12, Performance & Scalability] are the limiter axis (ADR-019 for the service tier, ADR-088 for the edge tier this base checks); [Rubric §13, Observability & Operability] is the correlation ID; [Rubric §29, Resilience & Business Continuity] is the readiness and active-probe pair; and [Rubric §14, Testability] plus [Rubric §34, Architecture Governance & Documentation] cover the fitness-function form (ADR-015, ADR-058).
  • Walkthrough
    • Client addresses (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MmcaGatewayHardeningTestsBase.cs:42-58): six constants, all inside the RFC 5737 TEST-NET-3 documentation range (203.0.113.0/24). Each gate burns its own address, so the fixture's shared host gives every one a fresh window (:28-29); ForwardedProxyIp (:58) stands in for the ingress proxy in the forwarded-header gate.
    • Abstract knobs, the only four a subclass must supply: Factory (:61), PermitLimit (:68, stated by the subclass rather than read from the kit so an operator can see the number), LimitedPath (:71) and DownstreamServices (:77).
    • Virtual knobs with kit defaults: BypassedPaths (:85-89, /.well-known/jwks.json and /health, exempt because probes and JWKS discovery run at high frequency by design), NamedPolicyPath (:95, null skips that gate) and NamedPolicyPermitLimit (:101), ActiveHealthChecksExpected (:111, default true; false fits a host whose clusters each front one platform-balanced address such as a Container Apps internal FQDN, where ejecting the only destination can only turn a slow request into a 503, the platform already routes only to ready replicas, and every probe bills an otherwise idle downstream replica at the active rate, :103-110), ActiveProbeInterval (:114, 30 seconds) and ActiveProbeTimeout (:117, 5 seconds), ActiveProbePath (:124, /alive and not /health, because readiness on a downstream flips during its own rolling deployment and ejecting a destination for that is the gateway treating a healthy deploy as an outage, :119-123), CorrelationHeader (:127), DownstreamCheckPrefix (:130) and ForwardedForHeader (:133).
    • Route_IsThrottledOnceTheClientExhaustsItsWindow (:135-155): sends exactly PermitLimit requests, then one more, asserting none of the first batch was a 429 and the overflow was (:150-154).
    • NamedPolicyRoute_IsThrottledAtItsOwnTighterAllowance (:157-191): returns early when the host declares no named policy (:162-165), a plain return rather than a declared dynamic skip because that would need xunit.v3.assert, which this shipped fixture library deliberately does not reference (:160-161). Otherwise it exhausts the tighter allowance, asserts the overflow is shed, and then asserts the same client on a route without the policy is still admitted (:185-190), which is what makes the rejection attributable to the route policy rather than the global limiter.
    • BypassedPaths_AreNotThrottledEvenAfterTheWindowIsExhausted (:193-218): guards against an empty bypass list (:196-197), burns the window and proves it is exhausted (:205-206), then asserts every exempt path still answers with something other than 429.
    • The two correlation gates (:220-236 and :238-253): a request with no header must come back with a generated non-blank correlation ID, and a caller-supplied ID must be echoed unchanged, so a trace that started upstream survives the hop through the edge.
    • Readiness_IncludesADownstreamCheckPerService (:255-281): reads the registered HealthCheckServiceOptions (:262-263) and, per downstream, asserts a check named {DownstreamCheckPrefix}{service} exists, is tagged ready, is not tagged live, and has FailureStatus = Unhealthy (:274-279). The comment is the reasoning: a gateway that cannot reach its services must leave the load balancer, but restarting the gateway process fixes nothing about a downstream outage, so the check must never fail liveness.
    • EveryCluster_CarriesAnActiveHealthCheckProbingAlive (:283-324): resolves the proxy config and the registered IProxyConfigFilter chain (:287-288), then runs each raw cluster through every filter (:296-302) because the provider hands out the pre-filter config and the kit's defaults only appear after the filters run, exactly as YARP's config manager does at load time. When ActiveHealthChecksExpected is false it asserts the opposite, that no cluster carries an enabled active check, and moves on (:308-313), so an active block left behind in configuration still fails the build. Otherwise it asserts each cluster carries an enabled active health check probing ActiveProbePath at the declared interval and timeout (:315-322). The gate keeps its name in both modes.
    • RateLimiter_PartitionsByForwardedClientIp_NotByProxyIp (:326-349): every request arrives from the same connection IP while X-Forwarded-For names a different caller. It exhausts caller A's window, confirms the exhaustion (:339-340), then asserts caller B behind the same proxy IP is still admitted (:346-348), which only holds if the forwarded-headers middleware runs before the limiter.
    • Helpers: SendAsync(path, clientIp) (:351-371) and SendForwardedAsync(path, proxyIp, forwardedFor) (:373-396), each shaping an HttpContext on the test server (method, HTTPS scheme, path, connection IP, and for the second one the forwarded header) and returning the response status code.
  • Why it's built this way: the gates are identical across gateways because the kit is; the only genuinely per-host facts are the route table and the configured numbers, so those are the four abstract members and everything else is a virtual with a kit default. Asserting the effective YARP config (post-filter) rather than the raw file is what keeps the health-probe gate honest about what the host will actually do (ADR-089). Turning the active probe off is a declared host fact rather than an absent assertion: the ActiveHealthChecksExpected switch flips the gate to assert the probe stays off, so the choice cannot drift silently in either direction (:103-110, :308-313).
  • Where it's used: both gateways, each a sealed subclass over its own Program. ADC (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GatewayHardeningTests.cs:29) states a 120-request allowance (:40), /Events/1 as the limited path (:43), four downstreams (:49-55), /hubs added to the bypass list because a SignalR connection is long-lived (:57-69), /Auth/login at 30 requests as its named policy (:72, :82), and declares active health checks off (MmcaGateway:HealthCheckDefaults:Active:Enabled=false) because each cluster fronts one Container Apps address and every probe bills an idle downstream replica (:84-89). Store (MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GatewayHardeningTests.cs:34) reads its PermitLimit straight out of the bound GatewayRateLimitingSettings options so the suite cannot drift from the section it exercises (:40-46), uses /Products/1 (:49), three downstreams (:55), adds /Payments/webhook to the bypass list because a throttled Stripe webhook becomes a retry storm and an unreconciled payment (:57-70), declares /Auth/login at 30 requests as its named policy (:73, :86; only the login and register submission routes carry it, because the UI replica refreshes sessions server-side with no X-Forwarded-For and a client-IP partition there would key the whole storefront on one container address, :75-85), and declares active health checks off for the same one-address reason as ADC (:88-93).
  • Caveats / not-in-source: the gates need a booted gateway host, which MMCA.Common's own test project does not have, so the framework can only guard the base's shape headlessly (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/MmcaGatewayHardeningTestsBaseTests.cs:21): that all eight gate names are still declared as [Fact] (:23-33, :43-54) and that exactly the four route-table members are still abstract (:35-41, :56-67), which keeps ActiveHealthChecksExpected a virtual, with an abstract SampleGatewayHardeningTests (:75, overriding ActiveHealthChecksExpected at :93) as compile coverage for the subclass surface.

OpenApiBreakingChangeKind

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeKind.cs:4 · Level 0 · enum

  • What it is: the public vocabulary of OpenAPI breaking changes the conformance detector can report, thirteen members, each documented in source (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeKind.cs:4-44).
  • Depends on: nothing.
  • Concept introduced, a closed taxonomy of contract breaks. Members group by what the baseline had: paths and operations (RemovedPath, RemovedOperation), responses (RemovedResponseStatusCode, RemovedResponseProperty), request bodies (RequestPropertyNewlyRequired, RemovedRequestBody, AddedRequiredRequestBody, RemovedRequestMediaType), parameters (RemovedParameter, ParameterNewlyRequired, AddedRequiredParameter, ChangedParameterType) and types (ChangedPropertyType). [Rubric §14, Testability]: a failing conformance run names a kind, not a vague diff.
  • Walkthrough: each OpenApiBreakingChange carries one kind plus a JSON pointer (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:479); OpenApiBreakingChange.cs holds the record, and the detector tests assert specific kinds.
  • Why it's built this way: an enum keeps assertions stable and exhaustive; additions (a new request property that is optional, a new path) are intentionally absent because they do not break clients.
  • Where it's used: the detector (13 references), its two test classes (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/OpenApiBreakingChangeDetectorParameterTests.cs, .../OpenApiBreakingChangeDetectorTests.cs) and OpenApiBreakingChange.cs. ADR-058 (runtime conformance suites as a package) mentions it.

OperationSite

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:497 · Level 0 · record (private sealed, nested)

  • What it is: a nested record identifying one operation under comparison: Path, Method, PathPointer, and the baseline and live JsonElement path items (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:497-508).
  • Depends on: System.Text.Json.JsonElement.
  • Concept introduced, carrying location with the data. It computes Pointer as PathPointer + "/" + Method (:505) and Describe(what) formats messages such as GET /Orders: query parameter 'page' (:507, upper-casing the method).
  • Walkthrough: created once per operation (:102), then passed to CompareParameters (:260), CompareParameterSchema (:301) and CompareRequestBody (:326) so every finding is worded and pointed consistently.
  • Why it's built this way: a record bundles four values that always travel together, avoiding long parameter lists.
  • Where it's used: only inside the defining file.

ParameterSite

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:511 · Level 0 · record (private sealed, nested)

  • What it is: a nested record for one parameter: location (In), Name, the raw Parameter element and its JSON Pointer (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:511-518).
  • Depends on: JsonElement and the detector's HasRequiredFlag helper (:204).
  • Concept introduced, effective required-ness. IsRequired is true for any path parameter regardless of its own flag, otherwise it follows the declared flag (:513-515); Describe() yields {In} parameter '{Name}' (:517).
  • Walkthrough: EffectiveParameters builds a dictionary keyed location|name, case-insensitive (:224-254), so CompareParameters can detect removed, newly required and added-required parameters (:260), and CompareParameterSchema compares types (:301).
  • Why it's built this way: OpenAPI mandates path parameters be required, so trusting only the flag would miss a break.
  • Where it's used: only inside the defining file.

RateLimiterTestExtensions

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RateLimiterTestExtensions.cs:11 · Level 0 · class (static)

  • What it is: a one-member helper that replaces a test host's global rate limiter with an unlimited partition, so a suite that drives dozens of requests through one host in a few seconds is not throttled by the production budget (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RateLimiterTestExtensions.cs:15-19).
  • Depends on: BCL and ASP.NET Core only, System.Threading.RateLimiting, Microsoft.AspNetCore.RateLimiting's RateLimiterOptions, Microsoft.AspNetCore.Http and the DI abstractions (:1-4). No first-party dependency. On the host side, the limiter it neutralizes is the one configured by RateLimitingSettings (ADR-019).
  • Concept introduced, neutralize the budget without removing the middleware. Two design points make this safe to apply everywhere (:20-25). First it runs as a PostConfigure, so it wins over whatever the host registered no matter when the host's own AddRateLimiter ran, which matters because a test factory's ConfigureTestServices can execute before or after the host's registration. Second only the global limiter is replaced: the limiter middleware stays in the pipeline, so MiddlewarePipelineOrderTestsBase still sees the RateLimiting step, and per-endpoint named policies are untouched, which is what lets a suite like MmcaGatewayHardeningTestsBase<TEntryPoint> still assert a named policy engages. [Rubric §14, Testability] assesses whether a production safety control can be stood down for a test without deleting it; [Rubric §12, Performance & Scalability] is the control itself.
  • Walkthrough
    • The class body is one C# preview extension(IServiceCollection services) block (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RateLimiterTestExtensions.cs:13), the same extension-member style the framework uses for DI registration (see primer §4).
    • NeutralizeGlobalRateLimiter(string partitionName = "tests") (:32-39): a single PostConfigure<RateLimiterOptions> that sets GlobalLimiter to a PartitionedRateLimiter.Create<HttpContext, string> returning RateLimitPartition.GetNoLimiter(partitionName) for every request (:34-36), then returns the collection for chaining (:38).
    • partitionName only ever surfaces in limiter diagnostics (:27-30), which is why the call sites use it as a label ("integration-tests", "cross-service-tests") rather than for behavior.
  • Why it's built this way: a production per-IP or per-user budget and a test suite firing its whole arrange phase through one in-process host are simply incompatible, and the alternatives are worse: removing the middleware would break the pipeline-order fitness test, and raising the configured limit would leave the suite silently coupled to a number in appsettings.
  • Where it's used: eight call sites, all in ADC, four per-service test factories (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Infrastructure/ConferenceTestWebApplicationFactory.cs:63, .../MMCA.ADC.Engagement.IntegrationTests/Infrastructure/EngagementTestWebApplicationFactory.cs:72, .../MMCA.ADC.Identity.IntegrationTests/Infrastructure/IdentityTestWebApplicationFactory.cs:47, .../MMCA.ADC.Notification.IntegrationTests/Infrastructure/NotificationTestWebApplicationFactory.cs:57) and four cross-service factories (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/ConferenceCrossServiceFactory.cs:55, .../EngagementCrossServiceFactory.cs:59, .../IdentityCrossServiceFactory.cs:36, .../NotificationCrossServiceFactory.cs:63). MMCA.Common covers all three behaviors: that it overrides a host limiter which would otherwise reject every request (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/RateLimiterTestExtensionsTests.cs:21), that it returns the same collection for chaining (:48), and that it still applies when the host configures its limiter afterwards (:61).
  • Caveats / not-in-source: no MMCA.Store or MMCA.Helpdesk test project calls it today; a workspace-wide search finds no NeutralizeGlobalRateLimiter reference under either repo's Tests/ tree.

SecurityHeadersTestsBase

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:16 · Level 0 · class (abstract)

  • What it is: a one-test conformance base that asserts a booted host emits the hardened set of security response headers on every response, so a later pipeline refactor cannot silently drop them. Authored once, re-run as a thin subclass per host under test.
  • Depends on: AwesomeAssertions and Xunit (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:1-2). It deliberately does not extend IntegrationTestBase<TFixture>: it needs only an HttpClient, so it takes one through an abstract factory rather than inheriting the SQL fixture machinery.
  • Concept: a runtime conformance check on the HTTP edge. [Rubric §11, Security] and [Rubric §26, Front-End Security] both assess defense in depth at the edge; this test pins the exact header values the shared AddCommonSecurityHeaders / UseCommonSecurityHeaders middleware (see SecurityHeadersMiddleware, ADR-023) is expected to emit. [Rubric §14, Testability] covers the reusable-base shape.
  • Walkthrough
    • ProbePath (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:19): overridable, defaults to /alive because the liveness endpoint always answers independent of any backend being reachable, so the header check is never flaky for the wrong reason (rationale in the class doc, :12-14).
    • AliveResponse_CarriesHardenedSecurityHeaders (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:21-36): the single [Fact]. It GETs ProbePath (:26-27, threading TestContext.Current.CancellationToken) and asserts six headers (:29-35): X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, a Permissions-Policy containing geolocation=(), a Content-Security-Policy containing frame-ancestors 'none', and (because the host under test boots in the Production environment) an HSTS Strict-Transport-Security header with a max-age=.
    • CreateClient() (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:42): abstract, the subclass supplies it from its WebApplicationFactory class fixture. Header(...) (:44-45) is the private helper that joins a header's values or returns null when the header is absent, which is what makes a missing header fail with a readable null-versus-expected message.
  • Why it's built this way: pinning literal header values (not just presence) turns "we harden responses" into an executable, per-host guarantee, and probing /alive keeps the test independent of application state. Booting the subclass fixture in Production is what makes the HSTS assertion valid, which is why the two adopters pair it with ProductionHostApplicationFactory<TEntryPoint>.
  • Where it's used: both gateway hosts subclass it with a single CreateClient override, MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/SecurityHeadersTests.cs:11-12 and MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/SecurityHeadersTests.cs:12-13, each also taking a ProductionHostApplicationFactory<Program> as its xUnit class fixture on the same declaration.

TestPolling

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/TestPolling.cs:9 · Level 0 · class (static)

  • What it is: a poll-with-timeout helper for asynchronous integration assertions. It repeatedly probes a value until a condition holds or a budget runs out, and hands back the last probed value either way.
  • Depends on: BCL only (Task, DateTime, TimeSpan). No first-party dependency and no assertion library, so the caller keeps ownership of the assertion.
  • Concept introduced, replacing the pre-assert sleep. Anything that travels the outbox to a broker and back, or any other eventually-consistent path, arrives at a time the test cannot know (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/TestPolling.cs:3-8). A fixed Task.Delay before the assertion is both slow and flaky: too short and the suite reds intermittently, too long and every green run pays the worst case. Polling returns as soon as the condition holds and bounds the wait. [Rubric §14, Testability] assesses whether the suite is deterministic; [Rubric §6, CQRS & Event-Driven] is why the problem exists at all, since the outbox (ADR-003) is asynchronous by design and offers no synchronous handle to await.
  • Walkthrough
    • PollUntilAsync<T>(Func<Task<T>> probe, Func<T, bool> isSatisfied, TimeSpan? timeout = null, TimeSpan? interval = null) (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/TestPolling.cs:22-41): null-guards both delegates (:28-29), computes a deadline from the 60-second default budget (:31) and a 500 ms default interval (:32), probes once before the loop (:33), then loops while the condition is unmet and the deadline has not passed (:34-38).
    • The return is the design decision worth noticing: it returns last unconditionally (:40) rather than throwing on timeout, so a timed-out poll still fails on the caller's real assertion message rather than on a bare timeout exception (the doc states exactly this at :11-14).
  • Why it's built this way: bounding the wait and returning the last value keeps two properties at once, a fast green path (the loop exits on the first satisfying probe) and a diagnosable red path (the failure message describes the domain expectation, not the plumbing).
  • Where it's used: the cross-service tiers of both apps route every eventual assertion through it and say so in their own docs (MMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/Infrastructure/CrossServiceTestBase.cs:15 and the ADC equivalent at MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/CrossServiceTestBase.cs), with call sites such as MMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/CrossService/ProductVariantChangedRoundTripTests.cs:28,48,51. MMCA.Common covers the helper itself, including the null-argument guards (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/TestPollingTests.cs:15-64).

CrossServiceFixtureBase

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:47 · Level 1 · class (abstract)

  • What it is: the shared scaffolding for the cross-service real-broker integration tier. It boots several service hosts in ONE process against a real Testcontainers SQL Server and a real Testcontainers RabbitMQ, so the genuine outbox to broker to consumer round-trip (and any real cross-service gRPC read) is exercised end to end rather than faked (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:17-25).
  • Depends on: CrossServiceDataSource (the per-source declaration), plus Microsoft.Data.SqlClient, Testcontainers.MsSql, Testcontainers.RabbitMq, and xUnit's IAsyncLifetime (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:1-4,47).
  • Concept introduced, the multi-host in-process topology and its configuration channel. Where SqlServerIntegrationTestFixtureBase<TEntryPoint> boots one host with the cross-service edges faked and no broker, this base owns a whole topology. Two mechanisms are load-bearing, and both are documented on the class. First, process environment variables are the only override channel these hosts honor (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:26-39): each host reads its connection string, MessageBus settings, and JWT settings from builder.Configuration at configure-time, before builder.Build(), which is before WebApplicationFactory.ConfigureAppConfiguration deltas apply, so in-memory config would arrive too late. Second, because the one genuinely per-host key is the SQL connection string, hosts must boot strictly sequentially, and that is safe precisely because a booted host has already snapshotted its connection (the data-source resolver, the context factory, the outbox processor, and the MassTransit bus are all built during StartAsync). [Rubric §7, Microservices Readiness] assesses whether extracted services really do collaborate over their declared transports; [Rubric §6, CQRS & Event-Driven] covers the outbox path (ADR-003); [Rubric §8, Data Architecture] covers database-per-service (ADR-006); and [Rubric §14, Testability] covers shipping the whole topology as a reusable base.
  • Walkthrough
    • State: the private DummyBearerAuthority constant (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:51), the original-environment snapshot map (:47), the two nullable containers (:49-50), and the public RabbitMqConnectionString (:53).
    • Subclass knobs: DataSources (:60, the logical sources in the order their databases are created), MigrationsAssemblyPrefix (:67, so each named source gets {prefix}.{LogicalName} exactly as production does), BootHostsAsync (:143) and DisposeHostsAsync (:146), plus the optional hooks OnContainersStartedAsync (:152) and ConfigureSharedEnvironment (:161).
    • SqlServerBaseConnectionString (:70-72) throws a clear InvalidOperationException when the container has not started, and the public static ComposeConnectionString(...) (:82-96) overlays a catalog, TrustServerCertificate, and an optional Application Name. It is deliberately pure and static so a fixture's connection-string composition is unit-testable with no Docker daemon (:74-81); BuildConnectionString (:168-169) is the instance shorthand over it.
    • InitializeAsync (:99-116) is the whole lifecycle in seven steps: create the containers, start both in parallel with Task.WhenAll (:102), read the broker connection string (:104), run the subclass hook (:105), pre-create the databases (:111), push the shared environment (:113), then boot the hosts (:115). The pre-create step carries the sharpest comment in the file (:107-110): CREATE DATABASE runs before EF's migration lock (sp_getapplock) is acquired, so a host booted twice (the real-Kestrel double-boot pattern) would otherwise race itself; with the databases already present EF skips the create and the migration lock serializes the actual migration run.
    • SetSharedEnvironment (:227-258) pushes ASPNETCORE_ENVIRONMENT=Testing (:229), one named data source per module (:243-246), the real broker settings MessageBus__Provider=RabbitMq and ConnectionStrings__rabbitmq (:249-250), and the dummy Bearer authority that only has to exist so AddForwardedJwtBearer's authority guard passes (:255; real validation is re-pointed at the committed test key by JwtTokenGenerator.ConfigureInProcessTokenValidation, :252-254), before handing control to the subclass (:257).
    • The named-source loop is the multi-host fix, and its comment (:231-242) is worth reading in full: EF Core caches a context type's model in a process-global cache keyed by (context type, source name). If every host let its entities collapse onto Default (as production does, one host per process), all hosts here would share ONE cached model and the first booted would win. SetNamedDataSource (:265-276) therefore composes each module's connection string with a distinct Application Name MMCA-{LogicalName} (:270) so it differs ordinally from the top-level ConnectionStrings value and the resolver keeps the named source, giving each host its own model-cache key.
    • SetHostConnectionString (:177) is the one key mutated between sequential boots, and SetEnvironmentVariable (:187-195) records only the first original value per key so re-pushing that key cannot clobber the restore point. DisposeAsync (:119-136) disposes the hosts, then RabbitMQ, then SQL, then calls RestoreEnvironment (:278-286).
    • CreateContainers (:203-204) is built inside a method rather than a field initializer so a subclass can be constructed and its non-container logic unit-tested on a machine with no Docker daemon, and it carries a documented CS0618 suppression for the parameterless Testcontainers module builders (:197-202). CreateDatabasesAsync (:207-225) issues one guarded IF DB_ID(...) IS NULL CREATE DATABASE per source with a scoped CA2100 suppression justified because the database names are the subclass's own compile-time constants (:218).
  • Why it's built this way: the environment-variable channel and the sequential boot are not style choices, they are the only way to give several hosts distinct databases in one process given configure-time configuration reads. Everything genuinely per-app (which databases, how many hosts and in what order, which extra settings) stays behind the four abstract members.
  • Where it's used: one fixture per app, both sealed subclasses, MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:24 (three databases for three REST hosts at :61-66, migrations prefix MMCA.ADC.Migrations.SqlServer at :69) and MMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:29 (two databases at :56-60, prefix MMCA.Store.Migrations.SqlServer at :63). MMCA.Common covers the container-free half of the base through its own private FakeCrossServiceFixture (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Fixtures/CrossServiceFixtureBaseTests.cs:119).
  • Caveats / not-in-source: this tier needs a Docker daemon. Where each repo schedules it is a CI decision recorded outside this class; the base itself says nothing about scheduling. It also proves the round-trip over RabbitMQ, not the production transport, which is what ServiceBusEmulatorFixtureBase exists to cover.

DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult>

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:38 · Level 1 · class (abstract)

  • What it is: an opt-in fitness function that builds a real ServiceCollection through a repo's own registration sequence, resolves the decorated command and query handlers out of the built provider, and asserts the runtime object graph nests the decorators in exactly the ADR-014 order.
  • Depends on: ICommandHandler<in TCommand, TResult> and IQueryHandler<in TQuery, TResult> from MMCA.Common.Application.UseCases (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:4), plus System.Reflection, Microsoft.Extensions.DependencyInjection, AwesomeAssertions, and Xunit (:1-5).
  • Concept introduced, verifying a decorator chain by unwrapping the constructed graph. The decorator pipeline itself is taught in group-05; what is new here is how you prove it. Scrutor's TryDecorate applies decorators in reverse registration order, so the outermost decorator is the last one registered, and an innocent-looking reorder of the AddApplicationDecorators() lines (or a module scan that runs after it) silently changes runtime behavior with no compile error (class doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:16-19). Rather than inspecting the registration list, this base resolves the service and walks the real chain by reflection (:29-32). [Rubric §6, CQRS & Event-Driven] assesses whether the command/query pipeline is coherent and intentional; [Rubric §2, Design Patterns] assesses correct application of the decorator pattern; [Rubric §14, Testability] covers turning an ordering convention into an executable check; and [Rubric §34, Architecture Governance & Documentation] covers the fact that a decision record is enforced here rather than merely written down (ADR-015 is the general case). It is also the one non-HTTP member of the ADR-058 conformance tier.
  • Walkthrough
  • Why it's built this way: asserting the constructed object graph is strictly stronger than asserting the registration list, it catches a decorator that was registered but never applied (for example because a module scan re-registered the handler afterwards). Comparing simple type names keeps the base free of a compile-time reference to the decorator classes, which live in MMCA.Common.Application.
  • Where it's used: four subclasses today. MMCA.Common self-tests the base against its own registration sequence with a synthetic PingCommand / PingQuery pair (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/DecoratorPipelineOrderTests.cs:24), and the three applications subclass it in their architecture tiers, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:30 and MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:30 over the real Identity ChangePreferencesCommand / GetUserPreferencesQuery pair, and MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/DecoratorPipelineOrderTests.cs:35 over the Tickets pair.
  • Caveats / not-in-source: each subclass pins one representative command/query pair, not every handler, so the guard proves the ordering is right, not that every handler is decorated.

ProductionHostApplicationFactory<TEntryPoint>

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:23 · Level 0 · class

  • What it is: the database-free boot path for host-level tests. It is a WebApplicationFactory that pins the hosting environment to Production and hangs on to the started IHost, so a test can both exercise production-only middleware branches and drive the host's own lifetime.
  • Depends on: Microsoft.AspNetCore.Mvc.Testing's WebApplicationFactory<TEntryPoint> (extended, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:23) and Microsoft.Extensions.Hosting's IHost / IHostBuilder (ProductionHostApplicationFactory.cs:1-2). No first-party dependency.
  • Concept introduced, the second boot path. The integration tier has two ways to get a running host: SqlServerIntegrationTestFixtureBase<TEntryPoint> for hosts that need a real database, and this one for hosts that do not (a YARP reverse-proxy gateway is the usual case, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:17-20). Both are named as the two paths in ADR-058. [Rubric §11, Security] is the reason Production is pinned: the restrictive CORS policy, HSTS emission, and other production-only middleware are branches a default Development boot skips entirely, which is exactly where host misconfiguration hides (ProductionHostApplicationFactory.cs:10-13). [Rubric §14, Testability] covers the second half, capturing the host is what makes a lifetime test possible at all.
  • Walkthrough
    • StartedHost (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:30): a public property with a private setter, nullable because WebApplicationFactory builds its host lazily, so it stays null until the first client is created (:25-28).
    • CreateHost(IHostBuilder builder) (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:33-40): null-guards the builder (:34), calls builder.UseEnvironment("Production") (:36), then assigns and returns base.CreateHost(builder) (:37-38). Three lines of override, and the assignment is the entire reason the class exists.
  • Why it's built this way: IHost.StopAsync is not reachable through the WebApplicationFactory surface alone (ProductionHostApplicationFactory.cs:13-15), so a graceful-shutdown test has no handle to pull without this capture. The class is deliberately left unsealed and non-abstract so it can be used directly as an xUnit IClassFixture<...> with no subclass.
  • Where it's used: as the default factory of GracefulShutdownTestsBase<TEntryPoint> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:32), directly as the class fixture of both gateway security-header tests (MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/SecurityHeadersTests.cs:11-12, MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/SecurityHeadersTests.cs:12-13), and as the base of ADC's gateway-hardening fixture, which subclasses it only to swap in the recording forwarder (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GatewayHardeningTests.cs:96-105).
  • Caveats / not-in-source: the doc is explicit that a host which migrates or seeds on startup needs its own fixture (ProductionHostApplicationFactory.cs:17-20); this factory does nothing about a database.

HostScopedAssemblyProvider

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:116 · Level 1 · class (private sealed, nested)

  • What it is: a private nested decorator over IEntityConfigurationAssemblyProvider that narrows the assemblies a host scans for entity configurations to the ones that host would load in its own process. It is the working half of CrossServiceHostIsolation.IsolateCrossServiceHost<TEntryPoint>(), the extension that CrossServiceFixtureBase subclasses call from ConfigureTestServices of every host factory (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:10-13,42).
  • Depends on: IEntityConfigurationAssemblyProvider (the interface it implements and wraps), EntityConfigurationOptions.AdditionalAssemblies (the explicit per-host registrations it keeps), and System.Reflection.Assembly (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:1-6,116-119).
  • Concept introduced, scoping a shared AppDomain back to one host. The default provider scans AppDomain.GetAssemblies, and with several hosts in one process that returns every host's module assemblies. Each host's entity registry then claims its peers' data sources, so its outbox and internal-command processors drain the PEER databases and a row whose handler lives only in the peer is dead-lettered as handler_missing by the wrong host (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:14-21). The provider restores the one-service-per-process view. [Rubric §7, Microservices Readiness] assesses whether extracted services behave as independent hosts; [Rubric §8, Data Architecture] covers database-per-service (ADR-006); [Rubric §14, Testability] covers keeping the in-process topology faithful to deployment.
  • Walkthrough
    • The primary constructor takes the wrapped inner provider, the hostAssemblyNames set, and the explicitAssemblies collection (:116-119).
    • GetConfigurationAssemblies() (:121-124) runs the inner provider and keeps an assembly when it is in explicitAssemblies or its simple name is in hostAssemblyNames; the result is materialized with a collection expression.
    • The name set is not built here. ReadHostAssemblyNames (:79-114) reads every runtime library the host's .deps.json lists plus the host assembly itself, and throws InvalidOperationException when the file is missing beside the assembly (:86-91). IsolateCrossServiceHost removes the existing non-keyed registrations (:47-53), reuses the last as inner (:55), and registers this class as a singleton (:56-59); CreateInner (:65-73) rebuilds the original from its descriptor, falling back to DefaultEntityConfigurationAssemblyProvider.
  • Why it's built this way: decorating rather than replacing keeps whatever provider the host registered and only narrows its result. Explicit AdditionalAssemblies bypass the filter because that registration is already per host (:23-28).
  • Where it's used: only inside its defining file, via IsolateCrossServiceHost<TEntryPoint>() (:56). No other site constructs it. Not determinable from source here: which consumer fixtures call the extension (the brief lists no usage sites).

InMemoryQueryableExecutor

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/InMemoryQueryableExecutor.cs:16 · Level 1 · class

  • What it is: a no-op, in-memory stand-in for the framework's IQueryableExecutor, the interface that carries the EF Core-specific parts of query execution (eager loading, split queries, materialization) out of application code. This implementation lets a unit test build the same query pipeline against a plain IQueryable<T> (an in-memory list, typically) with no EF Core provider in play at all.
  • Depends on: IQueryableExecutor (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/InMemoryQueryableExecutor.cs:16), the interface it implements, plus BCL LINQ only (System.Linq). No EF Core reference.
  • Concept introduced, the null-object executor. [Rubric §14, Testability] assesses how cheaply application code can be exercised without its infrastructure; a handler or service that takes IQueryableExecutor as a dependency stays unit-testable because a test can hand it this no-op instead of standing up a real DbContext. Every member either returns the query unchanged or runs the equivalent synchronous LINQ operator, so the executor adds no behavior of its own, it exists purely to satisfy the interface's shape over a query that is already fully in memory.
  • Walkthrough
    • Include<T>(IQueryable<T> query, string navigationPropertyPath) (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/InMemoryQueryableExecutor.cs:19-21): returns query unchanged, an in-memory collection has no navigation properties left to load.
    • AsSplitQuery<T>(IQueryable<T> query) (:24-26): likewise a pass-through, split-query behavior is an EF Core query-translation concern with nothing to do once the sequence is already materialized.
    • ToListAsync<T>(IQueryable<T> query, CancellationToken cancellationToken = default) (:29-30): wraps the synchronous query.ToList() in Task.FromResult, so an await at the call site still works with no real asynchrony underneath.
    • CountAsync<T>(IQueryable<T> query, CancellationToken cancellationToken = default) (:32-33): the same pattern over query.Count().
  • Why it's built this way: keeping every member a one-line pass-through or a synchronous LINQ call wrapped in Task.FromResult is what makes the type safe to reuse across every test that needs an IQueryableExecutor, there is no state to reset between tests and no behavior that could diverge from what an in-memory IQueryable<T> already does on its own.
  • Where it's used: as the IQueryableExecutor handed to application-layer tests across both apps and the framework itself, for example MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Application.Tests/Helpers/TestSupport.cs, .../SessionQuestions/UseCases/GetModerationQueueHandlerTests.cs, .../SessionQuestions/UseCases/GetSessionQuestionsHandlerTests.cs, .../LivePolls/DomainEventHandlers/LivePollVoteChangedHandlerTests.cs, .../LivePolls/Services/LivePollResultsBuilderTests.cs, .../LivePolls/UseCases/CastVoteHandlerTests.cs, .../LivePolls/UseCases/GetOpenPollsHandlerTests.cs, .../LivePolls/UseCases/GetPollResultsHandlerTests.cs, .../SessionQuestions/UseCases/SubmitQuestionHandlerTests.cs, MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.Application.Tests/UserNotificationExportServiceTests.cs, MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Services/Query/EntityQueryServiceTests.cs, and .../Services/Query/EntityQueryPipelineOrderingTests.cs, plus one more site not shown in the brief's usage-site slice.

IntegrationTestBase<TFixture>

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:13 · Level 1 · class (abstract)

  • What it is: the workhorse base class every integration test inherits. It owns the per-test HTTP client and lifecycle, typed request helpers, bearer-token management, and a thread-safe id counter, so a concrete test class is left with just its arrange/act/assert.
  • Depends on: IIntegrationTestFixture (the TFixture constraint, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:14), plus Xunit's IAsyncLifetime, System.Net.Http.Headers, and System.Net.Http.Json (:1-3).
  • Concept introduced, the xUnit async test lifecycle and per-test isolation. [Rubric §14, Testability]: the base implements IAsyncLifetime so InitializeAsync runs before each test and DisposeAsync after, and it hangs the database reset off that hook so every test starts from a clean database, the single most important property for reliable integration tests.
  • Walkthrough
    • Fields and properties: a static int _nextId = 1000 seed (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:16), and the Fixture / Client protected properties (:19-22).
    • Constructor (:24-28): stores the injected fixture and eagerly creates the HttpClient from it.
    • InitializeAsync (:31): a ValueTask that awaits Fixture.ResetDatabaseAsync() before each test. DisposeAsync (:34-39): suppresses finalization and disposes the client.
    • Auth helpers: SetBearerToken(string) / ClearAuthentication() (:42-48) set or clear the Authorization header, the hook through which a JwtTokenGenerator token is applied.
    • Typed HTTP helpers: GetAsync<T> (:51-56, which calls EnsureSuccessStatusCode then deserializes), and PostAsync<T> / PutAsync<T> / PutAsync / DeleteAsync (:59-72) returning the raw HttpResponseMessage so a test can assert status codes. The four write helpers are plain expression-bodied pass-throughs returning the client's Task directly (no async/await wrapper), while GetAsync<T> stays async because it awaits twice.
    • NextId() (:75): Interlocked.Increment over the shared seed, so parallel tests never collide on generated ids.
  • Why it's built this way: per-test database reset plus a per-test client is the isolation contract; centralizing the typed helpers keeps individual tests short and consistent. The static Interlocked counter is the cheapest safe way to hand out unique ids under xUnit's parallelism. Note the asymmetry: GetAsync<T> throws on a non-success status while the others hand the response back, which is what makes a read helper terse and a write assertion explicit.
  • Where it's used: the direct base of all three contract test bases in this unit (OpenApiContractTestsBase<TFixture>, ProblemDetailsContractTestsBase<TFixture>, ServiceInfoVersioningContractTestsBase<TFixture>), and of every concrete integration test in the downstream apps.

OpenApiBreakingChange

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChange.cs:12 · Level 1 · sealed record

  • What it is: one finding of the OpenAPI breaking-change gate: a OpenApiBreakingChangeKind plus the JSON pointer of the place in the baseline document that broke, with an optional human-readable Detail (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChange.cs:12).
  • Depends on: OpenApiBreakingChangeKind (the positional Kind parameter, :12) and nothing else; it is a plain value.
  • Concept introduced, a finding as a value record. [Rubric §9, API & Contract Design]: a sealed positional record gives value equality for free, which is exactly what lets the detector collect findings in a HashSet<OpenApiBreakingChange> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:73) and report a shared component once instead of once per reference site.
  • Walkthrough
    • Detail (:19): an init-only nullable string. Parameter and request-body findings fill it with the method, path and subject (for example GET /Orders: query parameter 'page'); the other kinds leave it null.
    • ToString() (:26): "{Kind} {JsonPointer}", or "{Kind} {JsonPointer} ({Detail})" when there is a detail, so the contract test can print one line per change (for example RemovedPath /paths/~1Orders).
  • Why it's built this way: Detail sits outside the positional parameters on purpose, so it takes part in equality only through the record's synthesized members while two findings at one pointer with different details stay distinct. The pointer alone does not name a parameter site well, hence the extra text.
  • Where it's used: produced by OpenApiBreakingChangeDetector and rendered by OpenApiContractTestsBase<TFixture>'s baseline test (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:157).

RecordingHttpForwarder

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RecordingHttpForwarder.cs:29 · Level 1 · sealed class

  • What it is: an IHttpForwarder that never proxies. It echoes the destination prefix, the matched cluster, and the ForwarderRequestConfig it was handed into response headers, so a gateway test can assert which cluster a route targets and which forwarder budget it carries (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RecordingHttpForwarder.cs:8-13).
  • Depends on: YARP only, Yarp.ReverseProxy.Forwarder (IHttpForwarder, ForwarderRequestConfig, HttpTransformer, ForwarderError) and Yarp.ReverseProxy.Model (IReverseProxyFeature), plus Microsoft.AspNetCore.Http and System.Globalization (:1-4). No first-party dependency. The trace headers it reads back are the ones the gateway kit's GatewayTraceHeaderTransformProvider stamps. The Yarp.ReverseProxy package reference lives in MMCA.Common.Testing itself rather than in every gateway test project, and the project file records why (MMCA.Common/Source/Hosting/MMCA.Common.Testing/MMCA.Common.Testing.csproj:43-46).
  • Concept introduced, the echoing fake at a framework extension point. Three ideas are worth taking from this type. First, echoing into response headers rather than storing state is what keeps a singleton fake safe for concurrent requests (:12-13): there is no shared mutable field to race. Second, replacing the DI singleton still intercepts under a config-driven route table, because YARP's own forwarder middleware resolves IHttpForwarder from DI, so MapReverseProxy() goes through the fake exactly as a hand-written MapForwarder call would (:14-22). Third, it runs the real transform pipeline against a throwaway outbound request before echoing (:23-27), so the request transforms are observable without a network hop, which is what makes an assertion about what a downstream would actually receive meaningful. [Rubric §2, Design Patterns] covers the test-double form (a recording fake, not a mock); [Rubric §14, Testability] covers keeping every gateway test off the network, since configured destinations are usually service-discovery names that resolve to nothing in a test host; and [Rubric §7, Microservices Readiness] is the topology this makes testable (ADR-089).
  • Walkthrough
    • Header constants (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RecordingHttpForwarder.cs:32-54): DestinationHeader, ClusterHeader, ActivityTimeoutHeader, VersionHeader, VersionPolicyHeader, plus RouteTraceEchoHeader and ClusterTraceEchoHeader for the two trace headers read back off the transformed outbound request. UnsetValue (:57) is the (unset) marker written when the forwarder config leaves a nullable setting unset, so a test asserts a value either way rather than distinguishing an absent header from an unset setting.
    • RouteTraceHeaderName (:63) and ClusterTraceHeaderName (:69): init properties defaulting to the shared kit's own header names, X-MMCA-Route and X-MMCA-Cluster, so a host with different header names can still use the fake.
    • The five-argument SendAsync overload (:72-78) simply delegates to the six-argument one with CancellationToken.None.
    • The real SendAsync (:81-115): null-guards context, config and transformer (:89-91); writes the destination prefix (:93); reads the matched cluster id off IReverseProxyFeature, falling back to UnsetValue (:94-95); writes the activity timeout, forwarded HTTP version and version policy (:96-101); runs transformer.TransformRequestAsync against a throwaway HttpRequestMessage (:106-108); echoes the two stamped trace headers (:110-111); then sets a 200 and returns ForwarderError.None (:113-114).
    • HeaderOrUnset (:121-124): reads one outbound header, joining multiple values with a comma, or returns the unset marker.
  • Why it's built this way: a gateway test wants to assert routing decisions and forwarder budgets, not the behavior of a backend. Swapping the forwarder is the smallest replacement that leaves the entire gateway pipeline (routing, transforms, limiter, correlation, health) genuinely running, which is what makes the same fixture reusable for both the route-map assertions and the hardening gates.
  • Where it's used: both gateway test projects register it by replacing the singleton. ADC uses it in the gateway-hardening fixture (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GatewayHardeningTests.cs:103) and in the route-map suite, which aliases all seven header constants at the top of the class (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/RouteMapTests.cs:42-60, registration at :430); Store does the same in both places (MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GatewayHardeningTests.cs:115, MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/RouteMapTests.cs:378, with the rationale written out at :21). MMCA.Common covers the fake itself, including the unset-marker paths and the custom trace header names (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/RecordingHttpForwarderTests.cs:17).

GracefulShutdownTestsBase<TEntryPoint>

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:25 · Level 1 · class (abstract)

  • What it is: a shutdown conformance base. It boots a real host, calls a real IHost.StopAsync under a bounded cancellation token, and asserts the host drained cleanly, firing ApplicationStopping and then ApplicationStopped inside the timeout.
  • Depends on: ProductionHostApplicationFactory<TEntryPoint> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:32), plus Microsoft.Extensions.Hosting's IHost / IHostApplicationLifetime, Microsoft.Extensions.DependencyInjection, AwesomeAssertions, and Xunit (:1-4).
  • Concept introduced, the bounded-stop drain check. [Rubric §29, Resilience & Business Continuity] (named in the class doc itself, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:10) assesses whether the system survives planned and unplanned interruption; a rolling deploy is the planned one. The failure this catches is a hosted service (a warm-up runner, service discovery, proxy infrastructure) that refuses to drain, which in production does not announce itself: it silently wedges a rolling deploy while the platform waits out its termination grace period (:13-17). [Rubric §13, Observability & Operability] is the operational half, lifetime events firing in order are what a platform's shutdown handling depends on. The recovery-objective framing is ADR-009; the base itself is one of the suites recorded in ADR-058.
  • Walkthrough
    • ShutdownTimeoutSeconds (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:29): virtual, defaults to 20 seconds. This number is the test: a host that drains slower than this fails.
    • CreateFactory() (:31): virtual, returns a plain ProductionHostApplicationFactory<TEntryPoint>. The doc says to override it only when the host needs a fixture beyond a Production-pinned boot (:18-21).
    • Host_StopsGracefully_FiringLifetimeEventsWithinTimeout (:33-62): the single [Fact]. It creates the factory (:36) and holds the disposal as a separate ConfiguredAsyncDisposable (:40), with an inline comment explaining why (:38-39): the shorter await using var factory = ....ConfigureAwait(false) form would retype factory and lose access to CreateClient and StartedHost.
    • It then creates and immediately disposes a client (:42-45), which is what forces the lazy host to build and start, asserts StartedHost is non-null (:47-48), resolves IHostApplicationLifetime (:49), and confirms ApplicationStarted already fired (:50).
    • The stop itself (:55-56): a CancellationTokenSource for ShutdownTimeoutSeconds and await host.StopAsync(timeout.Token). Reaching the next line already means the stop returned cleanly inside the budget, because a wedged hosted service makes the token cancel and StopAsync throw (:52-54).
    • The two closing assertions (:58-61): ApplicationStopping must have fired during the shutdown, and ApplicationStopped must signal once the host has fully stopped, each with its own because-reason.
  • Why it's built this way: the bounded token is the mechanism, not decoration. Without it a host that never drains would hang the test run instead of failing it, which is exactly the production symptom the test exists to surface. Driving a real IHost.StopAsync (rather than asserting registrations) is what makes this a runtime check.
  • Where it's used: both gateways, each a body-less one-line subclass, MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GracefulShutdownTests.cs:9 and MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GracefulShutdownTests.cs:9. Both run in the no-database host-test tier, so they are in each repo's CI.slnf.

Comparison

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:61 · Level 2 · class (private sealed, nested)

  • What it is: the private worker nested inside OpenApiBreakingChangeDetector. One instance holds the two parsed roots (baseline and live), the collected findings, and the set of schema pairs already visited, and walks the baseline looking for what the live document dropped or tightened (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:61).
  • Depends on: System.Text.Json (JsonElement), OpenApiBreakingChange and OpenApiBreakingChangeKind, plus two private records, OperationSite (:497) and ParameterSite (:511).
  • Concept introduced, a baseline-driven structural diff. [Rubric §9, API & Contract Design]: the walk iterates the BASELINE and looks each thing up in the live document, so additions are never even visited. That one-sided direction is what makes "additive changes pass" a property of the algorithm, not a filter.
  • Walkthrough
    • State: OperationNames (:63, the eight HTTP methods), _visited (:65), a private Direction enum of Request / Response (:67-71), and the Changes set (:73).
    • ComparePaths (:75): per baseline path, RemovedPath if gone; per baseline operation, RemovedOperation if gone, otherwise it builds an OperationSite and runs the three comparisons below.
    • CompareParameters (:260): parameters are keyed by location and name, name case-insensitive, via EffectiveParameters (:224), where an operation's own parameters override its path item's. Emits RemovedParameter, ParameterNewlyRequired and AddedRequiredParameter; CompareParameterSchema (:301) adds ChangedParameterType. A path parameter is always required (ParameterSite.IsRequired, :514).
    • CompareRequestBody (:326): RemovedRequestBody, AddedRequiredRequestBody (only when the live body carries required: true), RemovedRequestMediaType, then the content is compared in the request direction.
    • CompareResponses (:381): RemovedResponseStatusCode, otherwise content compared in the response direction. CompareContent (:403) then CompareSchema (:423).
    • CompareSchema (:423): resolves $refs, skips a pair already in _visited (which also stops recursive schemas, :433-437), reports ChangedPropertyType when the declared type sets differ (DeclaredTypes, :133, drops the null type first, :152, so nullability is not a type change), then recurses through properties and items.
    • CompareProperties (:455): a baseline property missing from live is RemovedResponseProperty only in the response direction; in the request direction a property newly in required is RequestPropertyNewlyRequired (:484-493).
    • Helpers: Escape (:116, RFC 6901 ~0/~1), TryGetObject (:119), Resolve (:169, follows local #/ refs with a 32-hop cap, :171), TryNavigate (:189).
  • Why it's built this way: direction matters. A caller breaks when a request property becomes required; a reader breaks when a response property disappears; a type change breaks both. Pointers move with $ref resolution so a finding names the component, not the reference site.
  • Where it's used: instantiated once per Detect call (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:52).
  • Caveats / not-in-source: only local #/ references are followed; external refs are left unresolved and the pair is compared as written.

CrossServiceHostIsolation

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:30 · Level 2 · static class

  • What it is: a static extension on IServiceCollection that narrows one in-process test host to its own modules. Its single entry point, IsolateCrossServiceHost<TEntryPoint>() (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:42), replaces the host's IEntityConfigurationAssemblyProvider with a filtering wrapper so its registry, outbox, internal-command and cleanup processors only enumerate that host's data sources.
  • Depends on: IEntityConfigurationAssemblyProvider, DefaultEntityConfigurationAssemblyProvider and EntityConfigurationOptions (its AdditionalAssemblies are kept regardless of the filter), plus the host's .deps.json. It is the isolation half of CrossServiceFixtureBase, whose class doc names it as the call every host factory makes from ConfigureTestServices (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:43).
  • Concept introduced, host isolation by dependency manifest. [Rubric §14, Testability]: when several hosts share one test process, the default AppDomain scan hands each host its peers' module assemblies and, through the per-module DataSources__{X} keys, its peers' databases, so each host's outbox would drain them all (CrossServiceFixtureBase.cs:40-44). The runtime closure of a host is exactly what its .deps.json lists, so filtering the scanned assemblies to that closure reproduces production's one-host-one-module-set shape (ADR-006). The method is declared in an extension(IServiceCollection services) block, the public DI-surface style of ADR-106.
  • Walkthrough
    • IsolateCrossServiceHost<TEntryPoint> (:42-62): reads the host's assembly names (ReadHostAssemblyNames), removes every non-keyed IEntityConfigurationAssemblyProvider descriptor (:47-53), remembers the last one as the inner provider, and registers a singleton HostScopedAssemblyProvider wrapping it (:56-59); returns the collection for chaining.
    • CreateInner (:65-73): rebuilds the removed registration by shape (none registered gives DefaultEntityConfigurationAssemblyProvider via ActivatorUtilities, then instance, factory or implementation type), throwing InvalidOperationException for any other shape.
    • ReadHostAssemblyNames (:79-114): takes the host assembly name plus the file name of every runtime entry under every targets library in the .deps.json beside the assembly (:93-111); throws InvalidOperationException when the file is missing (:86-91).
    • HostScopedAssemblyProvider (:116-125): keeps an inner assembly only if it is in the explicit AdditionalAssemblies set or its simple name is in the host's name set.
  • Why it's built this way: the manifest is the authoritative closure and needs no per-host allow-list, so a new module reference flows in automatically. Reading it requires the file beside the assembly, which WebApplicationFactory copies for every referenced host project (:89-90).
  • Where it's used: called from CrossServiceFixtureBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:43) and covered by MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Fixtures/CrossServiceHostIsolationTests.cs.

SqlServerIntegrationTestFixtureBase<TEntryPoint>

MMCA.Common.Testing · MMCA.Common.Testing.Fixtures · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:27 · Level 2 · class (abstract)

  • What it is: the reusable fixture that boots a real service host in-process against a throwaway SQL Server database, applies the module's migrations on first start, resets data between tests with Respawn, and drops the database on disposal. It is the concrete engine behind IIntegrationTestFixture for SQL Server hosts.
  • Depends on: IIntegrationTestFixture (implemented, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:27), CrossServiceFixtureBase (its static ComposeConnectionString helper, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:88), plus Microsoft.AspNetCore.Mvc.Testing (WebApplicationFactory), Microsoft.Data.SqlClient, Respawn, and Xunit's IAsyncLifetime (:1-4).
  • Concept introduced, the disposable-database integration fixture and environment-variable overrides. [Rubric §14, Testability] and [Rubric §8, Data Architecture]: real integration coverage needs a real relational database, and this fixture makes that cheap and hermetic, a fresh GUID-named database per fixture, migrated from scratch, Respawned between tests, dropped at the end. The database-per-service routing (ADR-006) is why the class doc stresses the DataSources collapse onto a single overridden connection string (:16-24).
  • Walkthrough
    • State (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:30-45): the recorded original-environment map, the server-base and database-name strings, the WebApplicationFactory, the Respawner, a _databaseCreated flag, and the public Client / ConnectionString. ConnectionString (:45) is exposed so SQL-fidelity tests can read raw tables (for example to assert an integration event landed in the outbox).
    • Services (:52): the booted host's root service provider, exposed so cross-service tests can resolve a consumer-side integration-event handler or a repository and drive the flow directly against the real database.
    • Abstract knobs: SqlBaseEnvironmentVariable (:58, names the env var holding the CI SQL base connection string), DatabaseNamePrefix (:61), and CreateFactory() (:134, where the subclass builds the host). CreateClient() (:64) satisfies the interface by delegating to the factory.
    • InitializeAsync (:67-101): resolves the server base from SqlBaseEnvironmentVariable or falls back to LocalDB (:69-70), composes a GUID-suffixed database name (:71) and builds the connection string through CrossServiceFixtureBase.ComposeConnectionString (:72), which overlays the catalog on the base string with a SqlConnectionStringBuilder and forces TrustServerCertificate (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:88-94), forces ASPNETCORE_ENVIRONMENT=Testing and pushes the top-level SQL connection string as environment variables (:75-76), lets the subclass push its own via ConfigureTestEnvironment (:77), and builds the factory (:79). It then sets _databaseCreated = true (:85) before creating the client, because the flag means "may exist", not "exists": the drop is guarded by IF DB_ID(...), so dropping a never-created database is a no-op, while a host that fails after its migration created the database still has it dropped instead of leaking it on the shared server (:81-85). Creating the client is what triggers the host's Migrate init to create the database and apply the module's migrations (:87-89). It then builds the Respawner, ignoring __EFMigrationsHistory (:91-100).
    • ResetDatabaseAsync (:104-117): returns immediately when no respawner exists, otherwise opens a connection and calls Respawner.ResetAsync.
    • DisposeAsync (:120-136): disposes client and factory, drops the database when the flag is set, and restores the environment.
    • ConfigureTestEnvironment (:147-149) is an empty virtual hook receiving the setter delegate. SetEnvironmentVariable (:151-160) records only the first original value per key so re-pushing a key cannot clobber the restore point; RestoreEnvironment (:162-170) puts them all back and clears the map.
    • DropDatabaseAsync (:172-193): clears pooled connections so the database is free to drop (:175), connects to master through the same ComposeConnectionString helper (:177), and runs an IF DB_ID(...) IS NOT NULL guarded SET SINGLE_USER WITH ROLLBACK IMMEDIATE plus DROP DATABASE (:185-188), with a scoped CA2100 suppression justified because the database name is a server-generated GUID, never user input (:184).
  • Why it's built this way: overrides go through process environment variables because the host reads its connection string at configure-time; forcing the Testing environment skips appsettings.Development.json (which would point DataSources at localhost) so the resolver collapses onto the single overridden top-level connection string, making the fixture behave like a clean single-database monolith. LocalDB-by-default keeps local runs zero-config while CI can point at a SQL service container. Note the contrast with CrossServiceFixtureBase, which needs the opposite (named sources per host so several EF models can coexist in one process); the two fixtures nevertheless share one pure, static connection-string composer, so the catalog overlay is identical in both and unit-testable without a container (CrossServiceFixtureBase.cs:80-88).
  • Where it's used: the base of every per-service integration fixture in both apps, four in ADC (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Infrastructure/ConferenceIntegrationTestFixture.cs:18, .../MMCA.ADC.Engagement.IntegrationTests/Infrastructure/EngagementIntegrationTestFixture.cs:18, .../MMCA.ADC.Identity.IntegrationTests/Infrastructure/IdentityIntegrationTestFixture.cs:23, .../MMCA.ADC.Notification.IntegrationTests/Infrastructure/NotificationIntegrationTestFixture.cs:18) and three in Store (MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Infrastructure/CatalogIntegrationTestFixture.cs:16, .../MMCA.Store.Identity.IntegrationTests/Infrastructure/IdentityIntegrationTestFixture.cs:16, .../MMCA.Store.Sales.IntegrationTests/Infrastructure/SalesIntegrationTestFixture.cs:17). Each is then the TFixture for that service's integration and contract tests.
  • Caveats / not-in-source: the fixture needs a reachable SQL Server, so these suites build but do not run without one; they execute in each repo's SQL-service CI job via the *.Integration.slnf filter.

ProblemDetailsContractTestsBase<TFixture>

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:22 · Level 2 · class (abstract)

  • What it is: a contract conformance base that asserts a host's error responses are RFC 9457 Problem Details documents, machine-readable bodies carrying status, title, and a diagnostic extension, across both error-shaping paths the framework uses.
  • Depends on: IntegrationTestBase<TFixture> (inherited, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:22), System.Net, System.Net.Http.Json, System.Text.Json, AwesomeAssertions, and Xunit (:1-5). Same live-guard shape as the OpenAPI base above.
  • Concept: still [Rubric §9, API & Contract Design], here the pinned contract is the error shape. The class covers the two distinct paths that produce errors (class doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:11-19): ASP.NET Core model validation (a 400 application/problem+json body) and the framework's HandleFailure Result-error mapping (see ApiControllerBase), which turns a Result failure such as an Error not-found into a 404 problem (ADR-013 defines that edge contract).
  • Walkthrough
    • Validation_400_HasProblemDetailsShape (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:30-40): sends the subclass's validation probe, asserts the shared shape at 400, then checks the problem+json content type and the model-validation-only extensions type, traceId, and errors (:35-38).
    • NotFound_404_HasProblemDetailsShape (:41-47): sends the 404 probe and asserts the shared shape.
    • Abstract probes: SendValidationErrorProbeAsync (:54) and SendNotFoundProbeAsync (:60), the only app-specific pieces, authenticating first when the endpoint requires it (the docs suggest pageNumber=0 against a [Range(1, int.MaxValue)] paged read, and reading an id that does not exist, :49-60).
    • AssertProblemDetailsShapeAsync (:67-83): the shared protected static assertion, JSON content type, echoed status, non-empty title, and at least one diagnostic extension (errors, traceId, or requestId, :76-80), returning the parsed body so a subclass can follow up.
  • Why it's built this way: pinning both the validation path and the HandleFailure path in one base means a regression in either error channel breaks CI, and factoring the shape assertion into a shared static keeps every host's error contract identical while still letting a host with a reachable 409-conflict path layer its own test on top (:16-18).
  • Where it's used: subclassed per host, four in ADC (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) and three in Store (MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Contract/ProblemDetailsContractTests.cs:20, .../MMCA.Store.Identity.IntegrationTests/Contract/ProblemDetailsContractTests.cs:16, .../MMCA.Store.Sales.IntegrationTests/Contract/ProblemDetailsContractTests.cs:16). ADC Conference is the one that adds a 409 stale-RowVersion conflict test on top of the inherited facts (.../MMCA.ADC.Conference.IntegrationTests/Contract/ProblemDetailsContractTests.cs:40-67, reusing AssertProblemDetailsShapeAsync at :67).

ServiceInfoVersioningContractTestsBase<TFixture>

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ServiceInfoVersioningContractTestsBase.cs:20 · Level 2 · class (abstract)

  • What it is: a contract conformance base that proves the API-versioning machinery actually works across more than one version: that /ServiceInfo is served by both v1.0 (deprecated) and v2.0, selected by the api-version header, and that the host reports supported and deprecated versions in response headers.
  • Depends on: IntegrationTestBase<TFixture> (inherited, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ServiceInfoVersioningContractTestsBase.cs:20), System.Net, System.Text.Json, AwesomeAssertions, and Xunit (:1-4).
  • Concept: [Rubric §9, API & Contract Design] again, the versioning axis (ADR-046). The class doc (:8-17) makes the point that without a second working version the whole versioning story would be untestable, so this base keeps the machinery exercised rather than merely asserted. Because the ServiceInfo controller ships in MMCA.Common.API (ServiceInfoControllerBase), the entire test body is identical across repos; a subclass supplies only its fixture.
  • Walkthrough
    • ServiceInfo_V1_ReturnsMinimalShape_AndIsReportedDeprecated (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ServiceInfoVersioningContractTestsBase.cs:28-42): requests v1.0, asserts 200, checks apiVersion == "1.0" and that the evolved supportedVersions list is absent in the v1 shape (:35-36), then asserts an api-deprecated-versions response header contains 1.0 (:38-40).
    • ServiceInfo_V2_ReturnsEvolvedShape_AndIsReportedSupported (:43-57): requests v2.0, asserts 200, checks apiVersion == "2.0" and that supportedVersions contains 2.0 (:50-52), then asserts an api-supported-versions header advertises 2.0 (:54-56).
    • GetServiceInfoAsync(string apiVersion) (:59-65): clears auth and sends the GET with the api-version header set to the requested version, which is the header-based selection ADR-046 standardizes.
  • Why it's built this way: keeping a real deprecated v1 and a real v2 side by side, and asserting both the payload shapes and the ReportApiVersions headers, is what proves version negotiation is wired end to end rather than configured and forgotten.
  • Where it's used: two adopters today, each a body-less one-line subclass, MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/ApiVersioningTests.cs:15 and MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Contract/ApiVersioningTests.cs:16.
  • Caveats / not-in-source: adoption is per-repo partial, one host each in ADC and Store, not every extracted REST service.

OpenApiBreakingChangeDetector

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:38 · Level 3 · static class

  • What it is: a pure function over two OpenAPI JSON strings. Detect(baselineJson, liveJson) returns only the changes that break an existing client (removed path, operation, status code, parameter, request body or media type; newly required property, parameter or body; removed response property; changed type) and passes everything else (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:44-58).
  • Depends on: System.Text.Json and System.Globalization (:1-2), the private nested Comparison worker, and the OpenApiBreakingChange record with its OpenApiBreakingChangeKind enum (thirteen members, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeKind.cs:4-44).
  • Concept introduced, structural rather than textual contract comparison. [Rubric §9, API & Contract Design]: both documents are read by member name, so property order never shows up as a change, and only the contract-bearing members are read (paths, parameters, in, name, responses, requestBody, content, schema, $ref, type, properties, required, items), so descriptions, summaries, examples and tags are ignored by construction (:14-22). This is the machinery behind the baseline opt-in described in ADR-058.
  • Walkthrough
    • Detect (:44-58): guards both arguments with ArgumentException.ThrowIfNullOrWhiteSpace (:46-47), parses with JsonDocument (:49-50), runs Comparison.ComparePaths (:52-53), and returns the findings ordered by pointer (ordinal) then kind (:55-57), so the report is deterministic.
    • Everything else lives in Comparison (:61-519).
  • Why it's built this way: a static, dependency-free function is trivially unit-testable against hand-written documents, which is how the framework tests it (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/OpenApiBreakingChangeDetectorTests.cs and OpenApiBreakingChangeDetectorParameterTests.cs); the contract base only supplies the two strings.
  • Where it's used: called by OpenApiContractTestsBase<TFixture>.OpenApiDocument_HasNoBreakingChangesAgainstTheBaseline (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:149) and see OpenApiContractTestsBase<TFixture>.

TenantResolutionTestsBase

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/TenantResolutionTestsBase.cs:27 · Level 3 · class (abstract)

  • What it is: a settings-level conformance base for multi-tenant hosts. A subclass supplies the host's TenancySettings and inherits three facts that must stay true: header resolution is an explicit opt-in, the signed claim wins over the header, and the health probes bypass tenant resolution (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/TenantResolutionTestsBase.cs:27-101).
  • Depends on: TenancySettings and TenantResolutionStrategy (the order and prefixes read through EffectiveResolutionOrder and EffectiveExcludedPathPrefixes), PathString for segment-prefix matching, and AwesomeAssertions. It does not boot a host, so it needs no fixture.
  • Concept introduced, tenant isolation as a configuration invariant. [Rubric §14, Testability]: a header-resolved tenant lets any caller name any tenant, so the dangerous configuration is caught as a failing test on the settings object rather than discovered in production (ADR-073, ADR-058).
  • Walkthrough
    • Knobs: ProbePaths (:30, /health and /alive), abstract Settings (:33), virtual AllowsHeaderResolution (:40, default false).
    • Header_resolution_is_an_explicit_opt_in (:47): passes when tenancy is disabled or the host opted in; otherwise EffectiveResolutionOrder must not contain TenantResolutionStrategy.Header, and the failure message tells the owner what to change.
    • Claim_is_tried_before_header (:67): when the order includes the header, the claim must appear and come before it.
    • Health_endpoints_are_excluded (:88): each probe path must be covered by an effective excluded prefix, matched with StartsWithSegments and case-insensitively as the middleware does, because a probe rejected for carrying no tenant takes the replica out of rotation.
  • Why it's built this way: the opt-in override exists for a host behind a trusted gateway that sets the header itself; making that a deliberate override keeps the exception visible in code review.
  • Where it's used: exercised in the framework by MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/TenantResolutionTestsBaseTests.cs and TenantResolutionConformanceTests.cs.

OpenApiContractTestsBase<TFixture>

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:32 · Level 2 · class (abstract)

  • What it is: a contract conformance base that boots a host and asserts its /openapi/v1.json document is served, is a well-formed OpenAPI 3.x document, and still describes the core public resources, so an accidental controller or route removal fails CI instead of silently changing the published contract.
  • Depends on: IntegrationTestBase<TFixture> (inherited, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:32), System.Net, System.Text.Json, AwesomeAssertions, and Xunit (:1-4), plus OpenApiBreakingChangeDetector for the opt-in baseline test.
  • Concept introduced, the contract guard on the live document. [Rubric §9, API & Contract Design] assesses whether the API surface is described and kept stable; the default of all three Level 2 bases is a live-document guard with no committed snapshot (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:15-17), the assertions run against the document the host actually serves, so new controllers can never leave a stale snapshot behind and a removed one is caught immediately. A subclass can additionally opt in to a committed baseline diff that fails only on breaking changes. This is one of the suites recorded in ADR-058, and the one with the widest adoption.
  • Walkthrough
    • Overridable and abstract knobs: OpenApiDocumentPath (:51, defaults to /openapi/v1.json), MinimumPathCount (:58, a coarse floor under the route surface), MinimumPathCountBecause (:65, the failure-message reason), CorePublicResources (:71, the resource paths that must keep being described), and BaselineDocumentPath (:80, null by default, which turns the baseline test into a no-op).
    • RefreshBaselineEnvironmentVariable (:41, "MMCA_UPDATE_OPENAPI_BASELINE"): the public constant that switches the baseline test into refresh mode; the private IndentedOptions (:43) indents the rewritten file.
    • OpenApiDocument_IsServed_AsWellFormedOpenApiDescribingTheApiSurface (:83-95): parses the JSON and asserts openapi starts with 3., info.title is non-empty, a paths object exists, and it holds at least MinimumPathCount entries.
    • OpenApiDocument_DescribesEveryCorePublicResource (:98-115): first guards against a vacuous pass (the subclass must pin at least one resource, :100-101), then checks every CorePublicResources entry is present, matching on name case-insensitively (:107-110) so presence, not exact casing, is the contract.
    • OpenApiDocument_HasNoBreakingChangesAgainstTheBaseline (:118-159): returns immediately when BaselineDocumentPath is null (:120-124). With the variable set to 1 it rewrites the baseline from the live document (indented, :131-132) and then fails on purpose (:135-140), so a regenerating run can never pass in CI. Otherwise it asserts the baseline file exists (:143), runs OpenApiBreakingChangeDetector.Detect (:149) and requires zero changes, listing each one plus the RefreshGuidance text (:176).
    • GetOpenApiJsonAsync (:165-174): clears auth (the document is anonymous outside Production), fetches the path, asserts 200 with a message naming the path, and returns the raw JSON. Indent (:180) re-serializes the document for a line-by-line diffable baseline.
  • Why it's built this way: asserting the live document by default keeps the guard maintenance-free while still catching the two failures that matter, the document disappearing and a public resource vanishing; the two assertions are deliberately coarse because a strict shape diff would fail on every ordinary additive change. Hosts that want a stricter contract opt in to the baseline, whose detector ignores additive and documentation-only changes, so the gate fires only on what breaks a client.
  • Where it's used: subclassed once per REST service host, four in ADC (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/OpenApiContractTests.cs:16, .../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 three in Store (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), each supplying the fixture, the path floor, and the pinned resource list.

InMemoryRefreshSessionStore

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/InMemoryRefreshSessionStore.cs:27 · Level 6 · sealed class

  • What it is: an in-memory IRefreshSessionStore fake for handler and application-service unit tests, giving a test both a seedable list of already-persisted sessions and a staged-insert buffer that only lands on the seeded list once SaveChangesAsync runs, so a test can assert what a handler wrote without a database.
  • Depends on: IRefreshSessionStore (implemented, MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/InMemoryRefreshSessionStore.cs:27) and RefreshSession (:29-30), the identifier alias UserIdentifierType, and System.Threading for CancellationToken.
  • Concept introduced, the stage-then-flush fake. Rather than mutate a single backing list on every call, the fake keeps two lists: _staged (:29) for rows an in-flight unit of work has added but not yet saved, and _sessions (:30) for what a real store would already have durably persisted. AddAsync (:63-67) only appends to _staged; only SaveChangesAsync (:155-162) moves the staged rows onto _sessions and increments SaveCount (:157). This mirrors the two-phase add-then-SaveChangesAsync contract every IUnitOfWork-backed repository has, so a test exercising a handler that forgets to call SaveChangesAsync sees the session absent from Sessions, the same gap a real database would show. [Rubric §14, Testability] is the fit: no database, no host, and the fake still models the commit boundary rather than eagerly persisting on every write.
  • Walkthrough
    • Backing state (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/InMemoryRefreshSessionStore.cs:29-30): the private _staged and _sessions lists. Sessions (:33) exposes _sessions read-only, in insertion order. SaveCount (:36) counts how many times the store was flushed.
    • RotationOutcome (:43): an optional Func<bool> a test sets to force TryRotateAsync to report a lost race, standing in for the database arbitrating two concurrent rotations of the same token where the loser writes nothing (doc :39-41).
    • Seeding helpers: Seed(RefreshSession) (:47) drops an already-built session straight onto _sessions, bypassing the staged path; SeedLive(userId, token, createdAt, expiresAt) (:55-60) builds one via RefreshSession.Create(...).Value! and seeds it, for a test that only has raw field values rather than a session in hand.
    • AddAsync (:63-67): stages the session; FindByTokenHashAsync (:70-71) does an ordinal-equality scan over _sessions (never _staged) by token hash; GetUnrevokedByUserAsync (:74-87) filters _sessions by user id and !IsRevoked, ordering by CreatedAt then Id to match the real store's contract; FindByIdAsync (:139-144) scans _sessions by id and user id together; FindByIdUntrackedAsync (:151) scans _sessions by id alone. The real store uses it to read past a tracked copy, but this fake has no tracked copy to see past (every read hands out the one persisted instance, which already reflects any rotation), so the "fresh" read returns the same instance.
    • Session-administration reads, all over _sessions only: GetAllByUserAsync (:90-103) returns every session for a user including revoked ones, ordered by CreatedAt then Id (the unfiltered counterpart of GetUnrevokedByUserAsync); GetUserIdsWithLiveSessionsAsync (:106-119) returns the distinct owner ids of sessions where IsActiveAt(now) holds; CountLiveSessionsByUserAsync (:122-136) null-guards userIds, then groups the active sessions of the requested users into a per-user count (users with no live session are absent from the dictionary).
    • SaveChangesAsync (:155-162): increments SaveCount, moves every staged row onto _sessions, clears _staged, and returns the count written.
    • TryRotateAsync (:165-188): null-guards both sessions (:171-172); consults RotationOutcome to simulate a lost race (:174-177); calls presented.Revoke(revokedAt, RefreshSession.ReasonRotated, successor.TokenHash) and bails out on failure (:179-182); on success, stages the successor and immediately flushes via AddAsync plus SaveChangesAsync (:184-185), so a caller sees the successor in Sessions without a separate save call.
  • Why it's built this way: the staged/persisted split is what lets a test distinguish "the handler called AddAsync" from "the handler's unit of work actually committed," which is exactly the bug class the framework's SaveChangesAsync-returns-1-by-default convenience (HandlerTestBase<THandler>) is meant to catch for other aggregates. FindByTokenHashAsync only ever sees _sessions, never _staged, so an uncommitted add is invisible to a lookup, matching how a real store would behave mid-transaction.
  • Where it's used: seeded and asserted against directly in MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Application.Tests/Users/AuthenticationServiceTests.cs (3 references) and across MMCA.Common suites, MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/RefreshSessionRevocationTests.cs (3), MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Users/DeleteUserHandlerBaseTests.cs (3), MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/AuthenticationServiceBaseTests.cs (2), MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/RefreshSessionManagementTests.cs (2), MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/Administration/UserSessionsAdministrationServiceTests.cs (1), MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/AuthenticationServiceIdentityCompletionsTests.cs (1), and MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/AuthenticationServiceLegalAcceptanceTests.cs (1).

MiddlewarePipelineOrderTestsBase

MMCA.Common.Testing · MMCA.Common.Testing.Conformance · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:31 · Level 11 · class (abstract)

  • What it is: the HTTP-edge counterpart of DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult>. It seeds the framework's default middleware step list, applies the host's own customization if it has one, and asserts the resulting step order is exactly the documented pipeline, plus that the startup-validated adjacency invariants still hold.
  • Depends on: MiddlewarePipelineBuilder and MiddlewarePipelineStepNames from MMCA.Common.API.Startup.Pipeline (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:2), plus AwesomeAssertions and Xunit (:1,3). This is the reference that makes MMCA.Common.Testing depend on MMCA.Common.API (MMCA.Common/Source/Hosting/MMCA.Common.Testing/MMCA.Common.Testing.csproj:51-53).
  • Concept introduced, order-as-data at the HTTP edge. In ASP.NET Core middleware order is behavior, not style, and the failures it produces do not look like ordering bugs. The class doc pins four adjacencies and names two runtime failures they guard against (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:13-17): a tenant that never resolves when tenant resolution runs before authentication, and a per-user rate cap that never engages when the limiter runs before authentication. It is explicit that the third adjacency, the pre-forwarded capture immediately before UseForwardedHeaders, has no runtime consequence today: nothing in the framework reads the captured Items, and the OIDC discovery endpoint derives jwks_uri from Jwt:Issuer, so that adjacency is pinned as structure only (:17-19). What makes the check cheap is that ADR-079 turned the order into data: MiddlewarePipelineBuilder.CreateDefault() produces a list of named steps that are inert until applied, so no WebApplication has to be built and the test runs in the fast unit tier with no database and no host (:27-28). [Rubric §17, DevOps & Deployment] assesses whether cross-cutting edge concerns are composed deliberately; [Rubric §11, Security] is why authentication before the rate limiter is load-bearing (ADR-019); [Rubric §14, Testability] and [Rubric §34, Architecture Governance & Documentation] cover the fitness-function form itself (ADR-015).
  • Walkthrough
    • Configure (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:37): virtual, defaults to null, meaning the host under test calls the zero-argument UseCommonMiddlewarePipeline() overload. A host that customizes the pipeline overrides this with the same Action<MiddlewarePipelineBuilder> its Program.cs passes (:22-24).
    • ExpectedStepNames (:39-60): the pinned order, outermost first, all eighteen steps named through MiddlewarePipelineStepNames constants rather than string literals: ExceptionHandler, CorrelationId, RequestLocalization, PreForwardedCapture, ForwardedHeaders, HttpsRedirection, ResponseCompression, Routing, Cors, Authentication, TenantResolution, RateLimiting, SoftDeletedUserFilter, Authorization, OutputCache, JwksEndpoint, OidcDiscoveryEndpoint, Controllers. It is virtual, so a host with a deliberately different pipeline states its own order.
    • EdgePipeline_OrdersSteps_InDocumentedOrder (:62-69): the first [Fact]. It builds the seeded builder and asserts builder.StepNames equals ExpectedStepNames, with a because-reason (:68) that spells out three adjacencies rather than just reporting a list mismatch: the pre-forwarded capture immediately before UseForwardedHeaders so the captured pre-forwarded scheme and host stay faithful, tenant resolution immediately after authentication so the claim strategy sees HttpContext.User, and the rate limiter after authentication per ADR-019 so the per-user cap engages.
    • EdgePipeline_SatisfiesLoadBearingInvariants (:71-79): the second [Fact]. It asserts builder.Build() does not throw. Build() (MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/Pipeline/MiddlewarePipelineBuilder.cs:251-274) re-checks four invariants at startup, PreForwardedCapture immediately before ForwardedHeaders (:253-256), Authentication immediately before TenantResolution (:258-261), Authentication before RateLimiting (:263-266), and ForwardedHeaders before HttpsRedirection (:268-271), so a pipeline that fails here would have thrown while the host was starting.
    • CreateBuilder (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:81-86): the two-line private helper both facts share, MiddlewarePipelineBuilder.CreateDefault() followed by Configure?.Invoke(builder).
  • Why it's built this way: the two facts are complementary rather than redundant. The first pins the exact order, so any reorder (including one that still satisfies every adjacency) fails visibly; the second re-runs the host's own startup validation in the unit tier, so an override that breaks an adjacency fails in a test rather than at boot. Naming steps through MiddlewarePipelineStepNames constants means a step rename is a compile error in the test rather than a silent string mismatch.
  • Where it's used: four subclasses, every one of them body-less because every host calls the zero-argument overload, MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/MiddlewarePipelineOrderTests.cs:12, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/MiddlewarePipelineOrderTests.cs:16, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Api/MiddlewarePipelineOrderTests.cs:16, and MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/MiddlewarePipelineOrderTests.cs:15. The framework's own UseCommonMiddlewarePipeline doc points back at this base as the way to freeze the order (MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/WebApplicationExtensions.cs:45).

HandlerTestBase<THandler>

MMCA.Common.Testing · MMCA.Common.Testing.Support · MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:38 · Level 14 · class (abstract)

  • What it is: the reusable Moq scaffold for command/query handler unit tests. It hands a derived test class a pre-configured Mock<IUnitOfWork>, a no-op logger typed to the handler under test, and two one-line helpers that register a repository mock into that unit of work.
  • Depends on: IUnitOfWork, IRepository<TEntity, TIdentifierType>, IReadRepository<TEntity, TIdentifierType>, AuditableAggregateRootEntity<TIdentifierType> and AuditableBaseEntity<TIdentifierType> (the two generic constraints), plus Moq, Microsoft.Extensions.Logging, and NullLogger<T> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:1-5).
  • Concept: the arrange-phase base class. Where IntegrationTestBase<TFixture> gives an end-to-end test a booted host, this gives an isolated unit test a mocked persistence boundary: no database, no host, no HTTP. The class doc (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:10-12) frames it as the shared replacement for the per-test copy-paste of Mock<IUnitOfWork> plus GetRepository wiring plus SaveChangesAsync setup. [Rubric §14, Testability] assesses whether the design permits fast isolated tests; the fact that handlers depend on IUnitOfWork (an Application-layer abstraction) rather than a DbContext is what makes this scaffold possible at all, which is [Rubric §3, Clean Architecture] paying off in the test tier (ADR-055 records that contract). [Rubric §15, Best Practices & Code Quality] covers the deduplication itself.
  • Walkthrough
    • Constructor (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:41-42): a single expression-bodied statement that pre-configures UnitOfWork.SaveChangesAsync(...) to return 1, the success path, so a happy-path test writes no persistence setup at all. Failure-path tests override it with their own Setup (doc :32-35).
    • UnitOfWork (:45): the Mock<IUnitOfWork> every registered repository is wired into, created by a property initializer so the constructor can configure it.
    • Logger (:48): NullLogger<THandler>.Instance, typed by the handler type parameter so it binds directly to the handler's ILogger<THandler> constructor parameter.
    • RegisterRepository<TEntity, TIdentifierType>() (:56-64): creates a Mock<IRepository<TEntity, TIdentifierType>>, wires that same object into both GetRepository<...>() and GetReadRepository<...>() (:61-62), and returns the mock for further Setup / Verify. Constrained to AuditableAggregateRootEntity<TIdentifierType> (:57), that is, to aggregate roots.
    • RegisterReadRepository<TEntity, TIdentifierType>() (:72-79): the read-only counterpart for non-aggregate child entities that expose no read-write repository, constrained to the looser AuditableBaseEntity<TIdentifierType> (:73) and wiring only GetReadRepository<...>() (:77).
  • Why it's built this way: wiring one repository mock into both accessors matters because a handler may read through GetReadRepository and write through GetRepository on the same aggregate; a test forced to register two mocks would have to keep their state in sync. Pre-succeeding SaveChangesAsync encodes the common case so only the interesting deviation appears in a test.
  • Where it's used: 123 test files today, the framework's own scaffold test (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/HandlerTestBaseTests.cs:13) plus 122 ADC Application-tier files spread across all four modules (for example MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Application.Tests/Categories/UseCases/CreateConferenceCategoryHandlerTests.cs:13, MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Application.Tests/LivePolls/UseCases/CastVoteHandlerTests.cs:13, MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Application.Tests/Users/UseCases/ChangePassword/ChangePasswordHandlerTests.cs:14, MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.Application.Tests/UserNotificationExportServiceTests.cs). The class doc carries a worked CreateEventHandlerTests example (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:19-31).
  • Caveats / not-in-source: adoption is uneven. MMCA.Store and MMCA.Helpdesk handler tests do not subclass it at all: a workspace-wide search finds no HandlerTestBase reference under either repo's Tests/ tree. Why those two arrange their handler tests by hand is not recorded in source.

AnonymousEndpointTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:30 · Level 0 · abstract class

  • What it is: an allow-list gate for authorization opt-outs. Every [AllowAnonymous] in the assemblies a subclass names must appear in an explicit, reviewed list, so an endpoint cannot quietly lose its authorization gate. A second gate, on by default and opt-out per repo, closes the complementary hole: an endpoint carrying no authorization attribute at all.
  • Depends on: [Fact] (xUnit), AwesomeAssertions, RuleHelpers.LoadableTypes, and reflection over attribute instances matched by full name: AllowAnonymousAttribute, AuthorizeAttribute, ControllerBase, and Blazor's RouteAttribute are four private full-name constants (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:32-35), not compile-time references.
  • Concept introduced, the two-directional allow-list gate. A one-directional "no anonymous endpoints" rule is unusable (login has to be anonymous) and a one-directional "these are allowed" rule rots. This base asserts both halves: no unlisted [AllowAnonymous] exists, and no list entry matches nothing. The second half is the subtle one, since a stale entry hides a renamed or re-gated endpoint behind a permission that is no longer being granted (reason string, line 114). [Rubric §11, Security] assesses whether authentication gates stay where they were put; [Rubric §14, Testability] assesses turning a review-only property into an executable one; [Rubric §34, Architecture Governance] applies because the exception list, with its per-entry justification comments, becomes the reviewed record of the anonymous surface.
  • Walkthrough
    • The subclass supplies TargetAssemblies (line 38), AllowedAnonymousEndpoints (line 45, identified as the type's FullName for a type-level attribute and FullName.MethodName for a method-level one, lines 41-43), and a MinimumScannedTypes floor (line 52, default 1).
    • Three allow-list [Fact]s. AnonymousEndpoints_AreAllowListed (line 80) subtracts the allow-list from the discovered set and names what is left. ScannedEndpointSet_IsNotEmpty (line 92) is the non-vacuity guard: with no controllers and no routable components discovered, the first assertion passes without having looked at anything (comment, lines 94-95). AllowList_HasNoStaleEntries (line 105) runs the comparison the other way.
    • Two shapes are scanned, both by reflection: IsController (line 159) walks the base chain for ControllerBase, and IsRoutableComponent (line 175) looks for RouteAttribute on the type, combined in IsScannedEndpointType (line 153).
    • AnonymousEndpoints() (line 183) is protected rather than private so a subclass can build a richer report over the same data (doc, lines 178-182); it flattens the assemblies, filters to the scanned shapes, and returns a distinct, ordinally-ordered set.
    • AnonymousEndpointsOf (line 255) is the load-bearing detail: type-level attributes are read with inherit: false (line 257) and methods are enumerated DeclaredOnly (line 264), so a framework base action is reported once at its declaration site instead of once per derived controller in every consumer repo (comment, lines 262-263).
    • The undecorated-endpoint gate is enforced by default: RequireExplicitAuthorizationDecision (line 69) returns true, and its companion list EndpointsWithoutAuthorizationAttribute (line 77) defaults to empty. Endpoints_DeclareAnAuthorizationDecision (line 119) fails any undecorated endpoint not in that companion list (lines 127-132). A repo that is not ready overrides the flag to false, and the fact then returns immediately without checking anything (lines 121-125); the earlier fallback that asserted the two scans stay disjoint is gone. UndecoratedAllowList_HasNoStaleEntries (line 137) is that list's own stale-entry guard, the same shape as AllowList_HasNoStaleEntries, and runs regardless of the flag.
    • UndecoratedEndpoints() (line 199) mirrors AnonymousEndpoints() but selects types and actions with no decision at all. UndecoratedEndpointsOf (line 207) skips abstract types on purpose, since the concrete controller inheriting the action set is where the decision belongs, and reports per type rather than per action, because a controller that forgot [Authorize] usually declares no method of its own (comment, lines 209-213). DeclaresAuthorizationAnywhere (line 229) walks the base-type chain checking both the type and its declared methods for either attribute; HasAuthorizationAttribute (line 250) is the shared Authorize-or-AllowAnonymous test both members reduce to.
  • Why it's built this way: the class doc is explicit about the limit (lines 18-24). Minimal-API endpoints opt out through the .AllowAnonymous() builder call, which produces endpoint metadata at map time and is invisible to static reflection, so the framework's own minimal-API anonymous surface (JWKS, OIDC discovery, app-association, session-cookie refresh, health) is outside this gate; catching it would need an endpoint-metadata check over a built host. Matching ASP.NET types by full name keeps the package free of an ASP.NET reference, the same stance the whole rule library takes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/MMCA.Common.Testing.Architecture.csproj:22-29). The stricter gate defaults on (line 69; doc lines 54-60): the allow-list check alone cannot see an endpoint carrying no authorization attribute at all, exactly the shape a forgotten [Authorize] produces, so a repo must decline it explicitly by overriding the flag to false rather than adopt it by remembering to. On a controller ASP.NET Core's fallback authorization policy now closes that hole at run time, but AuthorizeRouteView gates a routable Blazor component by reading attributes and ignoring the fallback policy entirely, so for pages this test IS the control (remarks, lines 61-68).
  • Where it's used: subclassed in all four repos, and the DeclaredOnly decision is what keeps each list local to what that repo declares. MMCA.Common's AnonymousEndpointTests lists the credential-exchange actions on its controller bases (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTests.cs:14, list at :22, floor of 21 at :43). ADC's covers the two Identity credential actions plus the public conference-browse reads, calendar exports and bookmark counts, with a floor of 79 (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/AnonymousEndpointTests.cs:23, floor at :108). Store's covers the public storefront reads, product images, registration and the Stripe webhook, with a floor of 32 (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Api/AnonymousEndpointTests.cs:10, floor at :54). Helpdesk's single entry is its whole TicketsController, because the seed ships without an Identity issuer so there is no token to require (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/AnonymousEndpointTests.cs:17, entry and reason at :26-30, floor of 1 at :35). MMCA.Common also carries the adversarial coverage for the base itself in AnonymousEndpointTestsBaseTests, which proves each assertion fails on its own drift through private DriftedTests, StaleAllowListTests and EmptyScanTests subclasses (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:163, :108, :117) plus a conformant control (:126).

ArchitectureAssert

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureAssert.cs:8 · Level 0 · static class

  • What it is: the shared failure-reporting helper for every architecture fitness function, a static class with two NoViolations overloads that turn a rule breach into a readable, offender-listing assertion failure.
  • Depends on: NetArchTest.Rules.TestResult and AwesomeAssertions' Should() fluent API, both global-imported for the whole package (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/GlobalUsings.cs:3-4). No first-party dependencies: this is the bottom of the fitness-function stack.
  • Concept introduced, architecture fitness functions. A fitness function is an automated test that asserts a structural property of the codebase (a layer never references another, a controller is sealed) rather than a behavioral one. This package makes those rules first-class, shared code (ADR-015). [Rubric §14, Testability] assesses how well invariants are guarded by executable checks; ArchitectureAssert is the reporting primitive that makes a failing invariant name its offenders instead of just going red. [Rubric §34, Architecture Governance] assesses whether architectural decisions are enforced rather than merely documented; every rule in this package funnels its verdict through here.
  • Walkthrough
    • NoViolations(NetArchTest.Rules.TestResult result, string reason) (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureAssert.cs:11) returns early when result.IsSuccessful (line 13), otherwise null-coalesces FailingTypes to an empty list, joins the full names into a bullet list (lines 18-19), and asserts IsSuccessful.Should().BeTrue(...) with the reason plus the violation list as the because argument (lines 21-22).
    • NoViolations(IEnumerable<string> violations, string reason) (line 26) materializes the sequence once and asserts list.Should().BeEmpty(...) (line 30), for the reflection-derived, IL-reading and file-scanning rules that produce a plain string list rather than a NetArchTest result.
  • Why it's built this way: the XML doc (lines 3-7) names it the un-drifted successor to the three per-repo ArchitectureTestHelper.AssertNoViolations copies: the reporting logic was duplicated in MMCA.Common, MMCA.Store, and MMCA.ADC, and centralizing it here removes the drift.
  • Where it's used: every rule in ArchitectureRules and several reflection-based test bases call one of these two overloads as their final step.

BrandColorTokenTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/BrandColorTokenTestsBase.cs:13 · Level 0 · abstract class

  • What it is: an abstract xUnit test base that fails the build when a landing-page stylesheet re-hardcodes the brand hex instead of sourcing it from the shared var(--mmca-primary) CSS custom property.
  • Depends on: [Fact] (xUnit), AwesomeAssertions, and Assembly.GetManifestResourceStream (BCL) to read embedded CSS. No first-party type dependency: it operates on the strings the subclass embeds.
  • Concept introduced, the drift fitness function. Unlike a layer rule that reflects over assemblies, a drift function reads committed text (CSS here) and asserts a single source of truth is used. [Rubric §20, Design System & Theming] assesses whether visual tokens have one authoritative definition; this base guards that consumers of the framework palette cannot silently fork the primary color.
  • Walkthrough
    • Two private constants pin the forbidden literal #1565C0 and the required token var(--mmca-primary) (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/BrandColorTokenTestsBase.cs:15-16).
    • The subclass supplies EmbeddedCssLogicalNames (line 22), the manifest-resource names of its landing-page stylesheets.
    • The single [Fact] LandingPageCss_SourcesBrandColorFromToken_NotHardcodedHex (line 25) first asserts the list is non-empty (a non-vacuity guard, lines 27-28), then for each stylesheet reads it via ReadEmbeddedCss (line 56, which throws a clear InvalidOperationException when the resource is missing, lines 58-60) and records a violation when the file is blank (line 37), when the token is absent (line 43), or when the raw hex is present (line 48, matched with OrdinalIgnoreCase so a lowercase spelling cannot slip past).
    • Resources are resolved from GetType().Assembly (line 33), that is, the subclass's assembly, which is what lets a package-shipped base read a consumer's stylesheet.
  • Why it's built this way: the doc (lines 3-12) explains the split. MMCA.Common's own BrandColorTokenTests guards the C#-to-CSS token definition (from BrandColors.Primary), while this base guards every downstream consumer of it, embedding the stylesheets as manifest resources so the package needs no file-system access into the consumer repo.
  • Where it's used: subclassed once per repo that ships a branded landing page, as BrandColorTokenTests in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/BrandColorTokenTests.cs:12) and Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/BrandColorTokenTests.cs:10).

CallGraphIndex

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/CallGraphIndex.cs:20 · Level 0 · internal sealed class

  • What it is: a read-only index over the compiled assemblies an IL-reading rule scans, built once per rule run. It holds every type by full name, every type's transitive ancestor names, and the reverse map from an ancestor name to the scanned types that implement or derive from it.
  • Depends on: Mono.Cecil (ModuleDefinition, TypeDefinition, TypeReference, MethodDefinition, MethodReference), which the package already carries transitively through NetArchTest, plus BCL dictionaries and HashSet<string>. No first-party dependencies.
  • Concept introduced, following an interface call in a static walk. IL records the static callee: a handler that calls IPointsAwarder.AwardAsync emits a callvirt naming the interface method, not the class that does the work. A walk that stops there never reaches the implementation, so a transitive rule would report nothing. The reverse implementor map is what closes that gap (doc, lines 9-13). The second design decision is equally load-bearing: ancestors are recorded by name, not by resolved definition, so a contract declared in an assembly the map does not register (typically a framework interface such as IDomainEventHandler<T>) is still recognized as an ancestor; only the walk through such a type stops, because its metadata is absent (doc, lines 14-18). [Rubric §14, Testability] assesses whether behavioral invariants inside method bodies can be checked at all; this index is the primitive that makes the body-reading rules in this package possible without a Roslyn or IL-rewriting dependency.
  • Walkthrough
    • Three ordinal-keyed dictionaries back the index: _types by full name, _ancestors per type, and _implementors from an ancestor name to the derived types (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/CallGraphIndex.cs:22-24).
    • The constructor (line 26) makes two passes: it registers every type in every module, nested types included (lines 28-31), then computes each type's ancestor set and inverts it into _implementors (lines 33-48).
    • Types (line 52) exposes the whole inventory. ElementFullName (line 55) strips a generic instantiation down to the open definition's name, which is how a reference to a closed generic still matches the type it was declared as.
    • Implements(type, ancestorFullName) (line 58) is the membership test the handler and cascade rules select on. Find(reference) (line 63) resolves a reference to a scanned type or null when it lives outside the scan.
    • TargetsOf(callee) (line 72) is the core: it resolves the declaring type, collects the directly matching methods, and returns them unchanged when the declaring type is neither an interface nor virtual (lines 80-84); otherwise it adds every matching method on every known implementor (lines 86-87). Overload resolution is approximated by name and parameter count, which deliberately over-approximates so no real path is missed (doc, lines 66-71).
    • MatchingMethods (line 94) requires a body and an equal parameter count, and accepts either an exact name match or a .-suffix match, because an explicit interface implementation is named Some.Ns.IFoo.Method in metadata (doc, lines 90-93).
    • ComputeAncestors (line 116) walks base types and interfaces with an explicit stack, adding each name once and descending only into definitions the scan actually contains (lines 122-138), which is both cycle-safe and tolerant of out-of-scan contracts.
  • Why it's built this way: the package's whole stance is zero compile references to the code it governs (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/MMCA.Common.Testing.Architecture.csproj:22-29), and reading Cecil metadata by name is the same discipline applied one level deeper. Building the index once per rule run, rather than resolving references per call site, is what keeps a transitive breadth-first walk over a whole repo's assemblies fast enough to sit in a normal test run.
  • Where it's used: by the three IL-walking rules that need to follow calls rather than just find them: AggregatesCascadeSoftDeleteToChildren (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.CascadeSoftDelete.cs:115) and DomainEventHandlersDoNotSave (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:93), surfaced through CascadeSoftDeleteConventionTestsBase and DomainEventHandlerSaveTestsBase.
  • Caveats / not-in-source: the type is internal, so a consumer repo cannot build its own call-graph rule on top of it; the reach is only through the public rules.

CostTagConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs:29 · Level 0 · abstract partial class

  • What it is: a cost-attribution gate over a consumer's IaC template. It parses the embedded infra/main.bicep and asserts that every Microsoft.App/containerApps resource carries a service: key on its tags: line, and that each per-service database carries the same service dimension, so one cost report splits compute and storage per service.
  • Depends on: [Fact] (xUnit), AwesomeAssertions, a source-generated [GeneratedRegex], and Assembly.GetManifestResourceStream. No first-party dependency and no IArchitectureMap: like ObservabilityConventionTestsBase it reads embedded text, not assemblies.
  • Concept introduced, the tagging-convention gate. [Rubric §17, DevOps] assesses whether the IaC template is the checked source of truth, and [Rubric §34, Architecture Governance] whether a convention is enforced rather than described; the failure closed here is the silent one, a new container app or database added without the tag, so its spend lands in an unattributed bucket. The deployed-cost model itself is the subject of the cost guide, not this base.
  • Walkthrough
    • Two abstract knobs: MinimumContainerApps (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs:36), the non-vacuity floor, and DatabaseNamePrefix (line 42), the prefix stripped from a database name to derive its tag (for example ADC_). Two virtual ones: BicepResource (line 45, default infra.main.bicep) and ResourceAssembly (line 51, defaulting to GetType().Assembly).
    • EveryContainerApp_CarriesAServiceTag (line 59) runs the ContainerAppDeclaration regex over the bicep, asserts at least MinimumContainerApps matches (line 64, so a drifted anchor fails instead of passing over nothing), then lists every match whose tags group lacks service: (line 69, an ordinal Contains). The check reads the key on that one line only, not the union shape: by convention the line is a union of commonTags with a service key, but any tags: line that names service: passes (doc comment, lines 54-56).
    • ServiceDatabases_CarryTheServiceTagOfTheOwningApp (line 83) requires a non-blank prefix, then asserts the bicep contains the exact line tags: union(commonTags, { service: toLower(replace(dbName, '<prefix>', '')) }) (lines 88-93). The database check, unlike the container-app one, does pin the union shape.
    • ReadEmbedded (line 96) throws an InvalidOperationException naming the assembly when the resource is not embedded.
    • The [GeneratedRegex] partial property ContainerAppDeclaration (lines 107-108, 2000 ms timeout) matches a container-app resource header and up to three body lines before its tags: line; a resource with no tags: line in that window still matches with an empty group, so it is reported as untagged rather than skipped (comment, lines 104-106).
  • Why it's built this way: the ResourceAssembly default is the same load-bearing choice as the other embedded-text bases: the base ships inside the framework package, so resolving against its own assembly would look for the consumer's bicep in the wrong DLL. A subclass needs only the EmbeddedResource entry for its template.
  • Where it's used: subclassed in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/CostTagConventionTests.cs), and exercised adversarially in MMCA.Common by MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/CostTagConventionTestsBaseTests.cs. Line numbers for those two files: Not determinable from this unit's source excerpt.

CrossEntityNavigationFinder

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:97 · Level 0 · private sealed class

  • What it is: a private ExpressionVisitor nested in ArchitectureRules that walks a specification's Criteria lambda and collects the names of other entity types it navigates into.
  • Depends on: System.Linq.Expressions.ExpressionVisitor, MemberExpression, PropertyInfo (BCL) and the RuleHelpers extension property InheritsAuditableEntity.
  • Concept introduced, expression-tree inspection as a fitness check. NetArchTest reasons about assembly-level references only; to catch a rule expressed inside a lambda body, the code instantiates the specification, reads its Criteria expression tree, and visits it. This backs the polyglot and database-per-service invariant (ADR-006): a Criteria that navigates to an entity in another physical data source produces an untranslatable join at runtime. [Rubric §8, Data Architecture] assesses cross-source data access discipline; this visitor is how that discipline is machine-checked.
  • Walkthrough
    • The primary constructor captures ownEntityType (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:97), and _navigated is the accumulating HashSet<string> (line 99).
    • Find(Expression body) visits the body and returns the set (lines 101-105).
    • VisitMember (line 107) resolves the accessed property's type through EntityTypeOf (line 121) and, when the result is an auditable entity other than the specification's own type, adds its name (lines 111-115).
    • EntityTypeOf (line 121) treats a direct entity property as a navigation (lines 123-126) and unwraps generic collection navigations such as ICollection<TChild> to their element type (lines 129-136).
  • Why it's built this way: filtering by a foreign-key column is engine-portable; navigating is not (notably on Cosmos, where the cross-source relationship is degraded out of the model). The finder is the enforcement half of ArchitectureRules.SpecificationsDoNotNavigateToOtherEntities (line 24), whose doc and failure message point authors at CrossSourceSpecification instead (line 15, lines 74-76). The rule is deliberately best-effort: only parameterless specifications can be instantiated and inspected (lines 37-41), and a specification whose constructor or Criteria throws on standalone evaluation is skipped rather than failing the suite (lines 49-59).
  • Where it's used: only inside that rule (line 66), which is surfaced through SpecificationConventionTestsBase.

FlagDeclaration

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FeatureFlags.cs:85 · Level 0 · private sealed record

  • What it is: one feature-flag constant flattened out of its [FeatureFlag] attribute, so the two feature-flag rules read as plain data checks (Name, Lifetime, RemoveBy, Owner, all nullable except Name, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FeatureFlags.cs:85).
  • Depends on: nothing; a private record nested in ArchitectureRules.
  • Concept: [Rubric §17, DevOps] and ADR-031 govern feature-flag lifecycle: a flag is either Permanent (a capability a host chooses) or Temporary with a parseable ISO yyyy-MM-dd RemoveBy. FeatureFlagsDeclareLifetime reads every public const string on a *Features class, reflects its [FeatureFlagAttribute] (matched by full name, line 10, since the attribute itself lives in MMCA.Common.Shared.FeatureFlags and this package takes no framework reference) through Describe (line 95), and checks the resulting FlagDeclaration for a missing declaration, a Permanent flag carrying a stray RemoveBy, or a Temporary flag with no parseable one (lines 38-57). TemporaryFeatureFlagsAreNotPastRemoveBy filters the same declarations to Temporary ones whose RemoveBy is in the past (lines 74-78): a failing build here IS the dead-toggle detector, since the branch the flag guards has outlived its rollout.
  • Where it's used: produced by Describe (line 95) and consumed by both rules in ArchitectureRules.FeatureFlags.cs, surfaced through FeatureFlagLifecycleTestsBase (line 28, line 70).

ForeignKeyDeleteFact

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DeleteBehavior.cs:88 · Level 0 · private sealed record

  • What it is: one declared foreign key's delete-behavior facts, read off a finalized EF Core model: a Description for failure messages, the DeleteBehavior name, whether the relationship IsOwnership, and the MMCA:DeleteBehaviorSource annotation value or null (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DeleteBehavior.cs:88-92).
  • Depends on: nothing; a private record nested in ArchitectureRules. Read purely by reflecting over EF's stable public metadata surface (GetEntityTypes, GetDeclaredForeignKeys, IsOwnership, DeleteBehavior, Properties, FindAnnotation), because the package carries no EF Core reference (doc, lines 97-103).
  • Concept: RestrictDeleteByDefaultConvention stamps every foreign key with MMCA:DeleteBehaviorSource = Explicit or Convention (line 6). CascadingForeignKeysAreExplicitlyOptedIn (line 34) flags a non-ownership relationship that cascades without an Explicit source, since an unwritten cascade deletes child rows below the aggregate's invariants, the soft-delete filter, and the domain events the framework raises (doc, lines 22-27); ownership foreign keys are exempt because EF requires their cascade rather than offering it (lines 29-31). ConventionForeignKeysRestrictDeletes (line 65) is the complementary half: every relationship left to the convention must restrict, so a model cannot drift onto a third, unchosen behavior. [Rubric §8, Data Architecture] assesses whether destructive data operations stay deliberate; this pair is that discipline made executable.
  • Where it's used: produced by ForeignKeyDeleteFacts (line 107) and consumed by both rules in ArchitectureRules.DeleteBehavior.cs (lines 38, 69).

Layer

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:9 · Level 0 · enum

  • What it is: the closed vocabulary of architectural layers a fitness function can reason about: Shared, Domain, Application, Infrastructure, Api, Ui, Grpc, Contracts, ServiceHost (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:11-19).
  • Depends on: nothing; a plain enum.
  • Concept introduced: the Clean Architecture layer taxonomy made into a type. The layer flow itself is taught in primer §1; here it becomes an enum the rule library keys off, so a rule that iterates layers is written once against the enum rather than hard-coded per repo. [Rubric §3, Clean Architecture] assesses whether the layering is explicit and enforced; this enum is the shared alphabet.
  • Walkthrough: the doc (lines 3-8) notes that Ui, Grpc, Contracts, and ServiceHost are optional: a repo simply omits them from its map when absent, so a rule iterating them is vacuously satisfied with no compile dependency on the missing assembly. ArchitectureMapBase.Segment translates each member to its namespace segment, and two of those translations are not the identity mapping: Api becomes "API" and ServiceHost becomes "Service" (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:105-117).
  • Where it's used: carried by LayerRef, projected by IArchitectureMap.OfLayer, and threaded through nearly every method in ArchitectureRules. Contracts is the one member no repo registers today, which is exactly why ServiceContractPurityTestsBase and ContractImplementationTestsBase are attribute-driven rather than layer-driven.

MessageBusBackpressureTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/MessageBusBackpressureTestsBase.cs:31 · Level 0 · abstract class

  • What it is: a broker-backpressure gate over each service's embedded appsettings.json. For every listed service it asserts the MessageBus section sets both PrefetchCount and ConcurrentMessageLimit to positive values, and that the prefetch window is at least the concurrency it feeds.
  • Depends on: [Fact] (xUnit), AwesomeAssertions, System.Text.Json (JsonDocument), and Assembly.GetManifestResourceStream. No first-party dependency: it reads settings text, not assemblies.
  • Concept introduced, the unbounded-default gate. A service whose MessageBus section omits either knob silently runs on the transport defaults, and a prefetch window below the concurrency starves its consumers; [Rubric §12, Performance & Scalability] is the category the failure message cites. [Rubric §34, Architecture Governance] applies because the bound becomes a checked property of every service's committed configuration instead of a review habit.
  • Walkthrough
    • Three public constants name the configuration: MessageBusSectionName (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/MessageBusBackpressureTestsBase.cs:34), PrefetchCountKey (line 37) and ConcurrentMessageLimitKey (line 40).
    • The subclass supplies Services (line 52), the short service names. ResourceAssembly (line 58) defaults to the derived type's assembly and AppSettingsResource(service) (line 63) defaults to services.{service}.appsettings.json.
    • EveryServiceAppSettings_BoundsBrokerBackpressure (line 70) asserts Services is not empty (the non-vacuity guard), runs Check over each service's embedded settings, and fails with the joined violation list.
    • Check(service, appSettingsJson) (line 93) is public static so the parse can be exercised over a fixture document directly. It parses with trailing commas and comments tolerated, returns a single "declares no MessageBus section" violation when the section is absent or not an object (lines 99-103), then reports each missing or non-positive knob (lines 109, 114) and a prefetch below the concurrency (line 119).
    • ReadInt (line 129) accepts a JSON number or a quoted number, because the configuration binder reads every value as a string (comment, line 141). TryGetProperty (line 151) matches names case-insensitively, mirroring configuration binding. ReadEmbedded (line 166) throws a message naming the assembly when the resource is missing.
  • Why it's built this way: parsing the JSON directly, rather than binding it, keeps the package free of the framework's options types, the same stance the rest of the rule library takes. Exposing Check publicly means the gate's own logic is tested with fixture strings instead of embedded files.
  • Where it's used: subclassed in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/MessageBusBackpressureTests.cs) and covered in MMCA.Common by MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/MessageBusBackpressureTestsBaseTests.cs. Line numbers for those two files: Not determinable from this unit's source excerpt.

ModuleConformanceTestsBase<TModule>

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:21 · Level 0 · abstract generic class

  • What it is: a per-module conformance gate. It asserts that one module's Name, Dependencies, and RequiresDependencies still say what the repo expects, and gives the subclass a hook to assert what RegisterDisabledStubs puts in the container.
  • Depends on: [Fact] (xUnit), AwesomeAssertions, and System.Reflection (GetInterfaces, GetProperty, BindingFlags). It has no Map: it reflects over a single module type, not over an assembly inventory. The contract it reads is IModule, matched by the full-name constant "MMCA.Common.Application.Modules.IModule" (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:24).
  • Concept introduced, gating a silent-drift contract. The three members this base checks are the whole surface ModuleLoader registers on: it resolves the topological (Kahn) registration order from Dependencies, matches ModulesSettings entries by Name, and decides between a hard start failure and stub registration from RequiresDependencies (doc, lines 4-10). Drift in any of the three throws nothing: it silently reorders registration, leaves a module permanently enabled, or swaps a real service for a disabled stub. That is exactly the failure class a fitness function exists for. [Rubric §7, Microservices Readiness] assesses whether module boundaries and their declared dependencies stay honest as modules move between hosts; [Rubric §14, Testability] assesses whether a silent contract is made loud; [Rubric §34, Architecture Governance] applies because the module contract is enforced rather than described. The module system itself is taught in Group 14.
  • Walkthrough
    • The type parameter is constrained where TModule : class, new() (line 22), so the default CreateModule() is just new() (line 68); a module without a parameterless constructor overrides that hook.
    • Three expectation members the subclass declares: the abstract ExpectedName (line 27), and the virtual ExpectedDependencies (line 30, defaulting to the empty list for a leaf module) and ExpectedRequiresDependencies (line 36, defaulting to false).
    • Four [Fact]s. Module_ShouldDeclare_ExpectedName (line 39) compares the read Name, with a because spelling out the consequence: renaming silently disables the module's configuration and drops it from other modules' dependency graphs (line 42). Module_ShouldDeclare_ExpectedDependencies (line 45) casts the value to IEnumerable<string>, asserts it is not null (the shape ModuleLoader sorts on, lines 49-50), then BeEquivalentTo the expectation (lines 52-54). Module_ShouldDeclare_ExpectedRequiresDependencies (line 58) pins the flag that turns a disabled dependency into a startup failure instead of a substituted stub (line 61). Module_ShouldRegister_ExpectedDisabledStubs (line 64) delegates to the AssertDisabledStubs hook.
    • AssertDisabledStubs (line 77) is deliberately empty by default: a module exporting no cross-module contract registers no stubs, so the fact passes vacuously and only a module that does export one overrides it (doc, lines 70-76).
    • The private ReadContractMember (line 81) is the load-bearing mechanism. It finds the IModule interface on the instance by full-name match (lines 85-87), asserts the module implements it at all (lines 89-90), resolves the named public instance property off the interface (line 92), and reads it (line 99). Reading through the interface property dispatches to the module's override when there is one and to the framework's default interface implementation when there is not (comment, lines 97-98).
  • Why it's built this way: the reflection-by-full-name approach is the same discipline the rest of the package uses, and for the same reason: it keeps the package free of the framework's transitive graph (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/MMCA.Common.Testing.Architecture.csproj:22-29). It also buys a second property the doc calls out (lines 11-18): dispatching through the interface property is what asserts a leaf module against IModule's defaults, the reach the hand-written per-repo tests needed an explicit (IModule) cast for.
  • Where it's used: subclassed once per module across the consumer repos, five times today: ADC's NotificationModuleTests (MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.API.Tests/NotificationModuleTests.cs:8, the fullest example: it expects ["Identity"], RequiresDependencies true, and overrides AssertDisabledStubs to assert the IUserNotificationExportService stub descriptor is a singleton) and IdentityModuleTests (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.API.Tests/IdentityModuleTests.cs:5), plus Store's Catalog, Sales, and Identity module tests (MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.API.Tests/CatalogModuleTests.cs:5, MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.API.Tests/SalesModuleTests.cs:8, MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.API.Tests/IdentityModuleTests.cs:5). MMCA.Common holds the adversarial coverage for the base itself in ModuleConformanceTestsBaseTests, which runs it against a leaf and a dependent fake module (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:51, :60) and proves each assertion actually fails on the drift it claims to catch through a private DriftedTests subclass (:131).

ObservabilityConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:30 · Level 0 · abstract partial class

  • What it is: an SLO alert-to-runbook pairing gate. It parses the SLO metric alerts a consumer's infra/main.bicep provisions and asserts each one keeps a matching, severity-correct triage section in that repo's infra/OPERATIONS.md, in both directions: a missing runbook section fails, and so does an orphan runbook section whose alert no longer exists. A fourth check, on by default, additionally requires the bicep to provision an operator-facing monitoring view (an Azure Monitor workbook or a portal dashboard) next to those alerts.
  • Depends on: System.Globalization, source-generated System.Text.RegularExpressions regexes, Assembly.GetManifestResourceStream, AwesomeAssertions, and [Fact] (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:1-2). No first-party dependency and, notably, no IArchitectureMap: it reads embedded text, not assemblies.
  • Concept introduced, the documentation-pairing gate. [Rubric §13, Observability & Operability] assesses whether an operator paged at 3am has usable guidance; the failure mode this base closes is the silent one, an alert added, renamed, or re-tiered while its runbook section stays behind. [Rubric §17, DevOps] applies because the source of truth is the IaC template itself, and [Rubric §34, Architecture Governance] because it makes the "every alert has a runbook" convention executable rather than aspirational.
  • Walkthrough
    • Overridable knobs: MinimumAlertSpecs (line 39, default 3), the two manifest-resource logical names BicepResource (line 42, infra.main.bicep) and RunbookResource (line 45, infra.OPERATIONS.md), and ResourceAssembly (line 51), which defaults to GetType().Assembly. A fifth knob, RequireWorkbook (line 60), defaults to true: an alert an operator cannot look at the signal behind is half a monitoring story, so every consumer gets the check without opting in. A subclass overrides it to false only for a deployment that deliberately has no operator-facing view, and says why in the override.
    • MonitoringWorkbookOrDashboard_IsProvisioned_WhenRequired (lines 67-79) returns immediately when RequireWorkbook is off; otherwise it asserts MonitoringViewResourceRegex finds at least one resource declaration of type Microsoft.Insights/workbooks or Microsoft.Portal/dashboards in the bicep (line 77), so the signals the alerts fire on also have a place an operator can look at them.
    • SloAlertSpecs_AreDiscovered_GateIsNotVacuous (lines 81-89) is the honesty guard: discovering fewer specs than the floor means the parse anchors drifted, not that alerts disappeared.
    • EveryProvisionedSloAlert_HasASeverityCorrectRunbookSection (lines 91-117) matches each alert key against the runbook headings through HeadingNamesAlert (line 101, built on the -alert- infix constant at line 32) and then asserts the heading also carries the (sev N) tag matching the bicep severity (lines 108-112).
    • EveryRunbookAlertSection_MapsToAProvisionedAlert (lines 119-131) is the reverse direction, flagging stale guidance, and reads headings through the same predicate (line 126).
    • HeadingNamesAlert (lines 156-178, protected static) is the one shared pairing predicate. It matches -alert-{key} as a whole token: an optional -v{digits} version suffix is allowed, then the heading must end, or continue with whitespace, ( or a closing backtick (regex at line 175, 2 second timeout). Any other hyphenated continuation is a different alert, so the section for server-response-time-p99 cannot answer for server-response-time, and an orphan x-extra section cannot hide behind a surviving x alert, as a plain substring test would have allowed.
    • DiscoverAlertSpecs (lines 133-154) slices the bicep between var sloAlertSpecs and resource sloAlerts (lines 137-140, each index assertion carrying its own because), runs AlertKeyRegex and AlertSeverityRegex over that block, and asserts the two match counts agree (line 145) so a changed spec shape fails loudly instead of silently mis-pairing. DiscoverRunbookAlertHeadings (line 180) keeps only the ### headings carrying the infix. The four [GeneratedRegex] partial properties sit at lines 191-201, each with a 2000 ms match timeout; the last, MonitoringViewResourceRegex (lines 200-201), is multiline and case-insensitive.
    • ReadEmbedded (lines 182-189) throws a message naming the assembly when a resource is missing.
  • Why it's built this way: the class doc (lines 23-28) records why the ResourceAssembly default is load-bearing. The base ships inside the framework package, so resolving resources against its own assembly would look for the consumer's bicep inside MMCA.Common.Testing.Architecture.dll and always throw. Defaulting to the derived type's assembly means a subclass needs no wiring beyond two EmbeddedResource entries in its csproj (the snippet is in the doc at lines 18-22).
  • Where it's used: subclassed in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7), where the single override raises MinimumAlertSpecs from the base default of three to six (:14) so the gate cannot pass vacuously on a drifted parse, and in Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7), whose one override raises the floor to five (:13); see ObservabilityConventionTests. Neither overrides RequireWorkbook, so both inherit the default true and their bicep must provision a workbook or dashboard. MMCA.Common subclasses it twice. ObservabilityConventionTestsBaseTests is a deliberate cross-assembly guard that repoints both resource names at local fixtures (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/ObservabilityConventionTestsBaseTests.cs:14, resources at :16-18) and adds one extra [Fact] pinning the ResourceAssembly default to the derived type's assembly rather than the base's (:24-29). SampleDeploymentObservabilityTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/SampleDeploymentObservabilityTests.cs:12) runs the gate over the framework's own samples/deployment bicep and runbook (:14-16) with a floor of four (:20), and restates RequireWorkbook => true explicitly (:24), since the sample is the deployment consumers copy.

PiiInventoryRow

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.PiiInventory.cs:79 · Level 0 · private sealed record

  • What it is: one parsed row of the personal-data inventory table in a repo's PRIVACY.md: Member is the Type.Property the row names (backticks trimmed) and Cells holds every cell keyed by its column header, compared case-insensitively (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.PiiInventory.cs:79).
  • Depends on: nothing beyond IReadOnlyDictionary<string, string>; a private record nested in the ArchitectureRules partial.
  • Concept: the privacy gate keeps the published policy and the code in step, and it needs the policy side as data. ParsePiiInventory (line 103) walks the Markdown line by line, accepts a table only when its header holds the Member, Purpose, Lawful basis and Retention columns, skips the separator row, and builds one PiiInventoryRow per data row (line 135). [Rubric §11, Security] and the privacy criteria assess whether declared personal data matches what the system actually marks; the record is what the orphan check compares against the live [Pii] members.
  • Walkthrough: PiiInventoryRowsMapToPiiMembers (line 61) takes the Type.Property keys of every [Pii] property in the supplied assemblies (PiiMemberKeys, line 85), then flags each parsed row whose Member is not among them (lines 66-69), failing through ArchitectureAssert.NoViolations (line 71) with the instruction to remove the row or restore the marker in the same change.
  • Where it's used: private to ArchitectureRules.PiiInventory.cs; no usage outside the defining file.

ProtoScopeKind

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:393 · Level 0 · private enum

  • What it is: the five block kinds the .proto parser tracks while walking a file: Service, Message, Enum, Oneof, and Block, the catch-all for any other braced block such as an rpc option body or an aggregate option value (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:393-402, Block at line 401).
  • Depends on: nothing; a private enum nested in ArchitectureRules.
  • Concept: the proto contract gate parses .proto files with a hand-rolled line scanner, not a protobuf compiler, so it needs an explicit notion of "which block am I inside" to decide how to render the current line, and to know which closing brace pops which scope. The kind is what DescribeMember branches on (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:283): a Block scope renders nothing, since it holds options only and nothing on the wire (lines 285-289), a Service scope renders rpcs (lines 293-304), an Enum scope renders values (lines 306-312), and anything else renders message fields (from line 314). [Rubric §9, API & Contract Design] assesses whether a published contract surface is pinned deliberately; this enum is the vocabulary that pinning is expressed in.
  • Walkthrough: TryPushScope (line 255) maps the regex-captured keyword to a member (lines 263-269), with Oneof as the discard fallback arm. Oneof exists specifically to be treated as transparent: a oneof block contributes no name segment because on the wire its members belong to the enclosing message (comment plus push, lines 271-273). Block is never produced by TryPushScope: the statement loop pushes an anonymous Block scope itself when a statement that is not a scope header ends with { (lines 161-166), so the block's closing brace pops that scope rather than the enclosing message or service.
  • Where it's used: carried by ProtoScope (line 407) and consumed by DescribeMember (line 283); both are private to the ArchitectureRules.Protos.cs partial.

RouteAuthorizationTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:22 · Level 0 · abstract class

  • What it is: an abstract test base that reflects over a UI assembly's routable Blazor pages and fails the build if a page the subclass marks as governed has lost its [Authorize(Roles = "...")] role gate.
  • Depends on: [Fact] (xUnit), AwesomeAssertions, RuleHelpers.LoadableTypes, and pure reflection over attribute instances matched by full name (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:24-25).
  • Concept introduced, the security-regression fitness function. [Rubric §11, Security] and [Rubric §25, Navigation & IA] assess whether protected routes stay protected; this base turns "the admin page must require the Organizer role" from a review checklist into a compiled assertion, so a page cannot silently regress from a role-scoped [Authorize] to a bare [Authorize] reachable by any authenticated user. It is the page-level counterpart to AnonymousEndpointTestsBase, which guards the opt-out side of the same question.
  • Walkthrough
    • The subclass supplies TargetAssembly (line 28), the exact RequiredRole (line 31), an IsGovernedPage strategy (line 40), and a MinimumGovernedPages non-vacuity floor (line 47, default 1).
    • GovernedPages_RequireDeclaredRole (line 50) collects pages that are routable, governed, and do not require the role, then asserts the offender set is empty, naming each offender's route templates (lines 52-60).
    • GovernedPageSet_IsNotEmpty (line 64) guards the guard: if a refactor moved namespaces so IsGovernedPage matched nothing, the first test would pass vacuously, so this one asserts the discovered count meets the floor (lines 68-73).
    • Detection is all reflection by attribute full name: IsRoutablePage (line 77), RequiresRole (line 86, which reads the Roles property off every AuthorizeAttribute instance, own and inherited, and passes when any one requires the exact role by ordinal match, so a bare [Authorize] or a different role alone fails; the attribute allows multiple instances and ASP.NET Core enforces all of them, so a policy-only sibling or a derived page's bare [Authorize] cannot hide the role gate, and the result no longer depends on which instance reflection returns first), the subclass helper HasAuthorizeAttribute (line 97), Routes (line 100), and the base-type walk IsOrDerivesFrom (line 108) that lets a derived authorize attribute still count.
  • Why it's built this way: matching attributes by full name keeps the shared package free of an ASP.NET Core reference (lines 16-20) while still inspecting ASP.NET attributes; the package's only dependencies are NetArchTest, AwesomeAssertions, and xUnit (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/MMCA.Common.Testing.Architecture.csproj:18-20). A reflection scan also covers future pages matching the strategy without hand-enumeration. Deliberately anonymous public pages and bare-[Authorize] self-service pages simply must not match IsGovernedPage (lines 12-15).
  • Where it's used: subclassed per module UI test project, five times today: MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/ManagementRouteAuthorizationTests.cs:19, MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/IdentityRouteAuthorizationTests.cs:16, and Store's Catalog, Sales, and Identity equivalents (MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.UI.Tests/CatalogRouteAuthorizationTests.cs:15, MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.UI.Tests/SalesRouteAuthorizationTests.cs:18, MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.UI.Tests/IdentityRouteAuthorizationTests.cs:15).

RuleHelpers

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/RuleHelpers.cs:14 · Level 0 · internal static class

  • What it is: the internal reflection toolbox the reflection-based fitness functions share: extension members for enumerating loadable types, matching suffix conventions on generic types, detecting base types and interfaces by open generic or name prefix, and classifying property setters as mutable or init-only.
  • Depends on: System.Reflection (Assembly, Type, PropertyInfo, ReflectionTypeLoadException, BindingFlags) only.
  • Concept introduced: the doc (lines 5-9) states the premise: NetArchTest cannot inspect method return types, generic-argument constraints, property accessors, or attribute usage, so those rules reflect over loaded types directly through these helpers. [Rubric §14, Testability] and [Rubric §15, Best Practices & Code Quality] apply: the reflection subtleties (partial assembly loads, init-only detection) are solved once here rather than re-derived per rule.
  • Walkthrough: the class body is three C# preview extension(T) blocks, so every helper is an extension property or method, not a classic this-parameter extension method (the syntax is taught in primer §4).
    • extension(Assembly assembly) (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/RuleHelpers.cs:16): LoadableTypes (line 19) tolerates a partially-resolvable assembly by catching ReflectionTypeLoadException and returning the types that did load through OfType<Type>(), which both filters nulls and narrows the element type (lines 27-31). ConcreteClasses (line 36) narrows that to non-abstract classes. Classes (line 45) returns every class including abstract ones (interfaces still excluded), for rules whose subject can legitimately be an abstract base a consumer derives from, such as an abstract handler base declared beside the contract it serves.
    • extension(Type type) (line 49): SimpleName (line 56) strips the generic-arity backtick so suffix conventions match generic types too. InheritsGeneric (line 67) walks the base chain and ImplementsGeneric (line 81) scans the interface set for an open generic. HasBaseTypeStartingWith (line 89) detects a framework base by full-name prefix without a compile dependency (for example FluentValidation's AbstractValidator). DeclaredPublicProperties (line 103) narrows to declared-only public instance properties. InheritsAggregateRoot (line 110) and InheritsAuditableEntity (line 117) hard-code the MMCA entity base full names (AuditableAggregateRootEntity<TIdentifierType> plus the AuditableBaseEntity<TIdentifierType> and BaseEntity<TIdentifierType> ancestors) so the entity rules can classify types cross-repo.
    • extension(PropertyInfo property) (line 123): HasPublicMutableSetter (line 130) is the immutability primitive. It reports false when there is no public setter, and false for init-only setters by looking for the System.Runtime.CompilerServices.IsExternalInit required custom modifier on the setter's return parameter (lines 140-144).
  • Why it's built this way: every helper avoids a compile-time reference to the type it detects (base types matched by string prefix), which is what lets one rule body run identically across four repos that do not reference each other. The class carries a file-level [SuppressMessage] for CA1708 (lines 10-13): with multiple extension(T) blocks in one static class the analyzer flags the compiler-generated grouping members as case-colliding, a documented false positive.
  • Where it's used: throughout the ArchitectureRules partials, inside CrossEntityNavigationFinder, and directly by RouteAuthorizationTestsBase and AnonymousEndpointTestsBase.
  • Caveats / not-in-source: the type is internal, so consumer repos cannot call these helpers directly; they reach the same behavior only through the public rules and bases. StateManagementConventionTestsBase is the one base that re-implements the tolerant type load privately rather than using this class (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:142).

LayerRef

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:31 · Level 1 · sealed record

  • What it is: an immutable record describing one assembly in a repo's architecture: its owning Module, its Layer, the compiled Assembly, and its RootNamespace (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:31).
  • Depends on: Layer and System.Reflection.Assembly.
  • Concept introduced: the atomic unit of an architecture map. Module is the empty string for framework (MMCA.Common) layers that belong to no business module (lines 22-30), which is how the same record models both a module assembly and a shared framework assembly. Every projection and every isolation rule keys off that one convention.
  • Walkthrough: a four-parameter positional sealed record (line 31), so it gets structural equality and immutability for free; its members are set once at construction by the map's DefineLayers.
  • Where it's used: ArchitectureMapBase stores a lazy IReadOnlyList<LayerRef> and derives every projection from it; its Framework and Module factory helpers are what build these. The namespace-cycle rule takes a LayerRef directly, using its RootNamespace to decide which namespace node a type belongs to (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:84); the [ServiceContract] purity rule iterates map.Layers directly rather than one projection (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:40); and the IL-reading rules resolve each ref's on-disk Assembly.Location through ScannableAssemblyLocations (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:88).

ProtoScope

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:407 · Level 1 · private sealed record

  • What it is: one open block in the .proto parser's scope stack: a ProtoScopeKind plus the block's name, empty for a transparent oneof (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:404-407).
  • Depends on: ProtoScopeKind. Private to the ArchitectureRules.Protos.cs partial.
  • Concept: the two-field record is what makes nested messages and enums render under their qualified name without a real grammar. DescribeProtoFile keeps a Stack<ProtoScope> (line 112), pushes on a scope header and pops on a line starting with a closing brace (lines 124-132), and QualifiedName (line 234) reverses the stack and joins the non-empty names under the file's package.
  • Walkthrough: a two-parameter positional sealed record (line 297), constructed only in TryPushScope (line 184). Because the Oneof arm passes string.Empty as the name, the length filter in QualifiedName (line 238) drops it, which is exactly the wire semantics: a oneof member is a field of the enclosing message.
  • Where it's used: DescribeProtoFile (line 109), TryPushScope (line 166), DescribeMember (line 194), and QualifiedName (line 234), all feeding ProtoContractTestsBase through BuildProtoContract (line 86).

SpanishAccentTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SpanishAccentTestsBase.cs:27 · Level 1 · abstract class

  • What it is: a localization-quality fitness function: no *.es.resx file under a repo's resource root may carry a common Spanish word with its accent or n-tilde missing (for example the unaccented sesion or contrasena spellings of the words that need an accent or an n-tilde), so a new Spanish UI string cannot silently regress.
  • Depends on: ArchitectureAssert (NoViolations, line 123), ArchitectureMapBase.FindRepoRoot (only in the subclass, to build the root), System.Xml.Linq (XDocument), System.Text.RegularExpressions, AwesomeAssertions.
  • Concept introduced, a text-quality gate over resource files. Like FormsConventionTestsBase it reads files rather than assemblies, but here the subject is translated UI text. A word list of unaccented spellings (DefaultUnaccentedWords, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SpanishAccentTestsBase.cs:33-40, 35 words including sesion, codigo, contrasena, numero, pagina, aqui, mas) is compiled into one case-insensitive whole-word regex with a one-second match timeout (lines 83-87), and every data/value pair in every *.es.resx is matched against it. Words that are correct both with and without the accent depending on meaning are deliberately left off the list: the class doc names esta, solo and aun ("even" unaccented, "still" accented) (lines 20-24).
  • Walkthrough: the subclass supplies one abstract member and may override three virtual ones.
    • ResourceRoot (line 46) is the folder scanned recursively, normally the repo's Source folder under ArchitectureMapBase.FindRepoRoot("<Repo>.slnx").
    • UnaccentedWords (line 52) defaults to the framework list; a subclass extends it with [.. base.UnaccentedWords, "extra"] for domain vocabulary.
    • AllowedEntries (line 59, default empty) lists intentional exceptions as {path relative to the root, forward slashes}:{resource key}, exactly as a failure prints them; the doc comment frames an exception as a decision, not a way to make the gate pass.
    • MinimumResourceFileCount (line 65, default 1) is the anti-vacuity floor: a wrong root that finds no *.es.resx fails instead of passing with nothing checked (lines 79-81).
    • The single [Fact] Spanish_resources_keep_their_accents (lines 67-68) asserts the root is set and exists (lines 70-71), enumerates and ordinally sorts the files while skipping build output through IsBuildOutput (lines 73-77, helper at line 130: any path segment bin, obj, node_modules or .git), skips non-string entries that carry a type or mimetype attribute (lines 99-102) and any AllowedEntries hit (lines 107-110), and collects file:key: words offenders reported through ArchitectureAssert.NoViolations (lines 118, 123-126).
  • Why it's built this way: the offender line is formatted to be pasted straight into AllowedEntries, so the fix path (correct the value, or record the exception) is in the failure itself. Keeping the word list short and the matcher whole-word keeps false positives rare enough that exceptions stay exceptional.
  • Where it's used: subclassed with only ResourceRoot overridden in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/SpanishAccentTests.cs:11, root at :13-14, so the framework's own UI strings are held to the rule every consumer inherits), in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/SpanishAccentTests.cs:9, root at :11-12) and in Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/SpanishAccentTests.cs:8, root at :10-11). MMCA.Common also pins the base's own behavior against fixture resources in SpanishAccentFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Localization/SpanishAccentFitnessTests.cs:12), through a private subclass that points ResourceRoot at a fixture tree (:37-39).

IArchitectureMap

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:39 · Level 2 · interface

  • What it is: the single per-repo abstraction every architecture fitness function keys off. Each repo supplies one implementation declaring its layer and module assemblies; the shared rule library and abstract test bases consume only this interface, so a rule is written once and runs identically across every repo that supplies a map (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:33-38, whose doc names MMCA.Common, MMCA.Store and MMCA.ADC; MMCA.Helpdesk supplies a fourth map).
  • Depends on: LayerRef, Layer, System.Reflection.Assembly.
  • Concept introduced, the architecture map as the fitness-function extension point. This is a classic Dependency Inversion: the rules depend on an abstraction (the map), and each repo provides the concrete inventory of its assemblies. [Rubric §1, SOLID] (DIP) and [Rubric §7, Microservices Readiness] apply: the map also models the per-module layers a would-be extracted service owns, so the isolation rules can check module boundaries the same way in any repo.
  • Walkthrough: the interface exposes identity (RepoToken line 42, ModuleNames line 45), the raw Layers inventory (line 48), and the projections the rules lean on: OfLayer (all assemblies of a kind, line 51), the per-module ModuleDomain/ModuleApplication/ModuleShared (lines 54-60), Infrastructure()/Api() across framework plus modules (lines 63-66), the lookups For(module, layer) (line 69) and ModuleOf(assembly) (line 72), namespace derivation RootNamespace(module, layer) (line 75), and OtherModuleNamespaces (line 81), which returns the same-layer namespaces of every other module (the forbidden targets for a module-isolation rule, empty for framework layers and single-module repos).
  • Why it's built this way: funneling every rule through one interface is what removed the drifting per-repo copies of the architecture-test suite (ADR-015); add a repo and you write one map, not a new rule set.
  • Where it's used: held as the protected abstract IArchitectureMap Map on nearly every *TestsBase in this group and passed to nearly every method of ArchitectureRules. ArchitectureMapBase is the reusable partial implementation, and CommonArchitectureMap / AdcArchitectureMap are two of the four concrete maps (Store and Helpdesk supply the others).

ArchitectureMapBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:11 · Level 3 · abstract class

  • What it is: the reusable base implementation of IArchitectureMap: a repo supplies only RepoToken and a DefineLayers() declaration, and every projection, namespace derivation, and module-isolation target computation is derived here (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:3-10).
  • Depends on: IArchitectureMap, LayerRef, Layer, System.Lazy, and System.IO (for FindRepoRoot).
  • Concept introduced, the template-method shape for a repo map: the base fixes the algorithm and the subclass fills two holes. It also centralizes every namespace and assembly string in one file, which the doc (lines 8-9) notes fixes Ubuntu CI case-sensitivity in one place.
  • Walkthrough
    • The constructor wraps DefineLayers() in a Lazy<IReadOnlyList<LayerRef>> (lines 15-16) so the assembly list materializes once, and Layers reads that value (line 25).
    • ModuleNames (line 28) filters out framework refs, then distinct-orders the module names ordinally.
    • OfLayer (line 35), ModuleDomain/ModuleApplication/ModuleShared (lines 39-45, via the private ModuleLayer, line 101), Infrastructure (line 48), and Api (line 51) are one-line LINQ projections over Layers.
    • For (line 54) and ModuleOf (line 59) are the lookups, both ordinal-comparison based.
    • RootNamespace (line 63) branches on module: framework layers become the MMCA.Common prefix plus the segment, module layers the repo token plus the module plus the segment (lines 64-66). OtherModuleNamespaces (line 69) maps every other module through it.
    • The static FindRepoRoot(solutionFileName) (line 79) walks up from AppContext.BaseDirectory to the directory containing the named .slnx, so doc and config consistency tests can read committed files regardless of the runner's working directory, throwing a clear InvalidOperationException when not found (lines 89-90).
    • The protected static Framework(...) (line 94) and protected Module(...) (line 98) factory helpers build LayerRefs with the right namespace, and the internal Segment (line 105) maps each Layer to its namespace token, throwing ArgumentOutOfRangeException on an unmapped member (line 116).
  • Why it's built this way: a per-repo map stays a flat declaration of assemblies (the two abstract members), and everything derivable is derived, so the maps cannot drift in how they compute namespaces.
  • Where it's used: each repo's concrete map subclasses this: CommonArchitectureMap (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommonArchitectureMap.cs:15), AdcArchitectureMap (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/AdcArchitectureMap.cs:14), StoreArchitectureMap (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/StoreArchitectureMap.cs:8), and HelpdeskArchitectureMap (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/HelpdeskArchitectureMap.cs:8), plus the private SpecTestMap fixture inside MMCA.Common's SpecificationFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:40). FindRepoRoot is called directly by every file-reading base and rule: DataResidencyTestsBase, FormsConventionTestsBase, FrameworkVersionConsistencyTestsBase, LocalizedTextConventionTestsBase, RawQueryableConventionTestsBase, StateManagementConventionTestsBase, UIArchitectureConventionTestsBase, the SortableColumnConventionTestsBase subclasses that build their own markup roots, and the proto rule ProtoContractsMatchFrozenList (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:46).

ConstructorDependencyCountTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:29 · Level 3 · abstract class

  • What it is: a single-responsibility-ceiling fitness function applied to three injection points a module composes: Application-layer *Service facades, API controllers, and CQRS command/query handlers. It fails the build if any concrete class in a population has a constructor with more than that population's accepted dependency count.
  • Depends on: IArchitectureMap (via Map.ModuleApplication() and Map.Api() in the default populations), [Fact], AwesomeAssertions, and reflection over constructors (System.Reflection.ConstructorInfo) and interfaces.
  • Concept introduced, quantifying the SRP smell across three populations. [Rubric §1, SOLID] assesses single-responsibility discipline; a ballooning constructor-dependency list is the canonical smell wherever it appears, and this base turns a previously implicit judgement call into an enforced, per-population ceiling so the next class cannot silently grow past it (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:5-28). Each ceiling is a ratchet, not a budget: a consumer sets it at its current high-water mark, raises it only as a recorded deliberate decision, and lowers it the moment remediation makes a lower number true (lines 14-19, enforced in both directions by the *_DoNotExceedConstructorDependencyCeiling and *_ConstructorDependencyCeilingIsTight facts). What gets measured is itself an extension point: the three populations and the constructors measured on each type are virtual, with defaults that reproduce the module scan, so a consumer that overrides nothing scans exactly the module populations (lines 22-26).
  • Walkthrough: the subclass supplies Map (line 37), plus one abstract ceiling per population: MaxConstructorDependencies for Application services (line 43), MaxControllerConstructorDependencies for API controllers (line 50), and MaxHandlerConstructorDependencies for CQRS handlers (line 58). Four virtual members say what is measured:
    • ScannedServices (line 64) defaults to every concrete class whose name ends in Service in Map.ModuleApplication() (lines 65-68).
    • ScannedControllers (line 74) and ScannedHandlers (line 81) default to the shared private Scan (line 240, flat-maps ConcreteClasses, excludes nested and compiler-generated types) over Map.Api() and Map.ModuleApplication(), filtered by IsController (line 257, matches the full name Microsoft.AspNetCore.Mvc.ControllerBase via HasBaseTypeStartingWith) or IsCommandOrQueryHandler (line 260, matches a generic interface whose definition's full name starts with the ICommandHandler/IQueryHandler prefix constants, lines 34-35).
    • MeasuredConstructors(Type) (line 89) defaults to type.GetConstructors(), the public instance constructors (line 92); the widest measured constructor is the type's dependency count, computed by the private WidestConstructor (line 230).
    • Three ceiling facts share one shape: materialize the population, assert non-vacuity, assert no offender. ApplicationServices_DoNotExceedConstructorDependencyCeiling (line 96), Controllers_DoNotExceedConstructorDependencyCeiling (line 119) and Handlers_DoNotExceedConstructorDependencyCeiling (line 143) each assert non-vacuity first (lines 100-101, 123-124, 147-148) and report offenders by name and count through the shared instance helper Offenders (line 247), which measures each type with WidestConstructor (line 252) and so honors the overridable MeasuredConstructors.
    • Three tightness facts enforce the other direction of the ratchet: ApplicationServices_ConstructorDependencyCeilingIsTight (line 168), Controllers_ConstructorDependencyCeilingIsTight (line 181) and Handlers_ConstructorDependencyCeilingIsTight (line 194) each delegate to the private AssertCeilingIsTight (line 207). It asserts the population is non-empty, finds the observed maximum and the classes sitting at it, and fails when the declared ceiling exceeds that maximum; the message names the population, the ceiling property, its value, the observed maximum and the widest classes, so the fix (lower the ceiling to the observed number) is in the failure itself.
    • Controller and handler matching is by full type-name string, not a compiled reference to Microsoft.AspNetCore.Mvc or MMCA.Common.Application, so the rule library keeps zero compile dependency on either (lines 31-35).
  • Why it's built this way: splitting the ceiling by population lets each stay honest: a controller is a thin translation layer over handlers, so a long injection list there means it serves more than one resource, while a handler serves exactly one use case and is the population where a long list is least defensible (lines 112-117, 136-141). The tightness facts exist because a ceiling left above the observed maximum is a budget, not a ratchet: the next class can grow into the slack without anything failing (lines 161-166). It overlaps the arity check in HandlerConventionTestsBase but scopes specifically to these three facades and lets a repo pin an exact number per population rather than take a shared default. Making the populations and the measured constructors virtual is what lets a repo without business modules adopt the same gate: per the summary doc, MMCA.Common overrides them to scan its framework assemblies (lines 22-23).
  • Where it's used: MMCA.Common subclasses it over its own framework in FrameworkConstructorDependencyTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencies/FrameworkConstructorDependencyTests.cs:25): the populations are every top-level class in the Application and Infrastructure assemblies, ControllerBase-derived classes in the API assembly, and handler implementations in the Application assembly, abstract classes included (:42-49), and MeasuredConstructors widens to public and protected constructors because the widest framework classes are abstract bases whose only constructors are protected (:51-53, rationale :8-12). Its ceilings are 7 for services (:33), 5 for controllers (:37, held by CrudEntityControllerBase, InboxController and UserAccountAuthControllerBase) and 7 for handlers (:40, held by ResetPasswordHandlerBase). The base's own behavior is pinned by ConstructorDependencyCountTestsBaseTests. It is also subclassed in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/ConstructorDependencyCountTests.cs:20) with MaxConstructorDependencies at 9 (:24, held by AuthenticationService, history at :9-18), MaxControllerConstructorDependencies at 10 (:52, held by SessionsController, history at :26-51), and MaxHandlerConstructorDependencies at 7 (:61, held jointly by ResetPasswordHandler and SubmitQuestionHandler, rationale at :54-60). Store subclasses it with a service ceiling of 8, one below ADC because Store is local-credential only and has no external-login email verifier (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/ConstructorDependencyCountTests.cs:26, ceiling at :24, rationale at :8-18). MMCA.Helpdesk deliberately does not adopt it and records why in a comment: its Application layer is handlers-only, and the base's anti-vacuity guard fails when it finds no *Service at all (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:177-179).

ArchitectureRules

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Api/ArchitectureRules.Controllers.cs:3 · Level 4 · static partial class

  • What it is: the reusable rule library: one large static partial class split across forty-four ArchitectureRules.*.cs files, whose methods each assert one architectural invariant across every applicable assembly a map declares. A repo's test classes reduce to a sealed subclass of the matching *TestsBase supplying its own map.
  • Depends on: IArchitectureMap, Layer, ArchitectureAssert, RuleHelpers, CallGraphIndex, NetArchTest (Types.InAssembly(...)), Mono.Cecil plus Mono.Cecil.Cil for the IL-reading rules, System.Xml.Linq for the props-file and .resx rules, source-generated System.Text.RegularExpressions for the proto and localized-text parsers, and, for the specification rule, System.Linq.Expressions plus CrossEntityNavigationFinder.
  • Concept introduced, the rule as a parameterized function. Each method takes an IArchitectureMap (or, for the file-reading rules, a path and an allowlist) and does its own loop, so the *TestsBase classes are thin [Fact] shells that delegate. The partial is organized by concern across the files ArchitectureRules.{Ai, Anonymization, CancellationTokens, CascadeSoftDelete, ClockReads, CloudVendors, CommandValidators, Contracts, Controllers, Cycles, DeleteBehavior, DomainEventHandlerSaves, DomainThrows, Entities, ErrorCatalog, Events, FeatureFlags, FolderWidth, ForwardedJwt, Governance, HandlerResults, Handlers, Idempotency, Immutability, InlineStyles, IntegrationEventPurity, Layers, LifetimeTokens, Localization, LocalizedText, Markup, Modules, Naming, PiiInventory, Protos, Purity, QueryHandlerRepositories, Slices, SoftDelete, Specifications, StronglyTypedIds, Substitutability, Transport, Upcasters}.cs, filed under seven concern folders (Rules/Api, Rules/Contracts, Rules/Cqrs, Rules/Domain, Rules/Governance, Rules/Layering, Rules/Ui), so the rule library follows the same feature-by-folder convention it enforces. [Rubric §3, Clean Architecture], [Rubric §4, DDD], [Rubric §7, Microservices Readiness], and [Rubric §34, Architecture Governance] all apply: this is where the codebase's structural decisions become executable assertions.
  • Walkthrough: six representative shapes, each the template for a family of rules.
    • NetArchTest shape, ControllersDoNotDependOnInfrastructure (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Api/ArchitectureRules.Controllers.cs:6): loops the map's per-module API layer refs, computes the forbidden Infrastructure namespace via map.RootNamespace(...), runs a Types.InAssembly(...) chain filtered to the Controller suffix, and reports through ArchitectureAssert.NoViolations(result, ...). ControllersDoNotDependOnEntityFrameworkCore (line 22) is the same loop against the literal Microsoft.EntityFrameworkCore namespace, keeping data access out of the API layer.
    • Layer-flow shape, ArchitectureRules.Layers.cs: one public method per forbidden edge, DomainDoesNotDependOnApplication (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Layers.cs:12) through UiDoesNotDependOnInfrastructure (line 60), all delegating to the private LayerNotDependOnLayer (line 139), which loops every assembly of the from layer and asserts no dependency on the to layer's namespace. Two non-vacuity rules sit alongside them, LayerMapDeclaresLayers (line 72) and the two ModulesDeclareLayers overloads (lines 89 and 113).
    • Reflection shape, ControllersAreSealed (ArchitectureRules.Controllers.cs:37): flat-maps map.Api() to each assembly's ConcreteClasses (the RuleHelpers extension property), filters non-sealed controllers via the private IsController (line 70, which matches on the Controller suffix or an MVC base type), and asserts the string offender list is empty. ControllersInheritApiControllerBase (line 54) is the same shape with a caller-supplied exempt set, accepting either ApiControllerBase or EntityControllerBase<TEntity, TEntityDTO, TIdentifierType> as the base.
    • Custom-rule shape, ServiceContractsDoNotDependOnServiceInternals (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:32): the one rule that hands NetArchTest a MeetCustomRule predicate (line 44) reading Cecil metadata directly, because the selector is an attribute, not a name or a namespace.
    • IL-reading shape, the rules that must see inside a method body, because neither NetArchTest nor reflection can: HardDeletesOnlyInAllowedTypes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:62), DomainThrowsOnlyArgumentGuards (ArchitectureRules.DomainThrows.cs:71), the two error-catalog rules ErrorCodesAreUnique and ErrorCodesUseAnAllowedPrefix (ArchitectureRules.ErrorCatalog.cs:63, :103), AggregatesCascadeSoftDeleteToChildren (ArchitectureRules.CascadeSoftDelete.cs:100), the transitive DomainEventHandlersDoNotSave (ArchitectureRules.DomainEventHandlerSaves.cs:76), DomainAndApplicationDoNotReadTheClock (ArchitectureRules.ClockReads.cs:59), and QueryHandlersUseReadRepositories (ArchitectureRules.QueryHandlerRepositories.cs:37). They open each mapped assembly with Mono.Cecil.ModuleDefinition.ReadModule (for example ArchitectureRules.SoftDelete.cs:73 over ScannableAssemblyLocations, line 88; ArchitectureRules.ErrorCatalog.cs:170 over its own ErrorCatalogAssemblyLocations, line 185) and match callees, base types and thrown exception types by full NAME, which is how the package keeps its zero-reference stance one level deeper than the reflection rules do.
    • Graph shape, NamespacesHaveNoDependencyCycles (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:45): builds a namespace-to-namespace graph per layer assembly from signature-level references (BuildNamespaceGraph, line 84), finds every strongly connected component (FindNamespaceCycles, line 253), and reports the shortest cycle path plus any extra members of the component. At 379 lines it is the second-largest rule file in the library, after the 438-line ArchitectureRules.Protos.cs.
  • Why it's built this way: ADR-015 records the intent: the rule bodies live once here, and each repo's architecture test project is a set of sealed subclasses supplying its map, so all four repos enforce identical rules. The compile-time MMCA.Common/Source/Build/MMCA.Common.LayerEnforcement.targets guards the same layer flow at build time as a second, faster gate.
  • Where it's used: every *TestsBase in this group calls into it; those [Fact] methods are its public surface. A handful of rules are also called directly from MMCA.Common's own fitness self-tests, for example BuildProtoContract/AssertProtoContract from ProtoContractFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ProtoContractFitnessTests.cs:14) and the cascade rule from CascadeSoftDeleteFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:17).
  • Caveats / not-in-source: the full method roster spans forty-four partials; only the entry file and representative methods are cited here. The authoritative fitness-method and base-class counts are generated into MMCA.Common/FACTS.md:45-50 and CI-gated, so read them there rather than counting by hand.

DataResidencyTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DataResidencyTestsBase.cs:14 · Level 4 · abstract class

  • What it is: a compliance-drift fitness function: the data-residency statement in a repo's PRIVACY.md must match the region where personal data is actually provisioned, and known-stale region claims must not reappear.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, System.IO (File.ReadAllText), AwesomeAssertions.
  • Concept introduced, a document-versus-infrastructure consistency gate. [Rubric §30, Compliance / Privacy / Data Governance] assesses whether privacy claims track reality; this base fails the build if either the deployed region or the privacy policy changes without the other, closing the gap where a policy once claimed a region the data never lived in (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DataResidencyTestsBase.cs:3-13).
  • Walkthrough: the subclass supplies Map (line 16), the optional ForbiddenResidencyClaims list (line 24), and implements ExtractDeployedRegion(repoRoot) (line 87) against its own source of truth (the doc cites ADC parsing the SQL region default out of deploy.yml and Store parsing infra/DISASTER-RECOVERY.md). The single [Fact] PrivacyPolicy_DataStorageRegion_MatchesDeployedRegion (line 27) locates the repo root via FindRepoRoot on the map's repo token (line 29), asserts the extracted region is non-blank (lines 32-33), reads PRIVACY.md (line 35), then asserts the policy states the region (line 37) and none of the forbidden claims (lines 40-44), both through ContainsRegionClaim.
    • ContainsRegionClaim(policyText, regionClaim) (line 57) is a protected static whole-token matcher. It normalizes both sides with Normalize (line 93: strips whitespace and upper-cases with ToUpperInvariant, per CA1308, so "West US 2" matches the "westus2" token), then walks every occurrence and accepts one only when the next character is not a digit and the text before it does not end with one of the DirectionalPrefixes NORTH, SOUTH, EAST, WEST, CENTRAL (lines 64-79, prefixes at line 89). So westus does not match "West US 2" and centralus does not match "South Central US" (lines 47-53), which keeps a forbidden region that is a prefix of the real one from failing the gate, and a stated region that is only a prefix of another from passing it.
  • Where it's used: subclassed in Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/DataResidencyTests.cs:12) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/DataResidencyTests.cs:12); the matcher itself is exercised by DataResidencyTestsBaseTests in MMCA.Common (see per-project test rollup). Module-less MMCA.Common has no deployed region, and Helpdesk records it as N/A for reduced-scope reasons (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:180).

FormsConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:27 · Level 4 · abstract class

  • What it is: a UX-safety fitness function: every admin *Create.razor form under Source/Modules must keep its unsaved-changes guard, dirty tracking, model-sourced validation and per-form error summary, and the Identity Profile page must keep its password validation, so those protections cannot silently regress.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, System.IO file enumeration, System.Text.StringBuilder, AwesomeAssertions.
  • Concept introduced, the markup-scanning fitness function (it reads .razor text, not assemblies). [Rubric §24, Forms / Validation / UX Safety] assesses whether navigate-away data loss and missing validation are prevented; the base checks for ten literal markers in the default RequiredMarkers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:70-82): the UnsavedChangesGuard component, an IsDirtyAccessor binding (through the live accessor, which pre-empts the one-render stale-IsDirty lag, a §19 concern), an _isDirty field, a MudForm element, then the model-bridge set: Model="_model", Validation="@_validate", an <ErrorSummary fed the failed save (Result="_saveResult") and the live form errors (Messages="_form?.Errors"), and the localized Validation.CorrectFollowing heading. The old per-field Required="true" and RequiredError markers are deliberately gone from the default list: a form still spelling them on every field would be the regression, not the convention (doc at lines 62-69). The marker list is virtual, so a repo can narrow or extend it.
  • Walkthrough: the subclass supplies Map (line 46) and optionally a higher MinimumCreateForms count (line 53, default 1) or a different ModulesRoot (line 59, virtual, default Source/Modules under ArchitectureMapBase.FindRepoRoot on the map's repo token, overridden to point the facts at a fixture tree). Three facts run over a shared private DiscoverCreateForms (line 233), which enumerates *Create.razor under ModulesRoot excluding obj and bin, and asserts the discovered count meets the floor.
    • AdminCreateForms_KeepUnsavedChangesGuardAndValidation (line 148) reads each form and records a violation naming each missing RequiredMarkers entry, found through the public static MissingMarkers (line 138), which strips Razor and HTML comments first so a commented-out guard cannot stand in for one that renders.
    • AdminCreateForms_ReadRequirednessOffTheirModel (line 180) checks the requiredness half of the bridge: the form's markup plus any sibling *FormFields.razor component in the same folder must contain ModelValidation.IsRequired, so extracting the field block does not weaken the scan (lines 7-26 describe the [Required] attribute on the form model being read back into the markup). Each file has its Razor and HTML comments stripped BEFORE it joins the scan (lines 187 and 192), so a commented-out ModelValidation.IsRequired does not count and an unterminated comment in one file cannot swallow the next, the same treatment the marker and password checks get (doc at lines 174-177).
    • ProfileForm_KeepsErrorSummaryAndPasswordValidation (line 212) reads ProfileFormPath (line 89; defaults to Source/Modules/Identity/{RepoToken}.Identity.UI/Pages/Users/Profile/Profile.razor, null only for a repo with no Profile page) and passes when RendersChangePasswordCard (line 257) finds an uncommented <ChangePasswordCard tag (ChangePasswordCardTag, line 30; the shared private StripAllComments, line 263, removes Razor and HTML comments first); otherwise the inline form must satisfy MissingPasswordFormMarkers (line 109, which also strips comments before matching, so commented-out markers and attributes do not count): the error summary, the live _passwordForm?.Errors feed, ValidateNewPassword and ValidateConfirmPassword wiring (PasswordFormMarkers, line 38), plus at least three Required="true" and three RequiredError occurrences. That method is public static so MMCA.Common can hold the shared ChangePasswordCard to the same list.
  • Why it's built this way: self-service forms with no navigate-away step (for example a single-section Profile password or delete form) carry no guard by design and simply must not match the *Create.razor glob; the Profile page gets its own fact instead (lines 22-24).
  • Where it's used: subclassed in the repos with admin create forms, Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/FormsConventionTests.cs:13) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/FormsConventionTests.cs:13). The base's marker helpers are pinned by FormsConventionTestsBaseTests in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/FormsConventionTestsBaseTests.cs), and the comment-stripping requiredness fact by FormsConventionRequirednessFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/FormsConventionRequirednessFitnessTests.cs:12), whose private subclass overrides ModulesRoot with a RazorFixtures/Forms tree (:38-43) and invokes the fact directly against a commented-out marker (:23) and a live marker in a sibling field component (:31).

FrameworkVersionConsistencyTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FrameworkVersionConsistencyTestsBase.cs:13 · Level 4 · abstract class

  • What it is: an evolvability and drift fitness function that makes ADR-016 executable: all MMCA.Common.* packages in a consumer's Directory.Packages.props must be pinned to one version, so a partial sweep is caught at CI time.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, System.Xml.Linq (XDocument), AwesomeAssertions.
  • Concept introduced, enforcing the lockstep release policy. [Rubric §15, Best Practices & Code Quality] and [Rubric §32, Dependency & Supply-Chain] assess coordinated versioning; the framework releases in lockstep with no phased rollout (ADR-016), and this gate fails if any MMCA.Common.* entry diverges (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FrameworkVersionConsistencyTestsBase.cs:3-11).
  • Walkthrough: the subclass supplies Map (line 15) and optionally MinimumCommonPackageCount (line 22, default 13). AllMmcaCommonPackages_ArePinnedToOneVersion (line 25) loads Directory.Packages.props from the repo root (lines 27-30), selects every PackageVersion element whose Include starts with the MMCA.Common. prefix (lines 31-41), asserts the count meets the floor (lines 43-44), asserts none has an empty version (lines 46-48), and asserts the distinct-version count is exactly one, listing what it found (lines 50-56).
  • Why it's built this way: MMCA.Common itself does not subclass this, because it declares no MMCA.Common.* pins; only consumers do (lines 10-11). The default floor of 13 is deliberately loose, and the doc points at MMCA.Common/FACTS.md for the authoritative released-package count (lines 17-21; that count is 17 today, generated at MMCA.Common/FACTS.md:19).
  • Where it's used: subclassed in Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/FrameworkVersionConsistencyTests.cs:9), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/FrameworkVersionConsistencyTests.cs:9), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:47). None of the three overrides the floor today, so all three run against the shared default of 13.

RawQueryableConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawQueryableConventionTestsBase.cs:30 · Level 4 · abstract partial class

  • What it is: an opt-in extraction-readiness gate: Application-layer code must not use the repository's raw IQueryable surfaces (Table, TableNoTracking, TableNoTrackingSingleQuery, TableNoTrackingSplitQuery on IReadRepository<TEntity, TIdentifierType>), because a handler written against a raw queryable is EF-coupled and its query shape cannot cross a gRPC boundary.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, ArchitectureAssert, System.IO enumeration, and a source-generated Regex (System.Text.RegularExpressions).
  • Concept introduced, the honest textual scan (and its stated limits). The doc is unusually candid about the tradeoff (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawQueryableConventionTestsBase.cs:13-23): NetArchTest and plain reflection cannot see member usage inside method bodies, and this base deliberately carries no IL or Roslyn dependency, so the rule reads .cs text instead. It cannot see through variable indirection (an interface alias re-exposing the queryable is missed), and because it skips only whole-line comments, a match inside a string literal or trailing comment is a rare false positive. [Rubric §7, Microservices Readiness] is the invariant being protected; [Rubric §8, Data Architecture] and [Rubric §15, Best Practices & Code Quality] apply to the handler style it pushes toward (focused repository methods, readers, queriers, specifications).
  • Walkthrough
    • AllowedFiles (line 38) is the adoption ratchet: a repo with existing violations subclasses, runs once, and moves the reported file names in, so new files stay clean while the list shrinks (lines 24-28).
    • ApplicationSourceDirectories() (line 45) defaults to locating each declared module's Application project directory under the repo's Source/ tree by project name (lines 47-57), and is virtual for a custom layout.
    • The [Fact] ApplicationLayer_DoesNotUseRawQueryableSurfaces (line 61) first asserts the directory list is non-empty, with a message telling the author to override the directory hook (lines 65-66), then enumerates every .cs file, skipping obj, bin, and allowlisted file names (lines 71-79), and reports through ArchitectureAssert.NoViolations (line 84).
    • ScanFile (line 88) yields a file-name, line-number and trimmed-line triple for every non-comment line matching RawQueryableAccessRegex (line 103), a [GeneratedRegex] partial property matching the Table and TableNoTracking member-access family with a 2000 ms match timeout.
  • Why it's built this way: making the offender message carry the file and line (lines 96-98) is what makes a textual gate actionable; combined with the allowlist ratchet it can be adopted in a repo that is not yet clean. Note the contrast with the IL-reading rules in ArchitectureRules, which do read method bodies: a member read on a property is what a text scan is cheapest at, while a call is what Cecil sees best.
  • Where it's used: subclassed opt-in in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:13), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:9), and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:11). Because it is a ratchet, a repo's AllowedFiles override is the record of its remaining debt.

RawSqlConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawSqlConventionTestsBase.cs:31 · Level 4 · abstract partial class

  • What it is: an opt-in extraction-readiness gate: module code must reach SQL only through the parameterizing overloads, never FromSqlRaw, SqlQueryRaw, ExecuteSqlRaw, or ExecuteSqlRawAsync, whose plain-string parameter lets an interpolated value be concatenated straight into the statement instead of being parameterized.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, ArchitectureAssert, System.IO file enumeration, and a source-generated Regex (System.Text.RegularExpressions).
  • Concept introduced, the same textual-scan shape as RawQueryableConventionTestsBase, aimed at a SQL-injection risk rather than an EF-coupling one. [Rubric §11, Security] assesses injection defenses directly; [Rubric §7, Microservices Readiness] and [Rubric §8, Data Architecture] apply to the parameterized alternatives it pushes toward, FromSql/SqlQuery/ExecuteSql or the framework's IRawSqlQueryExecutor, whose FormattableString parameters cannot be concatenated (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawSqlConventionTestsBase.cs:90-94).
  • Walkthrough
    • AllowedFiles (line 39) is the adoption ratchet, empty by default: a repo with existing raw-SQL call sites lists the file names here.
    • ScannedSourceDirectories() (line 48) defaults to every project directory the map attributes to a business module, located by project name under the repo's Source/ tree via FindRepoRoot (lines 50-58), and is virtual for a custom layout or to scan a framework project too.
    • The [Fact] ModuleCode_UsesParameterizedSqlOnly (line 65) first asserts the scanned-directory list is non-empty, with a message pointing at the override hook (lines 70-72), then enumerates every .cs file, skipping obj, bin, and allowlisted file names (lines 75-86), and reports through ArchitectureAssert.NoViolations (lines 90-94).
    • ScanFile (line 100) yields a file-name, line-number and trimmed-line triple for every non-comment line matching RawSqlAccessRegex (lines 105-110), a [GeneratedRegex] partial property matching the FromSqlRaw/SqlQueryRaw/ExecuteSqlRaw/ExecuteSqlRawAsync member-access family with a 2000 ms match timeout (lines 115-116).
  • Why it's built this way: mirrors RawQueryableConventionTestsBase's honest-textual-scan tradeoff (no IL or Roslyn dependency, cannot see through variable indirection), but where that base protects extraction readiness this one protects against injection: the offender message names the file and line so the finding is actionable, and the allowlist ratchet lets a repo with existing debt adopt the gate without going clean first.
  • Where it's used: subclassed opt-in in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/RawSqlConventionTests.cs) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/RawSqlConventionTests.cs).

StateManagementConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:18 · Level 4 · abstract partial class

  • What it is: a Blazor Server state-management gate: user and session state must live in per-circuit scoped services, never in mutable static members (which leak one user's state to another) or in singleton-registered stateful services.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, reflection over the UI assemblies, a Source/ file scan, source-generated System.Text.RegularExpressions ([GeneratedRegex], hence partial), and System.Runtime.CompilerServices.CompilerGeneratedAttribute.
  • Concept introduced, a reflection plus source-scan combined gate. [Rubric §19, State Management] assesses per-circuit state safety; Blazor Server shares one process across every circuit, so a static member is shared across every user (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:6-17).
  • Walkthrough: the subclass supplies Map (line 20, whose UI assemblies must be registered under Layer.Ui) and optionally AllowedStaticMembers (line 26).
    • UiAssemblies_CarryNoMutableStaticState (line 29) reflects over Map.OfLayer(Layer.Ui), first asserting the set is non-empty (lines 33-34), then skipping enums, interfaces, and compiler-generated types (line 41) before flagging any declared static field that is not readonly, not const, and not compiler-generated, plus any settable static property, minus the exempted members (lines 46-57).
    • UiProjects_RegisterStatefulServicesScoped (line 67) resolves the repo's Source/ directory, delegates to FindSingletonStateRegistrations (line 72), and asserts two things: that at least one UI file was scanned, so the gate cannot pass by seeing nothing (lines 74-75), and that no offender was found (lines 76-78).
    • FindSingletonStateRegistrations(sourceDir, out scannedUiFiles) (line 92) is protected static, so the scan is testable on its own. It judges each .cs path RELATIVE to sourceDir, directory segment by segment: a file counts only when one segment matches UiProjectSegment (a *.UI or *.UI.* project directory, line 121) and no segment is bin/obj or has Testing as a whole dot-separated part (IsTestingSegment, line 128, so MMCA.Common.Testing.UI is skipped but TestingHelpers and LoadTesting are still scanned; check at lines 102-103), so where the checkout lives cannot skip or include it. It then runs SingletonStateRegistration (line 140; the pattern at lines 136-139) over the whole file text. The regex is bounded by AddSingleton's own type-argument list or its own argument list (the argument branch tracks parenthesis nesting depth), so the generic, factory, typeof and instance forms and a registration wrapped across lines all match, while a call chained after it (for example .AddScoped<IWizardStateService, ...>()) is out of reach; each match becomes a File.cs:line offender computed from the match index (loop at line 110). Both [GeneratedRegex] properties carry a 1000 ms match timeout (lines 120, 139).
    • The private GetLoadableTypes (line 142) repeats the tolerant load locally rather than using the internal RuleHelpers, and IsCompilerGenerated (line 154) treats any member name containing an angle bracket as generated, or one carrying CompilerGeneratedAttribute.
  • Where it's used: subclassed in the repos with Blazor UI assemblies: MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/StateManagementConventionTests.cs:11), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/StateManagementConventionTests.cs:10), and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/StateManagementConventionTests.cs:9).

UIArchitectureConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/UIArchitectureConventionTestsBase.cs:14 · Level 4 · abstract class

  • What it is: a UI-architecture convention gate holding the container and presentational split with two mechanical line-count caps: a *.razor.cs code-behind stays within MaxCodeBehindLines, and a .razor file's inline @code block stays within MaxInlineCodeLines.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, System.IO file enumeration, AwesomeAssertions.
  • Concept introduced, enforcing a design convention by file metrics. [Rubric §18, UI Architecture] assesses the container/presentational discipline; a ballooning code-behind signals page logic that belongs in an injected UI service or an extracted sub-component, and putting a number on it moves the judgement from review to CI (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/UIArchitectureConventionTestsBase.cs:3-13).
  • Walkthrough: the subclass supplies Map (line 16); the caps MaxCodeBehindLines (line 22, default 400), MaxInlineCodeLines (line 29, default 120), MinimumCodeBehindFiles (line 35, a non-vacuity floor, default 1), and ExcludedPathFragments (line 41) are all overridable, and the doc says the caps should move only with a recorded decision (lines 11-12).
    • CodeBehinds_StayWithinTheLineCap (line 44) enumerates *.razor.cs, asserts the floor (lines 48-49), and flags files over the cap with their line counts (lines 51-59).
    • RazorFiles_KeepInlineCodeBlocksSmall (line 63) finds each .razor file's @code line and measures the tail block from there to end of file (line 77), flagging it when it exceeds the inline cap.
    • EnumerateSourceFiles (line 89) drives both, resolving the repo root and excluding obj, bin, and the excluded fragments.
  • Caveats / not-in-source: the inline-@code measurement assumes the block is the file's tail (stated in the doc at lines 25-28), so a .razor file with markup after its @code block would over-count.
  • Where it's used: subclassed in the repos with Blazor UI: MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:11), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:9), and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:10).

AggregateConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/AggregateConventionTestsBase.cs:10 · Level 5 · abstract class

  • What it is: the minimal DDD aggregate fitness base for repos with no business modules (MMCA.Common itself): it asserts the Domain layer exposes aggregate roots, each built through a static Create(...) factory returning Result<T> with no public constructor, and that every domain or value-object factory returns a Result.
  • Depends on: IArchitectureMap, ArchitectureRules, [Fact].
  • Concept introduced, the thin delegating test base shared by most Level-5 types in this group: a protected abstract IArchitectureMap Map plus one [Fact] per rule that forwards to an ArchitectureRules method, with no logic of its own. The factory-returning-Result idiom on AuditableAggregateRootEntity<TIdentifierType> is what these rules verify. [Rubric §4, DDD] assesses aggregate discipline.
  • Walkthrough: four [Fact]s, each a one-line delegate: Domain_ShouldExpose_AggregateRoots (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/AggregateConventionTestsBase.cs:15), AggregateRoots_ShouldHave_ResultReturningCreateFactory (line 18), AggregateRoots_ShouldHave_NoPublicConstructors (line 21, which targets the framework-specific DomainAggregateRootsHaveNoPublicConstructors rule at MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Entities.cs:90 rather than the module-scoped one at line 167), and DomainFactories_ShouldReturn_Result (line 24, delegating to ArchitectureRules.DomainFactoriesReturnResult, ArchitectureRules.Entities.cs:53).
  • Why it's built this way: module-bearing repos use the fuller EntityConventionTestsBase instead (doc, lines 7-8); this base exists so a module-less repo still guards its aggregates.
  • Where it's used: subclassed only in MMCA.Common's architecture test project (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/AggregateConventionTests.cs:9).

AiDependencyIsolationTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/AiDependencyIsolationTestsBase.cs:19 · Level 5 · abstract class

  • What it is: a two-rule delegating base enforcing the governed AI boundary: LLM-provider SDKs must not leak past the layer that governs them, and the framework's own governed-AI package must stay behind Infrastructure rather than be referenced directly from Domain, Application, or UI code.
  • Depends on: IArchitectureMap, ArchitectureRules.LanguageModelSdksStayAtTheModelBoundary and .GovernedAiPackageStaysBehindInfrastructure.
  • Concept: the same thin delegating-base shape as AggregateConventionTestsBase and ConcurrencyConventionTestsBase: a protected abstract IArchitectureMap Map plus one [Fact] per rule, no logic of its own. ADR-120 is the boundary this base makes executable. [Rubric §3, Clean Architecture] and [Rubric §7, Microservices Readiness] assess whether a provider-specific dependency stays isolated at one layer.
  • Walkthrough: two [Fact]s, each a one-line delegate (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/AiDependencyIsolationTestsBase.cs:19-30): LanguageModelSdks_ShouldStayAt_TheModelBoundary (line 24) forwards to ArchitectureRules.LanguageModelSdksStayAtTheModelBoundary(Map) (line 25), and GovernedAiPackage_ShouldStayBehind_Infrastructure (line 28) forwards to ArchitectureRules.GovernedAiPackageStaysBehindInfrastructure(Map) (line 29).
  • Where it's used: subclassed in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/AiDependencyIsolationTests.cs) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/AiDependencyIsolationTests.cs).

CancellationTokenConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CancellationTokenConventionTestsBase.cs:16 · Level 5 · abstract class

  • What it is: a cancellation-uniformity gate: every public async method on a public Application- or Infrastructure-layer type must declare a trailing CancellationToken cancellationToken parameter.
  • Depends on: IArchitectureMap and ArchitectureRules.AsyncMethodsDeclareTrailingCancellationToken (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CancellationTokens.cs:35).
  • Concept introduced, uniformity as the invariant (not presence). The doc states the reasoning (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CancellationTokenConventionTestsBase.cs:4-9): cancellation is only end-to-end if it is uniform. One method that swallows the token turns a cancelled request, an expired CQRS timeout budget, or a stopping host into work that keeps running against the database, and a token in a non-trailing position or under a different name defeats the mechanical forwarding the decorator pipeline and the repositories rely on. That is why the rule reports three distinct problems, not one: a missing token, a token that is not the last parameter, and a trailing token under the wrong name (ArchitectureRules.CancellationTokens.cs:115). [Rubric §12, Performance & Scalability] assesses whether abandoned work is actually abandoned; [Rubric §29, Resilience & Business Continuity] assesses graceful shutdown behavior; [Rubric §15, Best Practices & Code Quality] covers the async convention itself.
  • Walkthrough
    • The subclass supplies Map (line 19) and optionally CancellationTokenExemptMethods (line 26, empty by default), entries in "TypeName.MethodName" form. The doc is deliberately narrow about when to use them (lines 21-25): a shipped public API where adding the parameter would break consumers, each entry justified by a comment.
    • The single [Fact] AsyncMethods_ShouldDeclare_TrailingCancellationToken (line 29) forwards both to the rule.
    • The rule itself carries the interesting mechanics. Scope is methods returning Task, Task<T>, ValueTask, or ValueTask<T> (IsAwaitableReturn, ArchitectureRules.CancellationTokens.cs:105) declared on a publicly-visible, non-delegate, non-compiler-generated type (IsCandidateType, line 92). ExternallyFixedMethods (line 141) computes the signatures the repo does not own: overrides whose base definition is declared outside the map's assemblies, and implicit implementations of an interface declared outside the map (resolved through InterfaceTargets, line 178, with unmappable generic interfaces swallowed). Dispose/DisposeAsync and special-name members are excluded outright (line 97).
  • Why it's built this way: exempting only what is externally fixed keeps the gate honest. A framework contract such as IHostedService.StartAsync cannot grow a parameter, but a first-party service method can, so it is not excused just for implementing an interface.
  • Where it's used: subclassed today only in MMCA.Common, as CancellationTokenConventionTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenConventionTests.cs:10), which exempts exactly two methods: NotificationHub.JoinChannelAsync and NotificationHub.LeaveChannelAsync (:23-27). Its recorded reason is that a SignalR hub method signature is the client-visible RPC contract bound by name and argument list, and both methods already pass Context.ConnectionAborted into the group calls, so the work is cancellable through a path the rule cannot see (:14-22; the hub itself is NotificationHub).

CascadeSoftDeleteConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/CascadeSoftDeleteConventionTestsBase.cs:18 · Level 5 · abstract class

  • What it is: a delegating base for the cascade-soft-delete rule: an aggregate root that owns child entities must delete them in its own Delete() override, because a soft delete is an ordinary UPDATE and nothing cascades for free.
  • Depends on: IArchitectureMap and ArchitectureRules.AggregatesCascadeSoftDeleteToChildren (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.CascadeSoftDelete.cs:100), which reads IL through CallGraphIndex.
  • Concept introduced, the invariant a soft delete quietly breaks. Setting IsDeleted = true on a root hides the root behind the global query filter and leaves every child row ACTIVE: an order line of a deleted order, a session of a deleted event. Nothing reads those rows through the root any more, so the orphans stay invisible until a report, an export or a per-child query surfaces them, and an erasure request that walks the child table finds data whose owner was deleted months ago. The database cannot help, because there is no ON DELETE CASCADE to fire on an UPDATE (rule doc, ArchitectureRules.CascadeSoftDelete.cs:35-50). [Rubric §4, DDD] assesses whether an aggregate really owns its children's lifecycle; [Rubric §8, Data Architecture] and [Rubric §30, Compliance / Privacy / Data Governance] assess orphaned rows surviving into exports and erasure accounting (ADR-005).
  • Walkthrough
    • The subclass supplies Map (line 20) and optionally AllowedCascadeExemptTypes (line 27, type full names or namespace prefixes; empty by default, which requires every child-bearing aggregate to cascade). The doc frames the list as the point of the rule: it turns "delete cascades, mostly" into a reviewed inventory of every aggregate that leaves children behind on purpose (lines 10-16).
    • The single [Fact] AggregatesWithChildCollections_MustCascadeSoftDelete_InDelete (line 30) forwards both to the rule.
    • In the rule, a child collection is an instance field declared on the aggregate whose type is one of six generic collection types (ChildCollectionTypeNames, ArchitectureRules.CascadeSoftDelete.cs:25-33) over an element type deriving from the auditable-entity base. Auto-properties are covered by the same pass, because an auto-property is a compiler-generated instance field whose backing-field name is normalized back to the property name for the report (doc, lines 61-69).
    • What counts as cascading is equally precise (doc, lines 70-78): a direct call inside the Delete() override to DeleteChildren, or to a zero-argument Delete on something other than this, which is what a hand-rolled loop over the children compiles to. base.Delete() is deliberately NOT accepted, because C# emits it as a non-virtual call to a virtual method, a shape nothing else produces, so the rule can tell "delete myself" from "delete my children" without guessing.
  • Why it's built this way: the rule reads IL because neither NetArchTest nor reflection can see a call inside a method body, and it matches bases and callees by full name with the generic arity stripped, so the package keeps its zero-reference stance and still recognizes an aggregate whose base lives in an unregistered assembly (doc, lines 52-60). Four limits are stated outright (lines 84-93): only direct calls in the override are read, the rule proves a cascade EXISTS rather than that it covers every collection, only fields declared on the aggregate are inspected, and grandchildren are the child's own cascade to own.
  • Where it's used: subclassed in all three repos with aggregates plus the framework. ADC exempts exactly one type, Speaker, whose junction rows deliberately outlive the soft-deleted root because the Sessionize import reactivates them in place (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:12, exemption and reasoning at :17-26). Store exempts Category, which REFUSES the delete instead of cascading it (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:12, :17-25). MMCA.Common runs it as a ratchet over its own module-less Source/, which declares no child-bearing aggregate today (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:14); the rule's own behavior is proven against compiled fixtures in CascadeSoftDeleteFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:17).

ClockReadTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ClockReadTestsBase.cs:15 · Level 5 · abstract class

  • What it is: a delegating base for the ambient-clock rule: Domain and Application code must not read the wall clock directly, so behavior stays deterministic and testable through an injected time source instead.
  • Depends on: IArchitectureMap and ArchitectureRules.DomainAndApplicationDoNotReadTheClock.
  • Concept: the same thin delegating-base shape as AggregateConventionTestsBase and AiDependencyIsolationTestsBase: a protected abstract IArchitectureMap Map plus one [Fact] forwarding to an ArchitectureRules method, with no logic of its own.
  • Walkthrough: the subclass supplies Map (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ClockReadTestsBase.cs:15) and optionally AllowedClockReaders (line 22, empty by default): type full names, namespace prefixes, or single members (MMCA.X.Sales.Domain.Orders.Order.RepublishFulfillment) where reading the clock is the deliberate, reviewed choice. The single [Fact] DomainAndApplication_ShouldNotReadTheAmbientClock (line 25) forwards both to the rule.
  • Where it's used: subclassed in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ClockReadTests.cs:2) and MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:2).

CloudVendorIsolationTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/CloudVendorIsolationTestsBase.cs:15 · Level 5 · abstract class

  • What it is: the cloud-vendor fitness function (ADR-134): the assemblies a repo declares cloud-neutral name no Azure, AWS or Google Cloud SDK type, reference no vendor SDK or vendor broker-transport assembly, and never reference a vendor adapter package (MMCA.Common.Azure, MMCA.Common.Aws, MMCA.Common.Gcp, MMCA.Common.Testing.Azure) (doc, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/CloudVendorIsolationTestsBase.cs:3-7).
  • Depends on: two ArchitectureRules members, CloudNeutralAssembliesNameNoVendorType (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.CloudVendors.cs:74) and CloudNeutralAssembliesReferenceNoVendorAssembly (:107), the first reporting through ArchitectureAssert. Like DependencyVersionTestsBase it takes no IArchitectureMap: the subclass hands over raw assemblies, because the set it guards (service defaults, test packages) is wider than any map's layers.
  • Concept introduced, the runtime half of a two-gate boundary. The compile-time half is the EnforceCloudVendorBoundary target (MMCA.Common/Source/Build/MMCA.Common.LayerEnforcement.targets:130), which judges the declared package references; this base judges the compiled assemblies, so a vendor type that arrives through a transitive path is caught too (doc, lines 8-12). The rule doc names what is at stake: adapters reference the core, and the reverse edge turns "runs on any cloud" into "pulls one cloud's SDK, credential chain and advisory stream into every consumer" (ArchitectureRules.CloudVendors.cs:21-22). [Rubric §7, Microservices Readiness] and [Rubric §3, Clean Architecture] assess whether infrastructure choices stay at the edges; [Rubric §32, Dependency & Supply-Chain] assesses what every consumer is forced to pull in. It sits beside AiDependencyIsolationTestsBase, the same isolation shape for the AI packages.
  • Walkthrough
    • The subclass supplies CloudNeutralAssemblies (line 18) and optionally AllowedVendorPrefixes (line 24): per assembly name, the vendor prefixes it may still name, meant for a database engine such as Cosmos DB and never a cloud service. It defaults to an empty ordinal dictionary (line 25).
    • Two [Fact]s forward both: CloudNeutralAssemblies_ShouldNotName_ACloudVendorType (line 28) and CloudNeutralAssemblies_ShouldNotReference_ACloudVendorAssembly (line 32).
    • The type-level rule runs NetArchTest HaveDependencyOnAny against CloudVendorDependencies (ArchitectureRules.CloudVendors.cs:25-41: Azure, Microsoft.Azure, Amazon, Google, FirebaseAdmin, Grpc.Auth, the two MassTransit vendor transports, and the four adapter packages). The entries are whole namespace roots with no trailing dot, because NetArchTest matches by namespace segment and an entry ending in . matches nothing, which would let the rule pass vacuously (:14-19, :27-28). When an assembly has an allowance, only its types that do NOT depend on an allowed prefix are checked (:86-88).
    • The reference-level rule reads GetReferencedAssemblies() and flags any name matching CloudVendorAssemblyPrefixes (:48-62, plain string prefixes such as AWSSDK. and Google.) or containing .Azure. or ending in .Azure (IsCloudVendorAssembly, :133-136), minus the assembly's allowed prefixes (:117-122).
  • Why it's built this way: two rules, not one, because they fail differently. The type rule names the offending types; the reference rule still holds the whole assembly to the allowance, so excusing the types that use Cosmos does not excuse a stray vendor reference elsewhere in the same assembly (rule doc, :68-73). Google deliberately also matches Google.Protobuf; the doc says that if a cloud-neutral assembly ever needs it, the fix is one exact exemption, not a narrower Google entry (:16-18).
  • Where it's used: subclassed only in MMCA.Common, as CloudVendorIsolationTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/CloudVendorIsolationTests.cs:17, rolled up in the per-project rollup), which holds every layer of CommonArchitectureMap plus MMCA.Common.Aspire, the Testing and Testing.Aspire packages and Testing.Architecture itself (:19-26), with exactly one allowance: MMCA.Common.Infrastructure may name Microsoft.Azure.Cosmos, because Cosmos DB is one of the four EF engines rather than a cloud service a host opts into (:28-32, reasoning at :11-15). The rules themselves are proven against real vendor SDK assemblies in the per-adapter CloudVendorRulesTests (for example MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/CloudVendorRulesTests.cs:30, :38).

CommandValidatorCoverageTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CommandValidatorCoverageTestsBase.cs:22 · Level 5 · abstract class

  • What it is: a validation-coverage gate: every command that carries data and has a handler must be covered by a validator, either its own IValidator<TCommand> or the request-validator bridge.
  • Depends on: IArchitectureMap and two ArchitectureRules members, CommandsHaveValidators (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:48) and HandledCommandCount (:80), plus AwesomeAssertions for the count assertion.
  • Concept introduced, gating a pipeline stage that has nothing to run. The Validating decorator runs FluentValidation before the transaction opens, so a command with no validator carries whatever the caller sent straight into the handler: the stage exists, it simply has nothing to execute, and the gap is invisible until bad input reaches the domain (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CommandValidatorCoverageTestsBase.cs:4-7). The decorator pipeline itself is taught in primer §2. [Rubric §6, CQRS & Event-Driven] assesses the completeness of the pipeline's stages; [Rubric §11, Security] and [Rubric §24, Forms / Validation / UX Safety] assess whether untrusted input is checked before it acts.
  • Walkthrough
    • The subclass supplies Map (line 24) and optionally AllowedUnvalidatedCommands (line 31, empty by default) and MinimumCommands (line 38, default 1).
    • Two [Fact]s: Commands_ShouldHave_ValidationCoverage (line 41) forwards to the rule, and CommandInventory_ShouldNotBe_Empty (line 45) asserts the handled-command count meets the floor, so a map that resolves to no module Application assemblies cannot let the gate pass without reading anything (lines 46-48).
    • In the rule, a command is the command argument of every ICommandHandler implemented in the map's per-module Application assemblies, restricted to closed types with at least one public settable property; an init setter counts, so the usual positional-record command is inspected, and a payload-free marker command is skipped (ArchitectureRules.CommandValidators.cs:96-102, :135).
    • Coverage means one of two things (:112): a concrete IValidator<TCommand>, or the bridge, where the command implements ICommandWithRequest<TRequest> AND a concrete IValidator<TRequest> exists. Both halves of the bridge are required on purpose, because the framework registers the bridge validator for every such command whether or not a request validator resolves, and one that resolves none adds no rules (doc, lines 8-16).
    • Where it looks mirrors the DI reality (:31-36): both commands and validators are read from the module Application assemblies, because the framework registers validators from the module assembly, so a validator living anywhere else would not resolve at run time and must not count as coverage here either.
  • Why it's built this way: interfaces are matched by open-generic full NAME through a shared ClosedGenericArguments projection (:122), which keeps the package free of a compile reference to FluentValidation and to the framework's own Application layer. Two limits are stated (:37-41): a validator declared for a base command does not cover its derived commands, matching FluentValidation's closed-type resolution, and abstract validators are ignored since nothing registers them.
  • Where it's used: subclassed in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/CommandValidatorCoverageTests.cs:18), Store, whose allowlist is grouped by why each entry is there (identifier-only commands and two false positives the module-scoped scan cannot see through, with the frozen-debt group now empty, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/CommandValidatorCoverageTests.cs:17, rationale at :8-15), and Helpdesk, which deliberately keeps AllowedUnvalidatedCommands empty and writes real validators instead, because an allowlist entry names a type that a generated app or a scaffolded slice would never inherit (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:163, reasoning at :150-162, floor of 6 against 7 discovered commands at :173).

ConcurrencyConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ConcurrencyConventionTestsBase.cs:9 · Level 5 · abstract class

  • What it is: a one-rule delegating base asserting that no *UpdateRequest implements IConcurrencyAware, because the optimistic-concurrency token travels in the If-Match request header and not in the body.
  • Depends on: IArchitectureMap, ArchitectureRules.
  • Concept: cross-references the delegating-base shape from AggregateConventionTestsBase. The invariant is a single-source-of-truth one: a token in the request body would give the same conditional-update check a second, competing source (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ConcurrencyConventionTestsBase.cs:3-8). [Rubric §8, Data Architecture] assesses optimistic-concurrency handling (ADR-035); [Rubric §9, API & Contract Design] assesses keeping a precondition in the one place HTTP defines for it.
  • Walkthrough: one [Fact] UpdateRequests_ShouldNotImplement_IConcurrencyAware (line 13) delegating to ArchitectureRules.UpdateRequestsAreNotConcurrencyAware(Map) (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Governance.cs:24). The rule scans the map's module Application assemblies for classes or value types whose RuleHelpers.SimpleName ends with UpdateRequest and reports any that implement the full-name-matched contract (lines 26-34). The base's doc notes that modules with no mutable aggregate are legitimately vacuous (line 7).
  • Where it's used: subclassed in all four repos: MMCA.Common, where the module-less run is a ratchet that fires the moment the framework itself grows a body-token update request (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:13, rationale at :5-11), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:3), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:3), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:56).

ContractImplementationTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ContractImplementationTestsBase.cs:20 · Level 5 · abstract class

  • What it is: an encapsulation gate for the [ServiceContract] boundary: the interface is the published surface of a service, so the concrete class serving it must not be public.
  • Depends on: IArchitectureMap and ArchitectureRules.ServiceContractImplementationsAreNotPublic (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:81).
  • Concept introduced, the other half of a contract boundary. ServiceContractPurityTestsBase keeps the producer's internals out of the contract; this base keeps the contract's implementation out of the consumer's reach (doc, lines 10-13). A public implementation lets a consumer reference, construct or subclass the type, and each of those references is a coupling the extraction has to sever later, because an interface can be answered over a wire and a class cannot (lines 4-8, ADR-007). [Rubric §7, Microservices Readiness] assesses whether an extraction stays reversible; [Rubric §1, SOLID] (DIP) and [Rubric §9, API & Contract Design] assess binding to abstractions rather than to concretes.
  • Walkthrough: the subclass supplies Map (line 22) and optionally AllowedPublicImplementations (line 30, type full names or namespace prefixes for a shipped default a consumer is meant to construct, or a test double in a testing package). The single [Fact] ServiceContractImplementations_ShouldNotBe_Public (line 33) forwards both to the rule. In the rule, visibility is judged with Type.IsVisible (ArchitectureRules.Contracts.cs:92), so a public class nested inside an internal one is correctly treated as unreachable and passes; only concrete classes are scanned, so an abstract base carrying shared behavior for several implementations is exempt by construction (doc, lines 63-68). ServiceContractInterfaceOf (line 116) returns the first marked interface a type implements, matched by the attribute's full name through CarriesServiceContractAttribute (line 143).
  • Why it's built this way: like its twin, the rule is attribute-driven rather than Layer.Contracts-driven and honest about the vacuous case (doc, lines 14-18): a repo that marks no interface passes without asserting anything, and the value is the ratchet, with no test to remember to write when the first contract appears.
  • Where it's used: subclassed in all four repos: MMCA.Common as a ratchet, since the framework marks no interface (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ContractImplementationTests.cs:11), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/ContractImplementationTests.cs:20), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Contracts/ContractImplementationTests.cs:12), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ContractImplementationTests.cs:12).

ControllerConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/ControllerConventionTestsBase.cs:7 · Level 5 · abstract class

  • What it is: the presentation-layer convention base: controllers are thin and sealed, never reach Infrastructure or EF Core directly, and inherit the framework ApiControllerBase for consistent Result-to-HTTP mapping.
  • Depends on: IArchitectureMap, ArchitectureRules.
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); it adds a protected virtual ControllersExemptFromApiControllerBase list (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/ControllerConventionTestsBase.cs:12) for controllers that legitimately bypass the base (for example a webhook endpoint that owns its own response semantics). [Rubric §9, API & Contract Design] assesses consistent controller shape.
  • Walkthrough: four [Fact]s: Controllers_ShouldNotDependOn_Infrastructure (line 15), Controllers_ShouldNotDependOn_EntityFrameworkCore (line 18), Controllers_ShouldBe_Sealed (line 21), and Controllers_ShouldInherit_ApiControllerBase (line 24, passing the exempt list). The underlying rules live in ArchitectureRules.Controllers.cs at lines 6, 22, 37 and 54.
  • Where it's used: subclassed in every repo with business modules: Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Api/ControllerConventionTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/ControllerConventionTests.cs:3, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:84).

DeleteBehaviorConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/DeleteBehaviorConventionTestsBase.cs:21 · Level 5 · abstract class

  • What it is: a cascading-delete convention base (ADR-119): a foreign key that cascades must say so explicitly in code, and every foreign key left to convention must resolve to a restrictive delete.
  • Depends on: ArchitectureRules.CascadingForeignKeysAreExplicitlyOptedIn and ArchitectureRules.ConventionForeignKeysRestrictDeletes. Unlike most bases in this family it takes no IArchitectureMap; the subclass instead supplies the finalized EF Core model directly.
  • Concept introduced, judging a live model instead of IL. The base's Model is typed object on purpose: the testing package deliberately carries no EF Core reference (doc, lines 28-29), so the subclass passes dbContext.Model and the rule inspects it through reflection rather than a compile-time EF Core dependency. This pairs with RestrictDeleteByDefaultConvention, the model-finalizing convention that makes DeleteBehavior.Restrict the default for every relationship unless a mapping calls .OnDelete(DeleteBehavior.Cascade) (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/Conventions/RestrictDeleteByDefaultConvention.cs:11-20,101): a convention-created cascade is EF Core's silent default, and an accidental one deletes a subtree nobody meant to remove. [Rubric §8, Data Architecture] assesses deliberate, reviewable delete semantics; [Rubric §4, DDD] assesses whether aggregate boundaries are respected by cascading relationships.
  • Walkthrough: two [Fact]s, both driven by the one abstract Model property (line 31): CascadingDeletes_AreExplicitlyOptedIn (line 33) fails on a foreign key that cascades but carries no marker recording the opt-in; ConventionDeletes_AreRestricted (line 36) fails on a foreign key left at EF Core's own convention default that resolves to anything other than Restrict.
  • Where it's used: subclassed in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs) and in MMCA.Common itself (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs), each passing its own finalized model.

DependencyVersionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DependencyVersionTestsBase.cs:16 · Level 5 · abstract class

  • What it is: a dependency-pin fitness function guarding the MassTransit v9 commercial-license trap at build time: every MassTransit package must stay below major version 9, read out of Directory.Packages.props (ADR-016).
  • Depends on: ArchitectureRules.PinnedPackageMajorBelow (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Governance.cs:38), [Fact]. Note there is no Map on this base; it reads the props file directly through the rule, which walks up from the test assembly's base directory to find it (FindUpwards, ArchitectureRules.Governance.cs:63).
  • Concept introduced, enforcing a policy pin as a test. [Rubric §32, Dependency & Supply-Chain] assesses whether risky upgrades are guarded. The doc explains the trap: MassTransit v9 fails the startup license check and crashes every broker-enabled host, and CI never starts a broker, so a blanket bump otherwise stays green (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DependencyVersionTestsBase.cs:4-6); the assertion's own reason records that a blanket package update reintroduced v9 once before (lines 35-38).
  • Walkthrough: the single [Fact] MassTransit_MustNotExceed_MajorVersion8 (line 28) loops MassTransitPackageIds (lines 18-25: MassTransit, MassTransit.RabbitMQ, MassTransit.Azure.ServiceBus.Core, MassTransit.AmazonSQS, MassTransit.SqlTransport.PostgreSQL) and calls PinnedPackageMajorBelow with an exclusive ceiling of 9 (line 34). The list is virtual, so a repo that does not reference MassTransit can override it with an empty list, which makes the pin vacuous (doc, lines 7-8). The rule asserts both that the package is still pinned and that its major is below the ceiling, on the nullable major directly rather than dereferencing after a not-null assertion, a shape the comment records as avoiding a CI-only IDE0370 (ArchitectureRules.Governance.cs:40-45).
  • Why it's built this way: the doc is explicit (lines 8-14): the consumer repos (ADC, Store) pin only MassTransit.Azure.ServiceBus.Core, while MassTransit and MassTransit.RabbitMQ flow transitively via MMCA.Common.Infrastructure, so they must not subclass this base with the default list, or the "must remain pinned" assertion would fail on the pins they do not declare. The full v8 pin is enforced in MMCA.Common, where every MassTransit package is pinned; a consumer that ever needs its own guard overrides the list with the ids its own Directory.Packages.props declares.
  • Where it's used: subclassed only in MMCA.Common, as the body-less DependencyVersionTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DependencyVersionTests.cs:9).

DomainEventHandlerSaveTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/DomainEventHandlerSaveTestsBase.cs:22 · Level 5 · abstract class

  • What it is: a domain-event-handler purity gate: a handler must not persist, and the check is TRANSITIVE, walking the call graph out of every handler method rather than only looking for a save typed into the handler itself.
  • Depends on: IArchitectureMap and ArchitectureRules.DomainEventHandlersDoNotSave (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:76), which is built on CallGraphIndex.
  • Concept introduced, the second write inside the first one. Dispatch happens after SaveChangesAsync (and after commit inside an ITransactional command), so a handler that saves opens a second write in the middle of the first: it re-enters the change tracker, can raise a fresh event cascade, and persists work the outer transaction may still roll back. Handlers mutate state and let the owning unit of work flush it; an independent write belongs on the outbox (doc, lines 3-9; ADR-003 is where the outbox itself is decided). [Rubric §6, CQRS & Event-Driven] assesses event-handling discipline; [Rubric §8, Data Architecture] assesses transaction integrity; [Rubric §7, Microservices Readiness] assesses whether a handler stays movable.
  • Walkthrough
    • The subclass supplies Map (line 24) and optionally AllowedSavingTypes (line 33) and MaxCallDepth (line 40, default 6, which the doc justifies as covering the realistic handler to service to helper to repository chain while keeping the scan fast).
    • The default allowlist is ["MMCA.Common"] (line 33), and the reason is precise: the framework's outbox event bus persists by design, and a handler publishing an integration event is not the defect this rule hunts. A handler calling a save DIRECTLY is still reported, because detection happens at the call site and needs no descent, so the default silences nothing real (lines 26-32, and the rule doc at ArchitectureRules.DomainEventHandlerSaves.cs:50-60).
    • The single [Fact] DomainEventHandlers_ShouldNotReach_SaveChanges (line 43) forwards all three to the rule.
    • The rule identifies handlers by the IDomainEventHandler interface full name through CallGraphIndex.Implements (ArchitectureRules.DomainEventHandlerSaves.cs:96), then breadth-first walks to a save with a visited set, reporting the shortest chain it finds (FindSavePath, line 125). A save is a call to SaveChanges or SaveChangesAsync (line 12) on a declaring type whose name ends with DbContext, DbContextFactory, UnitOfWork or Repository (line 20), which is what keeps an unrelated SaveChanges out of the rule.
    • The walk follows direct calls, delegate creations (so lambdas and local functions are covered), and interface or virtual calls expanded to every implementation inside the scanned assemblies, and it descends into the compiler-generated state machine of an async or iterator method, whose MoveNext holds the real body (doc, lines 44-49).
  • Why it's built this way: an allowlist entry does two things, and the doc says so explicitly (lines 15-20 of the base): it silences the type it names AND stops the walk from descending into it, which is the correct reading of "we accept what happens in there". Four limits are stated in the rule (:61-69): the depth bound, unresolvable interface dispatch when the implementation lives outside the map, name-and-arity overload matching that over-reports rather than misses, and total blindness to reflection, DI-resolved delegates and expression trees.
  • Where it's used: subclassed in ADC, which accepts one deliberate cascade, the gamification points ledger written by the awarder its point-scoring handlers call (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/DomainEventHandlerSaveTests.cs:11, entry and reasoning at :22-27), and in Store, whose two entries are both the compensating-saga pattern, a handler opening its own DI scope for an independent, idempotent follow-up write (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/DomainEventHandlerSaveTests.cs:19, rationale at :11-17).

DomainPurityTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/DomainPurityTestsBase.cs:8 · Level 5 · abstract class

  • What it is: a framework-independence base: Domain and Shared stay free of infrastructure frameworks, and Application stays host-agnostic (no EF Core, no ASP.NET Core).
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Purity.cs, whose shared ForbiddenDomainDependencies list is at line 9).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); it adds an ExtraForbiddenDomainDependencies hook (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/DomainPurityTestsBase.cs:12) so a repo bans its own frameworks (the doc cites Store banning "Stripe" and ADC banning "RabbitMQ"). [Rubric §3, Clean Architecture] and [Rubric §4, DDD] assess the framework-free core.
  • Walkthrough: four [Fact]s: Domain_ShouldBe_FrameworkFree (line 15) and Shared_ShouldBe_FrameworkFree (line 18), both passing the extra-forbidden list, then Application_ShouldNotDependOn_EntityFrameworkCore (line 21) and Application_ShouldNotDependOn_AspNetCore (line 24), whose rules sit at ArchitectureRules.Purity.cs:23, :39, :54 and :68.
  • Where it's used: subclassed in all four repos (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/DomainPurityTests.cs:9, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Layering/DomainPurityTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/DomainPurityTests.cs:3, and Helpdesk at MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:10).

DomainThrowTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/DomainThrowTestsBase.cs:21 · Level 5 · abstract class

  • What it is: a Result-pattern purity gate (ADR-013): the domain returns Result, it does not throw. Any throw in the map's Domain assemblies whose exception is not an argument guard fails the build.
  • Depends on: IArchitectureMap and ArchitectureRules.DomainThrowsOnlyArgumentGuards (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:71), which reads IL with Mono.Cecil.
  • Concept introduced, why a thrown business failure is a defect and not a style choice. It skips Result.Combine invariant composition, arrives at the API as a 500 instead of the outcome the caller can act on, and pays an exception unwind on a path that is not exceptional (doc, lines 4-9; rule doc at ArchitectureRules.DomainThrows.cs:26-32). The three argument guards stay allowed because they report a caller BUG rather than a business outcome, and a caller cannot recover from passing null, so there is nothing for a Result to carry (:11-21). [Rubric §4, DDD] and [Rubric §15, Best Practices & Code Quality] assess the error-handling model; [Rubric §9, API & Contract Design] assesses the status code a caller actually sees.
  • Walkthrough
    • The subclass supplies Map (line 23) and optionally AllowedThrowingTypes (line 31, empty by default, which allows nothing beyond the argument guards).
    • The single [Fact] Domain_ShouldNotThrow_ExceptArgumentGuards (line 34) forwards both to the rule.
    • The rule opens each Domain assembly with ModuleDefinition.ReadModule over DomainAssemblyLocations (ArchitectureRules.DomainThrows.cs:111) and, for every throw instruction, reads the type of the exception constructed immediately before it (ConstructedExceptionType, line 143).
    • Three things it deliberately does not touch (doc, lines 39-48): a bare rethrow compiles to a distinct opcode and is ignored, so preserving a caught exception stays free; the ArgumentNullException.ThrowIfNull family emits a plain call with no throw in the caller, so the modern guard style always passes; and compiler-written bodies are skipped (the skeleton members of a C# extension block, and the explicit interface implementations on a compiler-generated type), because their exceptions are not anyone's code.
    • When the thrown value was not constructed in place (a prepared local, a field, a factory call), the type is not knowable from the instruction stream. Those sites are reported as UNVERIFIABLE in the failure message rather than passed or failed (doc, lines 49-54; WithUnverifiable composes the message at ArchitectureRules.ErrorCatalog.cs:152).
  • Why it's built this way: the allowlist is framed as an inventory rather than a mute button (doc, lines 14-19): adopting the rule in a repo with existing throws means converting each site to a Result.Failure or recording why throwing is right there, which turns "the domain returns Result, mostly" into a reviewed list of every place that does not.
  • Where it's used: subclassed in ADC, whose allowlist is an explicit triage placeholder filled from the first run (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/DomainThrowTests.cs:9, list at :14-17), and in Store, which overrides nothing at all because the scan over its three module Domain assemblies found no non-guard throw at adoption (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/DomainThrowTests.cs:14, that decision recorded at :8-12).

EntityConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/EntityConventionTestsBase.cs:10 · Level 5 · abstract class

  • What it is: the fuller DDD entity and aggregate convention base (the module-bearing counterpart to AggregateConventionTestsBase): entities are sealed and live only in Domain, aggregate roots use a Create(...) factory returning Result<T> with no public constructor, every domain and value-object factory returns a Result, entity properties carry no public setter, and DTOs and requests stay out of Domain and Infrastructure.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Entities.cs).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase). The property-setter rule is the one worth calling out on its own: mutation goes through named domain methods rather than a setter, which is what keeps an invariant enforceable in one place (doc, lines 6-8). [Rubric §4, DDD] and [Rubric §3, Clean Architecture] apply.
  • Walkthrough: eight [Fact]s: Domain_ShouldExpose_AggregateRoots (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/EntityConventionTestsBase.cs:15), AggregateRoots_ShouldHave_ResultReturningCreateFactory (line 18), AggregateRoots_ShouldHave_NoPublicConstructors (line 21, the module-scoped rule at ArchitectureRules.Entities.cs:167), DomainFactories_ShouldReturn_Result (line 24), DomainEntities_ShouldBe_Sealed (line 27), DomainEntities_ShouldReside_InDomainLayer (line 30), DomainEntityProperties_ShouldNotHave_PublicSetters (line 33, delegating to ArchitectureRules.EntityPropertySettersAreNonPublic, ArchitectureRules.Entities.cs:149), and DtosAndRequests_ShouldNotResideIn_DomainOrInfrastructure (line 36).
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/EntityConventionTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/EntityConventionTests.cs:3, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:89).

ErrorCatalogTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ErrorCatalogTestsBase.cs:20 · Level 5 · abstract class

  • What it is: a two-rule gate over the set of Error codes a repo's modules construct: one code means one thing (no literal code owned by two different types), and a code carries the prefix of the vocabulary that owns it.
  • Depends on: IArchitectureMap and three ArchitectureRules members, ErrorCodesAreUnique (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.ErrorCatalog.cs:63), ErrorCodesUseAnAllowedPrefix (:102) and DistinctErrorCodeCount (:135), plus AwesomeAssertions.
  • Concept introduced, the error code as a public vocabulary. A client switches on a code and a support ticket quotes it, so two modules that both ship the same code make that vocabulary ambiguous, and the ambiguity only surfaces in production (rule doc, ArchitectureRules.ErrorCatalog.cs:32-38). [Rubric §9, API & Contract Design] assesses the stability and legibility of a published error surface; [Rubric §15, Best Practices & Code Quality] assesses whether a code is traceable back to the code that raised it. Uniqueness is measured per declaring TYPE, which is the subtle and correct choice: reusing one code across two branches of the same class is one error with two exits and passes, while the same code owned by two different types is the collision the rule exists to catch (doc, lines 45-50).
  • Walkthrough
    • Four overridable members: AllowedCodePrefixes (line 29, defaulting to Map.ModuleNames, which is the convention), AllowedSharedCodes (line 35, defaulting to the three generic statics on the framework's Error class that exist precisely to be reused), MinimumErrorCodes (line 43, default 1), and IsCodePrefixAllowed (line 62), a hook for a convention a prefix list cannot express.
    • Three [Fact]s: ErrorCodes_ShouldBe_Unique (line 46), ErrorCodes_ShouldCarry_TheOwningModulePrefix (line 50, passing the delegate rather than the list, so the hook is what the rule actually calls), and ErrorCodeCatalog_ShouldNotBe_Empty (line 54), the non-vacuity guard that fails when the map registers no module Domain or Application assemblies.
    • Codes are read out of IL at the Error factory call sites across the per-module Domain and Application assemblies. The recognised members are every static factory on Error plus the primary constructor (ErrorFactoryNames, ArchitectureRules.ErrorCatalog.cs:17-30); the record's copy constructor is excluded by a first-parameter-is-string check, so a with expression is not mistaken for a new code (comment, lines 11-15).
    • A code that is not a literal (built by concatenation, or read from a field) cannot be judged statically, so it is listed as UNVERIFIABLE in the failure message rather than passed or failed (doc, lines 51-55), the same honesty the domain-throw rule shows.
  • Why it's built this way: framework layers are deliberately out of scope (doc, lines 39-44), because the framework's codes are not the consumer's catalog. Scoping uniqueness to the module Domain and Application assemblies is what keeps a shared framework code from being reported as a collision in every repo.
  • Where it's used: subclassed in ADC and Store, both of which namespace their codes by AGGREGATE rather than by module name, so both extend Map.ModuleNames with that vocabulary and treat the list as the reviewed inventory: a code under a new prefix fails until the prefix is added (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/ErrorCatalogTests.cs:20, prefixes from :28; MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Contracts/ErrorCatalogTests.cs:19, prefixes from :27). Both subclass docs record the same constraint: error codes are a public contract that reaches clients in problem-details payloads, so nothing already shipped is renamed to satisfy the rule, and the legitimately shared codes are frozen in AllowedSharedCodes instead.

EventConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/EventConventionTestsBase.cs:10 · Level 5 · abstract class

  • What it is: an integration-event convention base (ADR-010): every concrete integration event inherits BaseIntegrationEvent, declares an int SchemaVersion, carries [EventName] (a stable wire identity for its outbox and inbox rows), and lives in a *.IntegrationEvents namespace in the Shared layer. It also polices the upcasters that carry a retired contract forward.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Events.cs and ArchitectureRules.Upcasters.cs).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase). [Rubric §6, CQRS & Event-Driven] and [Rubric §9, API & Contract Design] assess versioned, discoverable cross-service event contracts. It pairs with IntegrationEventContractTestsBase, which freezes the exact shape.
  • Walkthrough: six [Fact]s. The four event-shape rules come first: IntegrationEvents_ShouldDeclare_SchemaVersion (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/EventConventionTestsBase.cs:15), IntegrationEvents_ShouldInherit_BaseIntegrationEvent (line 18), IntegrationEvents_ShouldDeclare_EventName (line 21, which fails any concrete event lacking [EventName] declared on the type itself, since the attribute is not inherited, so a rename or namespace move cannot orphan persisted rows), IntegrationEvents_ShouldResideIn_SharedIntegrationEventsNamespace (line 24), backed by ArchitectureRules.Events.cs:6, :17, :33 and :44. Two upcaster rules follow (ADR-090, doc lines 6-7): EventUpcasters_ShouldHave_UniqueSourceTypes (line 27, delegating to ArchitectureRules.Upcasters.cs:14, because with two IEventUpcaster implementations reading one source contract the message a handler receives would depend on DI registration order) and EventUpcasters_ShouldIncrease_SchemaVersion (line 30, delegating to ArchitectureRules.Upcasters.cs:36, which skips a source or target whose SchemaVersion is missing or non-int, that being the first rule's business). A repo with no upcasters passes both vacuously (doc, line 7).
  • Where it's used: subclassed in every repo that publishes integration events: Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Contracts/EventConventionTests.cs:3), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/EventConventionTests.cs:3), Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:38), and MMCA.Common itself under the name EventVersioningConventionTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventVersioningConventionTests.cs:12).

FeatureFlagLifecycleTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FeatureFlagLifecycleTestsBase.cs:10 · Level 5 · abstract class

  • What it is: a governance base over FeatureFlagAttribute: every declared flag must state a Lifetime, and a Temporary flag must not still be in the codebase past its own RemoveBy date.
  • Depends on: IArchitectureMap and ArchitectureRules.FeatureFlagsDeclareLifetime / ArchitectureRules.TemporaryFeatureFlagsAreNotPastRemoveBy.
  • Concept introduced, expiring a toggle as a build-time fact. [FeatureFlagAttribute] records a Lifetime (Permanent or Temporary) and, for a temporary flag, a RemoveBy date in yyyy-MM-dd (MMCA.Common/Source/Core/MMCA.Common.Shared/FeatureFlags/FeatureFlagAttribute.cs:10-11,34-46; RemoveByFormat, line 35). A temporary toggle that is never revisited becomes permanent by neglect, carrying dead branches indefinitely; this base turns "review this flag" from a calendar reminder someone has to remember into a test that goes red on its own. [Rubric §5, Modularity & Vertical Slicing] assesses whether a codebase actively retires its own temporary branching rather than accumulating it; [Rubric §31, Configuration & Feature Management] assesses lifecycle discipline over feature toggles.
  • Walkthrough: the subclass supplies Map (line 12) and can override Today (line 19, default DateOnly.FromDateTime(DateTime.UtcNow)) to freeze the judgment date, for example to a release date rather than the build clock. Two [Fact]s: FeatureFlags_ShouldDeclare_Lifetime (line 21) and TemporaryFeatureFlags_ShouldNotBe_PastRemoveBy (line 24, passing Today so the rule judges against a fixed date rather than "now" drifting the assertion's outcome between a local run and a CI run days later).
  • Where it's used: subclassed in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs).

FolderWidthTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FolderWidthTestsBase.cs:14 · Level 5 · abstract class

  • What it is: the folder-width fitness base (ADR-109): no folder under a repo's Source/ or Tests/ tree holds more than 12 direct code files, so a folder keeps naming a feature instead of drifting into a technical bucket (doc, FolderWidthTestsBase.cs:3-13).
  • Depends on: ArchitectureRules (FoldersStayNarrow, MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FolderWidth.cs:34) and, in every subclass, ArchitectureMapBase.FindRepoRoot. It does not take an IArchitectureMap: the defect it hunts is a layout one, so the rule walks the filesystem rather than IL (rule doc, ArchitectureRules.FolderWidth.cs:9-11).
  • Concept introduced, the layout rule as a test. [Rubric §5, Modularity & Vertical Slicing] assesses whether a codebase is organised by feature rather than by technical kind. A flat Services/ holding forty files is not a compiler error and no reviewer catches it in the diff that adds the forty-first file, so the convention is only enforceable if something asserts it continuously. This base is that assertion, and it is the same delegating shape as the other architecture bases (AggregateConventionTestsBase): overridable knobs, one [Fact], the judgement itself living in ArchitectureRules.
  • Walkthrough
    • Three members to override or accept: RepoRoot (line 20, abstract, the only required one), MaxDirectFiles (line 23, virtual, default 12), and ExemptFolderSuffixes (line 30, virtual, default empty).
    • One [Fact], Folders_stay_narrow (line 33), which passes all three straight to ArchitectureRules.FoldersStayNarrow.
    • The rule walks Source/ and Tests/ (skipping a tree that does not exist), collects every offending folder as relative: count direct code files (max N), sorts the list ordinally and hands it to ArchitectureAssert.NoViolations, so a failure reports every offender at once rather than the first (ArchitectureRules.FolderWidth.cs:42-70).
    • Counting is deliberate about what an author actually chose to put in a folder: a .razor file counts once and its co-located X.razor.cs code-behind counts with it, .resx satellites never count, and generated files (*.g.cs, *.generated.cs, *.Designer.cs) never count (ArchitectureRules.FolderWidth.cs:88-126). Whole trees are skipped by path segment: bin, obj, Migrations, Platforms, Resources, node_modules, wwwroot, .git (:78).
  • Why it's built this way: exemptions are matched as repo-relative path SUFFIXES with forward slashes (:85), not as leaf names, so a repo can exempt one specific folder without accidentally exempting every folder that shares its name. The alternative, a leaf-name allowlist, would silently widen with every new project that reuses the name.
  • Where it's used: two thin subclasses today. ADC supplies only its root and takes every default (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/FolderWidthTests.cs:10-12). MMCA.Common supplies a root plus three documented exemptions, and its doc records why each is deliberate: the decorator pipeline folder (nine cross-cutting concerns times command and query, where splitting yields nine two-file folders), its test twin, and the entity marker interfaces (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FolderWidthTests.cs:16-30).

ForwardedJwtAudienceTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/ForwardedJwtAudienceTestsBase.cs:11 · Level 5 · abstract class

  • What it is: a source-text gate for cross-service token validation: every AddForwardedJwtBearer(...) call in a repo's Source tree must resolve its audience fail-closed through JwtAudience.RequireConfigured(...) and carry no ?? fallback (ADR-004).
  • Depends on: IArchitectureMap (only for Map.RepoToken, which names the .slnx used to find the repo root) and ArchitectureRules.ForwardedJwtBearerAudienceIsFailClosed (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Api/ArchitectureRules.ForwardedJwt.cs:25). The runtime half is JwtAudience.RequireConfigured (MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/Auth/JwtAudience.cs:28), reading the Jwt:Audience key (:20).
  • Concept introduced, scanning text because the defect compiles. The rule doc (ArchitectureRules.ForwardedJwt.cs:10-21) names the failure: a host that read configuration["Jwt:Audience"] ?? "SomeApi" boots with an audience nobody configured and answers every request with a 401 that reads like a token problem. No type-level rule can see that, so the rule reads .cs text, blanks line comments (:72-74), finds each member-access call and reads its argument list to the matching close parenthesis. [Rubric §8, Security & Authorization] and [Rubric §7, Microservices Readiness] apply.
  • Walkthrough
    • The subclass supplies Map (line 13) and MinimumForwardedJwtHosts (line 19), normally the repo's number of extracted service hosts that are not the token issuer; the floor keeps the gate non-vacuous.
    • SourceRoots (line 24) is virtual and defaults to the repo's Source folder, located through ArchitectureMapBase.FindRepoRoot($"{Map.RepoToken}.slnx").
    • The single [Fact] ForwardedJwtBearerHosts_ResolveTheirAudienceFailClosed (line 28) forwards to the rule. A call whose arguments lack JwtAudience.RequireConfigured( is a violation (:46-49), so is one containing ?? (:50-53), a missing root is a violation (:34-38), and finding fewer calls than the floor fails (:58-61).
  • Where it's used: subclassed in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/ForwardedJwtAudienceTests.cs:10). MMCA.Common exercises the rule through its own fitness test (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/ForwardedJwtAudienceFitnessTests.cs).

HandlerConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerConventionTestsBase.cs:8 · Level 5 · abstract class

  • What it is: the CQRS handler convention base: handlers and validators live only in Application, handlers and services do not broker other handlers, and no *Service exceeds the god-class constructor-arity ceiling.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Handlers.cs).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); it adds a MaxServiceConstructorParameters override (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerConventionTestsBase.cs:12, default 8, matching the rule's own default at ArchitectureRules.Handlers.cs:44). [Rubric §6, CQRS & Event-Driven] and [Rubric §1, SOLID] apply; the CQRS decorator pipeline itself is taught in primer §2.
  • Walkthrough: six [Fact]s: Handlers_ShouldResideIn_ApplicationLayer (line 15), Handlers_ShouldNotInject_OtherHandlers (line 18), ApplicationServices_ShouldNotInject_Handlers (line 21), ApplicationServices_ShouldNotExceed_ConstructorArity (line 24, passing the max), Validators_ShouldResideIn_ApplicationLayer (line 27), EventHandlers_ShouldResideIn_ApplicationLayer_AndBeSealed (line 30). The rules sit at ArchitectureRules.Handlers.cs:6, :18, :31, :44, :70 and :82.
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/HandlerConventionTests.cs:13, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/HandlerConventionTests.cs:16, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:79). ConstructorDependencyCountTestsBase is the narrower, per-repo-pinned version of the arity check.

HandlerResultConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerResultConventionTestsBase.cs:16 · Level 5 · abstract class

  • What it is: an opt-in base asserting that every concrete command or query handler's TResult is Result or Result<T> (or a type derived from them), turning a runtime-only constraint into a build-time gate.
  • Depends on: IArchitectureMap and ArchitectureRules (ApplicationLayersDeclareHandlers, CommandHandlersReturnResult, QueryHandlersReturnResult, at MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.HandlerResults.cs:18, :39, :48).
  • Concept introduced, closing a deliberately unconstrained generic. The CQRS interfaces carry no compile-time constraint on TResult (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerResultConventionTestsBase.cs:6-7), but the decorator pipeline's short-circuit paths (feature gate, validation) fabricate failures through ResultFailureFactory, which throws InvalidOperationException at runtime for any non-Result TResult (lines 7-9). A handler with the wrong result type therefore compiles cleanly and only fails when a gate short-circuits it. This base moves that failure to CI. [Rubric §6, CQRS & Event-Driven], [Rubric §14, Testability], and [Rubric §15, Best Practices & Code Quality] apply.
  • Walkthrough: three [Fact]s. ApplicationLayers_DeclareAtLeastOneHandler (line 21) is the non-vacuity guard the doc calls out (lines 12-13): a mis-pinned assembly cannot make the other two pass by finding nothing. CommandHandlers_Return_ResultTypes (line 24) and QueryHandlers_Return_ResultTypes (line 27) delegate to the matching rules, which share the private HandlersReturnResult implementation (ArchitectureRules.HandlerResults.cs:51) and judge the result type by the two full-name constants at :9-10.
  • Why it's built this way: it is opt-in and map-driven like the rest of the family, so a repo adds it next to its other architecture test classes with the same Map and no other wiring.
  • Where it's used: subclassed in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:12), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:6), and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:8).

IdempotencyConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/IdempotencyConventionTestsBase.cs:10 · Level 5 · abstract class

  • What it is: a one-rule delegating base asserting that every POST action in a repo's API layer states, in code, whether a retried request replays the original response (IdempotentAttribute) or deliberately does not (NonIdempotentAttribute with a written reason).
  • Depends on: IArchitectureMap and ArchitectureRules.PostActionsDeclareIdempotencyIntent (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:44).
  • Concept introduced, gating an omission rather than a mistake. The rule's remarks make the argument (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:21-29): POST is the one verb HTTP does not define as idempotent, so a client retrying a timed-out POST cannot know whether the first attempt landed. The framework's Idempotency-Key filter (ADR-017) costs an existing client nothing, because it no-ops for a request with no key header. The only failure mode worth gating is therefore the silent omission: an action that should replay but does not. The gate leaves the opt-out available as long as it is written down. [Rubric §9, API & Contract Design] assesses contract-level retry semantics; [Rubric §29, Resilience & Business Continuity] assesses safe retry behavior.
  • Walkthrough: the subclass supplies Map (line 12); the single [Fact] PostActions_ShouldDeclare_IdempotencyIntent (line 15) forwards to the rule. In the rule, UndeclaredPostActions (line 67) enumerates public instance methods on concrete API-layer controller types, keeps those carrying HttpPostAttribute, and reports the ones carrying neither IdempotentAttribute nor NonIdempotentAttribute (lines 71-74). Attributes are matched by simple type name (HasAttributeNamed, line 80) with inherit: true, so a concrete controller inheriting a framework base action satisfies the rule through the base, and abstract controller types are skipped as declaration sites (remarks, lines 31-36).
  • Caveats / not-in-source: only [HttpPost] is recognised. An action routed through [AcceptVerbs("POST")] or a conventional route is out of scope, which the remarks justify on the grounds that neither appears in this framework or its consumers (lines 38-42).
  • Where it's used: subclassed in all four repos, as IdempotencyConventionTests in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:10), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:3), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:3), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:65, whose comment records that TicketsController's POST actions are all opted in, so the gate is non-vacuous, :61-64).

ImmutabilityTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ImmutabilityTestsBase.cs:8 · Level 5 · abstract class

  • What it is: an immutability convention base: DTOs, command and query messages, domain events, integration events, and value objects expose no public mutable (non-init) setter; value objects are additionally sealed and confined to the Shared layer.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Immutability.cs), which uses RuleHelpers.HasPublicMutableSetter underneath through the shared MutablePropertyViolations projection (ArchitectureRules.Immutability.cs:76).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); the init-only versus mutable distinction is exactly what HasPublicMutableSetter detects via the IsExternalInit modifier. [Rubric §15, Best Practices & Code Quality] and [Rubric §4, DDD] assess immutable contracts and value objects.
  • Walkthrough: five [Fact]s: Dtos_ShouldBe_Immutable (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ImmutabilityTestsBase.cs:13), CommandsAndQueries_ShouldBe_Immutable (line 16), DomainEvents_ShouldBe_Immutable (line 19), IntegrationEvents_ShouldBe_Immutable (line 22), ValueObjects_ShouldBe_ImmutableSealedAndInShared (line 25), backed by ArchitectureRules.Immutability.cs:6, :18, :34, :45 and :56.
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/ImmutabilityTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ImmutabilityTests.cs:3, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:94).

InlineStyleTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/InlineStyleTestsBase.cs:12 · Level 5 · abstract class

  • What it is: a design-system gate: no .razor file under the scanned roots carries an inline style, whether an HTML style= attribute, a MudBlazor Style= parameter, CellStyle= / HeaderStyle=, or any *StyleFunc=.
  • Depends on: ArchitectureRules.RazorMarkupCarriesNoInlineStyles (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.InlineStyles.cs:37). There is no Map on this base; the subclass hands over the repo root directly.
  • Concept introduced, a markup-scanning gate with an allowlist meant to stay empty. [Rubric §20, Design System & Theming] is cited by the rule doc (ArchitectureRules.InlineStyles.cs:8-15): layout and sizing belong in semantic classes in a stylesheet or scoped .razor.css, where one declaration changes a theme, and a genuinely dynamic value belongs in a class toggled from code or a CSS custom property. The matcher requires the name not to follow a word character or hyphen, so CSS text such as list-style and data-style stay out (:18-21). It shares the scanning idea with FormsConventionTestsBase.
  • Walkthrough
    • The subclass supplies RepoRoot (line 15, typically ArchitectureMapBase.FindRepoRoot("MyRepo.slnx")) and MinimumRazorFiles (line 18), the floor that stops a moved root from passing vacuously.
    • MarkupRoots (line 21) defaults to Source under the repo root, and AllowedInlineStyles (line 27) defaults to empty; each entry is a violation prefix relative to its markup root (Folder/File.razor or Folder/File.razor:12).
    • The single [Fact] RazorMarkup_CarriesNoInlineStyles (line 30) forwards to the rule, which reports a missing root (:47-51), applies the allowlist by StartsWith (:58), and fails when fewer files than the floor were scanned (:63-66).
  • Where it's used: subclassed in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/ComponentConventions/InlineStyleTests.cs:12) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/InlineStyleTests.cs:10); Common also pins the detector line by line in InlineStyleFitnessTests.cs.

IntegrationEventContractTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventContractTestsBase.cs:18 · Level 5 · abstract class

  • What it is: a frozen wire-contract guard: it rebuilds the live integration-event contract (one line per event, a key followed by its members in braces, where the key is the event's [EventName] value when it declares one and its full type name otherwise) and compares it to a committed snapshot the subclass supplies, so a renamed, removed, or retyped property, or a new event shipped without its consumer, fails the build.
  • Depends on: IArchitectureMap, ArchitectureRules.BuildIntegrationEventContract (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:66), and ArchitectureAssert.
  • Concept introduced, the snapshot fitness function, and why it compares as a SET. [Rubric §9, API & Contract Design] and [Rubric §7, Microservices Readiness] assess whether cross-service contracts stay stable; because a consumer in another service deserializes by shape, this gate makes any contract change a deliberate, coordinated commit. The member list inside each event's braces is compared as a set rather than a sequence (doc, lines 10-16), and the reasoning is worth reading: JSON carries no member order, so reordering two properties in a record's declaration changes nothing a consumer can observe, and failing the build for it would train the one gate that guards real breakage to be updated by rote. Everything observable stays a failure: a missing member, an extra member, a changed type, and any change to the set of events itself.
  • Walkthrough
    • The subclass supplies Map (line 20) and the committed ExpectedContract snapshot (line 33), whose lines take the shape Key { Prop:Type, ... } (doc, lines 30-31).
    • SnapshotOutputVariable (line 27) names the environment variable MMCA_CONTRACT_SNAPSHOT_OUT. When it is set to a file path, the fact writes the live contract there as ready-to-paste C# string literals, one per line, before comparing (lines 40-44). It only regenerates the snapshot text: the comparison still runs and still fails on drift (doc, lines 23-25).
    • IntegrationEventContracts_ShouldMatch_TheFrozenSnapshot (line 36) builds the actual contract (line 38) and funnels the differences through ArchitectureAssert with a message instructing the author to version the event, coordinate the consumer rollout, and update ExpectedContract in the same commit, and pointing at the environment variable as the way to get the live contract as literals (lines 46-52).
    • Compare (line 63) parses both sides and reports three classes of difference: an event the committed contract declares and the code no longer does (line 70), a NEW event shipped without a consumer rollout or a snapshot update (line 73), and per-event member differences (line 76).
    • CompareMembers (line 90) reports a same-named member with a different type as a retype rather than as an unrelated add and remove, because that is the failure a consumer actually sees: the property still deserializes, into the wrong shape (doc, lines 82-84). Missing and extra members are reported separately (lines 99, 111).
    • Parse (line 123) turns contract lines into an event-to-members map, and keeps a line that does not carry the braced shape whole as an event with no members, so a malformed committed literal surfaces as a mismatch rather than being silently dropped (doc, lines 117-119). Braces are stripped from that key deliberately, because the reported difference is formatted through the shared assertion helper and a stray brace in a reason string is a format hazard (comment, lines 133-134).
    • SplitTopLevel (line 163) splits a member list only on commas outside angle brackets, tracking the bracket depth (line 170), so a multi-argument generic member type such as IReadOnlyDictionary<String, Int32> stays one member rather than being cut in two (doc, lines 158-159); Parse calls it for every braced line (line 142).
  • Where it's used: subclassed in the repos publishing integration events: Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:3), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:3), and Helpdesk, whose one-line snapshot is a compact worked example (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:99, snapshot at :105-108). It complements EventConventionTestsBase and has a synchronous counterpart in ProtoContractTestsBase.

IntegrationEventPayloadPurityTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventPayloadPurityTestsBase.cs:33 · Level 5 · abstract class

  • What it is: a two-rule payload-purity gate for the cross-service async API (ADR-010): an integration event type must ship from a *.Shared assembly, and no property on its wire shape (including a nested payload record) may reach a type declared in a *.Domain assembly.
  • Depends on: IArchitectureMap, ArchitectureRules.IntegrationEventsLiveInSharedAssemblies and .IntegrationEventPayloadsAreDomainFree (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:61).
  • Concept introduced, why the public contract gets its own layer rule and its own domain-leak rule. The class doc draws the line this base enforces (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventPayloadPurityTestsBase.cs:6-10): a domain event is an internal fact inside one bounded context, free to change with the model that raises it; an integration event is the opposite, another service deserializes it by shape and cannot be asked to redeploy because a producer renamed a field. The *.Shared-only rule follows from the module system's own boundary, a consuming module may reference only another module's Shared assembly, so an event declared anywhere else is a contract nobody outside the producer is allowed to reference. The domain-freedom rule closes the leak the layer check alone would miss: publishing an entity or a domain value object on the wire exports the producer's internal model, so an internal refactor becomes a breaking change for every consumer, and fields the producer never meant to share ride along (lines 19-25). [Rubric §9, API & Contract Design] assesses the published shape; [Rubric §10, Messaging & Integration] assesses the cross-service contract discipline this base exists to hold; [Rubric §7, Microservices Readiness] assesses whether that contract keeps an extraction reversible.
  • Walkthrough
    • The subclass supplies Map (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventPayloadPurityTestsBase.cs:35).
    • IntegrationEvents_ShouldShipFrom_SharedAssemblies (line 38) delegates to the shared-assembly rule.
    • IntegrationEventPayloads_ShouldNotExpose_DomainTypes (line 41) delegates to the domain-freedom rule, which walks each event's public properties, including into nested payload records this repo declares, for a reference to a *.Domain-assembly type.
    • Both rules are vacuous for a module-less map (the class doc, lines 28-30): MMCA.Common itself is not a module, so its own integration events are governed by its public API baseline instead of this base.
  • Where it's used: subclassed in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs) and in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs). It complements IntegrationEventContractTestsBase, which freezes the SHAPE of an event once it already lives in a Shared assembly and stays domain-free; this base is the gate that keeps it eligible to be frozen there in the first place.

LayerDependencyTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/LayerDependencyTestsBase.cs:7 · Level 5 · abstract class

  • What it is: the Clean Architecture layer-flow base: fifteen [Fact]s asserting that the map declares the expected layers at all, and that each layer references only layers below it (Domain not on Application, Infrastructure, or API; Application not on Infrastructure or API; Infrastructure not on API; Shared on nothing above it; UI only on Shared).
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Layers.cs).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); this is the runtime half of the two-gate layer enforcement described in ADR-015, the compile-time half being MMCA.Common/Source/Build/MMCA.Common.LayerEnforcement.targets. [Rubric §3, Clean Architecture] is the whole point.
  • Walkthrough
    • Three overridable declarations come first: RequiredLayers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/LayerDependencyTestsBase.cs:16, defaulting to the five core layers Shared, Domain, Application, Infrastructure, Api), RequiredModuleLayers (line 27, defaulting to the same list), and ModuleRequiredLayerOverrides (line 49, empty by default).
    • The per-module override is the design decision worth understanding. Both the base doc (lines 29-38) and the rule doc (ArchitectureRules.Layers.cs:96-104) argue the same point: trimming the DEFAULT list to accommodate one deliberately thin module (a hub module that is Shared plus Application plus Api and nothing else) silently stops enforcing those layers for every OTHER module in the repo, so one thin module would buy blanket permission to forget an assembly anywhere. Naming the exception keeps the default strict and puts the weakening exactly where it is true. The base doc even carries the worked override snippet (lines 40-48).
    • Two non-vacuity [Fact]s guard the rest: LayerMap_DeclaresEveryExpectedLayer (line 52) and LayerMap_ModulesDeclareEveryExpectedLayer (line 55, passing the overrides through to the three-argument ModulesDeclareLayers at ArchitectureRules.Layers.cs:113). Without them a map that forgot an assembly would satisfy every dependency rule by having nothing to check (ArchitectureRules.Layers.cs:64-68).
    • Thirteen forbidden-edge [Fact]s follow, each a one-line delegate onto an ArchitectureRules.Layers.cs method: Domain_ShouldNotDependOn_Application (line 59) through Ui_ShouldNotDependOn_Infrastructure (line 95). The UI trio (lines 89-95) encodes the documented exception that UI depends only on Shared for Blazor WASM compatibility.
  • Where it's used: subclassed in all four repos (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyTests.cs:9, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Layering/LayerDependencyTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/LayerDependencyTests.cs:3, and Helpdesk at MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:5). NamespaceCycleTestsBase is the finer-grained companion that looks inside a layer assembly, and ModuleIsolationTestsBase the one that looks across modules.

LifetimeTokenConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LifetimeTokenConventionTestsBase.cs:13 · Level 5 · abstract class

  • What it is: a component-lifetime gate: no .razor or .razor.cs file under the scan root reads _cts.Token (or _cts!.Token) directly; components read it through _cts.LifetimeToken() instead.
  • Depends on: ArchitectureRules.ComponentsReadTheirTokenThroughLifetimeToken (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.LifetimeTokens.cs:28), and the runtime helper in MMCA.Common/Source/Presentation/MMCA.Common.UI/Common/ComponentLifetimeExtensions.cs.
  • Concept introduced, closing a disposal race with a text rule. The rule doc (ArchitectureRules.LifetimeTokens.cs:8-17) explains: reading Token off a disposed CancellationTokenSource throws ObjectDisposedException, so a load or handler that resumed after the user navigated away crashed the circuit instead of stopping. LifetimeToken() returns an already-cancelled token once Dispose has cancelled the source. The one permitted direct read is inside such a property's own expression-bodied declaration (:67-71). [Rubric §15, Best Practices & Code Quality] and [Rubric §14, Testability] apply.
  • Walkthrough
    • The subclass supplies ScanRoot (line 16, typically the repo's Source/Modules) and MinimumCodeBehindFiles (line 19), the floor on .razor.cs files reached.
    • ScanAllCodeFiles (line 25) is virtual and false by default; set it to also scan plain .cs files, for component bases written as plain classes (:43-45).
    • The single [Fact] Components_ReadTheirTokenOnlyThroughLifetimeToken (line 28) forwards to the rule, which fails on a missing root (:34-37) or on fewer .razor.cs files than the floor (:48-52).
  • Where it's used: subclassed in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/ComponentConventions/LifetimeTokenConventionTests.cs:12) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/LifetimeTokenConventionTests.cs:10).

LocalizationResourceTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizationResourceTestsBase.cs:10 · Level 5 · abstract class

  • What it is: an opt-in translation-coverage gate (ADR-027): a repo that ships localized .resx resources subclasses this and lists its required cultures; the build fails if any base .resx under Source/ lacks a complete, non-empty sibling for a required culture.
  • Depends on: ArchitectureRules.ResourceTranslationsAreComplete (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.Localization.cs:23), [Fact]. There is no Map on this base; it scans Source/ directly through the rule.
  • Concept introduced, a coverage fitness function for i18n. [Rubric §27, i18n] assesses translation completeness; this gate ensures a new English string can never ship without its translation (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizationResourceTestsBase.cs:3-9).
  • Walkthrough: the subclass supplies RequiredCultures (line 13, for example a single Spanish entry) and optionally MinimumBaseResources (line 21, a non-vacuity floor whose default of 0 skips the guard). The single [Fact] Translations_AreComplete_ForEveryRequiredCulture (line 24) passes both to the rule, which reads each .resx string entry with XDocument (ArchitectureRules.Localization.cs:81) and skips build output (:75).
  • Why it's built this way: single-locale repos need not subclass it (the rule is vacuous for an empty list). It pairs with LocalizedTextConventionTestsBase: this gate keeps the extracted resources translated, that gate keeps literals out of markup.
  • Where it's used: subclassed in MMCA.Common as LocalizationResourceTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizationResourceTests.cs:12) and in Store, ADC, and Helpdesk under the name TranslationCompletenessTests (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/TranslationCompletenessTests.cs:13, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/TranslationCompletenessTests.cs:12); Helpdesk's requires Spanish with a floor of 3, covering the two page resx pairs and the Tickets error resources (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:130, cultures at :132, floor at :135).

LocalizedTextConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizedTextConventionTestsBase.cs:14 · Level 5 · abstract class

  • What it is: a localized-text convention gate (ADR-027): user-visible literals must not be hard-coded in .razor or .razor.cs under Source/ (snackbar messages, page Title properties, page-title markup, breadcrumb labels) but resolve through IStringLocalizer resources.
  • Depends on: IArchitectureMap, ArchitectureMapBase.FindRepoRoot, ArchitectureRules.UserVisibleTextIsLocalized (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.LocalizedText.cs:62), which drives six source-generated regexes (:10-32).
  • Concept: cross-references the markup-scanning gate idea from FormsConventionTestsBase. [Rubric §27, i18n] assesses that visible strings follow the selected language.
  • Walkthrough: the subclass supplies Map (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizedTextConventionTestsBase.cs:16) and optionally MinimumScannedFiles (line 21, default 1) and AllowedFiles (line 28, whole-file exemptions; the preferred exemption is a per-line i18n: allow comment, per the class doc at lines 8-11). UserVisibleText_IsLocalized (line 31) resolves the repo root and delegates to the rule with the Source directory, the allowlist, and the floor (lines 33-37).
  • Where it's used: subclassed in all four repos (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:11, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:14, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:12, and Helpdesk at MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:142, which sets a floor of 5 against the seed's 8 razor files at :147). It pairs with LocalizationResourceTestsBase.

MicroserviceExtractionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/MicroserviceExtractionTestsBase.cs:8 · Level 5 · abstract class

  • What it is: a transport-boundary base for the modular-monolith to microservices path: MassTransit, gRPC, and Protobuf must never leak into Domain, Application, or Shared, so a module behaves identically in-process or extracted and the split stays reversible.
  • Depends on: IArchitectureMap, ArchitectureRules (TransportDoesNotLeakIntoCoreLayers, ArchitectureRules.Transport.cs:19, over the shared TransportDependencies list at :11).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); the extraction invariant (application and domain code talks to abstractions, transport choices live at the edges) is the ADR-006 / ADR-007 / ADR-008 story the doc cites (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/MicroserviceExtractionTestsBase.cs:3-7). [Rubric §7, Microservices Readiness] assesses exactly this reversibility. ServiceContractPurityTestsBase guards the same boundary from the contract side.
  • Walkthrough: one [Fact] CoreLayers_ShouldNotDependOn_Transport (line 13) delegating to ArchitectureRules.TransportDoesNotLeakIntoCoreLayers(Map).
  • Where it's used: subclassed in all four repos (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:11, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:3, and Helpdesk at MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:111).

ModuleIsolationTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleIsolationTestsBase.cs:8 · Level 5 · abstract class

  • What it is: a modular-monolith boundary base: a module must not reach another module's internal layers; cross-module communication goes only through the Shared (contract) layer. It is vacuous for single-module or module-less repos.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Modules.cs), which uses OtherModuleNamespaces to compute the forbidden targets through the private ModuleLayerIsolated (:98).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase). [Rubric §5, Vertical Slice] and [Rubric §7, Microservices Readiness] assess module autonomy. The IModule system is taught in Group 14.
  • Walkthrough: seven [Fact]s. Six name a specific pair worth its own failure message: ModuleDomains_ShouldBe_Isolated (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleIsolationTestsBase.cs:13), ModuleApplications_ShouldBe_Isolated (line 16), ModuleInfrastructures_ShouldBe_Isolated (line 19), ModuleApis_ShouldBe_Isolated (line 22), plus the two cross-layer reach rules ModuleDomains_ShouldNotReach_OtherModuleInfrastructures (line 25) and ModuleApplications_ShouldNotReach_OtherModuleInfrastructures (line 28).
    • The seventh, ModuleInternalLayers_ShouldNotReach_OtherModuleInternalLayers (line 36), closes the coverage the other six leave open: it runs the complete cross product of six source layers (Domain, Application, Infrastructure, Api, Shared and Ui) against every other module's four internal layers, Domain, Application, Infrastructure and Api (ArchitectureRules.Modules.cs:52, the loop at :54-63). The rule doc names the gap it fixes precisely (:31-41): a per-module layer rule only forbids the SAME module's higher layers, and the compile-time layer guard only knows about framework references, so a cross-module project reference such as one module's Domain onto another's Application passed every gate while compile-coupling two modules that ADR-007/008 promise can be extracted separately.
    • Shared and Ui are covered as SOURCES only, not as targets (:43-49): the per-layer Shared and Ui rules forbid only the same module's internal layers, so a module's Shared or Ui reaching another module's Domain, Application, Infrastructure or Api was unchecked, while a module's UI composing another module's UI, and any layer consuming another module's Shared contracts, are real, intended arrangements in the shipped apps (Shared to Shared has its own rule, ModuleSharedAreIsolated).
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Layering/ModuleIsolationTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/ModuleIsolationTests.cs:3, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:15, where the single-module seed makes it deliberately vacuous but future-proof).

NamespaceCycleTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/NamespaceCycleTestsBase.cs:15 · Level 5 · abstract class

  • What it is: an acyclicity gate one level below the layer rules: the top-level namespaces inside each layer assembly a map declares must form a directed acyclic graph.
  • Depends on: IArchitectureMap and ArchitectureRules.NamespacesHaveNoDependencyCycles (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:45).
  • Concept introduced, the namespace cycle as an early extraction warning. The doc frames it plainly (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/NamespaceCycleTestsBase.cs:4-8): a namespace cycle is the first visible symptom of a folder pair that has grown into one tangled unit, where neither half can be read, tested, or lifted into its own service without the other. That is the granularity the two coarse rules (LayerDependencyTestsBase and ModuleIsolationTestsBase) cannot see inside of. [Rubric §7, Microservices Readiness] assesses extractability; [Rubric §34, Architecture Governance & Documentation] assesses coupling that makes a folder unreadable on its own.
  • Walkthrough
    • The subclass supplies Map (line 18) and optionally AllowedCycleNamespaces (line 26), fully-qualified nodes whose cycle is accepted by design.
    • The allowance rule is the interesting part: a cycle is skipped only when every namespace on its reported path appears in the list, so an allowance can never hide a NEW cycle that merely touches an accepted namespace (doc, lines 20-25). The rule enforces that over the whole strongly connected component, not just the rendered shortest path.
    • The single [Fact] Namespaces_ShouldNotHave_DependencyCycles (line 29) forwards both to the rule, which builds a per-assembly namespace graph from base types, interfaces, field, property, method return and parameter types, and attribute types, with generic arguments and array or by-ref element types expanded (ArchitectureRules.Cycles.cs:84, :163, :174, :215, :228), then finds the strongly connected components (:253) and renders the shortest cycle through each (:330).
  • Caveats / not-in-source: the rule is signature-level reflection and blind to method bodies, because this rule carries no IL dependency, so a green result means "no STRUCTURAL cycle", not "no coupling" (doc, lines 9-13). Compiler-generated types are skipped deliberately so the answer stays a signature-level one.
  • Where it's used: subclassed today only in MMCA.Common, as NamespaceCycleTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleTests.cs:9). Its AllowedCycleNamespaces records the single accepted tangle in the framework, MMCA.Common.Infrastructure to Settings to Persistence and back (:39-44), with each of the three edges justified in the doc comment above it (:13-38).

NamingConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/NamingConventionTestsBase.cs:8 · Level 5 · abstract class

  • What it is: a naming and sealing convention base across the CQRS plus DDD building blocks: handlers, command and query messages, validators, DTOs, domain events, invariants, EF configurations, specifications, and repositories each follow their established suffix and sealing convention.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Naming.cs), which uses RuleHelpers.SimpleName to match suffixes on generic types and funnels the two message rules through the shared AssertMessageSuffix (:130).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase). [Rubric §15, Best Practices & Code Quality] and [Rubric §15, Best Practices & Code Quality] assess consistent, discoverable naming.
  • Walkthrough: ten [Fact]s: Handlers_ShouldBeSealed_WithHandlerSuffix (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/NamingConventionTestsBase.cs:13), Commands_ShouldHave_CommandOrRequestSuffix (line 16), Queries_ShouldHave_QuerySuffix (line 19), Validators_ShouldHave_ValidatorOrRulesSuffix (line 22), SharedDtos_ShouldHave_DtoOrLookupSuffix (line 25), DomainEvents_ShouldBeSealed_InDomainEventsNamespace (line 28), InvariantClasses_ShouldBe_Static (line 31), EfConfigurations_ShouldBeSealed_WithConfigurationSuffix (line 34), Specifications_ShouldBeSealed_WithSpecificationSuffix (line 37), Repositories_ShouldHave_RepositorySuffix (line 40), backed by the ten rules at ArchitectureRules.Naming.cs:21, :33, :37, :41, :54, :67, :79, :91, :103 and :116.
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/NamingConventionTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/NamingConventionTests.cs:3, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:74).

PiiConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/PiiConventionTestsBase.cs:19 · Level 5 · abstract class

  • What it is: a GDPR/CCPA right-to-erasure base (ADR-005) that checks both directions. Forward: any domain entity that declares a PiiAttribute-marked property must implement IAnonymizable, so it has an erasure path. Reverse: whatever Anonymize() actually overwrites must carry [Pii], checked behaviorally over a per-repo sample table, plus a coverage fact that every IAnonymizable module entity has a sample.
  • Depends on: IArchitectureMap, ArchitectureRules.EntitiesWithPiiImplementAnonymizable (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Governance.cs:11).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); the [Pii] plus IAnonymizable soft-delete-versus-erasure model is taught in Group 02 (ADR-005). [Rubric §30, Compliance / Privacy / Data Governance] and [Rubric §11, Security] assess erasure discipline. The IAnonymizable contract is matched by its full name (ArchitectureRules.Governance.cs:7) so a same-named local interface cannot satisfy the rule (comment, lines 5-6), while the [Pii] marker is matched by simple name (:50).
  • Walkthrough
    • The optional virtual AnonymizableSamples (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/PiiConventionTestsBase.cs:31) maps each IAnonymizable entity type to a factory that builds it through its own domain factory, populated so every erasable field starts at a value the erasure has to move. It is typed object because this package takes no framework reference, and it defaults to an empty table (NoSamples, line 21), so a repo with no IAnonymizable module entity passes both reverse facts unchanged.
    • EntitiesWithPiiProperties_ShouldImplement_IAnonymizable (line 34) delegates to the forward rule, which scans every Domain-layer type for a [Pii]-marked public instance property (HasPiiProperty, ArchitectureRules.Governance.cs:48) and reports the ones that do not implement the contract.
    • EveryAnonymizableDomainEntity_IsCoveredBySample (line 37) feeds the types of Map.ModuleDomain() and the sample table to ArchitectureRules.AnonymizableEntitiesAreCoveredBySamples (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Anonymization.cs:12). Only MODULE Domain assemblies are scanned (class remarks): the framework's own anonymizable types are checked once, in MMCA.Common's own subclass, rather than re-sampled in every consumer.
    • PropertiesOverwrittenByAnonymize_MustCarryPii (line 43) calls ArchitectureRules.PropertiesOverwrittenByAnonymizeCarryPii (ArchitectureRules.Anonymization.cs:49) over the samples, the reverse direction: a field that erasure overwrites but that is not marked [Pii] is invisible to the forward rule and the personal-data inventory.
  • Where it's used: subclassed in all four repos (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiConventionTests.cs, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Privacy/PiiConventionTests.cs:25, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Privacy/PiiConventionTests.cs, and Helpdesk at MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:116). The rules are also exercised by MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/AnonymizationRulesTests.cs (AnonymizationRulesTests).

PiiInventoryTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Privacy/PiiInventoryTestsBase.cs:33 · Level 5 · abstract class

  • What it is: a personal-data inventory base that keeps a repo's published PRIVACY.md and its [Pii]-marked code in step, in both directions. The subclass names the assemblies and the policy path; the base reads the file and runs two rules.
  • Depends on: PiiAttribute as the marker the rules look for, and ArchitectureRules.PiiMembersHaveInventoryRows (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.PiiInventory.cs:23) and .PiiInventoryRowsMapToPiiMembers (:61). There is no Map: the inputs are an assembly list and Markdown text.
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase) and the companion PiiConventionTestsBase. The policy holds an inventory table whose header carries Member | Purpose | Lawful basis | Retention, one row per Type.Property (ArchitectureRules.PiiInventory.cs:6, :12, :49-50). Code to document: every [Pii] property needs a row with the three cells filled, so personal data is never held without a stated reason, basis or end date. Document to code: every row must name a live [Pii] member, so an orphan row (renamed, removed or quietly unmarked) cannot keep promising something the code no longer marks. [Rubric §30, Compliance / Privacy / Data Governance] assesses this.
  • Walkthrough
    • Two abstract members: Assemblies (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Privacy/PiiInventoryTestsBase.cs:36) and PrivacyPolicyPath (line 39, the absolute path of PRIVACY.md).
    • PiiMembers_ShouldHave_CompleteInventoryRows (line 42) forwards to the code-to-document rule and InventoryRows_ShouldMapTo_LivePiiMembers (line 46) to the document-to-code rule.
    • The private ReadPrivacyPolicy (line 49) asserts the file exists, with the path in the failure message, before reading it, so a moved policy fails loudly instead of passing on empty text.
  • Where it's used: subclassed in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Privacy/PiiInventoryTests.cs) and exercised against fixtures in MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/PiiInventoryTestsBaseTests.cs (PiiInventoryTestsBaseTests).

ProtoContractTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ProtoContractTestsBase.cs:19 · Level 5 · abstract class

  • What it is: the frozen wire-contract guard for a repo's gRPC .proto files, the synchronous counterpart to IntegrationEventContractTestsBase. The subclass names its solution file, its proto files, and the committed snapshot; the base rebuilds the live contract and reports the diff.
  • Depends on: ArchitectureRules.ProtoContractsMatchFrozenList (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:38), which resolves paths through ArchitectureMapBase.FindRepoRoot (line 45) and parses through ProtoScope / ProtoScopeKind. There is no Map on this base: it reads committed .proto text, not assemblies.
  • Concept introduced, pinning a contract by what actually crosses the wire. The rule's remarks are explicit about the two halves of the decision (ArchitectureRules.Protos.cs:20-36). Pinned: the file's package, every service with each rpc (name, request type, response type, and both streaming flags), every message field (name, declared type, label, and field NUMBER), and every enum value and number, with nested messages and enums under their qualified name. Deliberately not pinned: syntax, import lines, and option declarations including csharp_namespace, because none of them changes a byte on the wire; including them would fail the gate on edits that break nobody, which is the fastest way to teach a team to update the snapshot without reading it. [Rubric §9, API & Contract Design] and [Rubric §7, Microservices Readiness] assess contract stability across a service boundary.
  • Walkthrough
    • Three abstract members: SolutionFileName (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ProtoContractTestsBase.cs:22, marking the repo root), ProtoFiles (line 25, repo-root-relative paths), and FrozenProtoContracts (line 30, the committed snapshot).
    • The single [Fact] ProtoContracts_ShouldMatch_TheFrozenSnapshot (line 33) forwards all three to the rule.
    • The comparison is two-directional: AssertProtoContract (ArchitectureRules.Protos.cs:59) reports lines present in the protos but not frozen with a plus marker and frozen lines missing from the protos with a minus marker (lines 67-70), then funnels both through ArchitectureAssert. A .proto path that does not exist yields one explicit missing-file line rather than silently contributing nothing (line 96), and the whole snapshot is distinct-and-ordered so the comparison is stable (line 103).
  • Why it's built this way: the remarks say the snapshot is meant to be regenerated deliberately by printing ArchitectureRules.BuildProtoContract(...) for the same files, as part of the commit that changes the contract, never edited to make a red test go green (Bases/ProtoContractTestsBase.cs:12-17). MMCA.Common ships no .proto of its own (it supplies the gRPC plumbing, not the contracts), so the framework does NOT subclass this (lines 9-11).
  • Where it's used: subclassed in the two repos with *.Contracts projects: ADC, pinning seven protos across four Contracts projects (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/ProtoContractTests.cs:3, file list at :11-17), and Store, pinning three (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Contracts/ProtoContractTests.cs:9, file list at :15-17). MMCA.Common exercises the underlying rule instead, from fixture protos including a deliberately drifted copy, in ProtoContractFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ProtoContractFitnessTests.cs:14).

QueryHandlerReadRepositoryTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/QueryHandlerReadRepositoryTestsBase.cs:15 · Level 5 · abstract class

  • What it is: a one-rule delegating base asserting that a query handler asks for the narrow read surface: every IQueryHandler<,> implementation in the map's Application assemblies must call IUnitOfWork.GetReadRepository, never IUnitOfWork.GetRepository (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.QueryHandlerRepositories.cs:11-16).
  • Depends on: IArchitectureMap, ArchitectureRules.QueryHandlersUseReadRepositories (ArchitectureRules.QueryHandlerRepositories.cs:37), which walks the map's Application assemblies through CallGraphIndex.
  • Concept introduced, an IL-level call-site rule rather than a signature rule. A read handler calling into the write repository is invisible to reflection, a call site only exists in the method body, so the rule reads IL through the Mono.Cecil NetArchTest already carries and matches the callee by name and declaring type full name against the constant IUnitOfWork full name and GetRepository method name (ArchitectureRules.QueryHandlerRepositories.cs:7-8, :20-24). The write repository exposes the mutation surface (add, remove, tracked reads) and exists only for aggregate roots, so a query reading a child entity cannot use it at all; the rule closes that misuse at the call site instead of trusting convention. [Rubric §4, DDD] and [Rubric §15, Best Practices & Code Quality] assess this narrow-interface discipline.
  • Walkthrough
    • The subclass supplies Map (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/QueryHandlerReadRepositoryTestsBase.cs:15) and optionally AllowedHandlers (line 20, empty by default, an adoption ratchet for existing offenders by type full name or namespace prefix).
    • The single [Fact] QueryHandlers_ShouldUseReadRepositories (line 26) delegates to the rule.
    • In the rule, every method of each IQueryHandler implementation is read together with its compiler-generated nested types, which is where an async state machine or a lambda display class actually lives (ArchitectureRules.QueryHandlerRepositories.cs:20-24); a call made in a helper type the handler delegates to is outside the handler and is not reported (lines 27-29). An abstract handler base is scanned like a concrete handler, so a base that reaches for the write repository is reported once, on the base (line 29). A non-vacuity guard fails if the map's Application assemblies contain no IQueryHandler implementation at all (lines 59-60).
  • Where it's used: subclassed in ADC with an empty allowlist (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/QueryHandlerReadRepositoryTests.cs:9-11, adopted because every query handler already reads through the narrow interfaces) and in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/Repositories/QueryHandlerReadRepositoryTests.cs). It complements the delegating-base shape shared with AggregateConventionTestsBase and ArchitectureRules, and pairs conceptually with NamingConventionTestsBase's Repositories_ShouldHave_RepositorySuffix fact in enforcing repository conventions from two different angles.

ServiceContractPurityTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ServiceContractPurityTestsBase.cs:20 · Level 5 · abstract class

  • What it is: a one-rule delegating base asserting that every type marked with the framework's ServiceContractAttribute stays free of the producing service's Domain, Application, and Infrastructure, so a consumer can take the contract package without taking the producer's internals.
  • Depends on: IArchitectureMap and ArchitectureRules.ServiceContractsDoNotDependOnServiceInternals (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:32).
  • Concept introduced, the attribute-driven ratchet. Two design choices are worth reading closely, and both are recorded in the base's remarks (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ServiceContractPurityTestsBase.cs:8-18). First, the rule is attribute-driven rather than Layer.Contracts-driven, because no repo registers that layer in its map today, so a layer-iterating rule would pass vacuously forever; scanning every registered assembly for the marker enforces the invariant wherever the contract types live. Second, the base is honest about the vacuous case: a repo that has marked no type yet asserts nothing, and the value is the ratchet, the invariant bites from the first marked type onward with no test left to remember. [Rubric §7, Microservices Readiness] assesses whether an extraction stays reversible; a contract that leaks a domain entity, a handler abstraction, or a persistence type forces every consumer to depend on the producer's internals, which is what makes an extraction irreversible (ArchitectureRules.Contracts.cs:13-19). [Rubric §9, API & Contract Design] assesses the published surface itself (ADR-007).
  • Walkthrough
    • The subclass supplies Map (line 22); the single [Fact] ServiceContracts_ShouldNotDependOn_ServiceInternals (line 25) forwards to the rule.
    • In the rule, ServiceInternalNamespaces (ArchitectureRules.Contracts.cs:120) collects the distinct, ordered root namespaces of every Domain, Application and Infrastructure ref in the map (lines 122-128) and the rule returns immediately when that set is empty (lines 34-38).
    • It then loops map.Layers and runs NetArchTest per assembly with MeetCustomRule(CarriesServiceContractAttribute) as the selector (lines 40-47). That predicate (line 132) reads Mono.Cecil.TypeDefinition custom attributes and matches the public constant ServiceContractAttributeFullName (line 10) by string, the same zero-reference stance the rest of the library takes; a reflection twin for the loaded-type rules sits at line 143.
    • Because the rule scans every registered assembly, a marked type that lives inside a Domain, Application or Infrastructure assembly fails by construction, and the remarks say that is the intent: a published contract belongs in a *.Contracts or Shared assembly (ArchitectureRules.Contracts.cs:25-29).
  • Where it's used: subclassed once per repo, in all four: MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ServiceContractPurityTests.cs:11), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/ServiceContractPurityTests.cs:9), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Layering/ServiceContractPurityTests.cs:9), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ServiceContractPurityTests.cs:9); see ServiceContractPurityTests. The ratchet has now engaged in the two multi-module apps: ADC marks five contract interfaces (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/Events/IEventLiveValidationService.cs, its Sessions/ISessionBookmarkValidationService.cs sibling, MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/UserSessionBookmarks/IBookmarkCountService.cs, MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/Exports/IUserEngagementExportService.cs, MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Shared/Users/IAttendeeQueryService.cs, plus MMCA.ADC/Source/Modules/Notification/MMCA.ADC.Notification.Shared/UserNotifications/IUserNotificationExportService.cs) and Store marks three (MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.Shared/Products/IProductVariantService.cs, MMCA.Store/Source/Modules/Identity/MMCA.Store.Identity.Shared/Customers/ICustomerService.cs, MMCA.Store/Source/Modules/Sales/MMCA.Store.Sales.Shared/Exports/IUserSalesExportService.cs), all of them in a module's Shared layer, which is exactly where the rule says a contract belongs. MMCA.Common and Helpdesk mark none, so their runs remain ratchets (MMCA.Common/Source/Core/MMCA.Common.Shared/Abstractions/ServiceContractAttribute.cs:12-14). It complements, and does not replace, ContractImplementationTestsBase, the transport-purity rule behind MicroserviceExtractionTestsBase, and the layer-purity rules behind LayerDependencyTestsBase (ADR-015).

SharedLayerTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/SharedLayerTestsBase.cs:7 · Level 5 · abstract class

  • What it is: a Shared (contract) layer base: a module's Shared is contracts-only, so it must not depend on its own internal layers, on another module's Shared, or on EF Core.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Modules.cs:67, :80, :84).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase). [Rubric §3, Clean Architecture] and [Rubric §5, Vertical Slice] assess a clean contract boundary a would-be extracted consumer can reference safely.
  • Walkthrough: three [Fact]s: ModuleShared_ShouldNotDependOn_OwnInternalLayers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/SharedLayerTestsBase.cs:12), ModuleShared_ShouldBe_Isolated (line 15), ModuleShared_ShouldNotDependOn_EntityFrameworkCore (line 18).
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Layering/SharedLayerTests.cs:3, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/SharedLayerTests.cs:3, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:20).

SliceCohesionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/SliceCohesionTestsBase.cs:10 · Level 5 · abstract class

  • What it is: a vertical-slice cohesion base: a use-case slice keeps its command or query, its handler, and its validator together in one namespace, so a feature is a cohesive unit rather than spread across horizontal Handlers/ and Validators/ folders.
  • Depends on: IArchitectureMap, ArchitectureRules (ArchitectureRules.Slices.cs:40, :55, with the namespace comparison at :103).
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase). [Rubric §5, Vertical Slice] assesses feature cohesion. The doc notes MMCA.Common scopes to its Notifications slices while ADC and Store scope to their module Application layers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/SliceCohesionTestsBase.cs:6-8).
  • Walkthrough: two [Fact]s: Handlers_ShouldBeCoLocatedWith_TheirContracts (line 15) and Validators_ShouldBeCoLocatedWith_TheirContracts (line 19). Both rules resolve the type a handler or validator is written for (HandlerContract, ArchitectureRules.Slices.cs:88; ValidatedType, :85) and only compare when the contract is a concrete type in the same assembly (:100), so a handler written against a framework abstraction is out of scope rather than misreported.
  • Where it's used: subclassed in all four repos (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionTests.cs:10, MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/SliceCohesionTests.cs:9, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/SliceCohesionTests.cs:8, and Helpdesk at MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:121).

SoftDeleteEnforcementTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SoftDeleteEnforcementTestsBase.cs:17 · Level 5 · abstract class

  • What it is: a soft-delete enforcement gate: EF Core's row-erasing members may only be called from the purge and erasure types a repo names in an allowlist. Everything else deletes by setting IsDeleted = true.
  • Depends on: IArchitectureMap and ArchitectureRules.HardDeletesOnlyInAllowedTypes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:62), which reads IL with Mono.Cecil.
  • Concept introduced, enforcing the framework's deletion model at the call site. Soft delete is the default taught in the primer, and what it buys is that a deleted row survives for audit, restore and erasure accounting; a hard delete bypasses all of it (rule doc, ArchitectureRules.SoftDelete.cs:26-35). [Rubric §8, Data Architecture] assesses the deletion model; [Rubric §30, Compliance / Privacy / Data Governance] assesses erasure accounting (ADR-005); [Rubric §13, Observability & Operability] applies because an audit trail with a silently removed row is not an audit trail.
  • Walkthrough
    • The subclass supplies Map (line 19) and AllowedHardDeleteTypes (line 27, empty by default, which bans hard deletes outright). The single [Fact] HardDeletes_ShouldOnlyOccurIn_AllowedPurgeTypes (line 30) forwards both.
    • The scope is precise. Four member names are watched (HardDeleteMemberNames, ArchitectureRules.SoftDelete.cs:10-11), and the two common ones (Remove, RemoveRange) are additionally scoped to four EF entity-set declaring types (EntitySetTypeNames, :18-24), which is what keeps every unrelated Remove on a dictionary, a list or a cache out of the rule.
    • An allowlist entry is a type full name or a namespace prefix, and it also covers the compiler-generated async state machines and closures nested inside the type it names (doc, :44-49). Matching is ordinal and case-sensitive.
    • The stated limit (:50-55) is the one worth remembering: the rule sees only direct calls compiled into the scanned assemblies, so a hard delete reached through an interface the repo owns is caught at the implementing type, not at the caller. That implementation belongs on the allowlist and the abstraction stays free to be used.
  • Why it's built this way: the doc frames the allowlist exactly as the cascade and domain-throw rules frame theirs (lines 10-15): adoption means moving each reported type into a fix or into the list with a comment, which turns "we soft-delete, mostly" into a reviewed inventory of every place that does not.
  • Where it's used: subclassed in all three repos that carry persistence plus the framework. MMCA.Common runs the rule back on its own author, naming four framework erasers individually rather than exempting a namespace, so a fifth would still fail and get reviewed: the repository's set-based delete escape hatch, outbox and inbox retention, audit-trail retention, and refresh-session retention (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:19, entries and per-entry reasoning at :24-47). Store allowlists the same four framework types plus its own image-blob side table (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:15, rationale at :8-13), and ADC subclasses it the same way (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:15). The framework subclass is what keeps the consumers' lists honest, since a new framework eraser would otherwise land in the packages and fail downstream instead of in the repo that wrote it.

SortableColumnConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/SortableColumnConventionTestsBase.cs:16 · Level 5 · abstract class

  • What it is: a MudDataGrid sorting gate: a column marked sortable must be a PropertyColumn, never a TemplateColumn.
  • Depends on: ArchitectureRules.SortableGridColumnsUsePropertyColumn (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.Markup.cs:42), [Fact]. There is no Map: the subclass supplies absolute directory roots directly.
  • Concept introduced, gating a defect that compiles and renders. Server-side sort reads the bound property off the column, and a TemplateColumn has none, so marking one sortable renders a header that toggles an arrow without ordering the data (doc, lines 4-8). The base doc says plainly why this belongs in a fitness function rather than a review: the defect compiles, renders, and is invisible until someone checks the order. [Rubric §24, Forms / Validation / UX Safety] assesses whether an interactive control does what it appears to do; [Rubric §18, UI Architecture] assesses using the right grid primitive for the job.
  • Walkthrough
    • The subclass supplies MarkupRoots (line 24), absolute directory paths scanned recursively for *.razor. The doc tells the author to build them from ArchitectureMapBase.FindRepoRoot so the scan is independent of the runner's working directory (lines 18-23).
    • The single [Fact] SortableColumns_ShouldNotBe_TemplateColumns (line 27) forwards to the rule.
    • The rule scans .razor TEXT rather than IL, because the defect lives in markup that compiles perfectly (rule doc, ArchitectureRules.Markup.cs:16-19). It reads each TemplateColumn tag to its closing bracket through quoted attribute values (TagEnd, :142), so attribute order, wrapped lines and a generic type argument are all handled, and it accepts the quoted, unquoted and expression-prefixed spellings of a true Sortable value while leaving a value bound to a field or property alone, since that value is not knowable from the markup (HasSortableTrue, :171; IsBoundToTrue, :198).
    • Commented-out markup is blanked before the scan, with spaces replacing the comment and newlines surviving (BlankRazorComments, :82), so a commented example neither fails the gate nor shifts the reported line numbers (doc, :31-35).
    • A missing root is reported as a violation rather than silently skipped (line 52), so a path typo cannot make the gate vacuous.
  • Where it's used: subclassed in ADC and Store, both pointing at the whole Source tree so the scan covers the three module UI projects and both web heads, and both freezing an already-clean state (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/SortableColumnConventionTests.cs:11, root at :16; MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/SortableColumnConventionTests.cs:11, root at :16).

SpecificationConventionTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SpecificationConventionTestsBase.cs:10 · Level 5 · abstract class

  • What it is: an opt-in base for the Specification pattern in polyglot / database-per-service repos: it guarantees no specification filters by navigating to another entity, which would not translate when that entity lives in a different physical source.
  • Depends on: IArchitectureMap, ArchitectureRules.SpecificationsDoNotNavigateToOtherEntities (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:24), backed by CrossEntityNavigationFinder.
  • Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); the Specification<TEntity, TIdentifierType> pattern is taught in Group 03. [Rubric §8, Data Architecture] assesses engine-portable query design.
  • Walkthrough: one [Fact] Specifications_ShouldNotNavigate_ToOtherEntities (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SpecificationConventionTestsBase.cs:16) delegating to the rule, which discovers specifications by base-type full-name prefix across the Application and Domain layers (ArchitectureRules.Specifications.cs:30-33) and resolves each one's entity type from the generic base (SpecificationEntityType, :79). The doc notes single-engine repos need not subclass it (lines 4-8).
  • Where it's used: subclassed in Store, ADC, and Helpdesk (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/SpecificationConventionTests.cs:7, MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/SpecificationConventionTests.cs:8, MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:29). MMCA.Common exercises the same rule from the other side, through SpecificationFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:13) and its private SpecTestMap fixture (:40).

StronglyTypedIdTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/StronglyTypedIdTestsBase.cs:9 · Level 5 · abstract class

  • What it is: an opt-in fitness base for the strongly typed identifier pattern (ADR-115): it guarantees every IStronglyTypedId wrapper in the repo is a readonly record struct whose only instance state is the wrapped Value, the shape the framework's EF value converter, JSON converter, and comparer are written against.
  • Depends on: IArchitectureMap, ArchitectureRules.StronglyTypedIdsAreReadonlyRecordStructs (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.StronglyTypedIds.cs:34).
  • Concept: sibling to the other opt-in *TestsBase fixtures (compare SpecificationConventionTestsBase above); strongly typed identifiers are the alternative to the alias-based {Entity}IdentifierType globals, on the same opt-in footing smart enumerations sit on.
  • Walkthrough: one [Fact] StronglyTypedIds_ShouldBe_ReadonlyRecordStructs (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/StronglyTypedIdTestsBase.cs:15) delegating to the rule, which scans every distinct layer assembly on the map for types implementing the open IStronglyTypedId interface and asserts each is a struct, is readonly, is a record, and declares no state beyond Value (ArchitectureRules.StronglyTypedIds.cs:38-59).
  • Where it's used: subclassed by StronglyTypedIdConventionTests in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/StronglyTypedIdConventionTests.cs) and by its ADC counterpart (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/StronglyTypedIdTests.cs).

SubstitutabilityTestsBase

MMCA.Common.Testing.Architecture · MMCA.Common.Testing.Architecture · MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SubstitutabilityTestsBase.cs:21 · Level 5 · abstract class

  • What it is: a Liskov substitutability gate: a member that overrides a base-class virtual or abstract member, or implements an interface member, must not throw NotSupportedException or NotImplementedException.
  • Depends on: IArchitectureMap and ArchitectureRules.SubstitutableMembersDoNotRefuseTheContract (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Substitutability.cs:50), which reads IL with Mono.Cecil.
  • Concept introduced, gating a refusal that only surfaces at run time. A caller holding the base type or the interface was promised the operation; a subtype that refuses it breaks every caller that substitutes it in, on the path nobody tested with that subtype (rule doc, ArchitectureRules.Substitutability.cs:17-23). Reflection decides which members are substitutable (an override has its base definition on another type; an implementation is a target of the type's interface map), and only an exception constructed in place and thrown is judged. A method that is neither may throw what it likes, compiler-written bodies are skipped, and an async override is judged on its own body (:25-37). [Rubric §15, Best Practices & Code Quality] assesses contract honesty.
  • Walkthrough
    • The subclass supplies Map (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SubstitutabilityTestsBase.cs:23) and may override ExcludedMembers (line 31, empty by default). An entry is Namespace.Type.Method, a nested type uses the reflection Outer+Inner spelling and a property accessor its get_/set_ name, for a refusal that is the deliberate, reviewed choice such as a write-only stream's Read.
    • The single [Fact] OverridesAndInterfaceImplementations_ShouldNotThrow_NotSupportedOrNotImplemented (line 34) forwards both to the rule.
    • Scope follows the map: a module-bearing (consumer) map scans only its module assemblies so the framework is not re-judged in every consumer, while the framework's own module-less map scans every layer (ArchitectureRules.Substitutability.cs:39-42).
  • Where it's used: subclassed in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/SubstitutabilityTests.cs) and in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/CommonSubstitutabilityTests.cs, CommonSubstitutabilityTests). The rule itself is exercised against fixtures in SubstitutabilityRuleTests.cs in the same folder (SubstitutabilityRuleTests).

AccessibilityViolationException

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/AccessibilityViolationException.cs:7 · Level 0 · sealed class

  • What it is: the exception thrown when an axe-core accessibility scan finds one or more WCAG violations on the page under test.
  • Depends on: System.Exception (BCL) only. Its XML doc names the single thrower, PageExtensions.AssertNoAccessibilityViolationsAsync (MMCA.Common.Testing.E2E/Infrastructure/AccessibilityViolationException.cs:3-6).
  • Concept introduced, the accessibility gate as a hard failure. Rather than logging a warning or returning a result object, a violated a11y scan throws, so a consumer E2E [Fact] that calls AssertNoAccessibilityViolationsAsync goes red and names the offending elements. [Rubric §21, Accessibility] assesses whether accessibility is verified rather than assumed; a dedicated exception type makes an a11y regression a first-class, catchable build failure. [Rubric §28, Front-End Testing] assesses whether the UI is exercised through realistic automated checks; this is the failure primitive those checks throw.
  • Walkthrough: three constructors, the parameterless, message, and message-plus-inner overloads (AccessibilityViolationException.cs:10, :15, :21), the standard exception shape. It carries no extra state: the human-readable violation summary is baked into the message string the thrower builds.
  • Why it's built this way: a purpose-named exception (not a bare Exception or InvalidOperationException) lets a test that deliberately probes a known-inaccessible page assert on exactly this type, and it reads clearly in a failure log.
  • Where it's used: thrown only by PageExtensions.AssertNoAccessibilityViolationsAsync (MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:355-356), which is in turn called by the ScanAsync/ScanGridAsync helpers on E2ETestBase and by the *_ShouldHaveNoAccessibilityViolations facts on the workflow bases.

AdminCredentials

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:80 · Level 0 · nested static class

  • What it is: a nested static class on E2ETestConfiguration that resolves the seeded admin login (email and password) for E2E runs, with an environment-variable override in front of a per-app default.
  • Depends on: System.Environment (BCL). It is the admin half of the credential pair; UserCredentials is its structurally identical regular-user twin.
  • Concept: the environment-over-default resolution taught in E2ETestConfiguration. DefaultEmail/DefaultPassword have public setters so a downstream app seeds its own admin identity from a [ModuleInitializer], while E2E_ADMIN_EMAIL/E2E_ADMIN_PASSWORD win when set. [Rubric §11, Security] assesses how test credentials are handled; keeping them out of committed app code and injectable per environment is the safe end of that.
  • Walkthrough: DefaultEmail is "admin@localhost" and DefaultPassword is "Admin123!" (MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:82-83); Email and Password read E2E_ADMIN_EMAIL/E2E_ADMIN_PASSWORD and fall back to those defaults (:71-75).
  • Where it's used: read by E2ETestBase.LoginAsAdminAsync (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:97-98). Both consumer suites overwrite the default from a module initializer: admin@mmca.com in Store (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Infrastructure/TestSetup.cs:10) and admin@adc.com in ADC (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Infrastructure/TestSetup.cs:14).

AuthOutcome

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:4 · Level 0 · internal enum

  • What it is: the three verdicts a post-submit login or registration attempt can reach: Succeeded, ErrorShown, and Silent (MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:7, :10, :13).
  • Depends on: nothing; a plain internal enum, reachable from the framework's own gallery suite through the InternalsVisibleTo in the package's csproj (MMCA.Common.Testing.E2E/MMCA.Common.Testing.E2E.csproj:12).
  • Concept introduced, naming the silent failure. Two of the three members are the obvious outcomes. The third, Silent, is the one worth teaching: a submit that produced neither a navigation nor a rendered error alert (a 500 that renders nothing, a dropped request, a JS exception mid-submit) is not a success and not a reported failure, and it needs its own name precisely so the wait can refuse to treat it as either (doc, :12-13). [Rubric §14, Testability] assesses whether a harness can tell "passed" from "never actually ran"; making the third state a first-class enum member is how this package does that.
  • Walkthrough: each member carries a doc comment stating the signal combination behind it: the page navigated away or a signed-in state is showing (:6-7), the auth page is still showing with an error alert on it (:9-10), or none of the three signals fired (:12-13).
  • Where it's used: returned by AuthOutcomeRules.Classify and branched on inside E2ETestBase.WaitForAuthResultAsync (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:446, :253), and asserted directly by AuthOutcomeRulesTests.

AxeOptions

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/AxeOptions.cs:9 · Level 0 · static class

  • What it is: the shared axe-core run options that scope every accessibility scan to one documented target, WCAG 2.1 AA, so the framework gallery and all downstream apps scan against the same rule set.
  • Depends on: Deque.AxeCore.Commons (NuGet: AxeRunOptions, RunOnlyOptions, RuleOptions, MMCA.Common.Testing.E2E/Infrastructure/AxeOptions.cs:1; see primer §3).
  • Concept introduced, the scoped accessibility target. A raw axe run also emits "best-practice" advisories that are not conformance failures; pinning RunOnly to the WCAG tag set makes the gate fail only on real WCAG 2.1 AA violations (AxeOptions.cs:11-16). [Rubric §21, Accessibility] assesses whether the accessibility bar is explicit and enforced; freezing the target in one shipped object is how three repos stay honest to the same standard. [Rubric §22, Responsive/Cross-Browser] also applies through the pager exception below, which documents a specific third-party component limitation.
  • Walkthrough: two static presets, both read-only properties initialized once.
    • Wcag21Aa (AxeOptions.cs:17) sets RunOnly to Type = "tag" with the four WCAG A/AA tag values wcag2a, wcag2aa, wcag21a, wcag21aa (:19-23). This is the target for every strict scan.
    • Wcag21AaExceptMudPagerCombobox (:35) repeats that tag set and adds a Rules dictionary disabling aria-input-field-name (:42-45), for grid list pages whose only violation is MudBlazor's internal MudTablePager "rows per page" select. The XML doc (:26-34) records the detail: MudBlazor 9.6.0 mirrored combobox semantics onto the hidden-input presenter, the pager's own select gets no accessible name, and it is not reachable from app markup (no Label or aria-label parameter on MudTablePager), so this is an accepted upstream limitation. The doc warns it must be used only on a page whose sole combobox is a pager.
  • Why it's built this way: shipping the options in the package rather than re-declaring them per test guarantees every consumer scans the identical rule set; the narrowly scoped pager exception keeps one known third-party gap from forcing a blanket rule-disable across all scans.
  • Where it's used: passed to PageExtensions.AssertNoAccessibilityViolationsAsync through E2ETestBase.ScanAsync (strict Wcag21Aa, MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:542) and .ScanGridAsync (the pager exception, :329), and directly by the *_ShouldHaveNoAccessibilityViolations facts on UserLoginTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/UserLoginTestsBase.cs:82), UserRegistrationTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/UserRegistrationTestsBase.cs:99), ProfileManagementTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/ProfileManagementTestsBase.cs:204), and PasswordResetTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:88, :99).

E2EPolling

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/E2EPolling.cs:18 · Level 0 · static class

  • What it is: a generic poll-until helper with two overloads, one that returns the last probed value of type T and one that returns a plain bool, both looping a probe until a condition holds or a deadline passes (MMCA.Common.Testing.E2E/Infrastructure/E2EPolling.cs:39, :72).
  • Depends on: System.Diagnostics.Stopwatch (BCL, Stopwatch.GetTimestamp/GetElapsedTime) for the deadline arithmetic; no Playwright type appears in the signature, so it is reusable against any async probe, a page reload, a locator read, an HTTP call.
  • Concept introduced, asserting on the real failure, not on a bare timeout. The doc on PollUntilAsync<T> states the design choice directly: the method always returns the last probed value, timeout or not, so a caller that times out still gets a real value to assert on and fails with that assertion's message rather than with an opaque "poll timed out" (E2EPolling.cs:26-30). [Rubric §14, Testability] assesses whether a failure explains itself; returning the last value instead of throwing on timeout is what makes that possible here.
  • Walkthrough: DefaultTimeout is 30 seconds and DefaultInterval is 500 ms (:21, :24). PollUntilAsync<T>(probe, isSatisfied, timeout, interval, cancellationToken) (:39-44) null-checks probe and isSatisfied (:46-47), computes the effective budget and delay from the optional parameters or the two defaults (:49-50), probes once up front, then loops Task.Delay plus a re-probe while the condition is unmet and there is still budget left for one more full interval (:53-58), and returns the last probed value either way (:60). The bare-bool overload (:72-77) is a one-line forward to the generic overload with isSatisfied fixed to identity (static present => present). Both overloads carry a SuppressMessage for RS0026 (multiple public overloads with optional parameters), justified as grandfathered public API released after the v1.152 baseline while RS0026/RS0027 were off, so changing either signature would be a breaking change (:38, :71).
  • Why it's built this way: checking Stopwatch.GetElapsedTime(started) + delay < budget rather than just < budget (:54) avoids starting one more delay-and-probe cycle that cannot possibly land before the deadline, so the method does not overrun its stated timeout by a near-full interval.
  • Where it's used: by ADC's SpeakerSelfServiceTests (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Conference/Speaker/SpeakerSelfServiceTests.cs, 3 call sites) and 2 more sites in the ADC E2E suite.

E2ETestConfiguration

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:15 · Level 0 · static class

  • What it is: the single environment-variable-driven configuration surface for the whole E2E package: base URL, headless mode, timeouts, browser engine, slow-motion and trace capture, plus the nested AdminCredentials and UserCredentials.
  • Depends on: System.Environment and System.Globalization (BCL) only.
  • Concept introduced, environment-driven test configuration. Every knob resolves as "read an E2E_* environment variable, else use a default", so the same compiled suite runs against localhost on a developer box and against a CI-provisioned host with no code change. A few Default* properties carry public setters so a consuming app supplies app-specific defaults through a [ModuleInitializer], while environment variables always take precedence (MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:5-14). [Rubric §17, DevOps] assesses whether the suite is CI-portable and configurable outside the binary; this class is that story. [Rubric §22, Responsive/Cross-Browser] applies because Browser selects the engine CI iterates over.
  • Walkthrough: teaching order.
    • DefaultBaseUrl (settable, https://localhost:7108, E2ETestConfiguration.cs:17) and BaseUrl, which prefers E2E_BASE_URL (:19-20). ADC's module initializer overrides that default to https://localhost:6002, the port its AppHost pins the Blazor UI to (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Infrastructure/TestSetup.cs:13).
    • Headless treats any value other than "false" as headless, compared case-insensitively (:22-23); SlowMo slows each Playwright action by an environment-set millisecond count (E2E_SLOWMO) for visual debugging, default 0 (:52-53).
    • DefaultTimeout (30_000 ms, from E2E_TIMEOUT, :25-26) is the general action timeout. AuthTimeout (:34-35) is a separately tunable ceiling for the slowest step, the post-auth wait, inheriting DefaultTimeout unless E2E_AUTH_TIMEOUT is set. AuthGraceTimeout (15_000 ms, E2E_AUTH_GRACE, :45-46) is the extra grace window that de-flakes the register/login success-detection race, the transient error-alert flash during a Server-mode forceLoad.
    • All four millisecond settings go through one private ParseMilliseconds(raw, fallback, minimum) helper (:73-78). It parses with CultureInfo.InvariantCulture and NumberStyles.Float | NumberStyles.AllowThousands, so 30,000 reads as 30000 under every machine culture, and it returns the fallback for any value that is not finite or sits below the minimum. The minimum is 1 for the three timeouts, because Playwright reads 0 as "no timeout", and 0 for SlowMo (:9-13).
    • Browser selects chromium (default), firefox, or webkit from E2E_BROWSER (:60-61); TracePath returns a non-empty E2E_TRACE path or null, enabling full-speed Playwright trace capture (:70-71).
  • Why it's built this way: separating AuthTimeout and AuthGraceTimeout from the general DefaultTimeout is deliberate. The auth round-trip (full sign-in plus forceLoad reload plus re-render) can spike past a normal action budget on a contended CI runner, so it is tuned independently rather than by inflating every timeout in the suite. The doc ties the grace window to the TD-06/07 contention cluster and names the rejected alternative, forcing WASM, which broke login (:37-44). The parse floor exists for the same reason: a stray E2E_TIMEOUT=0 or a culture-formatted number would otherwise silently disable or distort every wait in the run, so a bad value degrades to the default instead (:9-13, :73-78).
  • Where it's used: read throughout PlaywrightFixture (engine, headless, slow-mo) and E2ETestBase (base URL, timeouts, trace path, credentials).

ForgotPasswordPage

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.PageObjects · MMCA.Common.Testing.E2E/PageObjects/ForgotPasswordPage.cs:6 · Level 0 · sealed class

  • What it is: the Page Object for the shared /forgot-password screen, the entry point of the password-recovery flow. It exposes the address field, the submit button, the confirmation alert, and the return-to-login link, plus a one-call RequestResetAsync action.
  • Depends on: Microsoft.Playwright (IPage, ILocator, AriaRole) and the PageExtensions helpers GotoAndWaitForBlazorAsync and FillAndVerifyAsync (MMCA.Common.Testing.E2E/PageObjects/ForgotPasswordPage.cs:1-2).
  • Concept: the Page Object Model taught in LoginPage. One locator carries a design decision rather than a selector detail: ConfirmationAlert targets the success alert unconditionally, and the inline comment states why, the page lands on the same success alert whether or not the address has an account (ForgotPasswordPage.cs:15-16). That is the anti-enumeration contract of ADR-091 expressed as a test affordance: there is deliberately no "unknown address" locator to assert on, because the UI must not render one. [Rubric §11, Security] assesses whether account enumeration is closed off; a Page Object that cannot express the enumerating assertion is a small structural guard on that. [Rubric §28, Front-End Testing] applies as with every Page Object here.
  • Walkthrough: a private IPage field set in the constructor (ForgotPasswordPage.cs:8-10); EmailField located by label and SubmitButton by its accessible name "Send Reset Link" (:12-13); ConfirmationAlert as MudBlazor's .mud-alert-text-success class (:16); BackToLoginLink located by link role, with the comment recording that "Back to Sign In" is a MudButton with Href and therefore renders as an <a> (:18-19). GotoAsync full-loads /forgot-password and waits for interactivity (:21-22). RequestResetAsync fills the address through PageExtensions.FillAndVerifyAsync and clicks submit (:24-28).
  • Where it's used: driven by PasswordResetTestsBase for the unknown-address confirmation fact and the a11y fact (MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:44-58, :79-89), and by the framework's own gallery suite, which exercises it against the backend-less gallery host including the confirmation state's own separate scan (ForgotPasswordPageE2ETests, MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/ForgotPasswordPageE2ETests.cs:17, :28, :41, :50).

LoginPage

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.PageObjects · MMCA.Common.Testing.E2E/PageObjects/LoginPage.cs:6 · Level 0 · sealed class

  • What it is: the Page Object for the shared /login screen. It exposes the login form's controls as named ILocator properties and offers GotoAsync/LoginAsync actions, so a test says loginPage.LoginAsync(email, password) instead of hand-querying the DOM.
  • Depends on: Microsoft.Playwright (IPage, ILocator, AriaRole) and the PageExtensions helpers GotoAndWaitForBlazorAsync and FillAndVerifyAsync (MMCA.Common.Testing.E2E/PageObjects/LoginPage.cs:1-2).
  • Concept introduced, the Page Object Model. A Page Object wraps one screen behind an intention-revealing API, locating controls by their accessible name (GetByLabel("Email"), GetByRole(AriaRole.Button, Name = "Sign in to your account")) rather than by brittle CSS. That keeps tests coupled to what a user sees, not to MudBlazor's internal class names, and it centralizes each selector in one place. [Rubric §28, Front-End Testing] assesses whether E2E tests are maintainable; the Page Object is the canonical pattern for that. [Rubric §21, Accessibility] applies indirectly: locating by role and label only works if the component renders proper accessible names, so the test style pressures accessible markup.
  • Walkthrough: a private IPage field set in the constructor (LoginPage.cs:8-10); locator properties for EmailField, PasswordField, LoginButton, the ErrorAlert (MudBlazor's .mud-alert-text-error class), and the CreateAccountLink, which the inline comment explains is a MudButton with Href and therefore renders as an <a> located by link role (:12-18). GotoAsync navigates through GotoAndWaitForBlazorAsync("/login") (:20-21); LoginAsync fills both fields through the shared FillFieldAsync and then clicks (:23-28). The private FillFieldAsync delegates to PageExtensions.FillAndVerifyAsync (:31-32), guarding the Blazor re-hydration race without a fixed delay.
  • Where it's used: instantiated by UserLoginTestsBase for the invalid-password, create-account-link, and accessibility facts, and by PasswordResetTestsBase to reach the login screen before probing the recovery entry point (MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:28-29).
  • Caveats / not-in-source: the Page Object exposes no locator for the "Forgot your password?" link; the one test that asserts it locates it directly off the page (PasswordResetTestsBase.cs:31).

ProfilePage

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.PageObjects · MMCA.Common.Testing.E2E/PageObjects/ProfilePage.cs:6 · Level 0 · sealed class

  • What it is: the Page Object for the authenticated /profile screen, exposing the name, address, and password sections' fields and buttons as named locators.
  • Depends on: Microsoft.Playwright and PageExtensions.GotoProtectedAsync (MMCA.Common.Testing.E2E/PageObjects/ProfilePage.cs:1-2, :35).
  • Concept: the Page Object Model taught in LoginPage. One difference is load-bearing: GotoAsync uses GotoProtectedAsync("/profile") (ProfilePage.cs:34-35), client-side routing rather than a full page load, because /profile is [Authorize] and server-side rendering cannot read the JWT from browser storage, so a full load would bounce to /login. GotoProtectedAsync first makes sure the Blazor runtime is loaded (loading / if it is not), re-routes through / when already on another route so the target always gets a fresh component lifecycle, then calls BlazorNavigateAsync (MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:169-200). [Rubric §28, Front-End Testing] and [Rubric §11, Security] both apply: exercising the authenticated page correctly requires respecting the client-token boundary.
  • Walkthrough: three commented locator groups. Name (FirstNameField, LastNameField, SaveNameButton, :12-15); address (six fields plus SaveAddressButton, :17-24); password (CurrentPasswordField, NewPasswordField located with Exact = true so it does not also match "Confirm New Password", ConfirmNewPasswordField, ChangePasswordButton, :26-30). ErrorAlert is located by ARIA alert role rather than a MudBlazor class (:32). GotoAsync is the client-side navigation described above (:34-35).
  • Where it's used: by ProfileManagementTestsBase for all six of its facts. ADC's own ProfileManagementTests covers the same screen without deriving from the shared base or using this Page Object (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/ProfileManagementTests.cs:8).

PseudoLocalizedPage

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Globalization · MMCA.Common.Testing.E2E/Workflows/Globalization/PseudoLocalizationTestsBase.cs:23 · Level 0 · sealed record

  • What it is: the declaration of one page under the pseudo-localization gate: the Path to load, an EnglishProbe string owned by a resource rendered on that page, and an optional SettleSelector proving the page's async content arrived.
  • Depends on: nothing but the BCL. It is a three-parameter positional record with the third parameter defaulted to null (MMCA.Common.Testing.E2E/Workflows/Globalization/PseudoLocalizationTestsBase.cs:23).
  • Concept introduced, the probe string as a two-way canary. Under the qps-Ploc pseudo culture every letter gains a combining accent, so the plain en-US form of EnglishProbe appearing on the page can only mean some render path bypassed the localizer (doc, :14-18). The same probe is then re-used under the default culture to assert it IS present, which is what keeps the probe honest: a probe that drifted from its resource value would make the leak assertion pass vacuously. [Rubric §27, i18n] assesses whether localization is machine-verified rather than eyeballed; carrying the probe next to the path is what turns a per-repo page list into a data declaration instead of test code.
  • Walkthrough: Path is loaded through the culture endpoint by the base; EnglishProbe must be a string known to come from a resource rendered on the page (doc, :14-18); SettleSelector is a selector whose visibility proves the async content rendered (a seeded grid row, a card, a static section), and null waits only for the loading indicator to clear (doc, :19-22).
  • Where it's used: the element type of PseudoLocalizationTestsBase.ScannedPages (:60). ADC declares three, home with a .location-section settle selector, the speaker card grid, and the session grid (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/PseudoLocalizationTests.cs:43-48); Store declares three with no settle selectors at all (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/PseudoLocalizationTests.cs:41-46).

RegisterPage

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.PageObjects · MMCA.Common.Testing.E2E/PageObjects/RegisterPage.cs:6 · Level 0 · sealed class

  • What it is: the Page Object for the /register screen, exposing the registration form (name, email, password, the Terms checkbox, plus the optional address panel) and RegisterAsync and AcceptTermsIfShownAsync actions.
  • Depends on: Microsoft.Playwright and PageExtensions.GotoAndWaitForBlazorAsync/FillAndVerifyAsync (MMCA.Common.Testing.E2E/PageObjects/RegisterPage.cs:1-2).
  • Concept: the Page Object Model taught in LoginPage, applied to a longer form. PasswordField uses GetByLabel("Password", Exact = true) so it does not also match "Confirm Password" (RegisterPage.cs:15), and the optional address fields sit inside an expansion panel located by its text (:38). The Terms checkbox is the other load-bearing detail: the page renders it only when the host configures Legal:TermsUrl, and keeps "Create Account" disabled until it is ticked, so the Page Object ticks it only when it is present, with a single count of the rendered DOM rather than a wait for absence, and with Force = true because MudBlazor overlays its own icon on the native input (:30-35, :65-73). [Rubric §28, Front-End Testing] applies.
  • Walkthrough: locator properties for the five required fields plus RegisterButton and ErrorAlert (:12-18); EmailAlreadyRegisteredAlert, the warning alert (with the sign-in and reset-password links) the page shows when the typed address already has an account, which is deliberately NOT ErrorAlert: since v1.196.0 the duplicate-address conflict renders as a warning rather than the generic red error alert (:20-25); the AlreadyHaveAccountLink sign-in link (:28); TermsCheckbox, which targets the input inside the register-accept-terms test-id wrapper (:35); and the optional address panel and fields (:38-43). GotoAsync full-loads /register (:45-46); RegisterAsync fills the five required fields through the shared helper, reusing the same password for the confirm field, calls AcceptTermsIfShownAsync, then clicks (:48-57). AcceptTermsIfShownAsync must run after the page is interactive: the box renders with the form, so a zero count then means the host has no Terms URL (:65-73). The private FillFieldAsync delegates to PageExtensions.FillAndVerifyAsync (:79-80).
  • Where it's used: instantiated by UserRegistrationTestsBase for all four of its facts.

ResetPasswordPage

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.PageObjects · MMCA.Common.Testing.E2E/PageObjects/ResetPasswordPage.cs:6 · Level 0 · sealed class

  • What it is: the Page Object for the shared /reset-password screen, the redemption half of the recovery flow. It exposes the address, token, and new-password fields, both outcome alerts, the return-to-login link, and two navigation entry points: the bare page and the prefilled emailed-link form.
  • Depends on: Microsoft.Playwright and the PageExtensions helpers GotoAndWaitForBlazorAsync and FillAndVerifyAsync (MMCA.Common.Testing.E2E/PageObjects/ResetPasswordPage.cs:1-2).
  • Concept: the Page Object Model taught in LoginPage. What is worth teaching here is GotoWithLinkAsync, which reproduces the way a real user arrives: the emailed link carries the address and the token as query parameters, so both fields land prefilled and the test exercises the same route the mail does (ResetPasswordPage.cs:30-36). [Rubric §24, Forms/Validation/UX Safety] assesses whether the recovery form's real arrival paths are covered; modelling both the bare and the linked entry is how this Page Object does it.
  • Walkthrough: a private IPage field set in the constructor (:8-10); EmailField and TokenField located by label (:12-13); NewPasswordField located with Exact = true, with the comment spelling out that "New Password" is a substring of "Confirm New Password" so the default substring match would resolve to both fields (:15-17), and ConfirmPasswordField beside it (:18). SubmitButton is located by the accessible name "Reset Password" (:20); ErrorAlert and SuccessAlert are the two MudBlazor alert classes (:21-22); GoToLoginLink is again a link-role locator over a MudButton with Href (:24-25). GotoAsync loads the bare page (:27-28); GotoWithLinkAsync builds the /reset-password URL with the address and token query parameters, escaping both with Uri.EscapeDataString (:34-36); ResetAsync fills all four fields through FillAndVerifyAsync and clicks submit (:38-45).
  • Where it's used: by PasswordResetTestsBase for the empty-form validation fact and the a11y fact (MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:61-76, :92-100), and by the framework's gallery suite, which additionally asserts the query-string prefill round-trip (ResetPasswordPageE2ETests, MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/ResetPasswordPageE2ETests.cs:17, :31, :43).
  • Caveats / not-in-source: no test in this package submits a genuine token. PasswordResetTestsBase's doc states why (the token only reaches the user by email, so consuming one is an app-side integration-test concern, PasswordResetTestsBase.cs:10-16), so ResetAsync and SuccessAlert are shipped affordances that the framework's own suites do not currently drive end to end.

RoleAdminPage

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.PageObjects · MMCA.Common.Testing.E2E/PageObjects/RoleAdminPage.cs:18 · Level 0 · sealed class

  • What it is: the Page Object for the role-administration screens: the /roles roster list and the per-role /roles/{role} permission editor, exposing the roster table, the per-role edit link, the editor's permission checkboxes, save button and saved toast, and a one-call SetPermissionAsync action.
  • Depends on: Microsoft.Playwright (IPage, ILocator, Assertions) and PageExtensions.GotoProtectedAsync (MMCA.Common.Testing.E2E/PageObjects/RoleAdminPage.cs:39-43, :63-82).
  • Concept: the Page Object Model taught in LoginPage. Two comments carry the load-bearing detail. PermissionCheckbox targets input[data-permission='{permission}'] because MudCheckBox splats unmatched attributes straight onto its own input, so the permission name lands on the checkbox element itself rather than a wrapper (RoleAdminPage.cs:50-56). SetPermissionAsync sets the checkbox with Force = true because MudBlazor renders the real input at zero opacity underneath its icon button, which would otherwise fail Playwright's actionability check on a control a real user can click fine (:71-74). [Rubric §28, Front-End Testing] applies as with every Page Object here; [Rubric §12, Roles/Permissions] applies because the screen under test is the role-permission editor itself.
  • Walkthrough: constructor takes the IPage (:22). RoleTable is .mud-table, the signal that the roster loaded rather than failed (:25); EditLinks is every edit-role test-id (:28); SaveButton is the save-permissions test-id (:31); SavedToast is the "Stored permissions saved." text locator, which lives in the root layout and so survives a client-side navigation (:33-37). GotoListAsync/GotoEditorAsync navigate to /roles and /roles/{role} through GotoProtectedAsync (:39-43). RowByRole(role) filters the roster rows by text (:47-48). PermissionCheckbox(permission) locates the data-attributed input (:55-56). SetPermissionAsync(permission, granted) first waits (30 second timeout) for any earlier SavedToast to clear, force-checks the box, clicks save, then waits (15 second timeout) for the toast to be visible, because the save runs on the server side of the Blazor circuit and the click alone proves nothing (:63-82).
  • Why it's built this way: two failure modes shape the method. The leading wait exists because a previous save's toast is still visible after GotoEditorAsync, so it would satisfy the final wait before this save's PUT has landed, and two visible toasts make the text locator a strict-mode violation (:65-69). The final wait closes the other: navigating away before the PUT lands tears the circuit down, cancels the request, and the Identity service logs the write as "Operation cancelled by user"; a consumer deploy run once lost the write on all three tries before this wait was added (:77-82).
  • Where it's used: by ADC's RoleAdministrationTests (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/RoleAdministrationTests.cs, 4 call sites) and AccessibilityTests (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/AccessibilityTests.cs, 2 call sites).

UserCredentials

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:92 · Level 0 · nested static class

  • What it is: the regular (non-admin) counterpart to AdminCredentials, a nested static class on E2ETestConfiguration resolving the seeded customer login, environment override in front of a per-app default.
  • Depends on: System.Environment (BCL).
  • Concept: identical in shape to AdminCredentials; only the environment-variable names and the defaults differ.
  • Walkthrough: DefaultEmail is "user@localhost" and DefaultPassword is "User123!" (MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:94-95); Email/Password prefer E2E_CUSTOMER_EMAIL/E2E_CUSTOMER_PASSWORD (:83-87).
  • Where it's used: read by E2ETestBase.LoginAsUserAsync (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:100-101); the default is overridden to customer@mmca.com in Store and customer@adc.com in ADC (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Infrastructure/TestSetup.cs:11, MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Infrastructure/TestSetup.cs:15).

WebVitalsSample

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:76 · Level 0 · sealed record

  • What it is: an immutable record holding one page's measured Core Web Vitals: Lcp, Cls, Fcp, Ttfb, and Inp (milliseconds, except unitless CLS).
  • Depends on: System.Text.Json.Serialization.JsonPropertyName (BCL) for the lowercase wire names.
  • Concept introduced, the vitals value object. Each property is init-only with a short JSON name (lcp, cls, fcp, ttfb, inp, MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:78-86), so the record deserializes directly from the window.__vitals JSON the browser observers accumulate. [Rubric §23, Front-End Performance] assesses whether client-side performance is measured; this record is the typed shape those measurements land in.
  • Walkthrough: a sealed record with five init doubles and no behavior (WebVitalsCollector.cs:76-87). It is the deserialization target of WebVitalsCollector.CollectAsync, which falls back to a fresh all-zero instance when the JSON deserializes to null (:56).
  • Where it's used: produced by WebVitalsCollector.CollectAsync, wrapped by WebVitalsArtifact for the JSON artifact, asserted by WebVitalsBudget.AssertWithinBudget, and returned to the caller by WebVitalsPageExtensions.MeasureWebVitalsAsync for any further app-specific assertion.

AuthOutcomeRules

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:28 · Level 1 · internal static class

  • What it is: the decision behind E2ETestBase.WaitForAuthResultAsync, extracted as a single pure function that maps three observed browser signals onto an AuthOutcome.
  • Depends on: AuthOutcome only. No Playwright, no xUnit: that is the point.
  • Concept introduced, extracting the decision out of the wait. The surrounding wait is inherently browser-bound (three racing Playwright waits), which makes its verdict hard to test. Pulling the classification into a parameterless-of-browser function makes the interesting case, the silent failure, assertable in a unit test with no server that has to be staged into failing without rendering anything. The type doc spells out the bug this closes: a submit that failed with neither a navigation nor a rendered error alert used to leave the wait returning normally, the caller's follow-up interactivity wait was already satisfied by the still-rendered auth page, and login and registration then reported success on a sign-in that never happened (MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:20-26). [Rubric §14, Testability] assesses whether logic is separable from the infrastructure it runs on; [Rubric §15, Best Practices & Code Quality] applies because the precedence rule now lives in one readable place instead of inside a Task.WhenAny continuation.
  • Walkthrough: one method. Classify(bool navigatedAway, bool errorAlertVisible, bool logoutVisible) (:38) returns Succeeded when the page navigated away from the auth page OR the signed-in state's logout control is showing (:40-43), and otherwise ErrorShown if an error alert is visible, Silent if not (:45). The precedence is deliberate and documented (:30-34): a completed forceLoad is unambiguous, so an error alert flashed on the way out is not a failure, and a visible logout button is the same verdict reached through interactivity instead of through navigation.
  • Why it's built this way: the E2E package deliberately keeps its surface small, so this stays internal and is opened to exactly one assembly through InternalsVisibleTo on MMCA.Common.UI.E2E.Tests (MMCA.Common.Testing.E2E/MMCA.Common.Testing.E2E.csproj:12), which is the framework's own gallery test project. That is the narrow version of "make it testable" rather than widening a shipped public API for a test.
  • Where it's used: called once, by E2ETestBase.WaitForAuthResultAsync (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:441-444), and covered directly by six browser-free facts in AuthOutcomeRulesTests (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/AuthOutcomeRulesTests.cs:15, :22, :29, :34, :39, :44).
  • Caveats / not-in-source: both the class doc and the test class describe the rule as a "four-way classification" (AuthOutcomeRules.cs:18, AuthOutcomeRulesTests.cs:8), while AuthOutcome declares three members; the code is the authority here.

PlaywrightFixture

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:6 · Level 1 · sealed class

  • What it is: the xUnit collection fixture that owns the Playwright driver and one launched browser for the whole E2E collection, selecting the engine from the environment.
  • Depends on: Microsoft.Playwright (IPlaywright, IBrowser, BrowserTypeLaunchOptions) and xUnit's IAsyncLifetime (MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:1-2). It reads E2ETestConfiguration.
  • Concept introduced, the shared browser fixture. Launching a browser is expensive, so one instance is created once per test collection and shared across every test rather than created per test. [Rubric §22, Responsive/Cross-Browser] assesses cross-engine coverage, and the inline comment names that rubric category explicitly (PlaywrightFixture.cs:15-16): environment-selecting the engine here is what lets CI run the identical suite once per browser. [Rubric §14, Testability] applies too, since sharing one costly resource keeps the suite fast.
  • Walkthrough: Playwright and Browser are public properties with private setters (:8-9). InitializeAsync creates the driver (:13), switches on E2ETestConfiguration.Browser.ToUpperInvariant() to pick Firefox, WebKit, or (for any unrecognized value) Chromium (:17-22), and launches it with Headless and SlowMo from configuration (:24-28). DisposeAsync suppresses finalization, then disposes the browser and the driver defensively (:31-44): the browser is disposed only through an is { } browser pattern match and the driver through a null-conditional call, because a failed InitializeAsync (missing browser binaries, a launch timeout) leaves both null despite the null! declarations, and the resulting NullReferenceException from disposal used to replace the real launch error in the run output (:35-37).
  • Where it's used: bound to the collection by E2ETestCollection and injected into every E2ETestBase subclass, which opens a fresh browser context per test off this shared Browser.

WebVitalsArtifact

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:90 · Level 1 · sealed record

  • What it is: the small envelope record written to disk as web-vitals-{label}.json: a Label, the page Path, and the measured WebVitalsSample.
  • Depends on: WebVitalsSample; serialized with System.Text.Json.
  • Concept: the citable-artifact wrapper. Pairing the raw vitals with the label and path they were taken on makes the JSON file self-describing for a CI reviewer. [Rubric §23, Front-End Performance] assesses whether performance evidence is captured and traceable; the envelope is what makes an uploaded artifact interpretable.
  • Walkthrough: a three-parameter positional sealed record (MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:90), constructed inside WebVitalsCollector.WriteArtifactAsync and serialized with the shared WriteIndented = true options (:37, :69-71).
  • Where it's used: only by WebVitalsCollector.WriteArtifactAsync.

WebVitalsBudget

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:103 · Level 1 · sealed record

  • What it is: a per-page Core Web Vitals budget (five ceilings) plus the assertion mechanics every consumer's budget test shares: format the sample as one citable line, then assert each metric is within its ceiling.
  • Depends on: WebVitalsSample, AwesomeAssertions (the Should().BeLessThanOrEqualTo(...) calls, MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:4), and System.Globalization for culture-invariant formatting (:1).
  • Concept introduced, the shared budget with consumer-owned numbers. WebVitalsCollector measures; this record decides what counts as too slow, and the split is deliberate: the mechanics (which metrics, how they are formatted, how a missing INP sample is treated) belong in the framework, while the numbers stay with each consumer, whose runners and hosting differ and therefore whose calibrated maxima differ (WebVitalsCollector.cs:92-97). The defaults are the Core Web Vitals "good" band, so a consumer that measured nothing tighter can take the record as-is. [Rubric §23, Front-End Performance] assesses whether front-end performance carries an enforced budget rather than an occasional audit; [Rubric §12, Performance & Scalability] is the tag the source itself uses for the collector pair. [Rubric §17, DevOps] applies because in both consumer repos these assertions ride the deploy-gating chromium E2E leg.
  • Walkthrough: a positional record with five defaulted parameters, Lcp = 2500, Fcp = 1800, Ttfb = 800, Cls = 0.1, Inp = 500 (WebVitalsCollector.cs:103-108), all milliseconds except the unitless CLS. Two members.
    • The static Describe(label, path, sample) (:118) renders one invariant-culture line, [web-vitals:{label}] path=... LCP=...ms FCP=...ms CLS=... TTFB=...ms INP-sample=...ms, with CLS at three decimals and the rest at zero (:122-124), which is the record a reviewer greps for next to the uploaded JSON artifact.
    • AssertWithinBudget(sample, label, path, writeLine = null) (:137) invokes the optional sink with that line (normally ITestOutputHelper.WriteLine, :141), then asserts LCP, FCP, TTFB, and CLS against their ceilings (:143-146). INP is asserted only when sample.Inp > 0 (:148-151), because no interaction clearing the collector's 16 ms event threshold leaves the sample at 0, and 0 must read as neither a pass-by-absence nor a failure. Failure text comes from the private Message helper, which names the metric, the measured value, the ceiling, and the page path (:154-157).
  • Why it's built this way: keeping the numbers consumer-side while shipping the assert body is what lets ADC and Store hold different calibrated budgets without either repo re-deriving the INP-zero rule or the message format. The 0-INP carve-out is the subtle one, and it is pinned by its own unit test rather than left to a comment (WebVitalsBudgetTests, MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitals/WebVitalsBudgetTests.cs:59-63).
  • Where it's used: ADC holds one static default instance and takes the framework numbers as-is (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/WebVitalsTests.cs:36, re-asserted at :93); Store constructs a default one per measurement (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/WebVitalsTests.cs:42, :89). The framework's own gallery suite instead constructs one from local constants tuned for the backend-less host (WebVitalsE2ETests, MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitals/WebVitalsE2ETests.cs:17-21, :29-30), and WebVitalsBudgetTests covers the record's mechanics without starting a browser (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitals/WebVitalsBudgetTests.cs:12).

PageExtensions

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:23 · Level 2 · static class

  • What it is: the interactivity toolbox of the E2E package: C# extension(T) members over Playwright's IPage and ILocator that wait for Blazor to become interactive, navigate its InteractiveAuto pages correctly, fill and click through the re-hydration race, drive a MudDataGrid list page (search, delete-confirm, settle), and run an axe-core accessibility scan.
  • Depends on: Microsoft.Playwright (IPage, ILocator, Assertions), Deque.AxeCore.Playwright/Commons (RunAxe, AxeRunOptions), and System.Text.RegularExpressions (MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:1-5). It throws AccessibilityViolationException.
  • Concept introduced, waiting for Blazor InteractiveAuto interactivity. The apps render with InteractiveAuto plus prerendering, so a page first appears as static HTML before the WASM runtime (or the SignalR circuit) wires event handlers; a click or fill that lands in that window is silently ignored (PageExtensions.cs:9-14). The load-bearing detail is that "the runtime loaded" is a false green: window.Blazor._internal is set before components have attached handlers, so the wait has a second phase keyed on the data-mmca-interactive attribute that MmcaThemeProviders in MMCA.Common.UI stamps on the document element from its first interactive render (:41-48, remarks at :68-84). Signal-based waits instead of fixed sleeps are the difference between a flaky suite and a deterministic one. [Rubric §28, Front-End Testing] assesses whether the suite is reliable against real render timing; [Rubric §21, Accessibility] applies through the axe scan; [Rubric §22, Responsive/Cross-Browser] because the same waits must hold on all three engines. The type also demonstrates the extension(T) member syntax used across the framework (see primer §4).
  • Walkthrough: four private script constants and one public selector constant, then two extension blocks.
    • The constants. MudDataGridRowSelector is the public default data-row selector for a MudDataGrid in table layout (:29). BlazorRuntimeLoadedPredicate probes for any truthy window.Blazor._internal and is shared by the wait and by GotoProtectedAsync's readiness probe so the two cannot drift (:31-39). InteractiveMarkerPredicate probes for the data-mmca-interactive attribute (:47-48). TimedSettleScript is the marker-free settle, two animation frames plus a 500 ms delay, for a click that may land on a page with no marker at all (:50-57); SingleFrameSettleScript is the one-frame flush used once the marker has already confirmed interactivity (:60).
    • extension(IPage page) (:62). WaitForBlazorAsync runs the two-phase wait then flushes one frame (:85-97). GotoAndWaitForBlazorAsync navigates, waits for LoadState.Load rather than NetworkIdle (which never settles under a persistent SignalR socket), then waits for interactivity (:103-110). BlazorNavigateAsync drives Blazor's client-side router through Blazor.navigateTo, tolerating the context-destroyed race a forceLoad can cause, then polls window.location.pathname instead of WaitForURLAsync (whose default Load wait hangs on a same-document navigation) and re-asserts interactivity, retrying once if that itself races a reload (:120-160). The path may carry a query string or fragment: it is null-checked, only its path portion (split off at ? and #) is awaited because window.location.pathname never carries them, and the value reaches the page as a script argument (p => Blazor.navigateTo(p)) rather than being interpolated into the script, so quotes in a path are safe. GotoProtectedAsync reaches an [Authorize] page by first ensuring Blazor is up (loading a public page when it is not) and re-routing through / so the target always gets a fresh component lifecycle, then client-navigating (:169-200). WaitForPageAndBlazorAsync covers a full-page navigation's load-plus-timed-settle (:206-215). AssertNoAccessibilityViolationsAsync first waits up to 2 seconds for running CSS transitions to finish (only CSSTransition animations, because an infinite spinner animation would never end; a timeout is swallowed and the scan runs anyway), since a MudBlazor filled button fades its background over 250 ms and axe sampling mid-fade reports a contrast violation no user sees settled (:320-334). It then runs RunAxe (with optional AxeOptions), returns early on zero violations, and otherwise builds a per-node summary, each node line carrying the compacted HTML, the axe target selector and the check messages, and throws AccessibilityViolationException (:316-357).
    • The list-page trio, also on IPage. SearchAndWaitForRowAsync settles the grid, fills the search field through FillAndVerifyAsync, then waits for a row containing the term (:242-258); its remarks record why each of the three steps replaced a hand-rolled per-page copy, one of which slept a fixed 1.5 seconds (:217-237). ConfirmDeleteAsync clicks a delete affordance, confirms the dialog by data-testid rather than by a MudBlazor filled-button class (which silently changed meaning when a dialog gained a second filled action), and settles the resulting grid reload (:275-294, rationale at :260-271). WaitForGridToSettleAsync is a plain (non-async) method that returns the ToHaveCountAsync assertion's Task directly, waiting for zero [role='progressbar'] elements, and deliberately does NOT wait for a row, because an empty result set is a legitimate state (:296-308).
    • extension(ILocator locator) (:360). FillAndVerifyAsync fills, then auto-waits ToHaveValueAsync, and if the value was wiped by re-hydration it clears the field, re-types character by character with a 20 ms delay, and re-asserts (:372-391). This is the single shared fill helper the base and the Page Objects all call. ClickAndVerifyAsync waits for interactivity, then clicks and waits a third of the timeout for the expected effect, up to three clicks in total (maxAttempts = 3, :415), the last of which lets the assertion throw with full diagnostics (:406-437). An effect slower than timeout / 3 is treated as a swallowed click and re-issued, so a non-idempotent submit that can take longer than that must be called with a larger timeout (remarks at :398-404). ClickAndWaitForUrlAsync clicks a navigating link and re-clicks until the URL matches the supplied regular expression, for grid rows whose cells wrap content in MudLink so a row-center click lands on padding (:449-472).
    • The private CompactHtml collapses a violating node's markup to one trimmed line, truncated at 220 characters, so the failure message points at the exact offending element (:481-490). The private DescribeChecks appends axe's own check messages after the target (for color-contrast, the measured ratio and the foreground and background colors) joined by ;, or nothing when there are none, so an intermittent violation can be diagnosed from the failure text alone (:498-507).
  • Why it's built this way: the fill and click helpers exist because InteractiveAuto's prerender-then-hydrate model makes a bare fill or click a race on a fast host; auto-waiting assertions with a bounded re-type or re-click are strictly safer than fixed delays, since they succeed as soon as the value or effect appears. The interactivity marker is the honest gate, so a page that never stamps it fails the wait rather than proceeding to click dead controls (:82-83). Two [SuppressMessage] attributes document analyzer false positives across the extension(T) boundary: CA1708 on the class, where the compiler-generated grouping members read as case-colliding (:15-18), and IDE0051 for the private constants and for CompactHtml and DescribeChecks, which the SDK 10.0.201+ analyzer cannot see being referenced from inside an extension block (:19-22, :477-480, :492-497).
  • Where it's used: throughout the Page Objects (ForgotPasswordPage, LoginPage, ProfilePage, RegisterPage, ResetPasswordPage), inside E2ETestBase (FillFieldAsync, ScanAsync, ScanGridAsync, the navigation helpers), by WebVitalsPageExtensions, and directly by every workflow base in this group. The list-page trio is what the consumer Page Objects are built on: Store's ProductListPage and CategoryListPage and ADC's session, speaker, room, event, question, category and user list pages all delegate their search and delete to it (for example MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/PageObjects/Catalog/ProductListPage.cs:21, :42, and MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/PageObjects/Conference/Sessions/SessionListPage.cs:39, :51).

E2ETestCollection

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:48 · Level 2 · sealed class

  • What it is: the xUnit [CollectionDefinition] that binds PlaywrightFixture to the named "E2E" collection, so every E2E test class shares the one launched browser.
  • Depends on: xUnit's ICollectionFixture<PlaywrightFixture> and the [CollectionDefinition] attribute (MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:47-48).
  • Concept introduced, the xUnit collection fixture binding. A collection fixture is instantiated once and shared by every test class that opts into the collection by name. This class carries a public const string Name = "E2E" (:50) used both in its own [CollectionDefinition(Name)] and in each test's [Collection(E2ETestCollection.Name)], so the string is declared once and cannot drift. [Rubric §14, Testability] assesses fixture design; a single named constant binding is the robust way to share a fixture.
  • Walkthrough: an otherwise empty class body carrying the collection definition and the Name constant (:47-51). It exists purely as an xUnit marker, and it lives in the same file as the fixture it binds.
  • Where it's used: referenced by E2ETestBase's [Collection(E2ETestCollection.Name)] attribute (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:8), so every workflow base and every consumer subclass inherits collection membership.
  • Caveats / not-in-source: xUnit collection definitions do not cross assembly boundaries, so each consumer E2E assembly re-declares its own identically named definition over the same fixture type, and says so in its doc comment (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Infrastructure/E2ETestCollection.cs:3-11).

WebVitalsCollector

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:20 · Level 2 · static class

  • What it is: the measurement infrastructure for client-side Core Web Vitals. It installs browser PerformanceObserver scripts before first paint, reads the accumulated values back off a live page, and writes them as a citable JSON artifact.
  • Depends on: Microsoft.Playwright (IPage), System.Text.Json, and System.IO (MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:1-5). It produces WebVitalsSample and WebVitalsArtifact, and pairs with WebVitalsBudget.
  • Concept introduced, in-browser performance measurement with no third-party JS. Rather than shipping an analytics SDK, it injects a small init script that installs PerformanceObservers for LCP, CLS, FCP, and INP, each wrapped in try/catch so an engine lacking an entry type leaves that metric at 0 instead of throwing, and accumulates into window.__vitals (:26-35). The type doc is explicit that this is the client-side analogue of a backend load test, not a cross-engine field measurement: LCP and CLS are Chromium-only, so on Firefox and WebKit those fields stay 0 and budget assertions pass (:9-18). [Rubric §23, Front-End Performance] and [Rubric §12, Performance & Scalability] assess whether user-centric performance is measured; observing the vitals APIs directly, with no network egress, is a self-contained way to do it. The same doc states the class is only the measurement infrastructure, that WebVitalsBudget is the shared assert mechanics, and that consumers own which pages carry a budget and what the numbers are (:16-18).
  • Walkthrough: InstallAsync registers the observers through AddInitScriptAsync so they are active on the next navigation (:40-44). CollectAsync evaluates a script that stamps TTFB from Navigation Timing and returns window.__vitals as JSON, deserialized into a WebVitalsSample (:47-57). WriteArtifactAsync (a non-async method that returns the File.WriteAllTextAsync task directly) resolves the output directory from WEB_VITALS_OUTPUT_DIR or falls back to artifacts/ under the current directory, creates it, wraps the sample in a WebVitalsArtifact, and writes web-vitals-{label}.json indented (:63-72).
  • Why it's built this way: the observers install before the document's own scripts (through AddInitScript) so early metrics such as FCP are not missed, and the per-observer try/catch is what makes the same code run green on all three engines despite the Chromium-only metrics. The init script is kept as one concatenated string rather than a raw literal to stay clear of the MA0136 analyzer (:22-25).
  • Where it's used: through WebVitalsPageExtensions.MeasureWebVitalsAsync, which is the one call site that sequences install, navigate, collect, write and assert; the framework gallery, ADC and Store budget tests all go through it rather than calling the three methods themselves.

E2ETestBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:9 · Level 3 · abstract class

  • What it is: the shared base every E2E test class derives from. It gives each test a fresh isolated browser context and page off the shared PlaywrightFixture browser, plus the load-bearing auth helpers (login, register, deterministic session cleanup) and the accessibility scan helpers.
  • Depends on: PlaywrightFixture, E2ETestConfiguration, AxeOptions, AuthOutcomeRules, the PageExtensions helpers, xUnit's IAsyncLifetime and TestContext, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:1-4).
  • Concept introduced, the per-test browser context. The fixture launches one browser; this base opens a new IBrowserContext (an isolated cookie and storage jar) per test in InitializeAsync and disposes it in DisposeAsync, so tests cannot leak session state into each other. It is the E2E analogue of the integration-test base's per-test database reset. [Rubric §28, Front-End Testing] assesses realistic, isolated UI tests; [Rubric §21, Accessibility] applies through the scan helpers; [Rubric §14, Testability] through the shared, correctly sequenced auth helpers every workflow reuses. The auth-result handling also touches [Rubric §22, Responsive/Cross-Browser], since the same waits must survive Server-mode and WASM render timing on any engine.
  • Walkthrough: teaching order.
    • Lifecycle. The class carries [Collection(E2ETestCollection.Name)] (:8), the fixture arrives through the constructor, and the current Page is a protected property (:11-18). InitializeAsync (from :25) creates a context with IgnoreHTTPSErrors and the base URL, sets the default timeout from configuration, optionally starts trace capture with screenshots, snapshots, and sources when TracePath is set, and opens the Page. DisposeAsync (from :45) stops tracing, closes the page, and disposes the context, each step guarded by a null check because a failed InitializeAsync leaves the fields null and the resulting NullReferenceException would otherwise mask the real setup error. The private StopTracingAsync (from :73) carries the per-test trace policy: a plain file path keeps the single-file behavior, while a directory path writes a trace named after the current test only when that test failed (TestContext.Current.TestState?.Result == TestResult.Failed, :79-85), so a full-suite run yields just the failing traces with no overwriting.
    • Auth entry points. LoginAsAdminAsync and LoginAsUserAsync (:97-101, plain Task-returning methods that return the LoginAsync task directly) delegate to LoginAsync with the AdminCredentials and UserCredentials pair. LoginAsync (from :103) first clears any existing session when a sign-out button is visible, removing the auth_access_token and auth_refresh_token localStorage entries and issuing a DELETE /auth/session-cookie fetch, guarded against the context-destroyed race from an in-flight logout; it then navigates to /login, fills through FillFieldAsync, clicks, and awaits WaitForAuthResultAsync followed by WaitForInteractiveOrReloadAsync, and finishes with AcceptTermsIfPromptedAsync when AcceptsTermsAfterSignIn is true (:151-154). SignOutAsync (:320-331) clicks the sign-out button and then waits for the browser to actually land on /login, matching against the shared cached LoginUrlPattern regex on a 15 second budget (:338-339); a PlaywrightException that the private IsSupersededNavigation recognizes, by a message carrying NS_BINDING_ABORTED, Navigation canceled by policy check or net::ERR_ABORTED (:333-336), repeats the same wait once. RegisterNewUserAsync (from :341) generates a unique e2e-{id}@test.com email with the fixed password TestPass123!, fills the register form, ticks the Terms box through RegisterPage.AcceptTermsIfShownAsync when the host shows one (:361), submits, runs the same two post-auth waits, and returns the created credentials.
    • Terms acceptance. A consumer whose app renders the terms acceptance gate overrides the protected virtual AcceptsTermsAfterSignIn (default false, so a sign-in gains no step and no time, :171) in its own shared test base; LoginAsync and RegisterNewUserAsync then end with AcceptTermsIfPromptedAsync (:205-234). That method first asks the private ProbeTermsPromptAsync (:240) whether the dialog will open: it GETs the signed-in user's standing from TermsAcceptanceProbePath (virtual, default /api/Users/me/legal-acceptance, :181; null means the app maps no same-origin proxy) through the page's request context, which shares the browser context's cookies, so it works in every render mode. The internal static IsTermsPromptExpected (:272) mirrors the gate's own rule: a dialog is expected only when a current version is configured AND isCurrent is false, and it answers null for an unreadable body. A false answer returns at once; true waits up to 15 seconds for the agree checkbox (TermsPromptExpectedTimeoutMs, :13), and null (probe disabled, error status, odd body) falls back to a 10 second bounded wait (TermsPromptFallbackTimeoutMs, :14). A dialog that never appears is not a failure, because the gate renders nothing when its own read fails. Otherwise the method force-checks the box (MudBlazor overlays its own icon on the native input), clicks Accept, and waits for the dialog to hide.
    • Post-auth robustness. WaitForInteractiveOrReloadAsync (from :398) waits for interactivity and, on either a PlaywrightException or a TimeoutException, reloads once and re-waits rather than watching the same stuck boot; the comment records why both exception types are caught (Playwright's TimeoutException derives from System.TimeoutException, not PlaywrightException, so an earlier single catch skipped the retry entirely) and why a reload beats a re-wait (the framework assets are now HTTP-cached). WaitForAuthResultAsync (from :419) starts three signals through Task.WhenAny, leaving the auth page, the logout button appearing, or an error alert appearing, observes the two losers so their timeout faults never resurface as unobserved-task exceptions in an unrelated test, and then classifies the settled state through AuthOutcomeRules.Classify. Succeeded returns; the other two outcomes get one grace window, after which ErrorShown throws an InvalidOperationException carrying the alert text and Silent throws one naming the auth path, the total budget and the current URL. AuthSucceededWithinGraceAsync (from :487) implements that window, falling back to the logout-button signal when no navigation occurs.
    • Sign-out button name. Every "signed in" check in the base (the pre-login cleanup probe, SignOutAsync, WaitForAuthResultAsync) locates the button by the accessible name "Logout", the name the apps' app bar now renders. Helpers. NavigateAndWaitAsync (:507), the shared static FillFieldAsync delegating to PageExtensions.FillAndVerifyAsync (:515), UniqueId (:518), and the two scan helpers. ScanGridAsync (from :529) waits for a visible data row and for zero [role='progressbar'] elements, then scans with AxeOptions.Wcag21AaExceptMudPagerCombobox; ScanAsync (from :539) applies the progressbar guard only and scans strictly with Wcag21Aa.
    • Dark-theme helpers. SeedDarkThemeAsync adds an mmca_theme=dark cookie to the browser context, read by the Common UI's theme.js on the next page load. WaitForDarkModeAsync then waits for the theme toggle's accessible name to flip to "Switch to light mode", the signal that ThemeService has initialized from the cookie and MudThemeProvider has re-rendered with the dark palette; because the layout renders the toggle twice (the app bar and the mobile nav top row), the wait targets the desktop cluster's copy (.appbar-icon-actions .First), and the expected name is the English resource, matching the default E2E culture.
  • Why it's built this way: the auth helpers encode hard-won timing knowledge once (the forceLoad reload, the Server-versus-WASM hydration lag, the cookie-and-localStorage dual session store), so every consumer workflow inherits a deterministic sign-in instead of re-deriving the races. Clearing both token stores is essential: the Blazor Server host is cookie-only, so a localStorage clear alone would leave the next login authenticated as the wrong user (:100-105). Sign-out is a base helper for the same reason: a workflow that clicked the button and then waited for LoadState.Load was waiting on the CURRENT document, whose load event had already fired, so the next goto died with "execution context was destroyed" or "interrupted by another navigation"; the URL wait fixes that once for every caller, and the one-shot retry covers the engines that report the superseded navigation as an error: Firefox surfaces the abandoned first request as NS_BINDING_ABORTED when the logout forceLoad is superseded mid-flight by the app's own redirect onto /login, and WebKit raises the same supersession as "Navigation canceled by policy check" now that sign-out runs through the same-origin API proxy, while chromium raises net::ERR_ABORTED ("maybe frame was detached?") when a password change has just revoked the session and the app's own redirect onto /login overtakes the sign-out (remarks ending at :318). A sign-out that never reaches /login still fails on the second wait. The scan split lets grid pages accept the documented pager-combobox exception while every other page stays strict, and the grid wait keys off a data row rather than the loading bar hiding, which would resolve instantly before the transient unnamed progressbar even appears (:353-361). SeedDarkThemeAsync sets the cookie directly instead of driving the UI toggle so a workflow test can start already in dark mode without spending a page interaction on it; WaitForDarkModeAsync is a separate call, not folded into the seed helper, because the caller must navigate between seeding the cookie and checking the applied theme.
  • Where it's used: the base class of all eight workflow bases in this unit (AuthorizationTestsBase, LogoutTestsBase, PasswordResetTestsBase, ProfileManagementTestsBase, PseudoLocalizationTestsBase, UserLoginTestsBase, UserPreferencesTestsBase, UserRegistrationTestsBase) and, through them and directly, every E2E test class in the ADC and Store suites (for example ADC's own ProfileManagementTests, which derives from this base rather than the shared profile workflow, MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/ProfileManagementTests.cs:8).

WebVitalsPageExtensions

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Infrastructure · MMCA.Common.Testing.E2E/Infrastructure/WebVitalsPageExtensions.cs:13 · Level 3 · static class

  • What it is: the one-call Core Web Vitals measurement, an extension(IPage) block with two members that install the collector, load the page, drive an interaction (inline-optional in one, scripted in the other), collect, write the artifact, and assert the sample against a budget.
  • Depends on: WebVitalsCollector, WebVitalsBudget, WebVitalsSample, PageExtensions.GotoAndWaitForBlazorAsync, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Infrastructure/WebVitalsPageExtensions.cs:1).
  • Concept introduced, shipping the ORDER, not just the parts. The collector, the sample and the budget are each individually usable, and every consumer used to wire them together in a private MeasureAsync. The step order is the load-bearing part: the observers must be installed BEFORE the navigation or LCP, FCP and TTFB are never recorded for that load, which is exactly the kind of detail a per-repo copy gets wrong once and then carries (doc, :5-12). Lifting the sequence into the package turns an ordering convention into a compiler-checked call. [Rubric §23, Front-End Performance] assesses whether the performance gate is trustworthy; [Rubric §15, Best Practices and Code Quality] applies because three copies collapsed into one; [Rubric §33, Developer Experience] applies because a consumer now writes one line per measured page.
  • Walkthrough: MeasureWebVitalsAsync(label, path, budget, writeLine = null, interactionPlaceholder = null, interactionText = "test") (:32-38) null-checks the page and the budget (:40-41), installs the observers (:43), navigates through GotoAndWaitForBlazorAsync (:44), and, when an interaction placeholder is supplied, clicks and fills that input if it is visible and then waits 300 ms so the event-timing observer records what it just saw (:46-57). It then collects the sample (:59), writes the artifact (:60), calls budget.AssertWithinBudget(...) with the optional output sink (:62), and returns the sample for any further app-specific assertion (:63). MeasureWebVitalsWithInteractionAsync(label, path, budget, interaction, writeLine = null) (:84-89) is the second member, added alongside the first: it null-checks the page, the budget and the interaction delegate (:91-93), installs the observers and navigates in the same order (:95-96), then runs the caller-supplied Func<IPage, Task> unconditionally (:98, no placeholder-visibility check) before the same 300 ms wait, collect, write and budget assertion (:100-107). It carries its own name rather than overloading MeasureWebVitalsAsync, whose optional parameters would make the two ambiguous at the call site (:66-73).
  • Why it's built this way: the interaction is best-effort by contract in MeasureWebVitalsAsync; an absent or invisible field is skipped, and if no event clears the collector's 16 ms threshold INP stays 0 and its assertion is skipped by the budget (doc, :24-29). MeasureWebVitalsWithInteractionAsync trades that best-effort placeholder lookup for an arbitrary scripted interaction (a click, a keystroke, anything the event-timing observer can time), still leaving INP at 0 rather than a failure when the interaction clears nothing over the threshold (:77-81). Together the two members cover load-only pages, pages with a single fillable field, and pages whose interaction is not a simple placeholder fill.
  • Where it's used: MeasureWebVitalsAsync is called by every budget test in the workspace. The framework gallery measures three gallery pages against a locally tuned budget (WebVitalsE2ETests, MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitals/WebVitalsE2ETests.cs:47, :48, :56); ADC funnels four pages through one private wrapper that re-asserts the returned sample at the call site (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/WebVitalsTests.cs:90-93); Store calls it directly for home, catalog (with a search-box interaction) and login (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/WebVitalsTests.cs:42, :52, :62). MeasureWebVitalsWithInteractionAsync has no caller outside its own defining file yet.
  • Caveats / not-in-source: the type doc calls the output a "dated" artifact (:8); the file name WebVitalsCollector actually writes is web-vitals-{label}.json, with no date component (MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:70).

KeyboardOperabilityTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Accessibility · MMCA.Common.Testing.E2E/Workflows/Accessibility/KeyboardOperabilityTestsBase.cs:41 · Level 4 · abstract class

  • What it is: the reusable keyboard-operability workflow base. Authored once, it asserts four WCAG-style keyboard behaviors that a consumer app opts into by declaring its pages and one dialog: the skip link is the first Tab stop and moves focus into the main landmark, an open dialog traps Tab and Shift+Tab, Escape closes the dialog and returns focus to the opener, and the first few Tab stops on each page show a visible focus indicator.
  • Depends on: E2ETestBase, PlaywrightFixture, PageExtensions (GotoAndWaitForBlazorAsync), E2EPolling (PollUntilAsync), AwesomeAssertions, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Accessibility/KeyboardOperabilityTestsBase.cs:1-7).
  • Concept: the authored-once workflow base taught in AuthorizationTestsBase, here for keyboard accessibility instead of route security. [Rubric §21, Accessibility] assesses whether the UI is operable without a pointer; axe scans cannot see a focus trap or a skip link's destination, so this base drives real key presses.
  • Walkthrough: the subclass supplies three abstract members, PagePaths (the pages whose skip link and focus indicators are checked, :82), DialogPagePath (:85) and DialogOpener (the control that opens a role="dialog" on Enter, :91). Five virtual hooks have defaults: Dialog (the first role="dialog" element, :97), FocusReturnTarget (defaults to the opener, :103), ArrangeAsync (a no-op run before each fact's first navigation, for sign-in or data setup, :110), NavigateAsync (a full load that waits for Blazor, overridable to route through a client-side navigation that keeps an authorized session, :119) and CleanupAsync (a no-op, :128). Four [Fact]s (:130-139) each run through the private RunArrangedAsync (:144), which calls ArrangeAsync inside a try and CleanupAsync in finally, so an arrangement that throws part-way is still undone. SkipLink_IsFirstTabStop_AndMovesFocusIntoMainLandmark focuses a script-only probe at the top of the document, presses Tab, requires the focused element to be an anchor whose href contains #, presses Enter, and requires focus to land inside main or [role="main"]. OpenDialog_TrapsTabFocus opens the dialog by keyboard and presses Tab then Shift+Tab TrapTabPresses (12, :44) times each, waiting for focus to stop moving between presses (FocusSettledScript, :56) and requiring document.activeElement to stay inside the dialog. Escape_ClosesDialog_AndReturnsFocusToOpener presses Escape and expects the dialog count to reach 0 and FocusReturnTarget to be focused, within FocusTimeoutMs (10 seconds, :50). FocusedElement_ShowsVisibleFocusIndicator tabs IndicatorTabStops (3, :47) stops on each page and reads the focused element's computed outline and box-shadow through FocusStyleScript (:68).
  • Why it's built this way: both focus polls use E2EPolling.PollUntilAsync at a 100 ms interval (FocusSettlesAsync, :279) because focus moves asynchronously after a keypress in an interactive Blazor page, so a single read races it. A box-shadow counts as a focus indicator only when it differs from the same element's blurred shadow, since an elevated button carries a resting shadow whether focused or not; FocusStyleScript finishes running CSS transitions before each read so a blur does not report the focused value mid-transition. The skip-link and focus-indicator facts assert PagePaths is non-empty, the same non-vacuity guard AuthorizationTestsBase uses, so a subclass that declares no pages fails instead of passing silently. ArrangeAsync runs inside the try for the same reason CleanupAsync must tolerate a partial arrangement.
  • Where it's used: subclassed by ADC's MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Engagement/LiveModerationKeyboardTests.cs and by the Common UI E2E suite (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Layout/KeyboardOperabilityE2ETests.cs, .../Accessibility/FocusIndicatorRuleE2ETests.cs).

AuthorizationTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Identity · MMCA.Common.Testing.E2E/Workflows/Identity/AuthorizationTestsBase.cs:18 · Level 4 · abstract class

  • What it is: the reusable authorization workflow fitness base, authored once and re-run as a thin subclass per repo. It asserts that anonymous users are redirected off protected paths, that public paths stay reachable, that a registered non-admin can reach an authenticated page, and that a non-admin probing admin routes gets the Forbidden page.
  • Depends on: E2ETestBase, PageExtensions (GotoAndWaitForBlazorAsync, GotoProtectedAsync), AwesomeAssertions, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Identity/AuthorizationTestsBase.cs:1-6).
  • Concept introduced, the authored-once workflow fitness base. This is the pattern shared by all eight bases in this unit: the framework owns the assertions and the SSR-versus-client-navigation mechanics, and each consumer supplies only its own route lists through abstract or virtual members, so identical security behavior is verified across repos without copying test bodies (:10-17). [Rubric §11, Security] assesses whether authorization is actually exercised; this base machine-checks both the anonymous-redirect and the authenticated-non-admin-escalation directions. [Rubric §25, Navigation & IA] applies because it pins which routes are public and which are gated.
  • Walkthrough: the subclass supplies ProtectedPaths and PublicPaths (abstract, :26, :29) and optionally AuthenticatedUserPath and AdminPaths (virtual, defaulting to null and an empty list, :35, :44). Four facts follow. AnonymousUser_ProtectedPages_ShouldRedirectToLogin asserts each protected path bounces to /login (:46-58). AnonymousUser_PublicPages_ShouldBeAccessible asserts each public path stays put (:60-72). RegisteredUser_AuthenticatedPage_ShouldBeAccessible registers a non-admin, then client-navigates through GotoProtectedAsync because SSR cannot read the JWT, passing vacuously when no path is declared (:74-93). RegisteredUser_AdminPages_ShouldBeForbidden registers a non-admin, then asserts each admin path renders the shared Forbidden page, matching the level-1 heading (via GetByRole(AriaRole.Heading, new() { Level = 1 })) containing "Access Denied", with the comment noting that role denial is not a redirect so the page content is the only reliable signal (:95-120).
  • Why it's built this way: the two optional members use a no-dynamic-skip convention (an app with no such page simply passes) because the shipped library deliberately does not reference xunit.v3.assert for a declared skip (:77-78, :98-99). The non-empty assertions on ProtectedPaths and PublicPaths (:49-50, :63-64) are non-vacuity guards: a repo that declares no paths fails rather than passing silently.
  • Where it's used: subclassed in both consumer E2E suites with that app's route lists (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/AuthorizationTests.cs:11-21, MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/AuthorizationTests.cs:11-44, whose twelve-entry AdminPaths carries a comment explaining which authenticated-but-not-Organizer routes are deliberately excluded, :21-24); Store's subclass also adds one app-specific fact of its own, an anonymous order-detail deep link that must leak no order content (AuthorizationTests.cs:27-42).

LogoutTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Identity · MMCA.Common.Testing.E2E/Workflows/Identity/LogoutTestsBase.cs:9 · Level 4 · abstract class

  • What it is: the reusable logout workflow base. It verifies that sign-out returns the user to the login screen and that a logged-out user can no longer reach a protected page.
  • Depends on: E2ETestBase, PageExtensions (WaitForBlazorAsync), and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Identity/LogoutTestsBase.cs:1-5).
  • Concept: the authored-once workflow base taught in AuthorizationTestsBase. [Rubric §11, Security] assesses session teardown; this base guards that logout genuinely revokes access rather than merely returning to a login screen visually.
  • Walkthrough: two facts. Logout_ShouldRedirectToLoginPage registers, confirms the sign-out button (accessible name "Logout") is visible, clicks it, waits for the load state, and asserts the sign-in button appears (:16-29). Logout_ShouldPreventAccessToProtectedPages registers, waits for interactivity (because RegisterNewUserAsync can return with the button visible before JS interop is ready, :37-40), then clicks sign-out inside RunAndWaitForResponseAsync so it blocks until the best-effort DELETE /auth/session-cookie response arrives (:47-51), confirms the sign-in button is visible (:54-55), and then re-requests /profile up to six times until the server redirects to /login (:64-72), falling back to a clear URL assertion if it never does (:75).
  • Why it's built this way: waiting for the cookie-clear response is the fix for a real full-speed race. At speed the test otherwise reaches /profile before the DELETE finishes, so the HttpOnly cookie is still present and SSR re-authenticates. The bounded re-request loop converges deterministically where any slowdown (slow-mo, or even trace capture) would have hidden the race entirely (:42-46, :57-63).
  • Where it's used: subclassed in both consumer E2E suites (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/LogoutTests.cs:5, MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/LogoutTests.cs:5).

PasswordResetTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Identity · MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:17 · Level 4 · abstract class

  • What it is: the reusable password-recovery workflow base covering the shared /forgot-password and /reset-password pages: the entry point is reachable from the login screen, an unknown address gets the same confirmation as a known one, the reset form's client-side validation blocks an empty submit, and both pages are accessibility-clean.
  • Depends on: E2ETestBase, LoginPage, ForgotPasswordPage, ResetPasswordPage, PageExtensions, AxeOptions, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:1-6).
  • Concept introduced, drawing the E2E boundary around what a browser can honestly observe. The type doc states outright that the real token round-trip is deliberately not exercised here: the token only reaches the user by email, so redeeming one is an app-side integration-test concern, and what E2E owns is the reachability of the flow, the anti-enumeration confirmation, client-side validation, and WCAG 2.1 AA conformance of both pages (:10-16). That is a scope decision worth internalizing: a browser test that cannot reach the mailbox should assert the contract it can see rather than fake the one it cannot. [Rubric §11, Security] assesses whether the recovery flow leaks account existence; the unknown-address fact is the machine check on ADR-091's anti-enumeration rule. [Rubric §24, Forms/Validation/UX Safety] covers the empty-submit validation, [Rubric §21, Accessibility] the two scans, and [Rubric §25, Navigation & IA] the entry-point fact, since a locked-out user has no other way in.
  • Walkthrough: five facts, no abstract members, so a consumer subclass is a single line.
    • LoginPage_ForgotPasswordLink_NavigatesToForgotPasswordPage (:24-41) opens the LoginPage, asserts the "Forgot your password?" link is visible at all (the comment notes a user locked out of their account has no other entry point, :33), clicks it, and asserts the URL ends at /forgot-password (:40).
    • ForgotPassword_WithUnknownEmail_ShowsTheSameConfirmation (:43-58) submits an unknown-{UniqueId()}@test.com address that certainly has no account, then asserts the positive path exactly: the success confirmation appears (:55), the URL stays on /forgot-password (:56), and the back-to-login link is visible (:57). There is no error alert and no navigation to distinguish it from a real address, which is the whole point.
    • ResetPassword_WithEmptyForm_ShowsClientValidationErrors (:60-76) clicks submit on an untouched form; DataAnnotations block OnValidSubmit so nothing is sent, and the fact asserts the field-level texts "Email is required" and "Reset token is required" plus staying on /reset-password (:73-75).
    • ForgotPasswordPage_ShouldHaveNoAccessibilityViolations (:78-89) and ResetPasswordPage_ShouldHaveNoAccessibilityViolations (:91-100) each load their page and scan with AxeOptions.Wcag21Aa.
  • Why it's built this way: the validation fact asserts the field-level text rather than a page-level alert, and the source gives the reason: those messages are present in both render modes (Server prerender and WebAssembly) while a page-level alert is not, the same reasoning as the mismatched-password registration test (:70-72, and UserRegistrationTestsBase.Register_WithMismatchedPasswords_ShouldShowError). The a11y scans pass the strict Wcag21Aa preset explicitly rather than running an unscoped RunAxe, keeping this workflow on the same documented target as the rest of Identity (:85-88).
  • Where it's used: subclassed with no additions in both consumer E2E suites (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/PasswordResetTests.cs:5, MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/PasswordResetTests.cs:5). The two Page Objects it drives are additionally exercised against the framework's own backend-less gallery host by ForgotPasswordPageE2ETests and ResetPasswordPageE2ETests (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/ForgotPasswordPageE2ETests.cs:9, MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/ResetPasswordPageE2ETests.cs:9).

ProfileManagementTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Identity · MMCA.Common.Testing.E2E/Workflows/Identity/ProfileManagementTestsBase.cs:10 · Level 4 · abstract class

  • What it is: the reusable profile workflow base. It verifies that name, address, and password changes persist, that the profile page loads pre-filled with the registered data, an opt-in email-change journey, and that the profile page is accessibility-clean.
  • Depends on: E2ETestBase, ProfilePage, PageExtensions, AxeOptions, AwesomeAssertions, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Identity/ProfileManagementTestsBase.cs:1-6).
  • Concept: the authored-once workflow base taught in AuthorizationTestsBase, here driving a ProfilePage. [Rubric §24, Forms/Validation/UX Safety] assesses whether edit-and-persist journeys work end to end; [Rubric §21, Accessibility] applies through the a11y fact.
  • Walkthrough: one virtual switch, ProfileSupportsEmailChange, off by default (:23), and three virtual snackbar texts, NameSavedMessage ("Name updated successfully."), AddressSavedMessage and EmailSavedMessage (:30, :33, :36), which a consumer whose profile page words its confirmations differently overrides. One virtual hook, FillStateAsync(ProfilePage), sets the State field during the address test: the default null-guards the page object and types "TS" into a text field, and an app whose State field is a picker overrides it to choose an option, since a read-only combobox cannot be filled (:38-49). Six facts follow. ChangeName_ShouldUpdateProfileName clears and fills both name fields, saves, waits up to 30 seconds for the NameSavedMessage snackbar (:69), re-navigates, and asserts the values persisted (:51-78); ChangeAddress_ShouldUpdateProfileAddress does the same for the five address fields, setting State through FillStateAsync (:92), waiting on AddressSavedMessage (:96), and asserting on line 1 (:80-103). Both use Playwright's plain FillAsync for their text fields rather than the re-hydration-safe helper, since the profile page is reached by client-side navigation on an already interactive runtime. ChangePassword_WithValidCurrentPassword_ShouldSucceed fills the three password fields through the shared FillFieldAsync, waits for the "Password changed successfully." snackbar, then signs out through the base's SignOutAsync and logs back in with the new password (:105-134, sign-out at :131). ChangeEmail_ShouldUpdateEmail is opt-in and returns immediately unless ProfileSupportsEmailChange is overridden true; when it runs, it waits on EmailSavedMessage before reloading (:136-170, wait at :163). ProfilePage_ShouldLoadWithUserData asserts the form is pre-filled from registration (:172-190). ProfilePage_ShouldHaveNoAccessibilityViolations scans with AxeOptions.Wcag21Aa (:192-205).
  • Why it's built this way: each save waits for its confirmation snackbar rather than LoadState.Load because navigating away while the save is still in flight cancels the write, so without the wait the read-back could run before the save persisted (:25-29). The email-change fact is a declared opt-in rather than a DOM probe because the previous probing version passed vacuously when the field was absent, reporting coverage for a journey the app does not offer; overriding the flag makes a missing field fail loud (:17-22, :139-142). State setting is a virtual hook rather than a fixed FillAsync because the field's shape is the app's choice: a text field in the default, a picker in Store, and the address journey stays shared across both (:38-44). The sign-out-then-login step delegates to E2ETestBase.SignOutAsync instead of clicking and waiting inline, so this workflow inherits both halves of the fix (the /login URL wait rather than LoadState.Load, and the one-shot retry on a superseded navigation that Firefox and WebKit report as an error) from one place; the retained comment records why the load-state wait raced the in-flight logout navigation (:125-130).
  • Where it's used: subclassed only by Store (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/ProfileManagementTests.cs:5), whose one addition overrides FillStateAsync to open its State picker over the US postal regions and click the "Texas" item in the open popover, waiting up to 5 seconds for it to show (:11-19). Store does not override ProfileSupportsEmailChange, so the email-change fact passes without exercising a journey Store offers. ADC does not subclass this base: its profile page supports the avatar photo, password change, and account deletion but no name or address editing, so MMCA.ADC.E2E.Tests writes its own ProfileManagementTests directly on E2ETestBase with a password-change fact mirroring this one, a claims-page fact, and an avatar upload-replace-remove round trip that builds its two PNGs from base64 constants so the test needs no fixture file on disk (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/ProfileManagementTests.cs:8, :26, :56, :70, constants at :13-16).

PseudoLocalizationTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Globalization · MMCA.Common.Testing.E2E/Workflows/Globalization/PseudoLocalizationTestsBase.cs:52 · Level 4 · abstract class

  • What it is: the reusable pseudo-localization fitness gate. Each page a consumer declares is loaded under the qps-Ploc pseudo culture and asserted three ways (a visible bracket sentinel renders, the page's en-US probe does NOT render in plain English, and the page does not overflow horizontally), then re-loaded under the default culture to prove the sentinel never ships to a real locale.
  • Depends on: E2ETestBase, PseudoLocalizedPage, PageExtensions.GotoAndWaitForBlazorAsync, AwesomeAssertions, and System.Globalization (MMCA.Common.Testing.E2E/Workflows/Globalization/PseudoLocalizationTestsBase.cs:1-5).
  • Concept introduced, pseudo-localization as an executable gate. A pseudo locale transforms every localized string (here by bracketing it and adding a combining accent to each letter) without needing a translator. Three properties fall out of that, and this base asserts all three: the round trip from resource file through IStringLocalizer and the framework's PseudoStringLocalizer to markup is genuinely happening (the sentinel is visible), no string bypassed the pipeline (the plain en-US probe is absent), and the layout tolerates the roughly 40% text expansion a real translation brings (no horizontal document overflow), which is the rubric's layout-tolerance criterion (doc, :26-41). [Rubric §27, i18n] assesses whether localization is verified end to end rather than assumed; [Rubric §22, Responsive/Cross-Browser] covers the overflow half. The transform itself is taught with PseudoStringLocalizer; the supported-culture list including the pseudo locale is SupportedCultures.
  • Walkthrough: six virtual knobs and two facts.
    • The knobs. ScannedPages is the one abstract member (:60). PseudoLocale defaults to "qps-Ploc", restated here rather than referenced because the shipped fixture library deliberately does not reference MMCA.Common.Shared (:62-66). Sentinel defaults to "[!!" (:69), OverflowTolerancePx to 1 CSS pixel so sub-pixel rounding is absorbed without admitting a genuine sideways scrollbar (:71-75), ProbeVisibleTextOnly to false so the probe is searched in the whole document including attributes such as an input placeholder (:77-83), AssertProbePresentUnderDefaultCulture to true (:85-90), and Timeout to 15_000 ms (:93).
    • PseudoLocale_RendersSentinel_WithoutEnUsLeak_AndDoesNotOverflowHorizontally (:95-132) first guards non-vacuity on ScannedPages (:98-99), then per page activates the culture by loading /culture/set?culture=...&redirectUri=... (:106-107), settles (:109), asserts at least one VISIBLE sentinel (:112-113), asserts the probed text does not contain the en-US probe (:117-120), and evaluates scrollWidth - clientWidth on the scrolling element against the tolerance (:124-130).
    • DefaultCulture_DoesNotLeakPseudoSentinel (:134-157) re-loads each page with no culture cookie (each test gets a fresh browser context from E2ETestBase), asserts the page content contains no sentinel (:144-147), and, when AssertProbePresentUnderDefaultCulture holds, asserts the probe IS present so the leak check cannot pass vacuously (:149-155).
    • Two private helpers: ProbedTextAsync selects document content or body innerText per the flag (:164-167), and SettleAsync waits for the optional settle selector and then for zero [role='progressbar'] elements (:174-184).
  • Why it's built this way: activation goes through the culture COOKIE (GET /culture/set, the same endpoint the app's culture switcher uses) rather than a query-string culture provider, and the remarks explain why: a Blazor Server circuit takes its culture from the request that starts the circuit, which carries cookies but not the original page's query string, so a query-string-only activation would pseudo-localize the prerender and then revert on hydration (:42-51). qps-Ploc is allowlisted in Development only, which is what an Aspire-launched E2E stack runs, so the gate needs no host change; public pages are the deliberate subjects because skipping login keeps the gate robust. The mechanics reference ADR-027.
  • Where it's used: subclassed once per consumer, each contributing only its page list. ADC takes every default unchanged and declares three public pages (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/PseudoLocalizationTests.cs:29, list at :39-44); Store declares three and overrides ProbeVisibleTextOnly to true so an aria-label or serialized prerender payload carrying the plain form cannot fail the gate (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/PseudoLocalizationTests.cs:25, list at :41-46, override at :53). The framework's own gallery has a separate prerender-level equivalent in PseudoLocalizationE2ETests.

UserLoginTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Identity · MMCA.Common.Testing.E2E/Workflows/Identity/UserLoginTestsBase.cs:10 · Level 4 · abstract class

  • What it is: the reusable login workflow base. Valid credentials reach the home page and show the authenticated app bar, invalid credentials show an error and stay on /login, the create-account link navigates to /register, and the login page is accessibility-clean.
  • Depends on: E2ETestBase, LoginPage, PageExtensions, AxeOptions, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Identity/UserLoginTestsBase.cs:1-6).
  • Concept: the authored-once workflow base taught in AuthorizationTestsBase, here driving a LoginPage. [Rubric §28, Front-End Testing] and [Rubric §11, Security] apply.
  • Walkthrough: four facts. Login_WithValidCredentials_ShouldNavigateToHomePage registers (which auto-logs in), signs out through the base's SignOutAsync (:27), logs back in, and asserts the URL left /login, the sign-out button (accessible name "Logout") is visible, and the "Sign In" link is not (:18-41). Login_WithInvalidPassword_ShouldShowError drives LoginPage.LoginAsync with a nonexistent account and asserts the error alert appears and the URL stays on /login (:43-57). Login_NavigateToCreateAccount_ShouldGoToRegisterPage clicks the create-account link and asserts /register (:59-71). LoginPage_ShouldHaveNoAccessibilityViolations scans with AxeOptions.Wcag21Aa (:73-84).
  • Why it's built this way: the sign-out step is a single call into E2ETestBase.SignOutAsync rather than an inline click plus wait, and the comment above it (:23-26) keeps the reason on the call site: waiting for the logout forceLoad's /login URL rather than LoadState.Load is the fix for the sign-out-then-login race, where the current document's load event had already fired so the wait returned immediately and the pre-login cleanup evaluate died with "execution context was destroyed".
  • Where it's used: subclassed in both consumer E2E suites (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/UserLoginTests.cs:5, MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/UserLoginTests.cs:5).

UserPreferencesTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Preferences · MMCA.Common.Testing.E2E/Workflows/Preferences/UserPreferencesTestsBase.cs:21 · Level 4 · abstract class

  • What it is: the reusable culture-switch and theme-toggle workflow base. It verifies that switching to Spanish localizes and persists, that toggling dark mode applies and persists, and that both controls are reachable on a mobile viewport.
  • Depends on: E2ETestBase, PageExtensions (GotoAndWaitForBlazorAsync), AwesomeAssertions, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Preferences/UserPreferencesTestsBase.cs:1-5).
  • Concept introduced, the self-contained preferences fitness base. Unlike the identity bases, this one needs no app-specific overrides beyond the dark-palette value of an app that re-points the palette variable: the probe page is the shared /login, the probe string is the localized "Welcome Back" / "Bienvenido de nuevo", and persistence is the anonymous cookie pair (.AspNetCore.Culture plus mmca_theme), all owned by Common UI in every app (:9-20). [Rubric §27, i18n] assesses whether localization actually switches and persists; [Rubric §20, Design System & Theming] covers the theme toggle; [Rubric §22, Responsive/Cross-Browser] covers the mobile-parity fact. The source doc cites ADR-027 and ADR-028 for the localization and theming mechanics.
  • Walkthrough: a dark-background probe script that reads --mud-palette-background, strips whitespace, lower-cases it, and returns whether it is in the expected array passed in (:25-28); a virtual DarkPaletteBackgrounds list supplies that array, defaulting to MudBlazor's own PaletteDark background as either the raw hex #1a2027 or its rgba(26,32,39,1) form (:35-42); plus desktop and mobile action-cluster locators scoped by container (.appbar-icon-actions and .toprow-actions, :44-48) to disambiguate the duplicated NavMenu controls. Three facts follow. CultureSwitch_ToSpanish_ShouldLocalizeAndPersist opens the Language menu and clicks the Spanish item (its label carries the accented spelling in source) located as .mud-popover-open .mud-menu-item (the popover carries that class only once Blazor interactivity attached, and the items are not .mud-list-item and carry no menuitem role), then asserts the Spanish probe survives a fresh full page load (:50-73). ThemeToggle_ToDark_ShouldApplyAndPersist clicks the title-stable toggle (its aria-label flips with state, its title does not), asserts the palette variable flipped through AssertDarkPaletteAsync, asserts localStorage holds mmca_theme set to dark, then reloads and re-asserts (:75-94). MobileViewport_CultureAndTheme_ShouldBeReachable sets a 390x844 viewport and asserts the controls come from NavMenu's top row, then actually toggles the theme there rather than only checking that it rendered (:96-110). The private AssertDarkPaletteAsync polls the probe script with DarkPaletteBackgrounds.ToArray() as its argument and a 15 s ceiling (:112-116).
  • Why it's built this way: the mobile fact pins the v1.103.0 regression where the controls lived only in the app bar, hidden below 1024px; the selectors mirror the gallery's own MobileTopRowE2ETests exactly because the MudMenu activator exposes no literal aria-label attribute, so raw CSS attribute selectors do not match it (:14-19). The dark value is a virtual list rather than a hard-coded pair because an app's stylesheet may re-point the variable at its own dark surface; without the override the theme facts time out while dark mode works (:35-41).
  • Where it's used: subclassed in both consumer E2E suites. Store overrides DarkPaletteBackgrounds with #1a1416, because its stylesheet re-points --mud-palette-background at the brand's dark page (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Preferences/UserPreferencesTests.cs:15, override at :17, rationale at :10-14). ADC keeps the default palette and only opts into its terms acceptance gate through AcceptsTermsAfterSignIn (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Preferences/UserPreferencesTests.cs:10, :14).

UserRegistrationTestsBase

MMCA.Common.Testing.E2E · MMCA.Common.Testing.E2E.Workflows.Identity · MMCA.Common.Testing.E2E/Workflows/Identity/UserRegistrationTestsBase.cs:10 · Level 4 · abstract class

  • What it is: the reusable registration workflow base. Valid data navigates to the home page and logs the user in, mismatched passwords show the inline validation message and stay on /register, a duplicate email shows a warning naming the way out (sign in or reset the password), and the register page is accessibility-clean.
  • Depends on: E2ETestBase, RegisterPage, PageExtensions (FillAndVerifyAsync), AxeOptions, and Microsoft.Playwright (MMCA.Common.Testing.E2E/Workflows/Identity/UserRegistrationTestsBase.cs:1-6).
  • Concept: the authored-once workflow base taught in AuthorizationTestsBase, here driving a RegisterPage. [Rubric §24, Forms/Validation/UX Safety] assesses client-side validation and duplicate handling; [Rubric §21, Accessibility] applies through the a11y fact.
  • Walkthrough: four facts. Register_WithValidData_ShouldNavigateToHomePage registers a unique user through the Page Object and asserts the URL left /register and the app-bar button named "Logout" is visible (:17-34). Register_WithMismatchedPasswords_ShouldShowError fills every field through FillAndVerifyAsync, ticks the Terms box through RegisterPage.AcceptTermsIfShownAsync (on a host with a Terms URL the submit stays disabled until it is ticked, so the password mismatch is then the only reason the submit is refused), submits exactly once, and asserts the inline "Passwords do not match" validation text plus staying on /register (:36-67). Register_WithDuplicateEmail_ShouldShowError registers, then re-registers the same email and asserts the EmailAlreadyRegisteredAlert (a warning naming the way out, sign in or reset the password, rather than the generic red error alert the other server-side failures use) is visible, contains the email, and that the page stays on /register (:69-87). RegisterPage_ShouldHaveNoAccessibilityViolations scans with AxeOptions.Wcag21Aa (:89-100).
  • Why it's built this way: the mismatched-passwords fact submits once and asserts the field-level validation text rather than re-clicking, because the [Compare] validation fires OnInvalidSubmit and a re-clicking helper would make the message flicker out from under the wait; the text is asserted (not the alert element) because it renders in both the Server-mode and WebAssembly paths, while the page-level alert appears only on the Server-mode prerender path (:43-47, :60-64).
  • Where it's used: subclassed in both consumer E2E suites (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/UserRegistrationTests.cs:5, MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/UserRegistrationTests.cs:5).

BunitInteractionExtensions

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/BunitInteractionExtensions.cs:12 · Level 0 · static class

  • What it is: a small set of intention-revealing helpers over bUnit's element API so a component test can say "click the button labelled Save" instead of hand-rolling a DOM query, deliberately preferring accessible text over brittle CSS-path selectors (MMCA.Common.Testing.UI/Infrastructure/BunitInteractionExtensions.cs:7-11).
  • Depends on: no first-party types. Three externals: AngleSharp's IElement (the DOM node type bUnit returns, :1), bUnit's IRenderedComponent<TComponent> (:2), and Microsoft.AspNetCore.Components.IComponent as the generic constraint (:3). See primer §3 for the external stack.
  • Concept introduced, extension(T) blocks used for something other than DI. Everywhere else in this codebase the C# preview extension-member syntax registers services (primer §4); here the same feature bolts query and interaction members onto a third-party generic type. The declaration is extension<TComponent>(IRenderedComponent<TComponent> cut) where TComponent : IComponent (:14-15), so the extension block is itself generic over the component type and the members inside read as instance methods on the rendered component (cut.ClickButtonByText("Save")). [Rubric §28, Front-End Testing] assesses whether UI behavior is exercised through realistic automated checks; querying by the text a user actually sees is what keeps a component test asserting on behavior rather than on markup structure. [Rubric §15, Best Practices & Code Quality] assesses how well the code resists churn; a CSS-path selector breaks on any wrapper change, while a text query survives it.
  • Walkthrough: three members, all inside the single extension block.
    • FindButtonByText(string text) (:18-25) materializes every <button> via cut.FindAll("button") (:20), then takes the first whose TextContent contains text under StringComparison.OrdinalIgnoreCase (:21). On no match it throws an InvalidOperationException whose message enumerates the trimmed text of every button present, pipe-separated (:22-24), so a failing test names the buttons it could see instead of reporting a bare null.
    • ClickButtonByText(string text) (:28-29) is the action form: it delegates to FindButtonByText and calls bUnit's Click(), which raises the component's onclick through the renderer.
    • HasText(string text) (:32-33) is the read form, a case-insensitive Contains over cut.Markup. It answers on the whole rendered markup, not only visible text, which is the trade-off for having no dependency beyond the rendered string.
  • Why it's built this way: the diagnostic message is the point. bUnit's own Find(selector) throws with the selector, which tells you nothing when the button simply rendered with different copy; listing the actual buttons turns a red test into a readable one on the first run.
  • Where it's used: by the shared UI's own page tests, for example MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Notifications/NotificationSendTests.cs:99,125,362 (the send and cancel buttons of the notification composer) and MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Notifications/NotificationInboxTests.cs, and across the ADC and Store component suites that derive from BunitComponentTestBase.
  • Caveats / not-in-source: HasText matches attribute values and element names too, since it searches raw markup; a test asserting on user-visible copy that happens to collide with a CSS class name would pass for the wrong reason.

CapturedRequest

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:189 · Level 0 · sealed record

  • What it is: the immutable snapshot of one HTTP request a UI service sent, recorded by CapturingHttpMessageHandler so a test can assert on what went out, not only on what came back (MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:185-188).
  • Depends on: BCL only, System.Net.Http.HttpMethod, System.Uri, and IReadOnlyDictionary<string, string>. No first-party types.
  • Concept introduced, capture-then-assert on the outbound side. A canned-response fake proves the service handles a given payload; it says nothing about whether the service built the right URL, attached the bearer token, sent the right If-Match, or serialized the right body. Recording every request as a value object makes those assertions possible without a server. [Rubric §14, Testability] assesses how much of a component's contract can be verified in isolation; capturing the request turns the client's outbound contract (route, query string, headers, body) into something a unit test can pin. [Rubric §9, API and Contract Design] applies indirectly: these captures are where a UI service's assumed route and header shape is asserted against the API's actual one.
  • Walkthrough: six positional members plus one init member.
    • The positional six (:159-164) are filled by the handler's CaptureAsync (:82-88). Method is the verb; Uri is the full request URI and is nullable because HttpRequestMessage.RequestUri is; Path is uri?.AbsolutePath with an empty-string fallback (:85); PathAndQuery keeps the query string that Path drops (:86), which is what a paging or filter assertion needs; Authorization is the header rendered back to a string, or null when absent (:87); Body is the content read as text, left null when the request had no content (:75-79).
    • Headers (:179-180) is an IReadOnlyDictionary<string, string> over an OrdinalIgnoreCase comparer, defaulting to an empty dictionary and set through an object initializer by CaptureAsync (:89-91). It flattens request headers and content headers into one lookup, which is what makes headers the service layer sets itself assertable.
    • The XML doc records why Headers is a non-positional init member rather than a seventh positional parameter (:172-177): adding one would rewrite the record's primary constructor and Deconstruct, breaking every consumer that constructs or deconstructs a captured request. Authorization therefore stays where it is even though the same value is also reachable through Headers.
  • Why it's built this way: being a record, structural equality and a readable ToString() come for free, which is what makes a failing assertion legible. The deliberate refusal to move Authorization into Headers is a source-compatibility decision written into the type rather than left implicit.
  • Where it's used: exposed as the ordered IReadOnlyList<CapturedRequest> on CapturingHttpMessageHandler.Requests (:42) and filtered by its RequestsFor(method, absolutePath) (:61-65); consumed by every UI HTTP-service test in the three repos, for example the optimistic-concurrency assertion captured.Headers["If-Match"].Should().Be("\"v7\"") and the content-header pair in MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/CapturingHttpMessageHandlerTests.cs:208-210,226,229.

ErrorSummaryExtensions

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/ErrorSummaryExtensions.cs:10 · Level 0 · static class

  • What it is: a reader that pulls the shared ErrorSummary component's current messages out of a rendered component under test, so a form test asserts on the validation text a user would see rather than on raw markup (MMCA.Common.Testing.UI/Infrastructure/ErrorSummaryExtensions.cs:6-9).
  • Depends on: no first-party types at compile time, but it is coupled by contract to MMCA.Common.UI's ErrorSummary.razor, which renders the marker class on its title (MMCA.Common/Source/Presentation/MMCA.Common.UI/Components/Forms/ErrorSummary.razor:13). Externals: bUnit's IRenderedComponent<TComponent> and AngleSharp's element query API, plus Microsoft.AspNetCore.Components.IComponent as the constraint (:1-2).
  • Concept introduced, testing a component that renders two different shapes for the same state. ErrorSummary renders several messages as a <ul> of <li> items but a single message as plain text inside the alert (ErrorSummary.razor:15-29). A test that only queried li would read an empty summary exactly when one rule is broken, which is the most common form-validation case, and the failure would look like the component not rendering at all. This helper reads both shapes and strips the alert's title so the caller gets one list either way; the remark on the member says so directly (ErrorSummaryExtensions.cs:22-26). [Rubric §24, Forms/Validation/UX Safety] assesses whether validation feedback is coherent and verified; this is the assertion primitive that makes the form suites able to check it. [Rubric §28, Front-End Testing] is the capability it serves. [Rubric §21, Accessibility] applies obliquely: the two rendering shapes exist because several independent failures must be announced as several list items, and the reader has to understand that choice to see through it.
  • Walkthrough: one constant and one extension member.
    • ErrorSummaryTitleClass (:13) is the public const "mmca-error-summary-title", the class the component puts on its title and therefore the marker that distinguishes an error summary's MudAlert from any other alert on the page. Exposing it as a constant lets a consumer's own assertion use the same literal.
    • The extension<TComponent>(IRenderedComponent<TComponent> cut) block (:15-16) mirrors the shape BunitInteractionExtensions introduces.
    • ErrorSummaryMessages() (:27-47) guards the rendered component (:29), then finds the first .mud-alert that contains an element carrying the title class (:31-32). No such alert means no summary is showing, and it returns an empty list (:33-36). It then queries li descendants: when any exist, it returns their trimmed text one entry per rule (:38-42). Otherwise it takes the single-message shape, reads the title's text and removes that substring from the alert's whole TextContent ordinally, trims, and returns either an empty list or that one message (:44-46).
  • Why it's built this way: matching the alert by the title class rather than by position keeps the reader working on a page that renders other MudAlert instances (an info banner, an offline notice). Stripping the title by string removal rather than by DOM surgery keeps the helper read-only against the rendered tree.
  • Where it's used: covered directly by MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Components/Forms/ErrorSummaryExtensionsTests.cs:22,33,43,54, which pins all four branches (no summary, single-message shape, list shape, and an alert without the title class), and used by the ADC create-form suites, for example MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Events/EventCreateTests.cs:55, .../Question/QuestionCreateTests.cs:57, and .../Activity/ActivityCreateTests.cs:91.
  • Caveats / not-in-source: the single-message branch removes the title text by ordinal string replacement, so a message whose text happens to contain the title string would have that fragment removed as well.

FieldErrorAccessibilityExtensions

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/FieldErrorAccessibilityExtensions.cs:12 · Level 0 · static class

  • What it is: a bUnit assertion that a form field in an invalid state is exposed to a screen-reader user: one call, ShouldExposeFieldErrorAccessibly(inputId, expectedMessage), on a rendered component (MMCA.Common.Testing.UI/Infrastructure/FieldErrorAccessibilityExtensions.cs:14-26).
  • Depends on: no first-party types. Externals: bUnit's IRenderedComponent<TComponent> with the IComponent constraint (:14-15), AngleSharp element queries through FindAll, and FluentAssertions.
  • Concept introduced, asserting an accessibility contract rather than markup shape. Three facts together make a validation error usable without sight: an element with role="alert" is rendered, the input carries aria-invalid="true", and an id listed in its aria-describedby points at an element whose text contains the message. Each is easy to break independently (a renamed id leaves aria-describedby dangling while the visible text still looks right), so the helper checks all three and gives each assertion a because: reason that names the failing fact. [Rubric §21, Accessibility] assesses whether accessibility is verified; this is the unit-tier check that field-level errors are announced. [Rubric §24, Forms/Validation/UX Safety] assesses coherent validation feedback; this pins that the feedback is associated with its field. [Rubric §28, Front-End Testing] is the capability it serves, and it complements ErrorSummaryExtensions, which reads the summary text.
  • Walkthrough: one extension member inside an extension<TComponent>(IRenderedComponent<TComponent> cut) block (:14-15), the same shape as BunitInteractionExtensions.
    • Guards: cut non-null, inputId and expectedMessage not null or whitespace (:28-30).
    • Alert: at least one [role="alert"] element exists (:32-33).
    • Input: exactly one element carries id="{inputId}" (:35-36), and its aria-invalid equals "true" (:38-40).
    • Description: aria-describedby is split on spaces and must be non-empty (:42-45); every referenced id is resolved with FindAll, and the resolved elements' text must be non-empty, so a dangling reference fails (:47-52); finally at least one description contains expectedMessage by ordinal comparison (:53-55).
  • Why it's built this way: resolving the referenced ids instead of only checking the attribute is present is what catches a dangling aria-describedby; a presence-only check would pass on a reference to nothing.
  • Where it's used: no call sites outside the defining file in the current tree (the brief records none); the bUnit tier it belongs to is described in ADR-103.

FreshApiClientFactory

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:73 · Level 0 · sealed class

  • What it is: an IHttpClientFactory test double that returns a brand-new HttpClient on every CreateClient call, whatever name is asked for (in practice "APIClient"), all wired to one shared handler with a fixed base address (MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:66-72).
  • Depends on: System.Net.Http.IHttpClientFactory and HttpMessageHandler (BCL). Constructed with a CapturingHttpMessageHandler in practice, by UiHttpServiceHarness (:47) and by HttpTestDoubles.ClientFactory (MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:23-24).
  • Concept introduced, a fake whose lifetime semantics are load-bearing. The shared UI services acquire a client per call and dispose it afterwards, so a factory double that caches one instance would hand the second call a disposed client and fail with an ObjectDisposedException that looks like a product bug. The XML doc states exactly that ("A fresh instance per call is load-bearing", :69-71). [Rubric §14, Testability] assesses whether test doubles reproduce the real collaborator's contract; matching IHttpClientFactory's ownership semantics, not just its signature, is the difference between a double that works and one that misleads.
  • Walkthrough: a primary-constructor class taking (HttpMessageHandler handler, Uri baseAddress) (:73) with one member. CreateClient(string name) (:79-80) ignores the name and returns new HttpClient(handler, disposeHandler: false) { BaseAddress = baseAddress }. The disposeHandler: false flag is the second load-bearing detail: it lets the handler outlive each client, so the recorded CapturedRequest list survives across calls and the harness alone owns handler disposal (:63).
  • Why it's built this way: ignoring the client name keeps the double usable from any repo without knowing the named-client key the service under test resolves, while the shared handler keeps one capture log per test.
  • Where it's used: exposed as UiHttpServiceHarness.ClientFactory (:47,58) and returned from HttpTestDoubles.ClientFactory (HttpTestDoubles.cs:23-24); the fresh-instance contract and the shared base address are asserted directly in MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/SharedHttpTestDoublesTests.cs:22,27-28,38.

MarkupSnapshotResult

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:110 · Level 0 · readonly record struct

  • What it is: the two-field outcome of a MarkupSnapshot comparison, IsMatch plus a human-readable Message (MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:107-110).
  • Depends on: nothing. That is the entire design point.
  • Concept introduced, returning a result instead of asserting. A shipped test-infrastructure package that called an assertion API would drag an assertion library into every consumer's dependency graph and pin them to its version. Returning a value and letting the caller assert keeps the package dependency-free, which the class doc states explicitly ("kept dependency-free so the shipped package pulls in no assertion library", :11-12). This mirrors the Result pattern the product code uses for expected failures, applied here to a test helper. [Rubric §32, Dependency and Supply-Chain] assesses how carefully the dependency surface of shipped packages is managed; a readonly record struct with two fields adds nothing to a consumer's transitive closure. [Rubric §28, Front-End Testing] is the capability this serves.
  • Walkthrough: a positional readonly record struct with bool IsMatch (true when the markup matched the committed baseline or was just refreshed) and string Message (:102-104). readonly plus struct means no allocation per comparison; the record shape gives value equality and a printable form. Callers use it as result.IsMatch.Should().BeTrue(result.Message), so the diff text that MarkupSnapshot.BuildDiffMessage produced becomes the assertion failure message.
  • Where it's used: returned from every branch of MarkupSnapshot.Match (:45,51-53,58-59) and asserted by the golden-markup regressions in MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Components/PrimitivesSnapshotTests.cs:21,31,41,51,61 and by the ADC component snapshot suites.

MudProviderHandles

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:168 · Level 0 · protected sealed nested record

  • What it is: the three handles returned by BunitComponentTestBase.RenderMudProviders(), one per rendered MudBlazor provider, so a test can query the popover, dialog, or snackbar markup after triggering it (MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:167).
  • Depends on: bUnit's IRenderedComponent<T> and MudBlazor's MudPopoverProvider, MudDialogProvider, and MudSnackbarProvider (:157-160, imports at :2,11-12).
  • Concept: the provider-root problem, taught with BunitComponentTestBase. MudBlazor renders overlays into provider components that normally live in the app layout; in a component test there is no layout, so a dialog or snackbar has nowhere to go. Rendering the providers as separate roots gives them somewhere, and this record is how the test keeps a reference to each root to query it afterwards. [Rubric §20, Design System and Theming] assesses how the shared component library is adopted and exercised; overlay behavior is only testable once the design system's provider contract is honored in the test host.
  • Walkthrough: a positional record with Popover, Dialog, and Snackbar (:157-160), constructed once in RenderMudProviders() after the three Render<T>() calls (:150-153). Dialog is the handle a test queries for MudMessageBox confirm buttons; Snackbar is the one it queries for toast text.
  • Where it's used: returned by RenderMudProviders() (:148-154). Most callers discard the return value and only need the providers rendered, for example MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.UI.Tests/Pages/ShoppingCarts/ShoppingCartListTests.cs:75,90,105,119; a test that must click inside the dialog keeps it, as in MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Pages/Users/UserListTests.cs:349,357-359, which waits for the confirm button to appear in providers.Dialog and clicks it.

MutableAuthenticationStateProvider

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:173 · Level 0 · private sealed nested class

  • What it is: the AuthenticationStateProvider BunitComponentTestBase registers, holding one principal that the test can swap at any point, with change notification to listeners.
  • Depends on: Microsoft.AspNetCore.Components.Authorization (AuthenticationStateProvider, AuthenticationState, :5) and BCL ClaimsPrincipal.
  • Concept introduced, the two ways a Blazor page learns who the user is. Some components read the cascading AuthenticationState value; others inject AuthenticationStateProvider and call GetAuthenticationStateAsync() themselves. A hardcoded-anonymous provider serves neither well once a test needs the signed-in branch, so the base supplies a mutable one and drives both routes from the same principal. The class remark says exactly this: the provider is mutable because it is "a superset of a hardcoded-anonymous one" (:24-27). This provider is the only auth double left in the harness: the authorization decision itself is made by the framework's real DefaultAuthorizationService (:62), which evaluates whatever principal this provider holds, so the principal (and its role claims) is the whole of what a test controls. [Rubric §19, State Management] assesses how shared client state is owned and propagated; auth state is the canonical cascading state, and this is the test-side owner of it.
  • Walkthrough: a primary-constructor class taking the initial principal (:167), storing it in a mutable _principal field (:169). SetPrincipal(ClaimsPrincipal) (:171-175) assigns the field and then calls the base NotifyAuthenticationStateChanged with a fresh AuthenticationState, so an already-rendered <AuthorizeView> re-evaluates rather than keeping its first answer. GetAuthenticationStateAsync() (:177-178) returns Task.FromResult(new AuthenticationState(_principal)), so no async state machine is allocated per call.
  • Where it's used: instantiated as the base's _authProvider field seeded with Anonymous (:42), registered as a singleton AuthenticationStateProvider (:63), and mutated by SetUser (:126) and by every RenderAs call before rendering (:140). It has no usage outside the defining file, being a private nested type.

Route

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:170 · Level 0 · private sealed nested record

  • What it is: one registered canned response inside CapturingHttpMessageHandler: an HTTP method, an absolute path, a status code, an optional JSON body, and the method that turns all four into a fresh HttpResponseMessage.
  • Depends on: BCL HttpMethod, HttpStatusCode, HttpResponseMessage, StringContent, and System.Text.Encoding (MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:1-2). No first-party types.
  • Concept introduced, why the canned response is a recipe rather than an object. HttpContent is consumed when read, so handing the same HttpResponseMessage to two calls fails the second one, and the shared UI services run behind a retry pipeline that can legitimately send the same request twice. Storing the body as a string and rebuilding the response per request removes that whole class of false failure; the handler's own doc calls this out ("Responses are built fresh per request so a Polly retry pipeline never reuses a consumed HttpContent", :14-16). [Rubric §29, Resilience and Business Continuity] assesses how retry and recovery behavior is handled; a fake that cannot be retried would make the retry pipeline itself untestable.
  • Walkthrough: a positional record (HttpMethod Method, string Path, HttpStatusCode StatusCode, string? JsonBody) (:139) with one method. ToResponse() (:141-150) news up an HttpResponseMessage(StatusCode) and, only when JsonBody is not null, attaches a StringContent(JsonBody, Encoding.UTF8, "application/json") (:144-147), so a body-less status (204, 404) round-trips as a genuinely empty response rather than as an empty JSON document.
  • Where it's used: appended by SetResponse (:57) and selected by the handler's Respond using LastOrDefault on method plus case-insensitive path (:124-125), which is what makes "last registration wins" true and lets a test override an earlier canned route mid-test.
  • Caveats / not-in-source: the match ignores the query string entirely (only AbsolutePath is compared, :121-125), so two routes that differ only by query parameters cannot be registered separately. Query assertions go through CapturedRequest.PathAndQuery instead.

CapturingHttpMessageHandler

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:20 · Level 1 · sealed class

  • What it is: the canned-response, request-recording HttpMessageHandler that lets an HTTP-backed UI service be unit tested with no server: it answers every request from registered routes or a responder delegate, and records what was sent (MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:7-19).
  • Depends on: its two companions Route (private nested) and CapturedRequest. Externals: System.Net.Http.HttpMessageHandler (the BCL extension point), System.Text.Json for body serialization, and System.Net/System.Text (:1-3).
  • Concept introduced, faking at the transport boundary instead of the service boundary. The alternative would be mocking the UI service's own interface, which proves nothing about the service. Subclassing HttpMessageHandler puts the fake one layer lower, so the service under test runs its real URL construction, its real serialization, its real header setting, its real status-code handling, and its real error mapping; only the wire is simulated. [Rubric §14, Testability] assesses how much real behavior a unit test covers; this is the difference between testing the service and testing a mock of it. [Rubric §9, API and Contract Design] applies because the registered routes are a written-down expectation of the API's shape, and [Rubric §12, Performance and Scalability] indirectly: the whole tier runs in-process with no sockets, which is what keeps the UI test tier in the fast unit run.
  • Walkthrough: state, then the two configuration modes, then the send path.
    • Static WebJson (:22) is a single JsonSerializerOptions(JsonSerializerDefaults.Web) instance, matching what the WebAPI actually emits (camelCase, case-insensitive reads), so a body serialized here deserializes in the service exactly as a real response would.
    • _respond (:24) is the optional responder delegate; _routes (:25) and _requests (:26) are the registration and capture lists, and _sync (:27) is the one System.Threading.Lock that guards both.
    • The parameterless constructor (:33-35) selects route-registration mode; the delegate constructor (:41) selects responder mode. The two are not exclusive: routes always win, and the delegate is the fallback (:144-168).
    • Requests (:44-53) returns a snapshot of the captures, in order, as an IReadOnlyList<CapturedRequest>: it copies _requests into a new collection under the lock (:48-51), so a test that enumerates it while requests are still arriving cannot hit a "collection was modified" failure.
    • SetResponse(method, absolutePath, statusCode, body = null) (:60-72) normalizes the body through a switch: null stays null (empty body), a string is treated as raw JSON and passed through untouched, and anything else is serialized with WebJson (:62-67). It then appends a Route under the lock (:68-71).
    • RequestsFor(method, absolutePath) (:75-85) filters the captures by verb and case-insensitive path via a collection expression built under the lock, the assertion helper most tests use.
    • SendAsync (:87-96) is the override: capture first, then respond. The capture is awaited outside the lock and only the _requests.Add is taken under it (:89-93), so no await ever runs while the lock is held. CaptureAsync (:98-117) reads the request content to a string when present (:100-104) and builds the CapturedRequest from the URI, absolute path, path-and-query, and the Authorization header rendered to a string (:106-113), then sets its Headers from CaptureHeaders (:114-116). Reading the body here, before responding, is what makes the body assertable at all, since the content stream is consumed by the read.
    • CaptureHeaders (:124-142) builds one OrdinalIgnoreCase dictionary, copying every request header (:128-131) and then, when a body is present, every content header (:133-139), comma-joining multi-valued entries. Its doc gives the reason both halves are needed: If-Match lives on the request while Content-Type lives on the content, and a caller asserting either should not have to know which (:119-123).
    • Respond (:144-168) resolves the response in strict precedence: the last matching registered route, looked up under the lock (:146-160), else the responder delegate (:162-165), else 404 Not Found with an empty body (:167). The doc explains the 404 default as deliberate, mirroring the WebAPI's not-found behavior so an incidental call (a token refresh, say) does not have to be set up in every test (:12-14).
  • Why it's built this way: the two configuration modes serve two different test shapes. A service test that exercises one endpoint many ways wants the delegate ("answer everything this way"); a test that walks a multi-call flow wants named routes with last-registration-wins overrides. Supporting both in one handler avoids two parallel fakes drifting apart. The lock exists because UI services fan out: the class doc states that recording and route registration are thread-safe "so a service that fans requests out concurrently loses none", and that Requests returns a snapshot (:17-18). A plain List<T> appended from parallel SendAsync calls can drop entries or throw, which would turn a correct Task.WhenAll in a service into a flaky test.
  • Where it's used: owned by UiHttpServiceHarness (MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:24,39,43-45), constructed directly by tests that wire the pieces themselves (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/SharedHttpTestDoublesTests.cs:21,35), and used across the ADC UI service suites (for example MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Services/SessionLive/LivePollUIServiceTests.cs and MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Services/SpeakerDashboardServiceTests.cs). It is covered in its own right by MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/CapturingHttpMessageHandlerTests.cs:25,37,53,65, including Capture_RecordsEveryRequest_WhenRequestsArriveConcurrently (CapturingHttpMessageHandlerTests.cs:252), which drives parallel workers through one handler with Task.WhenAll (:267) to pin the concurrency guarantee.
  • Caveats / not-in-source: the lock covers the two lists only. The responder delegate (:164) is invoked outside it, so a delegate that keeps its own state across calls must be thread-safe itself if the service under test issues requests concurrently.

MarkupSnapshot

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:22 · Level 1 · static partial class

  • What it is: a dependency-free golden-markup regression helper. It takes a component's rendered markup, normalizes the parts that change per render, compares it to a committed baseline .html file next to the calling test, and returns the outcome for the caller to assert on (MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:6-22).
  • Depends on: MarkupSnapshotResult as its return type. Externals: System.Text.RegularExpressions with source-generated regexes and System.Runtime.CompilerServices.CallerFilePathAttribute (:1-2), plus BCL file I/O.
  • Concept introduced, snapshot testing without screenshots. A pixel screenshot baseline is OS-dependent and needs per-platform golden management; normalized markup is deterministic and identical on every CI runner, which is why this comparison can live in the fast in-solution unit tier rather than in a browser job. The class doc states both halves of that trade (:13-19). Two policies make it safe to rely on: a baseline is refreshed only when UPDATE_SNAPSHOTS=1 is set, and both a refreshing run and a missing baseline are reported as a non-match, so a regression can never pass silently, neither on an absent snapshot nor on a CI job or shell that leaves the variable set. [Rubric §28, Front-End Testing] assesses whether UI regressions are caught automatically; this is the structural-regression half of that (axe scans in the E2E tier are the accessibility half). [Rubric §20, Design System and Theming] assesses how the shared component library is governed; snapshotting the shared primitives is what makes an unintended change to a reused component fail somebody's build.
  • Walkthrough:
    • Match(markup, snapshotName, [CallerFilePath] callerFilePath = "") (:31-65) is the whole public surface. It guards both string arguments (:33-34), normalizes the markup (:36), and locates the baseline as Snapshots/{snapshotName}.html in the directory of the calling test file, creating the folder if needed (:37-39). [CallerFilePath] is the mechanism: the compiler bakes the caller's source path in, so baselines live beside the tests that own them with no configuration.
    • Three outcomes follow. With UPDATE_SNAPSHOTS=1 (compared ordinally, :41) it writes the file and returns a non-match whose message says the baseline was refreshed and must be reviewed and committed (:42-51). That is deliberate (comment :44-46): a run that regenerates baselines must never count as a pass, or a CI job that leaves the variable set would turn every markup regression green while rewriting the baselines. With no existing baseline it likewise writes the file and returns a non-match telling you to review and commit it (:53-59). Otherwise it reads the baseline, normalizes its line endings and trims (:61), and returns match or a built diff (:62-64).
    • Normalize(markup) (:69-75) does the work that makes the comparison stable: CRLF to LF and trim (:71), then replace both GUID forms with the literal {guid} (:72-73), then right-trim every line (:74). MudBlazor injects fresh GUIDs into element ids and ARIA associations on every render, so without this step no snapshot would ever match twice.
    • BuildDiffMessage (:77-97) walks both line arrays to the first difference and reports the line number with the expected and actual text plus the UPDATE_SNAPSHOTS=1 instruction (:89-91); if no line differs it reports a length mismatch (:95-96).
    • GuidRegex (:99-100) and Hex32Regex (:102-103) are [GeneratedRegex] partial properties, which is why the class is partial: the patterns compile at build time rather than being interpreted per call.
  • Why it's built this way: writing the baseline but still failing the test is the deliberate part, on first run and on a refresh alike. Writing and passing would let a regression bless itself on any machine where the file happened to be missing, or where UPDATE_SNAPSHOTS=1 was left set; failing but not writing would make creating a new snapshot a manual chore. Writing plus failing gives the file for free and forces a human to review and commit it, then re-run without the variable.
  • Where it's used: by MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Components/PrimitivesSnapshotTests.cs:21,31,41,51,61, which snapshots the shared UI primitives (EmptyState, PageHeader, PageErrorState, PageLoadingState) against baselines committed under that project's Snapshots/ folder, and by the per-module ADC component snapshot suites, for example MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Components/ComponentsSnapshotTests.cs:69,87,124,143, MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Components/ComponentsSnapshotTests.cs:122,135,147,160,175,197, and MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Components/ComponentsSnapshotTests.cs:33,43.
  • Caveats / not-in-source: the normalizer replaces any bare 32-character hex token, so a legitimate markup value of that shape (a content hash in an asset URL, for example) would also collapse to {guid} and stop being compared. Some ADC suites wrap the markup in a repo-local Stable(...) helper before calling Match, so the shipped normalizer is not always the only pass applied.

StubTokenStorageService

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/StubTokenStorageService.cs:13 · Level 1 · sealed class

  • What it is: the canned ITokenStorageService for UI HTTP-service tests. It returns fixed tokens with no platform storage behind it, lets the access-token read be swapped for a throwing delegate, and mutates its values on set and clear so login and logout flows are assertable (MMCA.Common.Testing.UI/Infrastructure/StubTokenStorageService.cs:5-12).
  • Depends on: ITokenStorageService from MMCA.Common.UI.Services.Auth (:1), the contract the real per-head storage implementations satisfy. That reference is one of the reasons the Testing.UI package carries a ProjectReference to MMCA.Common.UI, as its comment records (MMCA.Common.Testing.UI/MMCA.Common.Testing.UI.csproj:28-30).
  • Concept introduced, a stub with a failure switch. Most stubs only model the happy path, which leaves the interesting branch (what the service does when storage is unreachable) untested. Blazor has a real version of that branch: during prerender, JS interop is unavailable, so a browser-storage-backed implementation throws. Exposing the read as a replaceable delegate lets a test reproduce that exact failure without a browser. [Rubric §26, Front-End Security] assesses how tokens are stored and attached; the stub is what lets the bearer-attachment path be asserted at all. [Rubric §29, Resilience and Business Continuity] assesses how failure paths are handled; the swappable delegate is the extension point for testing them.
  • Walkthrough:
    • The constructor takes accessToken = "test-token" and refreshToken = "test-refresh-token" (:18-23), assigns both, and sets AccessTokenProvider to a closure returning the property AccessToken (:22), not the constructor argument, so later mutations are visible through the default provider.
    • AccessToken and RefreshToken (:26,29) are mutable auto-properties, readable by a test as post-condition assertions.
    • AccessTokenProvider (:36) is the swap point: replace it with a delegate that throws InvalidOperationException and GetAccessTokenAsync reproduces the prerender storage failure (:31-35).
    • The four interface members follow: GetAccessTokenAsync() invokes the provider (:39), GetRefreshTokenAsync() returns the property (:42), SetTokensAsync(access, refresh) assigns both and completes (:45-50), and ClearTokensAsync() nulls both (:53-58).
  • Why it's built this way: routing the read through a delegate while set and clear mutate plain properties keeps the common case zero-ceremony (construct it and go) and the failure case one assignment away, with no separate throwing subclass to keep in sync.
  • Where it's used: constructed by UiHttpServiceHarness and exposed as its TokenStorage property (MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:48,61), returned from HttpTestDoubles.TokenStorage (MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:28-29), and covered directly by MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/StubTokenStorageServiceTests.cs:19,28,38,51,65,76.

TestPrincipal

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/TestPrincipal.cs:7 · Level 1 · static class

  • What it is: the factory for the ClaimsPrincipal instances component tests render as, with the claim shape the shared pages actually read (MMCA.Common.Testing.UI/Infrastructure/TestPrincipal.cs:6).
  • Depends on: AuthClaimTypes from MMCA.Common.Shared.Auth (:2), for the sub claim name. Externals: BCL System.Security.Claims (:1).
  • Concept introduced, the authenticated-identity trap. A ClaimsIdentity is authenticated only when it was constructed with an authentication type; an identity built from claims alone reports IsAuthenticated == false and every <AuthorizeView> renders its anonymous branch, which is a confusing way for a test to fail. This factory always passes one (authenticationType: "TestAuth", :31), and the base class's Anonymous principal deliberately omits it (BunitComponentTestBase.cs:41), so the two states are produced by the same mechanism from opposite ends. [Rubric §11, Security] assesses how identity and claims are modelled; encoding the app-wide identifier claim names in one shared factory keeps every UI test honest to the claims the product code reads.
  • Concept, the dual-written user identifier. The identifier is written twice, under AuthClaimTypes.Subject (which is "sub", MMCA.Common/Source/Core/MMCA.Common.Shared/Auth/AuthClaimTypes.cs:34) and under ClaimTypes.NameIdentifier (TestPrincipal.cs:27-28), because a real principal reaches a page under either name: a token read directly carries the raw sub the token service emits, while the JWT bearer handler maps it onto NameIdentifier. A page reading through ClaimsPrincipalExtensions.FindUserIdValue resolves both, and writing both here also keeps a page that still reads a single claim type working under this double; the remark says exactly that (:14-21).
  • Walkthrough: two members.
    • AuthenticatedUser(string userId = "1", string name = "Test User", params string[] roles) (:22-32) builds a claim list with ClaimTypes.Name, the sub claim, and ClaimTypes.NameIdentifier (:24-29), appends one ClaimTypes.Role claim per supplied role (:30), and returns a ClaimsPrincipal over a ClaimsIdentity carrying "TestAuth" (:31).
    • InRole(string role, string userId = "1") (:90-91) is a named convenience over the first: an authenticated "Test User" carrying exactly one role, the role named by the caller. The framework declares no role vocabulary of its own, so a test states the role its own app uses (:84-85).
  • Why it's built this way: the test host evaluates authorization for real (BunitComponentTestBase registers DefaultAuthorizationService, MMCA.Common/Source/Hosting/MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:59-63), so a view's role requirement is checked against the principal's claims, and getting the role claims right on the principal is what makes an authorized-branch assertion meaningful.
  • Where it's used: passed to BunitComponentTestBase.RenderAs across all three repos, for example MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Pages/HappeningNow/HappeningNowTests.cs, MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Common/EventFilteredListPageBaseTests.cs, MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Layout/NavMenuTests.cs:54 (a named user), and MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Auth/SessionsTests.cs:86.
  • Caveats / not-in-source: the framework ships no built-in role names (the earlier Organizer convenience was removed in favor of InRole); every caller, in Common, ADC, or Store, states its own app's role string through InRole or through AuthenticatedUser(roles: ...) directly.

BunitComponentTestBase

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:38 · Level 2 · public abstract class

  • What it is: the one shared base class for bUnit component tests across all three repos. It stands up MudBlazor services and the vendor-neutral UI facades over them, puts JS interop in loose mode, wires the real authorization service over a mutable authentication-state provider, registers localization and a clock, and adds the helpers derived tests actually call (RenderUnderTest, RenderAs, RenderMudProviders, SetUser, ConfigureDataGridListPageHost) (MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:17-37).

  • Depends on: its two nested types MutableAuthenticationStateProvider and MudProviderHandles, plus TestPrincipal as the intended source of authenticated principals. It also registers first-party UI services: the AddCommonUiFacades() pair from MMCA.Common.UI (:53, defined at MMCA.Common/Source/Presentation/MMCA.Common.UI/DependencyInjection.cs:238) and the two list-page state services ListPageStateService and ListPageQueryStateService (:110-111). Externals: ASP.NET Core's DefaultAuthorizationService and IAuthorizationService from Microsoft.AspNetCore.Authorization (:3), bUnit's BunitContext base, JSRuntimeMode, AddBunitPersistentComponentState, and SetRendererInfo (:2), MudBlazor's AddMudServices, the three providers, and IBrowserViewportService (:11-12), Moq for the inert viewport double (:10), and Microsoft.Extensions.DependencyInjection plus its TryAdd extensions (:6-7).

  • Concept introduced, the component test harness. A Blazor component under test needs a renderer, a service provider, and whatever ambient services its markup injects; assembling that per test project is how three repos end up with three subtly different harnesses. This base assembles it once, in a shipped package, so a component that renders in Common's tests renders identically in ADC's and Store's. Several decisions carry the weight:

    • The vendor-neutral facades ship with the harness (:53): every migrated page and the shared Result helpers inject IToastService and the confirm-dialog facade. AddCommonUiFacades() wraps the MudBlazor services registered on the line above, so it belongs immediately after AddMudServices(); without it a consumer's component test fails to resolve IToastService and each repo re-registers the same pair in its own base. The registration uses TryAdd internally, so a test wanting a recording double registers one afterwards and last registration wins (:48-52).
    • Loose JS interop (:55): MudBlazor components probe JS during render (measurements, popover positioning). In bUnit's default strict mode every unplanned call throws, so a test would have to pre-plan interop it does not care about; loose mode returns defaults instead.
    • Real authorization, test-controlled principal (:56-63): AddAuthorizationCore() registers the genuine Blazor authorization plumbing, and the base then registers the framework's own DefaultAuthorizationService as the singleton IAuthorizationService (:62) next to the mutable provider instance (:63). The comment gives two reasons for the explicit registration (:58-61): bUnit pre-registers a placeholder IAuthorizationService that throws, and AddAuthorizationCore only TryAdds, so without it the placeholder would win; and the real service evaluates the requirements a view actually builds (Roles, the default deny-anonymous policy), whereas a permissive double would authorize every authenticated principal whatever the view asks for. <AuthorizeView> therefore runs its real code path, role checks included, and the only substituted input is the principal. [Rubric §11, Security] is the reason this matters: a role-gated branch that renders for any signed-in user in tests would hide exactly the authorization bug the test exists to catch.
    • Localization by default (:65-69): the comment ties this to ADR-027; components inject IStringLocalizer<T>, so registering the open generic once lets every component test render localized markup against the neutral resources in the component's own assembly with no per-test setup. [Rubric §27, i18n] assesses whether localization is systematic rather than incidental; making it the harness default is what keeps localized pages testable everywhere.
    • A clock, added with TryAdd (:71-75): TimeProvider.System backs the staleness measurements the notification bell's poll interval and the navigation-refresh window depend on. TryAddSingleton means a test that drives time deterministically registers a fake provider of its own and wins. The comment also notes that AddLogging above already brought in the options infrastructure, so IOptions<T> of an unconfigured settings class resolves to its defaults.
    • The viewer time zone, added with TryAddScoped (:81): ViewerTimeZone is the service the notification and session pages format instants with. Under loose JS interop the browser read returns null, so pages render in UTC unless a test sets up the getTimeZone call itself (:78-80).
    • Anonymous by default (:40,42): the seed principal has no authentication type, so tests opt in to the authorized branch explicitly rather than inheriting it.

    [Rubric §28, Front-End Testing] assesses whether UI behavior is verified automatically; this base is the entry point for the entire component tier. [Rubric §14, Testability] assesses how cheaply a unit renders in isolation. [Rubric §15, Best Practices & Code Quality]: the class remark documents that the bUnit v2 symbols (BunitContext, Render<T>) are confined to this file, so a hypothetical downgrade to bUnit v1 changes this one class and no derived test, because derived tests only ever call RenderUnderTest/RenderAs (:28-35).

  • Walkthrough: fields first, then the constructor, then the helpers.

    • Anonymous (:40) is a static readonly ClaimsPrincipal over a bare ClaimsIdentity, unauthenticated precisely because no authentication type is supplied. _authProvider (:42) is the MutableAuthenticationStateProvider seeded with it.
    • The constructor (:45-82) runs the registrations described above, in order: AddMudServices() (:46), AddCommonUiFacades() (:53), loose JSInterop.Mode (:55), AddAuthorizationCore() (:56), the singleton DefaultAuthorizationService (:62), the singleton provider instance (:63), AddLogging() and AddLocalization() (:68-69), TryAddSingleton(TimeProvider.System) (:76), and TryAddScoped<ViewerTimeZone>() (:81).
    • ConfigureDataGridListPageHost(isInteractive = true, rendererName = "Server", stubViewport = true) (:105-123) is the one helper whose call ordering is load-bearing, and the reason it exists as a single method. It registers the two list-page state services with TryAddScoped (:110-111), substitutes an inert Mock.Of<IBrowserViewportService>() when stubViewport is true (:113-116), calls AddBunitPersistentComponentState() because the list-page base persists grid state across the prerender/interactive boundary (:118-119), and finally calls SetRendererInfo(new RendererInfo(rendererName, isInteractive)) (:121-122). The doc spells out the trap: SetRendererInfo builds and freezes the bUnit service provider, so any registration made after it is silently ignored and the page resolves the framework default instead of the test's double; fifteen call sites across the repos hand-rolled this block and each had to remember that rule (:82-88). Two parameters encode real branches: isInteractive because list pages bound their prerender fetches on RendererInfo.IsInteractive, so a non-interactive renderer means no data ever loads (:98-102), and stubViewport because in bUnit no browser answers MudBlazor's breakpoint subscription, so the real service would leave the card-versus-grid choice up to timing (:89-96). The viewport double is registered with a plain Add rather than TryAdd on purpose: AddMudServices already registered the real implementation and last registration wins. [Rubric §22, Responsive/Cross-Browser] assesses whether responsive behavior is deterministic and verified; pinning the viewport is what makes the mobile-versus-desktop branch of a list page assertable at all.
    • SetUser(ClaimsPrincipal) (:126) forwards to _authProvider.SetPrincipal, changing the injected provider's answer and notifying listeners without rendering a new root, which is how a mid-test sign-in or sign-out is simulated.
    • RenderUnderTest<TComponent>(parameters) (:129-132) is the anonymous-render entry point and simply calls RenderAs(Anonymous, parameters).
    • RenderAs<TComponent>(principal, parameters) (:135-146) is the one that matters: it sets the provider's principal (:140) and then renders with a cascading Task<AuthenticationState> value added ahead of the caller's parameters (:143-144). Driving both routes from one principal is what makes the cascading consumers and the injecting consumers agree.
    • RenderMudProviders() (:153-159) renders MudPopoverProvider, MudDialogProvider, and MudSnackbarProvider as separate roots and returns the MudProviderHandles triple. The doc is explicit that it must be called before the component under test (:148-152).
  • Why it's built this way: registering the framework's own DefaultAuthorizationService plus a concrete provider instance, rather than mocks or a permissive decision double, means the authorization pipeline under test is the production one; only the principal is substituted, so a test that renders a role-gated view as a principal lacking the role sees the denied branch, as production would (:58-62). Moq is a deliberate dependency for exactly one thing, the viewport double, because a hand-written stub would have to track every member MudBlazor adds to IBrowserViewportService (MMCA.Common.Testing.UI/MMCA.Common.Testing.UI.csproj:17-21). And keeping the version-specific bUnit symbols in one file is a blast-radius decision recorded in the class remark rather than left to be rediscovered.

  • Where it's used: subclassed by a thin repo-local BunitTestBase in each consumer, which adds only that repo's extra registrations: MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/BunitTestBase.cs:17 (theme service, culture applier, capability fallbacks, public-link builder) and MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/BunitTestBase.cs:24 (the ADR-042 device-capability defaults, the public-link builder, the public session schedule service, and an empty IConfiguration), plus the equivalent bases in ADC's Identity and Engagement suites and in Store's Sales, Catalog, and Identity UI test projects. ConfigureDataGridListPageHost is called from every list-page suite, for example MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Common/DataGridListPageBaseTests.cs:35, MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.UI.Tests/Pages/ShoppingCarts/ShoppingCartListTests.cs:38, MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Sessions/SessionListEventFilterTests.cs:41 (stubViewport: false), and MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Common/EventFilteredListPagePrerenderTests.cs:42 (isInteractive: false, the prerender branch). The base's own registration contract is guarded by MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/BunitComponentTestBaseFacadeTests.cs:18, which derives from the shipped base rather than the repo-local one precisely so the facade registrations cannot be papered over by a consumer.

  • Caveats / not-in-source: the package does reference the shared UI: MMCA.Common.Testing.UI/MMCA.Common.Testing.UI.csproj:30 has a ProjectReference to MMCA.Common.UI, and the base now uses it for both the facades and the list-page state services. The harness is therefore not UI-agnostic, and a consumer cannot take the bUnit base without also taking MMCA.Common.UI.

UiHttpServiceHarness

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:12 · Level 2 · sealed class (IDisposable)

  • What it is: the one-line setup for a UI HTTP-service test. It owns the disposable plumbing every such test needs (the capturing handler, a fresh-client-per-call factory, and a token storage stub) so the test constructs one object and hands its parts to the service under test (MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:3-11).
  • Depends on: CapturingHttpMessageHandler, FreshApiClientFactory, and StubTokenStorageService, all first-party in this package. Externals: IHttpClientFactory and IDisposable (BCL).
  • Concept introduced, the aggregate fixture over the three-piece boundary. Assembling the same three collaborators by hand in each repo is exactly how the fresh-client rule (see FreshApiClientFactory) gets forgotten in one repo and not another. Collecting them behind one constructor makes the correct wiring the default, and gives one Dispose for the one thing that actually needs it. [Rubric §14, Testability] assesses how cheap a correct test setup is; two lines of setup is the practical bar this clears. [Rubric §15, Best Practices & Code Quality]: with the wiring shipped in a package, a fix to the plumbing reaches ADC, Store, and Common in one version bump rather than three edits.
  • Walkthrough:
    • DefaultBaseAddress (:15) is a static readonly Uri of https://gateway.test/, applied to every created client so services can build relative URIs the way they do against a real gateway.
    • Two public constructors mirror the handler's two modes and both funnel into one private constructor. The route-registration one (:23-26) takes accessToken = "test-token" and an optional base address and builds a route-registration handler; the responder one (:35-41) takes the delegate and builds a responder-mode handler.
    • The private constructor (:43-49) is where the wiring lives: store the handler, resolve the base address against DefaultBaseAddress (:46), build the FreshApiClientFactory over that handler and address (:47), and build the StubTokenStorageService with the canned token (:48).
    • Four read-only properties expose the parts: Handler (:52) for SetResponse registration and request assertions, BaseAddress (:55), ClientFactory (:58) typed as IHttpClientFactory to hand to the service, and TokenStorage (:61) typed concretely so a test can reach AccessTokenProvider.
    • Dispose() (:63) disposes only the handler. The clients are the caller's to dispose, which is consistent with disposeHandler: false on each created client, so the capture log outlives them.
  • Why it's built this way: passing accessToken: null produces an anonymous harness in one argument, which is how the "no bearer header when signed out" case is tested without a second fixture type. And keeping the two public constructors as thin forwarders means the mode choice is visible at the call site while the wiring exists once.
  • Where it's used: the default setup for the HTTP-backed UI service tests in all three repos. MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/UiHttpServiceHarnessTests.cs:19,33,45,48,56,67-68,78 covers the harness itself, including both modes, the default and custom base addresses, and the with-token versus anonymous pair.

HttpTestDoubles

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI · MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:12 · Level 3 · static class

  • What it is: the a-la-carte counterpart to UiHttpServiceHarness: standalone factory helpers for tests that wire the pieces individually, plus the canned-response builders both styles share (MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:7-11).
  • Depends on: UiHttpServiceHarness (for the shared default base address), FreshApiClientFactory, StubTokenStorageService, and ITokenStorageService as the returned contract (:3). Externals: System.Net.Http.Json.JsonContent and System.Net.HttpStatusCode (:1-2).
  • Concept introduced, canned responses that match the real API's failure shape. The response builders are not generic conveniences: ProblemResponse emits the { title, detail } body the WebAPI actually returns for a domain failure, so the UI-side error mapping under test sees the shape it will see in production (:40-51). A fake that returned a bare 400 would let the mapping pass while being wrong. Its doc names the consumer on the other side, ProblemDetailsResultReader, and records what it does with this shape: the plain-ProblemDetails body becomes a single error coded "Http.{status}" typed from the status code, and a test that needs the individual error codes preserved sends an errors array through JsonResponse<T> instead (:41-46). [Rubric §9, API and Contract Design] assesses whether error contracts are consistent and honored on both sides; this helper is where the UI tier's assumption about the API's error body is written down. [Rubric §29, Resilience, Reliability & Business Continuity] applies because that ProblemDetails shape is produced by shared middleware rather than by any one endpoint.
  • Walkthrough: five members, all static.
    • BaseAddress (:15) is aliased to UiHttpServiceHarness.DefaultBaseAddress, so both wiring styles share one origin and a test can compare across them.
    • ClientFactory(handler, baseAddress = null) (:23-24) returns a FreshApiClientFactory over the given handler, defaulting the address to BaseAddress.
    • TokenStorage(accessToken = "test-token") (:28-29) returns a StubTokenStorageService typed as the interface; pass null for an anonymous client.
    • JsonResponse<T>(payload, statusCode = OK) (:33-34) builds a response whose content is JsonContent.Create(payload), which serializes with web defaults, matching what the WebAPI sends.
    • EmptyResponse(statusCode = NoContent) (:37-38) builds a body-less response, the 204 shape a command endpoint returns.
    • ProblemResponse(detail, title = "Domain Exception", statusCode = BadRequest) (:47-51) delegates to JsonResponse with an anonymous { title, detail } object.
  • Why it's built this way: the harness is the right default, but a test that needs two handlers, or a handler shared with a non-HTTP collaborator, would have to fight it. Keeping the same primitives available individually means neither style forks its own copies, and the response builders stay identical across both.
  • Where it's used: by the responder-delegate style of service test across the consumer UI suites, and covered directly by MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/SharedHttpTestDoublesTests.cs:22,27-28,38,47-49,56,67,75-76,86,97, which pins the shared base address, the fresh-client contract, the three token-storage cases, and all three response builders including the custom-status ProblemDetails form.
  • Caveats / not-in-source: the responses these builders return are single-use HttpResponseMessage instances. Handing one to a responder delegate that returns the same instance twice would fail on a retry, which is why the delegate is invoked per request (see Route) and why these helpers are normally called inside the delegate rather than before it.

RoleAdminEditPageTestsBase<TPage>

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI.Pages · MMCA.Common/Source/Hosting/MMCA.Common.Testing.UI/Pages/RoleAdminEditPageTestsBase.cs:22 · Level 4 · class

  • What it is: a shared BunitComponentTestBase subclass that drives any consumer's single-role permission editor page through the same four facts, so ADC and Common each supply only their own page type and role/permission names rather than re-writing the suite.
  • Depends on: BunitComponentTestBase for the render harness; IRoleAdminUIService, mocked, as the page's client; RolePermissionsResponse and PermissionCatalogResponse as the response shapes the mock returns.
  • Concept: a generic TestsBase<TPage> that a consumer closes over its own concrete page type, so the suite runs against ADC's RoleEdit and Common's own RoleAdminEditPageTests sample page without duplication. [Rubric §28, Front-End Testing] (assesses whether component behavior is pinned rather than only rendered smoke-tested): the base pins that a compiled-in permission renders ticked and disabled and an editable one does not (lines 76-89), and that the one write this page performs submits the role's complete stored set rather than a diff (lines 95-112).
  • Walkthrough: the constructor registers the mocked Roles client (line 28); five abstract members (EditedRole, OtherRole, CompiledPermission, StoredPermission, RosterRoute, lines 34-52) let the consumer supply its own names, plus a virtual RoleParameterName defaulting to "Role" (line 55) for a page whose route parameter is spelled differently. TheRouteRole_ReachesTheComponentAndItsCatalogRenders (line 58) asserts two permission inputs render. ACompiledPermission_IsLocked_AndAnEditableOneIsNot (line 76) targets the checkbox's own <input data-permission> rather than a wrapper, because MudCheckBox splats unmatched attributes straight onto that element (lines 82-83). TickingAPermissionAndSaving_SubmitsItAsTheStoredSet (line 96) changes one checkbox, clicks save, and verifies the client received exactly the one ticked permission as the stored list (lines 106-111). ThePage_LinksBackToTheRoleRoster (line 119) asserts an anchor to the consumer's RosterRoute. RenderEditor (line 134) is the shared render entry point, and SetupDefaultClient (line 142) stubs all three service calls: it runs at render rather than in the constructor because the role and permission names are abstract members a constructor cannot call before the derived class initializes (lines 140-141), and every Result-returning setup is explicit because an unstubbed one hands back null rather than a success.
  • Why it's built this way: a permission the host compiles in is locked because removing it is a code change, not a data edit, and the server refuses to store it either way (lines 71-73), the ownership split recorded in ADR-058 that keeps the compiled and stored permission sets from being confused.
  • Where it's used: MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Administration/RoleAdminEditPageTests.cs and MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Pages/Roles/RoleEditTests.cs, each a thin subclass supplying its own page type and role/permission constants.

RoleAdminListPageTestsBase<TPage>

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI.Pages · MMCA.Common/Source/Hosting/MMCA.Common.Testing.UI/Pages/RoleAdminListPageTestsBase.cs:21 · Level 4 · class

  • What it is: the companion base for the role roster page that RoleAdminEditPageTestsBase<TPage> edits into, pinning that every listed role renders its compiled/stored counts and an edit link, and that a failed load hides the roster rather than showing an empty one.
  • Depends on: BunitComponentTestBase; IRoleAdminUIService, mocked; RolePermissionsResponse.
  • Concept: shares the generic-base pattern with its edit-page sibling. [Rubric §28, Front-End Testing]: WhenTheFetchFails_RendersNoRoleRows (line 234) is the load-bearing fact here, because a failed fetch renders zero rows, which is visually identical to a genuinely empty roster (lines 229-231), so the page's shared inline error state has to take over instead of the empty-roster message.
  • Walkthrough: the constructor registers the mocked Roles client (line 182); two abstract role names (lines 187-191) and an abstract EditRoute(string) (line 250) let the consumer supply its own routing. RendersOneRowPerRoleWithItsCompiledAndStoredCounts (line 194) asserts two [data-testid=compiled-count] and two [data-testid=stored-count] cells. EachRow_LinksToTheRoleEditorRoute (line 216) asserts the edit-link hrefs equal EditRoute applied to each role in order, because the edit link is the one required parameter the component takes, so a page that stopped passing it would not compile, but a page that passed the wrong shape would compile and dead-end the operator (lines 210-214). SetupRoles (line 258) and the private SetupDefaultRoles (line 265) stub GetAllAsync; the default roster fixture gives the first role two compiled permissions and no stored ones and the second one compiled permission plus a stored permission already granted elsewhere (lines 266-270), enough variety to distinguish the two counts.
  • Why it's built this way: like its edit-page sibling, roles and permission names are supplied per consumer through abstract members rather than hardcoded, so the same fact set runs against ADC's and Common's own role vocabularies without copying the test bodies (ADR-058).
  • Where it's used: MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Administration/RoleAdminListPageTests.cs and MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Pages/Roles/RoleListTests.cs.

ConfirmEmailPageTestsBase

MMCA.Common.Testing.UI · MMCA.Common.Testing.UI.Pages · MMCA.Common/Source/Hosting/MMCA.Common.Testing.UI/Pages/ConfirmEmailPageTestsBase.cs:26 · Level 5 · class

  • What it is: a shared BunitComponentTestBase subclass pinning the email-confirmation page's link-driven confirm flow and its resend affordance, so both apps' ConfirmEmail pages run the same seven facts.
  • Depends on: BunitComponentTestBase; IEmailConfirmationUIService, mocked; the framework's ConfirmEmail page component it renders; NavigationManager's [SupplyParameterFromQuery] routing.
  • Concept: [Rubric §28, Front-End Testing]: the constructor pre-stubs both service calls to succeed (lines 299-304) so each fact only overrides the one call it needs to fail, and RenderPage (line 422) routes through NavigationManager.GetUriWithQueryParameters rather than passing component parameters directly, because bUnit cannot pass a [SupplyParameterFromQuery] value as a component parameter and must instead navigate exactly as the emailed link's browser click would (lines 414-418).
  • Walkthrough: WithACompleteLink_ConfirmsOnceAndReportsSuccess (line 314) confirms on the initial render, forces a second render, and verifies the confirmation call still ran exactly once, so a re-render after success does not spend the single-use token a second time (lines 320-321). WithARefusedToken_ShowsTheApisMessage_AndKeepsTheFormWithAResend (line 328) stubs a refused-token error and asserts the API's own message surfaces alongside a resend affordance and no success marker. WithNoToken_DoesNotCallTheServiceAndShowsTheForm (line 352) asserts a link with nothing to redeem lands on manual entry with no service call and no error, because posting a request that can only be refused would be pointless (lines 347-349). SubmittingWithoutAToken_ExplainsWhatIsMissingAndDoesNotCallTheService (line 368) covers the manual-submit path to the same rule. Resend_AsksForAFreshLinkAndSaysSoWithoutConfirmingTheAccountExists (line 386) asserts the resend notice says "unconfirmed account" rather than promising a delivered email, because the endpoint answers 202 either way (lines 381-383). ResendWithNoAddress_DoesNotCallTheService (line 401) guards the same anti-enumeration posture when the address itself is missing. Email (line 311) is virtual, defaulting to "ada@example.com", so a consumer can override it to an app-shaped address.
  • Why it's built this way: the single-use-token and anti-enumeration behaviors are shared server-side contracts (an unauthenticated confirm and an unauthenticated resend), so pinning them once here keeps ADC's and Common's pages from drifting apart on the one screen a mis-click on an old link actually reaches (ADR-058).
  • Where it's used: MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Auth/ConfirmEmailPageTests.cs and MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Pages/Users/ConfirmEmailTests.cs.

SampleGridRow

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Pages · MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:9 · Level 0 · record (sealed, public)

  • What it is: the row DTO behind the virtualized-grid gallery page. It is deliberately plain, because the point of /grid is the windowing behaviour of DataGridListPageBase<TDto>, not the shape of the data (MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:5-8).
  • Depends on: nothing first-party. Five BCL positional members only.
  • Concept introduced: cross-reference the DTO and manual-mapping conventions taught in 00-primer.md. What is worth noting here is the absence of the usual machinery: no identifier-type alias, no validator, no mapper, no audit fields. A test-fixture DTO exists to give the component under test something to bind, so it carries exactly the column types the grid needs to exercise, one integer, two strings, a DateTime, and a decimal. [Rubric §14, Testability] assesses how cheaply a component can be driven in isolation; a five-member positional record is the smallest possible stand-in for a real paged API contract.
  • Walkthrough: one line. public sealed record SampleGridRow(int Id, string Name, string Category, DateTime CreatedOn, decimal Amount) (:9). The member set is chosen to cover the four PropertyColumn formatting paths the page binds: a plain integer, ordinal strings, a DateTime rendered with Format="yyyy-MM-dd", and a decimal rendered with Format="F2" (MMCA.Common.UI.Gallery/Pages/GridGallery.razor:28-32). It is public rather than internal (unlike every stub in this unit) because it is the generic argument of the page's base class, @inherits DataGridListPageBase<SampleGridRow> (MMCA.Common.UI.Gallery/Pages/GridGallery.razor:4).
  • Where it's used: the element type of SampleGridData.All (MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:25), the T of the page's MudDataGrid (MMCA.Common.UI.Gallery/Pages/GridGallery.razor:17), and, through the page, the payload GridPageE2ETests asserts against by naming the exact rendered value "Row 0001" (MMCA.Common.UI.E2E.Tests/Layout/GridPageE2ETests.cs:39, :55, :80).

GalleryFakeAuthenticationHandler

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:21 · Level 1 · class (sealed, internal)

  • What it is: a cookie-toggled fake ASP.NET Core authentication handler for the backend-less gallery. A request carrying gallery_auth=1 authenticates as a fixed "Gallery Visitor" principal that also holds the notifications:manage permission claim; every other request stays anonymous (MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:30-47).
  • Depends on: Microsoft.AspNetCore.Authentication.AuthenticationHandler<AuthenticationSchemeOptions> (the abstract base), the base constructor triple IOptionsMonitor<AuthenticationSchemeOptions> / ILoggerFactory / System.Text.Encodings.Web.UrlEncoder (MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:21-25), and BCL System.Security.Claims. Two first-party names from the Shared layer supply the permission claim: AuthClaimTypes.Permission (using MMCA.Common.Shared.Auth, :5) and NotificationPermissions.Manage (using MMCA.Common.Shared.Notifications, :6), used at :42.
  • Concept introduced, the backend-less gallery stub pattern and its one non-inert member. The gallery host (GalleryHost) renders the real MMCA.Common.UI components with no live API behind them, so every consumer-supplied extension point the shared UI expects is replaced by a benign stub. This type is the exception that proves the rule: it is not inert, because the shared notification and sessions pages carry a real [Authorize]. MapRazorComponents surfaces that attribute as endpoint metadata, and the authorization middleware then needs a genuine authentication scheme registered (without one it throws) and a genuine authenticated principal (without one the pages redirect to /login instead of rendering for the scan). The doc comment records exactly that reasoning (MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:10-20). Rather than removing the guard for testability, the gallery supplies a real scheme whose only decision input is a cookie. [Rubric §28, Front-End Testing] assesses whether the UI has real-browser render and accessibility coverage; toggling sign-in per test with a cookie is what lets one host scan both the anonymous chrome (/login, /register, /components, /grid) and the signed-in guarded pages. [Rubric §11, Security] assesses how authentication is implemented; note the deliberate inversion here, the handler trusts an unsigned cookie value, acceptable only because this assembly is unpackaged test infrastructure (the doc comment closes with "never copy into a real host", MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:18-19).
  • Walkthrough: two internal constants pin the contract shared with the host and the tests, SchemeName = "GalleryFake" (:27) and CookieName = "gallery_auth" (:28). HandleAuthenticateAsync() (:30) is the single override. It short-circuits first: when Request.Cookies[CookieName] is not exactly "1" it returns AuthenticateResult.NoResult() (:32-35), which means "this scheme has no opinion", leaving the request anonymous rather than failing it. Otherwise it builds a ClaimsIdentity with two claims, a ClaimTypes.Name of "Gallery Visitor" and an AuthClaimTypes.Permission claim whose value is NotificationPermissions.Manage (:39-43), and, critically, passes SchemeName as the authentication type (:44): supplying an authentication type is what makes Identity.IsAuthenticated true. The permission claim exists because the notification history and compose pages gate on notifications:manage, as does their navigation entry, so the signed-in scan reaches the pages rather than an Access Denied view (comment, :37-38). It wraps that principal in an AuthenticationTicket and returns AuthenticateResult.Success (:45-46). Everything is synchronous through Task.FromResult, so no I/O occurs.
  • Why it's built this way: keeping the guard real and faking only the credential means the E2E scan exercises the same authorization pipeline the deployed hosts run, so an accidental loss of [Authorize] on the shared notification or sessions pages cannot be papered over by a permissive test host.
  • Where it's used: registered as the default (and only) authentication scheme by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:72-75), followed by AddAuthorization() (MMCA.Common.UI.Gallery/GalleryHost.cs:76) and the UseAuthentication() / UseAuthorization() middleware pair (MMCA.Common.UI.Gallery/GalleryHost.cs:119-120). The cookie is seeded on the Playwright browser context by NotificationPagesE2ETests.SeedSignedInCookieAsync (MMCA.Common.UI.E2E.Tests/NotificationPagesE2ETests.cs:90-99) before each of its five guarded-page scans (:23, :38, :54, :67, :80), by SessionsPageE2ETests for the devices page (MMCA.Common.UI.E2E.Tests/Auth/SessionsPageE2ETests.cs:23, :39-40), and by MobileTopRowE2ETests for its signed-in top-row check (MMCA.Common.UI.E2E.Tests/Layout/MobileTopRowE2ETests.cs:93-97).

NullTokenRefresher

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/NullTokenRefresher.cs:9 · Level 1 · class (sealed, internal)

  • What it is: an ITokenRefresher that never has a session to refresh. The gallery has no API to refresh against; the stub exists only so the DI graph stays complete (MMCA.Common.UI.Gallery/Stubs/NullTokenRefresher.cs:5-8).
  • Depends on: ITokenRefresher from MMCA.Common.UI.Services.Auth. Nothing else.
  • Concept introduced: the purest instance of the stub pattern introduced in GalleryFakeAuthenticationHandler, collapsing an outbound extension point to a constant. [Rubric §14, Testability] assesses how cheaply a component renders in isolation; reducing token refresh to a constant is what keeps the scan from ever reaching a token endpoint.
  • Walkthrough: one member. AcquireAccessTokenAsync(CancellationToken = default) (:11-12) is an expression body returning Task.FromResult<string?>(null), which is the interface's own documented "no valid session exists" answer (MMCA.Common.UI/Services/Auth/Tokens/ITokenRefresher.cs:19-24), so any caller takes its null-token path.
  • Where it's used: registered scoped by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:64) in the pre-AddUIShared stub block. In the shared UI the only constructor taking an ITokenRefresher is AuthUIService (MMCA.Common.UI/Services/Auth/AuthUIService.cs:44-47), which the gallery replaces with NoOpAuthUIService, so this registration is graph-completeness insurance rather than a live collaborator.

NullTokenStorageService

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/NullTokenStorageService.cs:11 · Level 1 · class (sealed, internal)

  • What it is: an in-memory-empty ITokenStorageService: there is no stored session in the gallery. It exists so the AuthDelegatingHandler that AddUIShared registers resolves cleanly, and it is never actually invoked because the gallery makes no API calls (MMCA.Common.UI.Gallery/Stubs/NullTokenStorageService.cs:6-10).
  • Depends on: ITokenStorageService from MMCA.Common.UI.Services.Auth.
  • Concept introduced: the same stub pattern as NullTokenRefresher, covering storage rather than refresh, and the first place where a stub exists purely to satisfy a transitive constructor. AddUIShared registers AuthDelegatingHandler as transient (MMCA.Common.UI/DependencyInjection.cs:122) and attaches it to the named "APIClient" pipeline (MMCA.Common.UI/DependencyInjection.cs:126, :101), and that handler's primary constructor takes an ITokenStorageService (MMCA.Common.UI/Services/Auth/AuthDelegatingHandler.cs:15-16). Nothing in AddUIShared supplies one, so the host must. [Rubric §26, Front-End Security] assesses how auth tokens are stored and handled; this stub deliberately holds nothing, so the test host persists no credential material at all, not even in memory.
  • Walkthrough: four members, each inert but shape-complete so DI binds. GetAccessTokenAsync() (:12) and GetRefreshTokenAsync() (:14) return Task.FromResult<string?>(null); SetTokensAsync(accessToken, refreshToken) (:16) and ClearTokensAsync() (:18) discard their inputs and return Task.CompletedTask.
  • Where it's used: registered scoped by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:63), inside the block placed ahead of AddUIShared (MMCA.Common.UI.Gallery/GalleryHost.cs:58-66).
  • Caveats / not-in-source: unlike NoOpAuthUIService, this registration is not a TryAdd override. AddUIShared makes no ITokenStorageService registration anywhere in its body (MMCA.Common.UI/DependencyInjection.cs:40-213); the concrete storage services are supplied per host head instead, so in the gallery this stub is the only registration rather than a winning one.

SampleGridData

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Pages · MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:16 · Level 1 · class (static, internal)

  • What it is: the gallery's in-memory stand-in for a paged API. The rows are generated once from fixed inputs, with no randomness and no clock, so every E2E run, on every browser engine, scrolls over an identical data set and a row assertion can name an exact value (MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:11-15).
  • Depends on: SampleGridRow and BCL System.Globalization.CultureInfo. Nothing else.
  • Concept introduced, determinism as a precondition for real-browser assertions. The stubs above collapse an extension point to a constant; this one goes further and makes a whole data set a constant. A generated fixture is only usable in an E2E assertion if it produces byte-identical output on every run and every engine, so the builder takes its three inputs (index, a fixed five-item category array, a fixed UTC epoch) and derives everything else arithmetically. There is no Random, no DateTime.UtcNow, and the one string is formatted with CultureInfo.InvariantCulture so a CI runner's locale cannot change it. That is what lets GridPageE2ETests assert on the literal text "Row 0001" rather than on a shape. [Rubric §28, Front-End Testing] assesses real-browser coverage of the UI; a deterministic 1,000-row set is what makes "the DOM holds a small fraction of the data" a falsifiable claim. [Rubric §23, Front-End Performance] assesses whether the front end bounds the work it does per render; this data set exists specifically so the virtualization window can be measured instead of asserted by inspection.
  • Walkthrough:
    • RowCount = 1000 (:19) is a public constant, documented as "the row count the E2E windowing assertion is written against". The test mirrors it as its own private DataSetRowCount = 1000 (MMCA.Common.UI.E2E.Tests/Layout/GridPageE2ETests.cs:19) rather than referencing it, since the test assembly does not see this internal type.
    • Categories (:21) is a static readonly five-item array, and Epoch (:23) a static readonly 2026-01-01T00:00:00Z with an explicit DateTimeKind.Utc.
    • All (:25) is an IReadOnlyList<SampleGridRow> auto-property initialized once from Build(), so generation happens a single time per process and every request serves the same instances.
    • Build() (:27-42) fills a SampleGridRow[RowCount]. For index i it computes a one-based id (:32), formats Name as Row {id:D4} through string.Create(CultureInfo.InvariantCulture, ...) (:35) giving the zero-padded "Row 0001" the test asserts on, cycles Category with Categories[i % Categories.Length] (:36), spaces CreatedOn one hour apart with Epoch.AddHours(i) (:37), and derives Amount as id * 37 % 10000 / 100m (:38), an integer modulo followed by a decimal divide, which keeps every amount inside 0.00 to 99.99 with two decimal places.
  • Why it's built this way: a virtualization test needs enough rows that rendering all of them would be obviously wrong, and few enough that generation costs nothing at startup. 1,000 rows against the test's MaxRenderedRows = 200 ceiling (MMCA.Common.UI.E2E.Tests/Layout/GridPageE2ETests.cs:26) gives a fifth-of-the-data-set margin, which the test's own comment explains is generous headroom over the roughly 50 rows a 70vh viewport at 52px per row plus MudBlazor overscan actually renders (MMCA.Common.UI.E2E.Tests/Layout/GridPageE2ETests.cs:21-25).
  • Where it's used: MMCA.Common.UI.Gallery/Pages/GridGallery.razor reads SampleGridData.RowCount for its on-page caption (:12) and SampleGridData.All inside its static FetchPageAsync (:63), which sorts by the requested column and direction (:65-82), applies Skip((pageNumber - 1) * pageSize).Take(pageSize) (:84), and returns a Result<(IReadOnlyList<SampleGridRow> Items, int TotalItems)> (:86-87). That delegate deliberately matches IEntityService.GetPagedAsync's shape and 1-based page contract (comment, MMCA.Common.UI.Gallery/Pages/GridGallery.razor:52-53), so DataGridListPageBase<TDto>.LoadVirtualizedServerDataAsync (MMCA.Common.UI/Pages/Common/DataGridListPageBase.cs:790) maps the virtualization window onto it exactly as it would onto a real API.

GalleryUIModule

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/GalleryUIModule.cs:14 · Level 3 · class (sealed, internal)

  • What it is: a minimal IUIModule whose Assembly is the gallery itself, so the shared Blazor Router (Routes.razor, which scans UIModules.Select(m => m.Assembly)) discovers the gallery's own /components and /grid pages. Its nav links make the host browsable when run interactively (MMCA.Common.UI.Gallery/Stubs/GalleryUIModule.cs:9-13).
  • Depends on: IUIModule and NavItem from MMCA.Common.UI.Common, SharedResource from MMCA.Common.UI.Resources, plus MudBlazor.Icons and BCL System.Reflection.Assembly.
  • Concept introduced: the UI-module contribution pattern (taught with IUIModule in the Common UI framework group) reused for a test host: a module contributes route-bearing assemblies and nav links to the shared shell. [Rubric §18, UI Architecture] assesses how the front end composes independently-owned UI slices; the gallery participates in the exact module-discovery mechanism the real apps use, which is what makes the E2E evidence say something about that mechanism rather than about a bespoke test shell. [Rubric §25, Navigation & IA]: the four nav entries flow through the same NavItem record the deployed apps' menus are built from, supplying its first four positional members (title key, href, icon, resource type) and leaving RequiredRole, RequiredClaim, Section, and Group at their defaults (MMCA.Common.UI/Common/NavItem.cs:16). [Rubric §27, i18n]: passing typeof(SharedResource) as TitleResource opts the labels into the resource-key lookup ADR-027 defines, and because a key the resource type does not declare renders as the raw string, the untranslated gallery labels stay legible rather than blank (MMCA.Common.UI/Common/NavItem.cs:13-18).
  • Walkthrough: NavItems (:16-22) is a collection-expression IReadOnlyList<NavItem> of four entries, Login (/login), Register (/register), Components (/components), and Grid (/grid), each pairing a label, a route, a MudBlazor Material icon, and typeof(SharedResource) (:18-21). Assembly (:24) is an expression-bodied property returning typeof(GalleryUIModule).Assembly, so the Router additionally scans the gallery assembly for routable components, which is how MMCA.Common.UI.Gallery/Pages/ComponentsGallery.razor:1 (@page "/components") and MMCA.Common.UI.Gallery/Pages/GridGallery.razor:1 (@page "/grid") become reachable. The optional IUIModule members the interface declares beyond these two are left at their defaults.
  • Where it's used: registered as a singleton IUIModule by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:97); the pages it makes routable are what ComponentsPageE2ETests and GridPageE2ETests scan.

StubNotificationInboxUIService

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/StubNotificationInboxUIService.cs:11 · Level 4 · class (sealed, internal)

  • What it is: a canned INotificationInboxUIService returning fixed inbox data so NotificationBell and the notification inbox page render populated, real markup for the axe and render scans, with no backend (MMCA.Common.UI.Gallery/Stubs/StubNotificationInboxUIService.cs:7-10).
  • Depends on: INotificationInboxUIService from MMCA.Common.UI.Services.Notifications, the Result, PagedCollectionResult<T>, and PaginationMetadata types from MMCA.Common.Shared.Abstractions, the UserNotificationDTO contract, and the solution-wide UserNotificationIdentifierType alias.
  • Concept introduced: the stub pattern extended from inert no-ops to canned data. For components whose whole purpose is displaying content, an empty stub renders an empty (and therefore untested) tree, so this stub returns representative rows instead. Note that it still speaks the real Result contract on every member, so the pages take their success branches through the same unwrapping code the deployed apps run. [Rubric §28, Front-End Testing]: populated markup is what lets axe evaluate contrast, roles, and the read/unread affordances against a realistic notification list rather than an empty state.
  • Walkthrough: GetInboxAsync(pageNumber = 1, pageSize = 20, cancellationToken) (:13-14) builds a two-item UserNotificationDTO[] (:16-29): an unread "Welcome to MMCA" with a fixed UTC SentOn of 2026-01-02 09:00, and a read "Scheduled maintenance" carrying both ReadOn and SentOn, so the inbox exercises both visual states. It wraps them in a PagedCollectionResult<UserNotificationDTO> with new PaginationMetadata(items.Length, pageSize, pageNumber) (:30-31), so the pager renders from real metadata rather than a hardcoded count. GetUnreadCountAsync() (:34-35) returns a successful constant 3 so the bell badge renders non-empty. MarkReadAsync(id, ct) (:37-38) and MarkAllReadAsync(ct) (:40-41) are no-ops returning Result.Success(), so the buttons are present and clickable and their handlers take the success path without any state change.
  • Where it's used: registered scoped by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:82), alongside the scoped NotificationState (MMCA.Common.UI.Gallery/GalleryHost.cs:81). The scans that consume it are NotificationPagesE2ETests.NotificationInbox_Renders_AndHasNoWcag21AaViolations (MMCA.Common.UI.E2E.Tests/NotificationPagesE2ETests.cs:34-44) plus its two deep-link cases, .NotificationInboxDeepLink_ToAnUnknownNotification_DegradesToThePlainInbox (:47-62) and .NotificationInboxDeepLink_ToAKnownNotification_HighlightsThatCard (:64-75), each ending in an AxeOptions.Wcag21Aa scan.
  • Caveats / not-in-source: the badge count 3 (:34-35) is a hardcoded display value and does not reconcile with the two rows GetInboxAsync returns; it exists to render a non-empty badge for the scan, not to be internally consistent.

StubPushNotificationUIService

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/StubPushNotificationUIService.cs:11 · Level 4 · class (sealed, internal)

  • What it is: a canned IPushNotificationUIService so the notification history and compose pages render populated, real markup for the axe and render scans, with no backend (MMCA.Common.UI.Gallery/Stubs/StubPushNotificationUIService.cs:7-10).
  • Depends on: IPushNotificationUIService from MMCA.Common.UI.Services.Notifications, Result, PagedCollectionResult<T> and PaginationMetadata, plus the PushNotificationDTO and SendPushNotificationRequest contracts.
  • Concept introduced: the same canned-data variant of the stub pattern as StubNotificationInboxUIService, applied to the send and history side. [Rubric §24, Forms/Validation/UX Safety]: the compose page is a form, and echoing the submitted Title and Body back in the returned DTO lets the render smoke reach the post-submit state without a real send.
  • Walkthrough: SendAsync(request, cancellationToken) (:12-13) is an expression body returning a successful PushNotificationDTO that echoes request.Title and request.Body and fixes the rest, Id = 99, SentByUserId = 1, RecipientCount = 42, Status = "Sent", CreatedOn 2026-01-04 10:00 UTC (:14-17). GetHistoryAsync(pageNumber = 1, pageSize = 10, cancellationToken) (:19-20) builds a three-item history array (:23-45): one row with Status = "Sent", one with Status = "Failed", both RecipientCount = 128, and a third with Status = "Pending" and RecipientCount = 64 added on purpose, NotificationList renders the Warning-coloured chip for every status that is neither Sent nor Failed, and without a pending row the axe scan never reached the one chip whose palette colour was failing contrast (:37-39, :40-44), then wraps them in a PagedCollectionResult<PushNotificationDTO> with PaginationMetadata (:47-48).
  • Where it's used: registered scoped by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:83). The scans that consume it are NotificationPagesE2ETests.NotificationHistory_Renders_AndHasNoWcag21AaViolations (MMCA.Common.UI.E2E.Tests/NotificationPagesE2ETests.cs:20-32) and .NotificationCompose_Renders_AndHasNoWcag21AaViolations (:77-87); the history scan runs under AxeOptions.Wcag21AaExceptMudPagerCombobox because MudBlazor's pager combobox has no accessible name and is not fixable from app markup (MMCA.Common.UI.E2E.Tests/NotificationPagesE2ETests.cs:32).

StubRoleAdminUIService

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/StubRoleAdminUIService.cs:14 · Level 4 · class (sealed, internal)

  • What it is: a canned IRoleAdminUIService so the gallery's /admin/roles and /admin/roles/{role} hosts render the shared RoleAdminList and RoleAdminEdit components populated for the axe and render scans, with no backend (MMCA.Common.UI.Gallery/GalleryHost.cs:90-92).
  • Depends on: IRoleAdminUIService from MMCA.Common.UI.Services.Administration, Result and Error, the RolePermissionsResponse and PermissionCatalogResponse contracts, and AdministrationPermissions for the ManageRoles entry in the catalog.
  • Concept introduced: canned data with a tiny in-memory model behind it, one step beyond StubNotificationInboxUIService. A single static readonly RolePermissionsResponse[] (MMCA.Common.UI.Gallery/Stubs/StubRoleAdminUIService.cs:16-20) holds two roles, Admin (registered orders:read, orders:write; stored reports:run) and Member (registered orders:read; nothing stored), and every read member is derived from it, so the list, the editor and the catalog stay consistent with one another. [Rubric §28, Front-End Testing]: populated registered and stored permission sets are what let axe evaluate the real checkbox lists and edit affordances rather than an empty state.
  • Walkthrough: GetAllAsync (:22-23) returns the array as a successful Result. GetAsync(role) (:25-31) finds the role with an ordinal comparison and otherwise returns Result.Failure with Error.NotFoundError("Authorization.RoleNotFound", "The role was not found."), so the editor's not-found branch is reachable. GetCatalogAsync (:33-36) returns a PermissionCatalogResponse of the role names plus the permission list orders:read, orders:write, reports:run and AdministrationPermissions.ManageRoles. SetStoredPermissionsAsync(role, permissions) (:38-45) looks up the role's registered permissions (empty when unknown) and echoes a new RolePermissionsResponse(role, compiled, permissions), without mutating the array, so a save takes the success path but nothing persists between requests.
  • Where it's used: registered scoped by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:92). The scans that consume it are RoleAdminPagesE2ETests.RoleList_Renders_AndHasNoWcag21AaViolations (MMCA.Common.UI.E2E.Tests/Administration/RoleAdminPagesE2ETests.cs:22-31) and .RoleEditor_Renders_AndHasNoWcag21AaViolations (:34-43), each ending in an AxeOptions.Wcag21Aa scan.

NoOpAuthUIService

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:14 · Level 6 · class (sealed, internal)

  • What it is: a no-op IAuthUIService for the backend-less gallery. The gallery renders the real Login, Register, and Sessions pages for accessibility and render-smoke scanning only, so every operation returns a benign default (MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:8-13).
  • Depends on: IAuthUIService from MMCA.Common.UI.Services.Auth, the Result and Error types from MMCA.Common.Shared.Abstractions, and the LoginRequest, RegisterRequest, AuthenticationResponse, and RefreshSessionSummaryResponse contracts from MMCA.Common.Shared.Auth.
  • Concept introduced, registration order as the override mechanism. This stub is registered before AddUIShared, whose TryAddScoped<IAuthUIService, AuthUIService>() (MMCA.Common.UI/DependencyInjection.cs:170) then defers to it, exactly as the class doc comment states (MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:11-12). TryAdd* is first-registration-wins, so a test host overrides only the extension points it names and inherits every other registration the shared UI makes, with no fork of the composition root. [Rubric §11, Security] assesses how authentication is handled; here the client-side auth boundary is neutralized entirely so the scan touches the real login and register markup without any credential flow. [Rubric §14, Testability]: substituting the top-level UI auth service for constants is what makes those pages renderable in isolation. [Rubric §1, SOLID]: the stub is only possible because the shared pages depend on the IAuthUIService abstraction rather than on AuthUIService, so swapping the implementation needs no change to any page.
  • Walkthrough:
    • Shared constants (:17-24): two static readonly session GUIDs, CurrentDeviceSessionId (all-ones) and OtherDeviceSessionId (all-twos), built from their component parts rather than parsed, with a comment recording why, the MA0176 analyzer rejects parsing a constant at runtime (:16). Unavailable (:23-24) is a single shared Error.Failure("Gallery.AuthUnavailable", "The gallery has no backend.") reused by every failing member.
    • Credential operations (:26-33): LoginAsync, RegisterAsync, and ExchangeOAuthCodeAsync each return Result.Failure<AuthenticationResponse>(Unavailable), so the pages render and submit but never establish a session.
    • Session lifecycle (:35-41): LogoutAsync() returns Task.CompletedTask, TryRefreshTokenAsync(ct) returns false, and ChangePasswordAsync(currentPassword, newPassword, ct) returns Result.Failure(Unavailable).
    • Password reset (:45-49): RequestPasswordResetAsync(email, ct) and ResetPasswordAsync(email, token, newPassword, ct) also fail with Unavailable. The comment at :43-44 records why that still produces useful scan coverage, the Forgot Password page shows its confirmation regardless of the result (anti-enumeration), so the post-submit state renders anyway. ForgotPasswordPageE2ETests asserts exactly that (MMCA.Common.UI.E2E.Tests/Auth/ForgotPasswordPageE2ETests.cs:27-38).
    • Device sessions (:54-75): GetSessionsAsync(ct) is the one member that returns real data. It builds two RefreshSessionSummaryResponse rows, a current device (all-ones id, a Chrome-on-Windows user agent, IpAddress: "203.0.113.7", IsCurrent: true) and another device (all-twos id, a Safari-on-iOS user agent, a null IpAddress, IsCurrent: false). The comment at :51-53 gives the reason, the axe scan has to reach the populated table, the current-device chip, and a live revoke button, which an empty state would hide, and one row is flagged current so both branches of the actions cell render.
    • Revoke (:77-81): RevokeSessionAsync(sessionId, ct) (:77-78) and RevokeAllSessionsAsync(ct) (:80-81) both return Result.Failure(Unavailable), so the per-row revoke buttons and the account-wide sign-out are present and wired without changing anything. The shared Sessions page shows a failed account-wide revoke and stays put rather than signing out locally (MMCA.Common.UI/Pages/Auth/Sessions.razor.cs:182-186, :191-194), so in the gallery that button keeps the page on its error path.
  • Why it's built this way: placing the stub ahead of AddUIShared exploits the shared UI's TryAdd* idempotence rather than requiring the shared registration extension to grow test hooks, which keeps the production DI code free of test-only branches. Returning a real failure Result rather than a null also keeps the pages on their genuine error-handling paths, which is what makes an error alert's markup part of the scanned surface.
  • Where it's used: registered scoped by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:62), the first of the pre-AddUIShared stub registrations. It is consumed indirectly by LoginPageE2ETests, RegisterPageE2ETests, ForgotPasswordPageE2ETests, ResetPasswordPageE2ETests, and SessionsPageE2ETests, the last of which asserts on the rendered form of these very rows, the visible texts "Chrome on Windows" and "This device" and the buttons "Sign out of Safari on iOS" and "Sign out of every device, including this one" (MMCA.Common.UI.E2E.Tests/Auth/SessionsPageE2ETests.cs:28-32).
  • Caveats / not-in-source: the two user-agent strings are supplied raw (MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:63, :70) while the test asserts on friendly names. The parsing that turns one into the other lives in the shared sessions page, not in this stub or this unit.

GalleryAuthenticationStateProvider

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery.Stubs · MMCA.Common.UI.Gallery/Stubs/GalleryAuthenticationStateProvider.cs:16 · Level 8 · class (sealed, internal)

  • What it is: the gallery's Blazor AuthenticationStateProvider. It mirrors the request's authentication in both render phases, so AuthorizeView and CascadingAuthenticationState agree with whatever GalleryFakeAuthenticationHandler decided for the request. Without the gallery_auth cookie both phases yield anonymous, preserving the deliberate signed-out chrome of the login, register, components, and grid scans (MMCA.Common.UI.Gallery/Stubs/GalleryAuthenticationStateProvider.cs:6-15).
  • Depends on: Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider (the abstract base) and IHostEnvironmentAuthenticationStateProvider (the interface the Blazor Server host calls into) at :16-17, IHttpContextAccessor injected as a primary-constructor parameter (:16), and BCL ClaimsPrincipal / ClaimsIdentity.
  • Concept introduced, the two render phases of interactive-server Blazor. A page first renders as static SSR inside the HTTP request, then, once the circuit connects, re-renders interactively over a WebSocket where there is no ambient HttpContext. An auth-state provider therefore has to answer correctly in two different worlds. This class handles both: SSR reads the request user through IHttpContextAccessor, and for the interactive circuit the framework pushes the handshake user in through IHostEnvironmentAuthenticationStateProvider.SetAuthenticationState. The doc comment records that this replaced a former always-anonymous stub, which could not represent the signed-in state the guarded notification pages need (:7-9). [Rubric §19, State Management] assesses how client state is owned and propagated; auth state is the canonical cascading state, and this shows the two supply routes it has under interactive server rendering. [Rubric §28, Front-End Testing]: getting both phases right is what stops a guarded page from flipping to a signed-out tree mid-scan and producing a false pass on the wrong markup.
  • Walkthrough: Anonymous (:19-20) is a static readonly AuthenticationState wrapping an empty ClaimsPrincipal(new ClaimsIdentity()), unauthenticated precisely because no authentication type is supplied (contrast GalleryFakeAuthenticationHandler, which passes one). _hostState (:22) is the nullable task the framework may have pushed in. GetAuthenticationStateAsync() (:24) checks _hostState first and returns it verbatim when present (:26-29): the circuit's handshake user always wins. Otherwise it falls back to the SSR path, reading httpContextAccessor.HttpContext?.User (:31) and returning a new AuthenticationState(user) only when user?.Identity?.IsAuthenticated == true, else the shared Anonymous (:32-34). Both branches use Task.FromResult, so no async machinery is allocated per call. SetAuthenticationState(Task<AuthenticationState>) (:37-38) is the IHostEnvironmentAuthenticationStateProvider implementation and simply stores the task; it does not call NotifyAuthenticationStateChanged.
  • Why it's built this way: the gallery mirrors rather than fabricates. Deriving the component-tree state from whatever the request actually authenticated as keeps one source of truth (the cookie) for the middleware, the endpoint authorization, and the render tree, so a scan cannot land in a split state where the endpoint admitted the request but the tree still renders signed-out.
  • Where it's used: registered scoped as the AuthenticationStateProvider by GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:66), immediately after AddHttpContextAccessor() (MMCA.Common.UI.Gallery/GalleryHost.cs:65), which supplies its dependency.
  • Caveats / not-in-source: the class implements IHostEnvironmentAuthenticationStateProvider but is registered only under the AuthenticationStateProvider service type (MMCA.Common.UI.Gallery/GalleryHost.cs:66). Whether the Blazor Server host resolves this same instance when it calls SetAuthenticationState, and at what point in the render sequence it does so, is framework behavior and is not determinable from this repository's source.

GalleryHost

MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery · MMCA.Common.UI.Gallery/GalleryHost.cs:24 · Level 9 · class (public, static)

  • What it is: a static builder that assembles the entire backend-less Blazor gallery host. It renders the real MMCA.Common.UI auth pages (/login, /register, /profile/sessions), the shared notification pages, and the shared role administration pages (/admin/roles, /admin/roles/{role}), and the gallery's own primitives showcase (/components) and virtualized grid (/grid) against stub implementations of every consumer extension point, so a real-browser axe accessibility scan can run against the shared UI inside MMCA.Common's own CI (MMCA.Common.UI.Gallery/GalleryHost.cs:18-23).
  • Depends on: ASP.NET Core WebApplication / WebApplicationBuilder, MudBlazor (AddMudServices, MMCA.Common.UI.Gallery/GalleryHost.cs:56), the shared MMCA.Common.UI surface (AddUIShared, the gallery's own App root component in MMCA.Common.UI.Gallery/Components/App.razor, and MMCA.Common.UI._Imports as the additional-assembly marker), and SupportedCultures from MMCA.Common.Shared.Globalization. It wires in every stub in this unit: NoOpAuthUIService, NullTokenStorageService, NullTokenRefresher, GalleryAuthenticationStateProvider, GalleryFakeAuthenticationHandler, StubNotificationInboxUIService, StubPushNotificationUIService, StubRoleAdminUIService, and GalleryUIModule, plus the shared NotificationState and NullNotificationScopeProvider, and the shared ServiceWorkerEndpointExtensions (MapCommonServiceWorker, MMCA.Common/Source/Presentation/MMCA.Common.UI.Web/Pwa/ServiceWorkerEndpointExtensions.cs:60).
  • Concept introduced, a self-hostable test host as one buildable unit. The whole host build lives in BuildApp(string[] args) (:31) rather than in Program.cs, so two callers share the identical configured app: the dotnet run entry point, which RunAsync()s it (MMCA.Common.UI.Gallery/Program.cs:7-8), and the E2E collection fixture GalleryHostFixture, which StartAsync()s it on an ephemeral Kestrel port. [Rubric §28, Front-End Testing] assesses real-browser UI coverage; this host is the render target for CI's ui-e2e job (MMCA.Common/.github/workflows/ci.yml:228), whose chromium, firefox, and webkit matrix legs (:236-237) are all required merge gates (:238-240). [Rubric §33, Developer Experience]: MMCA.Common.UI.Gallery/Program.cs:3-6 records the rationale, one BuildApp for both entry points avoids the separate dotnet run plus health-poll that made ADC's e2e cold start fragile.
  • Walkthrough:
    • Assembly name and base dir (:36-37): typeof(GalleryHost).Assembly.GetName().Name is captured without a null-forgiving operator; the comment at :33-35 explains that CI's nullable analysis treats AssemblyName.Name as non-null and would flag ! as an unnecessary suppression (IDE0370), and the value is only interpolated into a filename, which is null-safe either way.
    • Static web assets (:48-51): the load-bearing fix. RCL _content/* CSS and JS plus _framework/blazor.web.js resolve from the entry assembly's manifests and auto-load only in Development; when the E2E suite self-hosts in-process the entry assembly is the test host and the environment is Production, so neither default holds. The loader is pointed explicitly at {galleryAssemblyName}.staticwebassets.runtime.json and forced on with UseStaticWebAssets(). Without it (comment, :41-47) the pages render unstyled and never become interactive, so axe's contrast checks would be meaningless and the page would never signal Blazor readiness.
    • Rendering services (:53-56): AddRazorComponents().AddInteractiveServerComponents(), then AddMudServices().
    • Extension-point stubs, before AddUIShared (:62-66): scoped IAuthUIService, ITokenStorageService, and ITokenRefresher, then AddHttpContextAccessor() and the scoped AuthenticationStateProvider. The ordering comment (:58-61) states the mechanism, AddUIShared's TryAdd* registrations defer to whatever is already present.
    • Real authentication and authorization (:72-76): AddAuthentication(GalleryFakeAuthenticationHandler.SchemeName) plus AddScheme<AuthenticationSchemeOptions, GalleryFakeAuthenticationHandler>(...), then AddAuthorization(), because the guarded pages' [Authorize] surfaces as endpoint metadata (comment, :68-71).
    • Canned notification services (:81-83): scoped NotificationState, INotificationInboxUIService, and IPushNotificationUIService, so the notification pages discovered from the MMCA.Common.UI assembly render populated markup (comment, :78-80).
    • Notification scope (:88): scoped INotificationScopeProvider bound to the shared NullNotificationScopeProvider, whose GetCurrentScopeKeyAsync returns null (MMCA.Common.UI/Services/Notifications/NullNotificationScopeProvider.cs:11-12). The comment (:85-87) explains the choice: the send page injects the scope provider to caption its auto-applied target, and the gallery is unscoped, so the null provider keeps the page rendering with the caption absent, which is the framework-default look the scan should measure.
    • Canned role administration (:92): scoped IRoleAdminUIService bound to StubRoleAdminUIService, so the shared /admin/roles and /admin/roles/{role} pages render populated for the scan (comment, :90-91, citing ADR-116).
    • Module contribution (:97): the singleton IUIModule, so the shared Router discovers the gallery's own /components and /grid pages (comment, :94-96).
    • Shared UI (:102): AddUIShared(builder.Configuration) registers the ApiSettings / LayoutSettings binding, the "APIClient" HttpClient, and the remaining shared services; the in-memory Api:ApiEndpoint from appsettings.json satisfies validation and deliberately points at the unroutable http://api.gallery.invalid (MMCA.Common.UI.Gallery/appsettings.json:2-4), and the client is never invoked because IAuthUIService is stubbed (comment, :99-101).
    • Build (:104): builder.Build(), after which only middleware and endpoints are configured.
    • Request localization (:111-115): builds galleryCultures as [.. SupportedCultures.All, SupportedCultures.PseudoLocale] and applies it as the supported and supported-UI culture set over SupportedCultures.Default. The comment (:106-110) is explicit that this mirrors the real hosts' ADR-027 allowlist but additionally enables qps-Ploc unconditionally, because this host is unpackaged test infrastructure that is never deployed and the pseudo pass here is a required CI gate (PseudoLocalizationE2ETests, the rubric §27 resource-round-trip and text-expansion evidence). Production keeps qps-Ploc Development-only via UseCommonRequestLocalization, and the comment closes with "Do not copy this into a real host". [Rubric §27, i18n] assesses whether localization is enforced rather than aspirational; this host is where the pseudo-locale evidence is produced.
    • Middleware (:119-124): UseAuthentication() then UseAuthorization() (WebApplication inserts UseRouting ahead of them automatically, comment :117-118), then UseAntiforgery(), required because Razor Component endpoints carry anti-forgery metadata even though the gallery's interactive forms never POST over HTTP (comment, :122-123).
    • Endpoints (:128-140): MapStaticAssets is given the gallery's own {galleryAssemblyName}.staticwebassets.endpoints.json for the same in-process self-host reason as above (comment, :126-127); a /health endpoint returns Results.Ok("Healthy") (:131); MapCommonServiceWorker() (:134) maps the installable-web-app worker that every consumer host maps and that the gallery's App.razor registers through pwa-register.js (comment, :133), so the gallery serves the same PWA surface the real hosts do (ADR-136); and MapRazorComponents<App>().AddInteractiveServerRenderMode().AddAdditionalAssemblies(typeof(MMCA.Common.UI._Imports).Assembly) (:138-140) makes the real shared pages routable alongside the gallery's own (comment, :136-137).
    • Return (:142): the built-but-not-started WebApplication, leaving the start mode to the caller.
  • Why it's built this way: keeping the whole build in BuildApp rather than Program.cs lets the E2E fixture host the identical configured app in-process on a real bound port via StartAsync, not WebApplicationFactory's in-memory TestServer, which Playwright cannot reach over the wire. GalleryHostFixture therefore clears the URLs, binds http://127.0.0.1:0, and reads the ephemeral address back from IServerAddressesFeature (MMCA.Common.UI.E2E.Tests/Infrastructure/GalleryHostFixture.cs:26-37). This is deliberate CI infrastructure and is never shipped: the csproj sets IsPackable=false and records that the project is deliberately outside MMCA.Common.slnx (MMCA.Common.UI.Gallery/MMCA.Common.UI.Gallery.csproj:3-6), so it builds only by csproj path.
  • Where it's used: consumed by MMCA.Common.UI.Gallery/Program.cs:7 (the dotnet run entry) and by every E2E test through GalleryHostFixture (MMCA.Common.UI.E2E.Tests/Infrastructure/GalleryHostFixture.cs:26), against which the axe and render suite runs: LoginPageE2ETests, RegisterPageE2ETests, ForgotPasswordPageE2ETests, ResetPasswordPageE2ETests, SessionsPageE2ETests, ComponentsPageE2ETests, GridPageE2ETests, NotificationPagesE2ETests, DarkModeE2ETests, MobileTopRowE2ETests, StickySidebarE2ETests, WebVitalsE2ETests, ServiceWorkerE2ETests (MMCA.Common.UI.E2E.Tests/Pwa/ServiceWorkerE2ETests.cs:17), and PseudoLocalizationE2ETests. NotificationPagesHiddenRoutingTests also calls BuildApp directly (MMCA.Common.UI.E2E.Tests/NotificationPagesHiddenRoutingTests.cs:109).
  • Caveats / not-in-source: the class summary (MMCA.Common.UI.Gallery/GalleryHost.cs:18-23) lags the body in two ways. It names only /login, /register, and /components while BuildApp also serves the guarded notification and sessions pages and the /grid virtualization page, and it still describes the stub set as including an "anonymous auth state", whereas GalleryAuthenticationStateProvider mirrors the request rather than forcing anonymous.

Per-project test rollup

The remaining test types in G25 are logged here by project rather than as individual ### sections (per the TESTS note): each project below is a Microsoft Testing Platform / xUnit v3 assembly, and the table gives its purpose and testing style (unit, integration, architecture-fitness, E2E, or component). The table covers 61 assemblies and 2995 types.

Test project (assembly) Types Purpose and style
MMCA.ADC.Conference.API.Tests 28 Unit tests for the Conference module's API layer: one test class per controller (events, sessions, speakers, partners, sponsors, rooms, activities, categories, questions, session assets), plus policy and convention gates for permission grants (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.API.Tests/Authorization/ConferencePermissionGrantsTests.cs:21), export authorization (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.API.Tests/Conventions/EntityExportAuthorizationTests.cs:23), conditional writes (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.API.Tests/Conventions/ConditionalWriteConventionTests.cs:22), and localized error resources (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.API.Tests/Localization/ConferenceErrorResourcesTests.cs:15).
MMCA.ADC.Conference.Application.Tests 200 Unit tests for Conference command/query handlers, validators, and DTO mappers: the module's largest suite, covering CQRS handler behavior in isolation.
MMCA.ADC.Conference.Domain.Tests 36 Unit tests for Conference domain entities and invariants: factory methods, Result<T> validation rules, and aggregate behavior.
MMCA.ADC.Conference.Infrastructure.Tests 12 Unit tests for Conference infrastructure: EF entity configuration against a test DbContext (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Persistence/ConferenceEntityConfigurationTests.cs:18), the module database seeder, the Sessionize import service, output-cache eviction of session scores, and the AI session-scoring path: the scoring service, its response guardrail, and the module's AI guardrail registration, each driven through a private stub IChatClient (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/SessionScoringServiceTests.cs:22, MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/SessionScoreResponseGuardrailTests.cs:18, MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/ConferenceAiGuardrailsRegistrationTests.cs:17).
MMCA.ADC.Conference.IntegrationTests 47 Integration tests exercising the Conference module against a real SQL Server instance, covering repositories, handlers, and the outbox end to end.
MMCA.ADC.Conference.Scoring.Evaluation.Tests 10 Evaluation suite for the AI session scorer: a golden corpus (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/GoldenCase.cs:106) replayed through GoldenReplayTestsBase (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/GoldenReplayTests.cs:35), the scoring prompt contract pinned through PromptContractPinTestsBase (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/PromptContractTests.cs:180), and a live-judge test class (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/LiveJudgeTests.cs:39).
MMCA.ADC.Conference.Shared.Tests 9 Unit tests for the Conference Shared project: the DTO contracts (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Shared.Tests/Events/EventDTOTests.cs:6, plus the session, speaker and category DTO tests), current-event defaults and selection (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Shared.Tests/Events/CurrentEventSelectorTests.cs:11), the disabled event-live validation service (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Shared.Tests/Events/DisabledEventLiveValidationServiceTests.cs:12), and the Sessionize code format (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Shared.Tests/Events/SessionizeCodeFormatTests.cs:9).
MMCA.ADC.Conference.UI.Tests 129 bUnit component tests for Conference Blazor pages and components: rendering, parameter binding, and interaction behavior.
MMCA.ADC.CrossService.IntegrationTests 13 Cross-service integration tests: CrossServiceFixture hosts Identity, Conference and Engagement in one process against a real Testcontainers SQL Server and a Testcontainers RabbitMQ, so the genuine MassTransit outbox-to-broker-to-consumer round trip runs, and runs Notification as two replicas over a Testcontainers Redis SignalR backplane (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:15-:16, :28, :87).
MMCA.ADC.E2E.Tests 97 Playwright end-to-end tests driving the deployed/running ADC UI through full user journeys (registration, check-in, bookmarks, admin flows).
MMCA.ADC.Engagement.API.Tests 12 Unit tests for the Engagement module's API layer: one test class per controller (bookmarks, check-ins, points, live polls, live-poll voting, session questions), plus permission grants (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.API.Tests/Authorization/EngagementPermissionGrantsTests.cs:22), the owner-or-admin filter vocabulary (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.API.Tests/Authorization/OwnerOrAdminFilterVocabularyTests.cs:24), conditional-write conventions (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.API.Tests/Controllers/ConditionalWriteConventionTests.cs:13), and localized error resources (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.API.Tests/Localization/EngagementErrorResourcesTests.cs:15).
MMCA.ADC.Engagement.Application.Tests 73 Unit tests for Engagement command/query handlers, validators, and DTO mappers (points, badges, check-ins).
MMCA.ADC.Engagement.Domain.Tests 12 Unit tests for Engagement domain entities and invariants.
MMCA.ADC.Engagement.Infrastructure.Tests 8 Unit tests for Engagement EF Core configuration and persistence-layer helpers.
MMCA.ADC.Engagement.IntegrationTests 22 Integration tests exercising the Engagement module against a real SQL Server instance.
MMCA.ADC.Engagement.Shared.Tests 6 Unit tests for Engagement cross-cutting Shared-project helpers.
MMCA.ADC.Engagement.UI.Tests 54 bUnit component tests for Engagement Blazor pages and components (check-in scanning, badges, points displays).
MMCA.ADC.Gateway.Tests 11 In-process host tests for the ADC YARP Gateway: route mapping over a WebApplicationFactory (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/RouteMapTests.cs:38), hardening, security headers and robots.txt over ProductionHostApplicationFactory<TEntryPoint>, graceful shutdown through GracefulShutdownTestsBase<TEntryPoint>, and AppHost-to-Bicep parity (AppHostBicepParityTests, MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/AppHostBicepParityTests.cs:24).
MMCA.ADC.Identity.API.Tests 11 Unit tests for the Identity module's API controllers (auth, registration, MFA).
MMCA.ADC.Identity.Application.Tests 36 Unit tests for Identity command/query handlers, validators, and authentication/authorization services.
MMCA.ADC.Identity.Domain.Tests 6 Unit tests for Identity domain entities and invariants.
MMCA.ADC.Identity.Infrastructure.Tests 12 Unit tests for Identity EF Core configuration and persistence-layer helpers, plus the avatar normalizer SkiaSharpImageProcessor, which must turn an untrusted upload into an exact-size square JPEG with all metadata stripped and fail an unsupported format or an oversized declared frame as a validation error rather than an exception (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Infrastructure.Tests/Services/Users/SkiaSharpImageProcessorTests.cs:14).
MMCA.ADC.Identity.IntegrationTests 40 Integration tests exercising the Identity module (auth flows, roles, MFA) against a real SQL Server instance.
MMCA.ADC.Identity.Shared.Tests 2 Unit tests for Identity Shared-project contracts: the role-to-permission grant map and the disabled attendee query service.
MMCA.ADC.Identity.UI.Tests 18 bUnit component tests for Identity Blazor pages and components (login, registration, account management).
MMCA.ADC.Notification.API.Tests 1 Unit tests for the Notification module's API surface.
MMCA.ADC.Notification.Application.Tests 4 Unit tests for Notification command/query handlers and recipient-resolution logic.
MMCA.ADC.Notification.IntegrationTests 10 Integration tests exercising the Notification module's outbox and delivery paths.
MMCA.ADC.ServiceBusEmulator.IntegrationTests 3 Integration tests verifying the local Azure Service Bus emulator round-trips messages used by the outbox/event-bus pipeline.
MMCA.ADC.Services.Tests 23 Unit tests for the service-host gRPC servers and their client adapters invoked directly (no booted host): wire round trips, RpcException-to-Result mapping, the live-channel ingress bounds, the export timestamp contract (including the Conference feedback-answer half of the user-data export), and the Conference and Identity warm-up paths.
MMCA.ADC.UI.Web.Tests 9 In-process host tests for the ADC Blazor Web host, each class sharing one ConferenceUiHostApplicationFactory built on ProductionHostApplicationFactory<TEntryPoint> with Program as the entry point (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/ConferenceUiHostApplicationFactory.cs:17): security headers (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/SecurityHeadersTests.cs:17), UI rate limiting (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/UiRateLimitingTests.cs:18), fallback authorization (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/FallbackAuthorizationTests.cs:18), the client-config endpoint (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/ClientConfigEndpointTests.cs:14), Serilog bootstrap (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/SerilogBootstrapTests.cs:16), and the bounded circuit handler (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/BoundedCircuitHandlerTests.cs:16).
MMCA.Common.AI.Tests 39 Unit tests for the framework's AI package: the chat-client decorators (bounded, guardrail, prompt tagging, usage recording), provider selection and the adapter factory (MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/Providers/AdapterFactoryTests.cs:16), DI registration and settings binding, the guardrail set (content policy, request redaction, streamed guardrails, tool policy, and their registration, for example MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/Guardrails/ContentPolicyGuardrailTests.cs:12), and the evaluation helpers (recorded responses, the replay chat client, and reference golden-replay and prompt-contract pins built on the shared bases, MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/Evaluation/ReferenceGoldenReplayTests.cs:17, MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/Evaluation/ReferencePromptContractTests.cs:11). Stub chat client, guardrail and tool-policy doubles live under Fixtures/.
MMCA.Common.API.Tests 215 Unit tests for the framework's API base classes and conventions: controller bases, versioning, rate limiting, and parameter descriptors.
MMCA.Common.Application.Tests 428 Unit tests for the framework's CQRS/Application layer: command/query handler bases, the decorator pipeline (caching, authorization, multi-tenancy), and validation conventions. The framework's largest single suite.
MMCA.Common.Aspire.Hosting.Tests 6 Unit tests for the framework's Aspire AppHost extensions used by consumer AppHost projects.
MMCA.Common.Aspire.Tests 54 Unit tests for the framework's Aspire integration helpers: readiness budgets, environment gates, and probe paths.
MMCA.Common.Aws.LocalStack.Tests 15 Emulator tier for the AWS adapter: one pinned LocalStack container per assembly (S3, SQS, SNS, Secrets Manager, Systems Manager and DynamoDB) with a fixed dummy key pair that never touches the AWS default credential chain (MMCA.Common/Tests/Hosting/MMCA.Common.Aws.LocalStack.Tests/LocalStackFixture.cs:19), driving the SQS transport round trip (.../AmazonSqsTransportLocalStackTests.cs:40), S3 file storage (.../S3FileStorageLocalStackTests.cs:15), SNS push (.../SnsPushLocalStackTests.cs:26), the data-protection key ring (.../AwsDataProtectionLocalStackTests.cs:16) and Secrets Manager configuration (.../AwsSecretsManagerConfigurationLocalStackTests.cs:17). Integration style; needs Docker.
MMCA.Common.Aws.Tests 19 Unit tests for the AWS adapter package: the SQS transport (MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Messaging/AmazonSqsTransportTests.cs:21), S3 storage, object URLs and the client factory (.../Storage/S3FileStorageServiceTests.cs:20), SNS native push, device registration and its DI call (.../Notifications/SnsNativePushSenderTests.cs:21, .../Notifications/AddAmazonSnsPushTests.cs:19), RDS IAM authentication (.../Rds/AwsRdsIamAuthenticationTests.cs:18), the Secrets Manager configuration provider and its extensions (.../Secrets/AwsSecretsManagerConfigurationProviderTests.cs:19), data protection and service registration, and the vendor's half of the cloud-vendor rules (.../CloudVendorRulesTests.cs:13). Unit style.
MMCA.Common.Azure.Tests 17 Unit tests for the Azure adapter package: the Azure Service Bus transport and the emulator branch's detection (MMCA.Common/Tests/Hosting/MMCA.Common.Azure.Tests/Messaging/AzureServiceBusTransportTests.cs:20, .../Messaging/ServiceBusEmulatorSupportTests.cs:15), AzureNotificationHubDeviceRegistrar and the native-push user tags (.../Notifications/AzureNotificationHubDeviceRegistrarTests.cs:17, .../Notifications/NativePushUserTagsTests.cs:11, ADR-044), Blob file storage (.../Storage/AzureBlobFileStorageServiceTests.cs:17), Key Vault configuration and blob-backed data protection (.../AzureKeyVaultConfigurationExtensionsTests.cs:27, .../AzureDataProtectionExtensionsTests.cs:19), Entra SQL authentication (.../SqlClient/SqlClientEntraAuthenticationTests.cs:14), the Azure Monitor exporter and the Live Metrics off switch (.../Telemetry/AzureMonitorExporterTests.cs:16, .../Telemetry/LiveMetricsConfigurationTests.cs:21), service registration, and the vendor's half of the cloud-vendor rules (.../CloudVendorRulesTests.cs:13). Unit style.
MMCA.Common.Benchmarks 6 BenchmarkDotNet performance benchmarks for hot paths in the framework (not a correctness suite).
MMCA.Common.Domain.Tests 67 Unit tests for the framework's base Domain types: entities, aggregate roots, specifications, and domain events.
MMCA.Common.Gateway.Tests 10 Unit tests for the framework's reusable YARP Gateway extensions.
MMCA.Common.Gcp.Emulator.Tests 7 Emulator tier for the Google Cloud adapter: a pinned fake-gcs-server container, run as a generic container because it has no Testcontainers module (MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Emulator.Tests/FakeGcsServerFixture.cs:18), and a Firestore emulator fixture (.../FirestoreEmulatorFixture.cs:14), driving Cloud Storage file storage (.../GcsFileStorageEmulatorTests.cs:14), the GCS-backed data-protection key ring (.../GcsDataProtectionEmulatorTests.cs:24) and FCM push over Firestore through a recording messaging client (.../FcmPushFirestoreEmulatorTests.cs:22). Integration style; needs Docker.
MMCA.Common.Gcp.Tests 34 Unit tests for the Google Cloud adapter package: Cloud Storage file storage, object URLs and the client factory (MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Storage/GcsFileStorageServiceTests.cs:21), the KMS-encrypted GCS data-protection key ring (.../DataProtection/GcpKmsXmlEncryptionTests.cs:15, .../DataProtection/GcsXmlRepositoryTests.cs:14), FCM native push and device registration (.../Notifications/FcmNativePushSenderTests.cs:21), Secret Manager configuration (.../Secrets/GoogleSecretManagerConfigurationProviderTests.cs:19), Cloud SQL IAM authentication (.../CloudSql/CloudSqlIamAuthenticationTests.cs:16), the Google Cloud telemetry exporter and its auth handler against an in-process OTLP receiver (.../Telemetry/GoogleCloudTelemetryExportTests.cs:22, .../Telemetry/OtlpTestReceiver.cs:19), and the negative half of the cloud-vendor rules: an assembly declared cloud-neutral that names a Google SDK type, references a Google SDK assembly or references the adapter fails them (.../CloudVendorRulesTests.cs:20). In-memory fakes stand in for each Google client. Unit style.
MMCA.Common.Grpc.Tests 28 Unit tests for the framework's gRPC infrastructure helpers, including the service-token authentication used between internal callers.
MMCA.Common.Infrastructure.PostgreSQL.Tests 19 Tests for the framework's PostgreSQL engine: the ApplicationDbContext implementation and the built-in PostgreSQL broker transport (MessageBus:Provider=PostgreSql) run against a real server, where the transport schema is created before the bus needs it, the ambient MMCA-* headers survive the SQL transport's message rows, a failed consume comes back through the transport's native delayed delivery, and a registered data-source configurator (the IAM token path) authenticates the broker's own connections (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.PostgreSQL.Tests/PostgreSqlBrokerTransportTests.cs:59, with its fixture at :34). Integration style over Testcontainers.
MMCA.Common.Infrastructure.Redis.Tests 4 Integration tests that run the framework's Redis-backed caches and the distributed rate limiter's Lua counter against a real Redis Testcontainer.
MMCA.Common.Infrastructure.SQLServer.Tests 8 Integration tests that run the shipped SQL Server context and DbContextFactory against a real SQL Server in Docker (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.SQLServer.Tests/SQLServerPersistenceTests.cs:44).
MMCA.Common.Infrastructure.Tests 583 Unit tests for the framework's core Infrastructure layer: SQL Server ApplicationDbContext, interceptors, repositories, outbox processing, and EF conventions. The framework's largest suite overall.
MMCA.Common.LoadTests 14 Weekly load tests of the framework's real persistence stack over a temp-file SQLite database: the dynamic paged-query path at 100,000 rows and outbox throughput at 10,000 events (MMCA.Common/Tests/Performance/MMCA.Common.LoadTests/PagedQueryLoadTests.cs:13).
MMCA.Common.Shared.Tests 64 Unit tests for the framework's Shared-layer cross-cutting helpers (identifier types, common value objects).
MMCA.Common.Testing.Aspire 10 Not a test assembly but the separate MMCA.Common.Testing.Aspire package (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Aspire/MMCA.Common.Testing.Aspire.csproj:3): a collection fixture that boots a real Aspire AppHost and waits for per-resource health, plus typed assertions for the framework wiring contracts (health, liveness, JWKS, h2c, data sources) (:4), consumed by other projects' AppHost smoke tests.
MMCA.Common.Testing.Aspire.AppHostTests 3 Architecture-fitness/smoke tests validating the framework's own sample AppHost boots correctly under the Testing.Aspire harness.
MMCA.Common.Testing.Aspire.Tests 8 Unit tests for the MMCA.Common.Testing.Aspire package's own fixture types.
MMCA.Common.Testing.Azure 1 Not a test assembly but the separate MMCA.Common.Testing.Azure package: its one type is ServiceBusEmulatorFixtureBase, the bounded, phase-named Service Bus emulator broker-parity fixture a consumer subclasses (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Azure/Fixtures/ServiceBusEmulatorFixtureBase.cs:54).
MMCA.Common.Testing.Azure.Tests 4 Unit tests for the MMCA.Common.Testing.Azure package: the container-free contract of ServiceBusEmulatorFixtureBase (the image pin, the bounded-phase budgets, the admin-plane connection-string composition, the no-bus default, and the process-global MassTransit entity defaults the emulator's TTL quota forces, MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Azure.Tests/Fixtures/ServiceBusEmulatorFixtureBaseTests.cs:15), plus the package's half of the cloud-vendor rules (.../CloudVendorRulesTests.cs:15). Unit style.
MMCA.Common.Testing.E2E.Tests 2 Unit tests for the E2E test configuration: the millisecond settings parse with the invariant culture and refuse zero, negative and non-finite timeouts by falling back to the default (a 0 means no timeout to Playwright), and because each fact sets and restores a process-wide environment variable the class runs alone in its own collection (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.E2E.Tests/Infrastructure/E2ETestConfigurationTests.cs:21, :9). Unit style.
MMCA.Common.Testing.Tests 36 Unit tests for the framework's general-purpose MMCA.Common.Testing package (test doubles, builders, assertion helpers) shipped for consumer use.
MMCA.Common.UI.E2E.Tests 33 Playwright end-to-end tests for the framework's shared UI Gallery/demo host, including the installable-web-app service worker in a real browser (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Pwa/ServiceWorkerE2ETests.cs:15).
MMCA.Common.UI.Tests 269 bUnit component tests for the framework's reusable Blazor UI package (shared components, pages, services).
MMCA.Common.UI.Web.Tests 38 Unit tests for the framework's Blazor Web host services (used by consumer Web hosts): server-side token storage, web form-factor detection, UI rate limiting and the bounded circuit handler (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/Hardening/BoundedCircuitHandlerTests.cs:20), the trusted-caller handler, header registration and service discovery (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/Security/TrustedCallerHandlerTests.cs:13), the Blazor CSP policy provider (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/Security/BlazorCspPolicyProviderTests.cs:26), and the same-origin API proxy end to end (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/SameOriginProxy/SameOriginApiProxyTokenTests.cs:15).

Test suites by module

MMCA.Common, the framework suite (Tests/ mirrors Source/)

Test project (assembly) Types What it covers · style
MMCA.Common.Shared.Tests 64 The innermost layer: the Result/Error/ErrorType pattern and its extensions, value objects (Money, Email, Address, DateRange, ...) and their factory-method invariants, DTO/paging contracts, and the striped keyed lock behind KeyedSemaphoreStripe (mutual exclusion per key, independent progress across stripes, release on the exception path, and a bounded table size no matter how many caller-supplied keys arrive, MMCA.Common/Tests/Core/MMCA.Common.Shared.Tests/Concurrency/KeyedSemaphoreStripeTests.cs:12). Two files defend contracts that only bite in a head with no ASP.NET pipeline: MoneySerializationTests pins the round trip of Money's private [JsonConstructor], which is also the constructor EF Core uses to materialize the owned type, so a materializer yielding a null currency must fail fast rather than surface a half-built value object (.../ValueObjects/MoneySerializationTests.cs:11); and SupportedCulturesTests pins SupportedCultures.ResolveClosest, the fallback chain a head applies when it resolves its own culture instead of getting request localization's Accept-Language matching for free, since an Android device reports a specific culture (es-MX) while the allowlist holds a neutral one and without the language-level match a Spanish phone would silently start in English (.../Globalization/SupportedCulturesTests.cs:11, ADR-027 / §27). The smart enumeration Enumeration<TEnumeration> has reflection-based member discovery, Result-returning FromValue/FromName lookups (case-insensitive by name, an UnknownValue/UnknownName error rather than an exception) and type-guarded equality pinned, so two enumerations that happen to share an integer value are never equal and never collide in a hash set (.../ValueObjects/EnumerationTests.cs:10). KeysetPaginationTests covers the keyset ("seek") paging value types, the request's clamp semantics, the page result, and the cursor codec's round-trip, version gate and rejection of anything malformed, which matters because a cursor is a client-held token and a corrupted one must be refused rather than quietly paged from the top (.../Abstractions/KeysetPaginationTests.cs:10, ADR-055); RoleValueTests pins that RoleValue.Validate compares roles case-insensitively no matter which comparer the caller's set was built with, after a set built with the default ordinal comparer rejected a correctly spelled role that differed only in casing (.../Auth/RoleValueTests.cs:13). Its newest four all exist because a contract is shared by callers that must never disagree: ErrorTypeSeverityTests pins the severity ranking every transport edge uses to pick the status for a combined result, including that the 400-family ranks equal and that ties keep the earliest error (.../Abstractions/ErrorTypeSeverityTests.cs:10); ProblemDetailsResultReader parses an RFC 9457 payload back into Error instances and lives here rather than in the API package precisely so the UI can reach it (.../Http/ProblemDetailsResultReaderTests.cs:15); NotificationScopeKey ships its formatter and the regex that guards it together, and every key the formatter produces is asserted against that pattern, which is the default the notification hub enforces (.../Notifications/NotificationScopeKeyTests.cs:14); and ModuleNameConventionsTests pins the MMCA.{App}.{Module}.{Layer} parse shared by persistence (SQL schema and data-source names) and the logging decorators' scope enrichment, including the namespaces that carry no module at all (.../Conventions/ModuleNameConventionsTests.cs:15). ConcurrencyETag, the weak entity tag that carries the concurrency token, is pinned here too, one layer below the filter that consumes it (.../Http/ConcurrencyETagTests.cs:10, ADR-035). PasswordComplexityTests pins the shared strong-password rule that both the server validator and the client form attribute evaluate, with Unicode-aware character classes: an accented or CJK letter is a letter (never the special character), a non-ASCII uppercase letter is uppercase, and a non-ASCII decimal digit is a digit (MMCA.Common/Tests/Core/MMCA.Common.Shared.Tests/Auth/PasswordComplexityTests.cs:12). RetryAfterHeaderTests pins both halves of the 429 Retry-After contract: the server formats a lease's hint in whole seconds rounded up, and the client reads either header form back and refuses to wait out one longer than HttpResilienceDefaults.MaxRetryAfterWait (MMCA.Common/Tests/Core/MMCA.Common.Shared.Tests/Resilience/RetryAfterHeaderTests.cs:13, RetryAfterHeader); and UsPostalRegionsTests covers UsPostalRegions, the USPS regions by code, code/name lookup and the ZIP / ZIP+4 format (.../ValueObjects/Contact/UsPostalRegionsTests.cs:10). Pure unit tests, no DI or DB.
MMCA.Common.Domain.Tests 67 The entity hierarchy (BaseEntity→AuditableBaseEntity→AuditableAggregateRootEntity), domain-event collection, SetItems<T>/GetChildOrNotFound<T>, specifications, and the PiiAttribute/anonymization boundary plus the logging/telemetry redaction half of the [Pii] contract (masks marked members so a data subject's values never reach logs, MMCA.Common/Tests/Core/MMCA.Common.Domain.Tests/Privacy/PiiRedactorTests.cs:12, ADR-005 / §30, PiiRedactor). OwnedByUserSpecification<TEntity, TIdentifierType>, the reusable "rows this caller created" criteria, is covered with a fake that overrides the virtual CreatedBy getter, because that audit field is stamped by the infrastructure layer through EF's change tracker and is otherwise unsettable from a test (.../Specifications/OwnedByUserSpecificationTests.cs:8). SpecificationCompositionTests pins HOW the boolean composers build their criteria rather than only what the composed predicate answers, and neither property is visible from IsSatisfiedBy: the composed tree must contain no InvocationExpression, because a provider that cannot unwrap one (Cosmos) throws at translation time on an ANDed specification, and it must be built once per instance, because the query pipeline reads Criteria on every request while the old implementation rebuilt the tree every time (.../Specifications/SpecificationCompositionTests.cs:18, ADR-018). QuerySpecificationTests covers the builder state a QuerySpecification carries beyond its predicate (includes, ordering, paging, tracking, soft-delete scope) and pins the base chain the SpecificationsDoNotNavigateToOtherEntities fitness rule keys on (.../Specifications/QuerySpecificationTests.cs:14). OutputCacheEvictionRequested is asserted as a shape rather than a behavior (members, schema version, empty-tags default) because it is the framework's own published integration event and every host that consumes it must be able to deserialize it forever (.../IntegrationEvents/OutputCacheEvictionRequestedTests.cs:12, ADR-010). Its newest file is the RefreshSession record (BR-205/206): the token is stored only as an upper-case hex SHA-256 digest and never in the clear, captured client metadata is truncated to its column width rather than overflowing at insert, revocation records when, why and the successor, and re-revoking keeps the first reason (.../Auth/RefreshSessionTests.cs:13, ADR-097). Its two newest types are fixtures inside PiiRedactorTests, PiiBase and PiiOverride (MMCA.Common/Tests/Core/MMCA.Common.Domain.Tests/Privacy/PiiRedactorTests.cs:32, :38). Pure unit tests over the framework domain primitives.
MMCA.Common.Application.Tests 428 The CQRS engine: the decorator pipeline in its registered nesting order, which now carries two validating rings (commands: FeatureGate→Authorization→Logging→Caching→Validating→Timeout→Transactional→handler, MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:134-140; queries: FeatureGate→Authorization→Logging→Caching→Validating→Timeout→handler, .../DependencyInjection.cs:138-143, both registered innermost-first because Scrutor's TryDecorate applies in reverse, .../DependencyInjection.cs:115), the opt-in MiniProfiler decorators added by AddApplicationProfiling (.../DependencyInjection.cs:565) and the CqrsMetrics counters/histograms (MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Decorators/CqrsMetricsTests.cs:20), ModuleLoader topological ordering, DomainEventDispatcher plus the swallow-and-log SafeDomainEventHandler base (.../DomainEvents/SafeDomainEventHandlerTests.cs:14), validation, the IMessageBus abstraction, entity-query projection/paging and the per-type filter strategies, the cross-source CrossSourceSpecification helper, the magic-byte upload sniffer behind ImageContentSniffer (.../ImageContentSnifferTests.cs:12, ADR-045), and the notification read/send handlers driven by an injected TimeProvider test clock. Two older files defend non-obvious properties rather than behavior: PagingMathTests pins the page arithmetic (.../Services/Query/PagingMathTests.cs:12), and QueryFilterServicePropertyCacheTests asserts on the real static property cache inside QueryFilterService to prove that caching a miss never happens, since filter names arrive in the query string and a negatively-cached miss would let any caller grow a process-lifetime dictionary one bogus name at a time while the request still returned a tidy 400 (.../Services/Filtering/QueryFilterServicePropertyCacheTests.cs:14, §11/§12). The hoisted user use cases hold a large block: ChangePasswordHandlerBaseTests pins verify-before-write ordering and the no-save-on-invariant-failure rule (.../Users/ChangePasswordHandlerBaseTests.cs:15, ADR-032); DeleteUserHandlerBaseTests pins the owner-or-privileged-role gate, the delete-then-anonymize-then-save ordering and the post-commit queue (.../Users/DeleteUserHandlerBaseTests.cs:14); UserOwnershipRule gets its own tests for the self-service authorization check that was written out four times across the two apps before the hoist (.../Users/UserOwnershipRuleTests.cs:11); SoftDeletedUserValidatorTests proves the BR-133 check is one query with the soft-delete global filter deliberately bypassed (.../Users/SoftDeletedUserValidatorTests.cs:13); and ExportUserDataHandlerBaseTests drives ExportUserDataHandlerBase<TUser, TQuery> through the ownership gate, the read-only account load and the best-effort section fan-out, where a section that throws or reports itself unavailable must degrade and let the rest of the package travel (a data-subject request is a legal deadline, so a partial export beats a failed one) while a cancelled section propagates instead (.../Users/ExportUserDataHandlerBaseTests.cs:17, ADR-076 / §30). The password-recovery pair is the newest addition to that family and both files are written around anti-enumeration: a malformed address, an unknown address, a throttled request and a failed send are all indistinguishable successes, and the mail that IS sent carries both the prefilled link and the raw token (.../Users/ForgotPasswordHandlerBaseTests.cs:20), while every rejection on the reset leg collapses to one generic Auth.InvalidResetToken, the write happens only when the aggregate accepts it, the token is consumed before the save, and a successful reset clears the lockout so the new credential is immediately usable (.../Users/ResetPasswordHandlerBaseTests.cs:18, ADR-091). RefreshSessionManagementTests covers the per-device surface layered on that workflow: the sid claim stamped on every issued access token, a refresh stamping the successor session's id rather than the predecessor's, an older access token with no sid still accepted, and a device list that carries the client fields and never the token material (.../Auth/RefreshSessionManagementTests.cs:26, ADR-097). Alongside them, SoftDeletedUserCache has its key shape and culture invariance pinned because the API middleware reads that exact key (.../Auth/SoftDeletedUserCacheTests.cs:13); CachingDecoratorConstructorSelectionTests pins that the container picks the logger-bearing constructor on both caching decorators, since if selection ever flipped the decorators would keep working and every test would stay green while production silently stopped reporting cache failures (.../Decorators/CachingDecoratorConstructorSelectionTests.cs:21); and CachingDecoratorTenantScopingTests covers the tenancy half of the same pair, where ICacheService is a singleton that cannot see the scoped tenant, so the key transformation is the isolation (.../Decorators/CachingDecoratorTenantScopingTests.cs:16, ADR-073 / §11). The two newer decorator rings have their own files: AuthorizationCommandDecoratorTests (.../Decorators/AuthorizationCommandDecoratorTests.cs:11) and its query twin cover the permission check that runs inside the pipeline rather than only at the controller, and TimeoutCommandDecoratorTests (.../Decorators/TimeoutCommandDecoratorTests.cs:9) covers the per-handler execution budget. Read-pipeline determinism and projection pushdown form their own block (ADR-055, §12): EntityQueryPipelineOrderingTests pins that a paginated read always ends up with a total order, because Skip/Take over a partial order is undefined and lets one row appear on two consecutive pages while another appears on none, while an unpaginated read is deliberately left alone (.../Services/EntityQueryPipelineOrderingTests.cs:16), with the tie-break parameter covered directly and its omission pinned as behavior-preserving (.../Services/QueryFieldServiceTieBreakTests.cs:11); EntityQueryService gains a projection path that selects DTO columns and never calls the mapper, with a fallback to materialize-then-map when the read cannot be projected (.../Services/EntityQueryServiceProjectionTests.cs:19), and its two-constructor arrangement is proved to actually resolve under Microsoft.Extensions.DependencyInjection, which has no notion of an optional dependency (.../Services/EntityQueryServiceResolutionTests.cs:19); PushNotificationDTOProjectorTests is the safety property underneath all of that, since the two paths are chosen by whether a projector is registered and a divergence would make the response depend on a DI detail (.../Notifications/PushNotificationDTOProjectorTests.cs:16). BestEffortTests covers the helper the fire-and-forget call sites share: the success path is transparent, a failure is swallowed with exactly one Warning and one metric increment, and cancellation is deliberately NOT part of the swallow (.../Services/BestEffortTests.cs:13). The newest block is composition and contract guards. EventUpcasterRegistry covers the schema-evolution ladder: identity for an unregistered contract, a V1→V2→V3 chain applied in order even when registered out of order, envelope (MessageId/DateOccurred) preservation across every hop whether or not the author copies it, and the three constructor-time misconfigurations (duplicate source, cycle, self-map) that must throw naming the offender (.../Services/EventUpcasterRegistryTests.cs:19, ADR-090); its sample contracts live in the test assembly only so the framework's frozen contract snapshot never churns on them. ApplicationPipelineCompositionTests covers AddMmcaApplicationPipeline, the one call that replaces the manual AddApplication → scan → AddApplicationDecorators sequence, and the registration-time seal that turns a handler registered after the decorators (which Scrutor would silently leave undecorated) into a startup failure (.../ApplicationPipelineCompositionTests.cs:20, ADR-014). PackageGraphPurityTests reads the csproj itself, because the IL-based fitness rules can only see what the code uses and are blind to what a PackageReference drags in: one web or persistence package would enter the graph of every consumer of this host-agnostic package and nothing else in the build would notice (.../PackageGraphPurityTests.cs:15, §32). CqrsContractInspectorTests covers the inspector behind the fitness rule that a command marker is never handled as a query and that a request's declared result type matches its handler's (.../UseCases/CqrsContractInspectorTests.cs:7), and ScopedIntegrationEventHandlerBaseTests pins the base that opens one DI scope per delivery (integration-event handlers are singletons and cannot hold a scoped service), disposes it on every path, logs a failure exactly once before letting it propagate so the delivery mechanism can redeliver, and passes cancellation through unlogged because host shutdown is not a delivery failure (.../DomainEvents/ScopedIntegrationEventHandlerBaseTests.cs:15). Its newest types are fixtures: Harness and RecordingSetter back MarkAllNotificationsReadHandlerTests (MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Notifications/MarkAllNotificationsReadHandlerTests.cs:141, :207), and ScanFailingAssembly backs ModuleLoaderTests (.../Modules/ModuleLoaderTests.cs:325). Three newer files: AuthenticationServicePasswordRehashTests pins the sign-in rehash, where a password that verifies against a hash the hasher reports as outdated is re-derived and stored, driven by a hasher mock that answers true for the 64-byte legacy shape and false for the 69-byte current one, the same split the shipped hasher makes (MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/AuthenticationServicePasswordRehashTests.cs:29, ADR-102); ChangePreferencesCommandValidatorBaseTests pins the shared preference-change validator, where a missing payload is one clear failure, a null field means leave unchanged, and culture and theme are checked against the same definitions the domain uses (.../Users/UseCases/ChangePreferences/ChangePreferencesCommandValidatorBaseTests.cs:18), beside its handler tests (.../Users/UseCases/ChangePreferences/ChangePreferencesHandlerBaseTests.cs:16); and UsAddressValidationRulesTests covers the US address rules, a USPS region code and a 5-digit or ZIP+4 ZIP code, with an empty field reporting only that it is required (.../Validation/UsAddressValidationRulesTests.cs:12). The framework's largest suite by breadth; fast unit tests with mocked infrastructure.
MMCA.Common.Infrastructure.Tests 583 The widest layer: EF repositories + Unit of Work, the multi-database resolver/registry (DataSourceResolver, EntityDataSourceRegistry, DbContextFactory) with its transaction coverage (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/DbContexts/DbContextFactoryTransactionTests.cs:37) and the cross-data-source degrade convention (.../Persistence/DataSources/CrossDataSourceDegradeConventionTests.cs:24), the outbox processor plus its wake signal (.../Persistence/Outbox/Processing/OutboxSignalTests.cs:13) and the consumer-side EfInboxStore idempotency ledger (.../Persistence/Inbox/EfInboxStoreTests.cs:27, ADR-021), caching, JWT issuance + JWKS + the login-attempt lockout service (.../Auth/LoginProtectionServiceTests.cs:14), column-level encryption (.../Persistence/Encryption/EncryptedStringConverterTests.cs:6), the filtered-unique-index soft-delete convention (.../Persistence/Conventions/SoftDeleteUniqueIndexConventionTests.cs:24), the SignalR push + live-channel plumbing, the message-bus implementations, the polyglot Cosmos-config portability suite (ADR-018), and the in-repo disaster-recovery database-restore drill (.../Resilience/DatabaseRestoreDrillTests.cs:18, a CI-gated RTO baseline, ADR-009 / §29). Three files are pure §12 performance guards and are the only place the emitted SQL or the tracker's work is inspected at all: QueryParameterizationTests asserts that the dynamic-LINQ filter and sort strategies send their values as SQL parameters rather than inlined literals, which is what decides whether SQL Server reuses a plan and whether EF's compiled-query cache hits (.../Persistence/Specifications/QueryParameterizationTests.cs:26); SaveChangeDetectionTests pins that a save runs change detection exactly once and that suppressing the extra passes lost the tracker no actual changes (.../Persistence/Interceptors/SaveChangeDetectionTests.cs:24); and PeriodicBackgroundServiceTests drives PeriodicBackgroundService deterministically through a FakeTimeProvider clock to cover the enablement gate, the startup delay, interval-driven cycles, and the failing-cycle-never-kills-the-loop contract (.../Scheduling/PeriodicBackgroundServiceTests.cs:15). A second cluster hardens the save path: DbContextFactorySaveIntegrityTests pins the post-loop assertion that turns silent data loss into a failure, since the save loop is bounded and in-process domain-event dispatch runs inside it, so a handler can leave tracked changes behind that the loop will never reach (.../Persistence/DbContexts/DbContextFactorySaveIntegrityTests.cs:29); DbContextFactoryCommitAmbiguityTests covers the case where a commit-phase failure has an unknowable outcome, because the database may have applied the transaction and lost only the acknowledgement (.../Persistence/DbContexts/DbContextFactoryCommitAmbiguityTests.cs:34); DomainEventCaptureExclusionTests proves event capture is scoped-exclusion aware (.../Persistence/Interceptors/DomainEventCaptureExclusionTests.cs:26); and EFRepositoryAuditStampTests pins that the repository's own save entry points stamp the acting user like the unit of work does, after they were found calling the plain EF overloads and attributing everything written through them to the system sentinel (.../Persistence/Repositories/EFRepositoryAuditStampTests.cs:32). One pair sits outside persistence, the IDistributedLock pair, where RedisDistributedLock must acquire through a single atomic SET NX PX carrying a TTL and release through a compare-and-delete on the owner token (.../Concurrency/RedisDistributedLockTests.cs:15), with InProcessDistributedLock held to the same contract as the no-Redis fallback (.../Concurrency/InProcessDistributedLockTests.cs:12). The enterprise wave contributes four clusters here. Tenancy: ApplicationDbContextTenantFilterTests proves over real SQLite that the named Tenant global filter composes with SoftDelete rather than replacing it, that a null-tenant system context still sees every row, and that one cached model serves two tenants at once with disjoint results (.../Persistence/Tenancy/ApplicationDbContextTenantFilterTests.cs:15); TenantSaveChangesInterceptorTests covers the write side, where an insert is stamped from the scope and any modify/delete that would cross the boundary throws (.../Persistence/Tenancy/TenantSaveChangesInterceptorTests.cs:12); TenantDataSourceTargetTests covers the (source, tenant) expansion the background sweeps run on, because a tenant with its own database has its own outbox, inbox and trail tables and nothing else opens that database (.../Persistence/Tenancy/TenantDataSourceTargetTests.cs:18); AddMultiTenancyTests pins the DI surface, the fail-closed defaults and the startup validation (.../Persistence/Tenancy/AddMultiTenancyTests.cs:19); and TenantContextTests pins the one-write scope (.../Context/TenantContextTests.cs:10). Audit trail: AuditTrailSaveChangesInterceptorTests asserts every case against a real SQLite round-trip, so what is asserted is what commits, one summary row per insert/delete, one row per changed property on a modify, nothing for an entity marked modified but unchanged, and a [Pii] property recording the redaction token and never the clear value (.../Persistence/AuditTrail/AuditTrailSaveChangesInterceptorTests.cs:18, ADR-075 / §30); AuditTrailReader covers newest-first paging with clamped arguments and an empty answer when the source has no trail table (.../Persistence/AuditTrail/AuditTrailReaderTests.cs:20); AuditTrailCleanupJobTests covers the retention purge and its two do-nothing paths (.../Persistence/AuditTrail/AuditTrailCleanupJobTests.cs:22). Scheduler: ScheduledJobRunner is driven over a FakeTimeProvider and an in-memory ScheduledJobs table so the schedule arithmetic is exact rather than timing-dependent, covering registration sync, the configuration override, the claim lease, outcome recording, and the missed-run policy where a clock that jumped past many occurrences runs once and advances rather than storming (.../Scheduling/ScheduledJobRunnerTests.cs:22, ADR-074); CronosNextOccurrenceTests pins the cron grammar, the strictly-after semantics and the UTC-only clock that keeps a schedule stable across both daylight-saving transitions (.../Scheduling/CronosNextOccurrenceTests.cs:11); AddScheduledJobsTests pins the opt-in DI shape (.../Scheduling/AddScheduledJobsTests.cs:16); SchedulerMetrics pins the meter name a host registers for export and the instrument names, units and tags an operator builds dashboards on (.../Scheduling/SchedulerMetricsTests.cs:13); and SchedulerModelGateTests builds the real model under each combination of Scheduler:Enabled and data source to prove a host that never opted in gets exactly the model it had before the scheduler shipped (.../Scheduling/SchedulerModelGateTests.cs:26). HybridCache: HybridCacheService runs against a real in-process AddHybridCache with a recording L2, and the key-shape assertions carry the whole design, since an entry written by the old cache at the same logical key must be a clean miss, never the WRONGTYPE fault that motivated the split (.../Caching/HybridCacheServiceTests.cs:24, ADR-077). The read contract (ADR-055) is covered against a real provider rather than a mock, because all of it is translation: EFReadRepositoryKeysetPagingTests covers keyset paging end to end, cursor round-trips, next-page detection, the non-unique sort key that makes the identifier tie-break load-bearing, and the two rejection paths of an unknown sort column and a malformed cursor (.../Persistence/Repositories/Read/EFReadRepositoryKeysetPagingTests.cs:16); EFReadRepositorySpecificationTests covers the specification-driven members plus tracking and soft-delete scope (.../Persistence/Repositories/Read/EFReadRepositorySpecificationTests.cs:16); EFReadRepositoryProjectedFilterTests pins the ignoreQueryFilters flag on GetProjectedAsync against the production named soft-delete filter, because without it any caller needing deleted rows (an admin restore screen, a GDPR export) had to abandon the projection (.../Persistence/Repositories/Read/EFReadRepositoryProjectedFilterTests.cs:16); SpecificationEvaluator is exercised over SQLite for the ordering chain bound back to its concrete key type by reflection, includes with the collection split-query switch, and Skip/Take (.../Persistence/Specifications/SpecificationEvaluatorTests.cs:15); and PushNotificationProjectionTranslationTests proves a real provider translates the projection into SQL, enum-to-string conversion included, a failure that would otherwise surface only at runtime (.../Persistence/PushNotificationProjectionTranslationTests.cs:15). Its newest work is the auth, outbox and messaging edges. Auth: PasswordResetTokenServiceTests verifies the token lifecycle single use, the wrong-token attempt cap, the per-email request throttle, the address normalization that keeps casing variants on one record, and the hash-at-rest guarantee (.../Auth/PasswordResetTokenServiceTests.cs:18, ADR-091); EFRefreshSessionStoreRotationTests asserts the rotation claim against a real SQLite database because the guarantee is the database's, not the change tracker's, since two requests presenting the same still-live token each load their own tracked copy with RevokedAt null and an in-memory check-then-act would let both mint a successor (.../Persistence/Auth/EFRefreshSessionStoreRotationTests.cs:27, ADR-050); RefreshSessionCleanupServiceTests covers the retention sweep, where a row is a candidate once it stopped being usable rather than once it was created (.../Persistence/Auth/RefreshSessionCleanupServiceTests.cs:33); and PasswordHasherSecurityTests pins the cryptographic parameters rather than the round trip, since a hasher that hashes and verifies with the same weak settings still round-trips: two known-answer tests recompute the digest independently, a negative one proves the iteration count participates in verification, and reflection pins the private constants so lowering one fails the build (.../Auth/PasswordHasherSecurityTests.cs:18, ADR-102). Outbox and migrations: OutboxAdministration is the operator path that gives a dead-lettered event a way back into delivery instead of leaving "wait for the retention sweep" as the only ending, exercised over a real SQLite outbox table so the set-based replay update is the one that would run in production (.../Persistence/Outbox/Administration/OutboxAdministrationTests.cs:27); OutboxProcessorOrderingTests covers ordered delivery, where rows sharing an OrderingKey must reach the bus one at a time in OccurredOn order and that guarantee must survive the two things a batch-local sort cannot cover, a successor arriving in a later batch and a second replica polling the same table (.../Persistence/Outbox/Processing/OutboxProcessorOrderingTests.cs:35); DependencyInjectionOutboxGateTests pins the registration gate, where an in-process host pays for no outbox table or poll loop, a broker host always gets one, and asking for a broker without the outbox fails at registration rather than silently dropping every cross-service event (.../DependencyInjectionOutboxGateTests.cs:15, ADR-003); MigrationApplyProofTests runs a committed migration from a separate fixture assembly against a real SQLite file through DbContextFactory, the same call the "Migrate" strategy makes, so "the framework applies migrations" is proved rather than assumed (.../Persistence/MigrationApplyProofTests.cs:35), with DbContextFactoryMigrationTargetTests proving which sources the factory acts on (.../Persistence/DbContexts/DbContextFactoryMigrationTargetTests.cs:25). Messaging: UpcastingIntegrationEventConsumer is the draining consumer bound to a retired contract, deduping on the original message id, upcasting to the terminal contract, dispatching to that contract's handlers, and recording the inbox row only after every handler succeeded (.../Messaging/Consumers/UpcastingIntegrationEventConsumerTests.cs:26, ADR-090); EventUpcasterStartupValidatorTests covers the startup check over the registered ladders (.../Messaging/Consumers/EventUpcasterStartupValidatorTests.cs:20); IntegrationEventConsumerHarnessTests drives the consumers through a real MassTransit bus on the in-memory transport, because the registration binding, the MessageId surviving serialization, and a handler exception genuinely becoming the Fault<TEvent> that FaultIntegrationEventConsumer subscribes to are broker behaviors no direct Consume call can show (.../Messaging/Consumers/IntegrationEventConsumerHarnessTests.cs:28, and the fault consumer's own file at .../Messaging/Consumers/FaultIntegrationEventConsumerTests.cs:15, ADR-087); and InboxDisabledWarningService covers the one startup line that keeps a disabled dedup store from looking exactly like an enabled one (.../Persistence/Inbox/InboxDisabledWarningServiceTests.cs:11). Two more round it out: SqlServerUniqueConstraintViolationDetectorTests covers both halves of the detector, the authoritative provider error numbers and the message fallback, building a real SqlException through the provider's own non-public factory and reporting itself skipped rather than failing the build if a provider upgrade moves that factory (.../Persistence/SqlServerUniqueConstraintViolationDetectorTests.cs:17); and ConnectionStringSettingsValidatorTests covers the one startup rule that spans two configuration sections, a host must be able to reach some database declared either top level or as a named DataSources entry, which is why it is a validator rather than a [Required] annotation (.../Settings/ConnectionStringSettingsValidatorTests.cs:15, ADR-070). The permission-grant snapshot matches role names case-insensitively on read and keeps an entry across the gap before the next rebuild while still expiring it when refreshes keep failing (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/Auth/PermissionGrantCacheTests.cs:18), beside its EF store (.../Persistence/Auth/EFPermissionGrantStoreTests.cs:29). Three files pin engine differences: the keyset seek predicate has to follow each engine's null placement (SQL Server and SQLite sort nulls first ascending, PostgreSQL the reverse), evaluated by SQLite because the defect is about three-valued SQL logic (.../Persistence/Repositories/KeysetQueryBuilderNullOrderingTests.cs:22); the notification configurations must emit PostgreSQL index filters when a PostgreSQL-only host substitutes its engine (.../Persistence/Configuration/NotificationConfigurationEngineTests.cs:23); and CosmosDbContextTests covers the Cosmos context (.../Persistence/DbContexts/CosmosDbContextTests.cs:12). Its newest files are privacy, startup and transport guards. AuthCacheKeyPrivacyTests pins that a user's email address never appears in a cache key the login-protection, password-reset or email-confirmation services read, write or remove, so it can never reach a Redis key name, a log line or an exception message, while two spellings of one address still land on one key (.../Auth/AuthCacheKeyPrivacyTests.cs:27); OutputCacheNamespaceTests pins the per-application namespace on the Redis output-cache store, so two applications sharing one Redis never serve or evict each other's entries (.../Caching/OutputCacheNamespaceTests.cs:16); CloudAdapterTripwireTests covers the startup tripwire that fails a host whose enabled NativePush section still resolves to the inert core sender, naming the adapter call (.../Configuration/CloudAdapterTripwireTests.cs:19); and StartupConfigurationSummaryService is held to one Information line per host start naming the version, engines, bus, cache and tenancy the host resolved, never a secret, plus a Warning for a RabbitMQ host with delayed redelivery off (.../Configuration/StartupConfigurationSummaryServiceTests.cs:20). The broker-transport extension point has three files: BrokerTransportRegistrationTests covers how AddBrokerMessaging resolves the transport at registration time (.../Messaging/Transports/BrokerTransportRegistrationTests.cs:17), BrokerTransportPoliciesTests pins the public receive policies every transport applies, so an adapter transport applies exactly what the built-in one does (.../Messaging/Transports/BrokerTransportPoliciesTests.cs:15), and PostgreSqlBrokerTransportTests covers the built-in PostgreSQL transport's selection, policies, shared data source and schema migration without opening a connection, leaving the wire round trip to MMCA.Common.Infrastructure.PostgreSQL.Tests (.../Messaging/Transports/PostgreSqlBrokerTransportTests.cs:24). NpgsqlDataSourceConfiguratorTests pins the optional INpgsqlDataSourceConfigurator extension point, where every context for one connection string shares a single configured NpgsqlDataSource (one pool, one password provider) built once and disposed with the container (.../Persistence/DataSources/NpgsqlDataSourceConfiguratorTests.cs:26). Mostly unit with EF-InMemory/SQLite boundaries (no real SQL Server here: that tier is MMCA.Common.Infrastructure.SQLServer.Tests).
MMCA.Common.Infrastructure.Redis.Tests 4 The one tier in the framework that runs the shipped caches against a real Redis. The unit tier mocks IDistributedCache, which means it asserts the calls the cache makes and never the storage format Redis ends up holding, and that is a blind spot with teeth: Redis keys are typed, INCR creates a string, and the IDistributedCache Redis provider stores every entry as a hash of absexp/sldexp/data, so mixing the two at one key round-trips flawlessly against a mock and answers WRONGTYPE in production, on the ADR-029 rate-limit and lockout counters. DistributedCacheServiceRedisTests starts a redis:7-alpine Testcontainer, builds DistributedCacheService exactly as AddCaching does when both a distributed cache and a multiplexer are registered, and proves the increment-then-read round-trip, that increments carry a TTL so a counter can never lock a subject out forever, that concurrent writers may undercount but must never leave the key unreadable (the honest statement of the current read-modify-write contract), prefix invalidation over a real SCAN, and a plain set/get/remove smoke (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Redis.Tests/DistributedCacheServiceRedisTests.cs:27). HybridCacheServiceRedisTests is its ADR-077 sibling and exists for the same reason at a higher stake: only a real server can distinguish "the two substrates share a keyspace" from "they do not", so it proves an entry written by either cache is a soft miss (never a fault) for the other, that prefix eviction runs both patterns and also drops the evicting process's own local copy, and that increments stay monotonic across two instances sharing one Redis (.../HybridCacheServiceRedisTests.cs:35). RedisFixedWindowRateLimiterRedisTests covers the distributed rate limiter the same way: the unit tier only asserts the Lua script text and what the limiter sends to a mocked database, so whether the script increments, sets a TTL on the creating increment, and returns a count the limiter can compare is proven here against the server (.../RedisFixedWindowRateLimiterRedisTests.cs:16). Needs Docker, so the project is outside the slnx and runs in the redis-integration CI job (MMCA.Common/.github/workflows/ci.yml:741). Integration style.
MMCA.Common.Infrastructure.SQLServer.Tests 8 The real-server tier for the shipped SQLServerDbContext and DbContextFactory. The unit tier mocks or substitutes the provider, so it cannot see three behaviours only a server has: SET IDENTITY_INSERT is session state that holds only on the connection that ran it, a rowversion is generated by the server on every write, and the outbox row has to reach the database in the same SaveChanges as the aggregate that raised the event (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.SQLServer.Tests/SQLServerPersistenceTests.cs:44). The other seven types are in-file fixtures (SqlThing and its configuration, RecordingDomainEventDispatcher, FixedCurrentUserService, FixedAssemblyProvider, NoTenantContext, .../SQLServerPersistenceTests.cs:223). It needs a Docker daemon, so it sits outside MMCA.Common.slnx and runs in its own sqlserver-integration CI job (MMCA.Common/.github/workflows/ci.yml:806). Integration style.
MMCA.Common.API.Tests 215 The presentation pipeline: ApiControllerBase.HandleFailure ErrorType-to-HTTP mapping, the exception-handler chain, the [Idempotent] filter + Idempotency-Key replay, permission policies/ownership filters, correlation, the JWKS and OIDC-discovery endpoints, the session-cookie auth handler/refresher/jar, the shared notification + device controllers, the public-endpoint output-cache policy, the database-initialization startup (the SQLite-EnsureCreated-under-Migrate path), and the error-message localization edge (localizes the human-readable message while leaving the machine Code/ProblemDetails title untouched, ADR-027 / §27). UserAccountAuthControllerBaseTests covers the shared account-management controller base that landed with the hoisted user use cases, driving change-password, preferences and delete through mocked handlers plus ICurrentUserService (MMCA.Common/Tests/Presentation/MMCA.Common.API.Tests/Controllers/Auth/UserAccountAuthControllerBaseTests.cs:17), and PasswordResetAuthControllerBaseTests is its recovery sibling, pinning the two endpoints and their attributes, because both actions are anonymous by necessity so losing the per-IP policy or the idempotency marker would break nothing visible and simply leave an unauthenticated endpoint unthrottled (.../Controllers/PasswordResetAuthControllerBaseTests.cs:23, ADR-091). The rate-limiting files are worth reading together (ADR-019, §11): RateLimitPartitionTests drives the global limiter's exemption and partition-key logic directly (infrastructure paths and gRPC content types bypass, anonymous traffic gets the no-limiter partition, an authenticated caller partitions by name then user_id then remote IP) and then the per-IP anti-spray policy, which deliberately fails open, since an unattributable request gets no limiter rather than sharing one bucket with every other unattributable caller (.../Startup/RateLimitPartitionTests.cs:17); AuthControllerBaseRateLimitTests asserts the attachment by reflection, because the policy is applied with an attribute and a dropped attribute breaks nothing loudly (the endpoint simply stops being throttled), pinning LoginAsync/RegisterAsync carrying the auth-IP policy and RefreshAsync deliberately not carrying it, since refresh is automatic and every Blazor Server circuit shares the UI host's IP (.../Controllers/AuthControllerBaseRateLimitTests.cs:21); RateLimitingSettingsTests guards the defaults and their binding, load-bearing because the permit-count overload of AddCommonRateLimiting delegates to them (.../RateLimiting/RateLimitingSettingsTests.cs:13); RedisFixedWindowRateLimiter covers the permit comparison, the one-shot TTL on the key that opens a window, and the fail-open posture on a Redis fault, since a limiter that failed closed would turn a cache outage into a site-wide 429 storm (.../RateLimiting/RedisFixedWindowRateLimiterTests.cs:16); and RateLimitAlgorithmSelectionTests resolves each partition's factory and inspects the limiter it actually builds, because the partition key alone never says whether a request is counted in memory or against the shared Redis counter (.../Startup/RateLimitAlgorithmSelectionTests.cs:21). Enterprise-wave files: TenantResolutionMiddlewareTests covers TenantResolutionMiddleware at the edge, the configured claim-then-header strategy order, the trimmed value, the fail-closed RequireTenant rejection as ProblemDetails, the excluded paths, and the two shapes that must pass every request straight through (.../Middleware/TenantResolutionMiddlewareTests.cs:17); the CSV export pair holds CsvWriter to RFC 4180 field by field (quote only what needs it, double an embedded quote, CRLF line endings, exactly one BOM, .../Export/CsvWriterTests.cs:8) while EntityControllerBaseExportTests drives the page-loop that exists because the query pipeline has no IAsyncEnumerable path, asserting the fan-in across pages, the short-first-page early stop, and the truncation marker appended exactly at the row cap including both page-boundary edges (.../Controllers/EntityControllerBaseExportTests.cs:25, ADR-078); DataExportControllerBaseTests pins the shipped DSAR endpoint, the dated download a subject receives, the ProblemDetails failure path, and the two attributes that are its whole security posture, asserted directly because nothing else fails when one is dropped (.../Controllers/Privacy/DataExportControllerBaseTests.cs:29, ADR-076 / §30). Conditional writes are covered in layers: SupportsIfMatchAttributeTests pins where the filter takes the token from and which status a conflict gets (.../Concurrency/SupportsIfMatchAttributeTests.cs:17) and EntityControllerBaseETagTests pins that a single read emits the token as a weak ETag so a client can turn straight around and send it as If-Match, while a DTO with no token gets no header (.../Controllers/EntityControllerBaseETagTests.cs:22, ADR-035). Next to them IdempotencyFilterPassthroughTests guards a premise rather than a behavior: a request without an Idempotency-Key header passes through untouched, which is what makes attaching [Idempotent] to an existing POST a non-breaking change for every client already calling it (.../Idempotency/IdempotencyFilterPassthroughTests.cs:22). OutputCacheEvictionHandler closes the cross-service cache loop: every tag on the message is evicted, a single tag's failure is swallowed and logged rather than rethrown (rethrowing would redeliver the message, re-evict the tags that already succeeded, and eventually dead-letter a message whose only consequence is a cache entry that expires on its own), and cancellation still propagates (.../Caching/OutputCacheEvictionHandlerTests.cs:20, ADR-026). Its newest block is contract and startup guards. OpenApiBaselineTests fetches the framework-owned OpenAPI document from a real in-memory host, normalizes it and diffs it against a committed openapi-baseline.v1.json, so an SDK or Asp.Versioning.OpenApi bump, a change to the framework's OpenAPI registration, or a change to the generated ProblemDetails schema fails here instead of reaching consumers unnoticed (.../OpenApi/OpenApiBaselineTests.cs:35, §9); ProblemDetailsRoundTripTests closes the loop between the two halves of the error contract by emitting through the real controller path with the serializer options MVC uses on the wire and reading the payload back with ProblemDetailsResultReader, which is what keeps the reader honest (.../Controllers/ProblemDetailsRoundTripTests.cs:22); MiddlewarePipelineBuilder covers the named-step edge pipeline, its default order, the insert/replace/remove operations a host customizes it with, and the invariants Build re-checks so a customized pipeline fails at startup rather than misrouting at runtime (.../Startup/MiddlewarePipelineBuilderTests.cs:12); ForwardedJwtBearerSecurityTests pins two executable security invariants, that the signature algorithm stays RS256 and that RequireHttpsMetadata resolves secure-by-default everywhere except Development, by executing the real registration and reading the resulting options back with nothing fetched from the authority (.../Startup/ForwardedJwtBearerSecurityTests.cs:22, §11), with the authority-resolution half in .../Startup/JwtAuthorityExtensionsTests.cs:13; EntityControllerBaseReadSpecificationTests pins the read-scoping hook, that every read action (both list overloads, lookup, by-id and export) asks GetReadSpecificationAsync for the rows this caller may see while a controller that overrides nothing queries exactly as unscoped as it always did (.../Controllers/EntityControllerBaseReadSpecificationTests.cs:29); and ModuleHostExtensionsTests covers the host-side module wiring (.../Startup/ModuleHostExtensionsTests.cs:22). CurrentUserTargetingContextAccessorTests rounds the row out on the feature-flag side, pinning which claim carries the user id (the Targeting filter hashes it, so a rollout is only sticky per user if that id is stable) and that an anonymous request produces an empty context rather than an error, because a feature filter must never be able to fail a request (.../FeatureManagement/CurrentUserTargetingContextAccessorTests.cs:15). The OpenAPI pair is pinned from the host side: AddCommonOpenApiHostRegistrationTests has this project play the host so the XML-comment source generator intercepts its own AddOpenApi() calls, and a host swapping its hand-written registration for the framework pair must get exactly the document plain AddOpenApi() produces (MMCA.Common/Tests/Presentation/MMCA.Common.API.Tests/OpenApi/AddCommonOpenApiHostRegistrationTests.cs:36), while MapCommonOpenApi() declares its document anonymous under the fallback authorization policy and still maps nothing in Production (.../OpenApi/MapCommonOpenApiAuthorizationTests.cs:23). DesignTimeDatabaseInitializationTests pins the guard that skips schema initialization during build-time OpenAPI generation, honoured only in Development (.../Startup/DesignTimeDatabaseInitializationTests.cs:23). The claims-only session-cookie mode set by the same-origin proxy hands script a claims-only token instead of the credential (.../SessionCookies/ClaimsOnlySessionCookieEndpointsTests.cs:24), and that token keeps every claim and the expiry but carries no signature (.../SessionCookies/SessionClaimsTokenTests.cs:15). ErrorHttpMappingTests is an exhaustiveness pin so a new ErrorType cannot silently fall back to 400 (.../Middleware/ErrorHttpMappingTests.cs:12); a cancellation the client did not cause falls through to the generic handler instead of being reported as a 499 disconnect (.../Middleware/OperationCanceledExceptionHandlerTests.cs:14); the CSV export decides end-of-data from the data, because a full clamped page can be shorter than the requested page size (.../Export/EntityCsvExporterTests.cs:13); and the two fail-closed ownership gates answer 403 for an unresolvable owner claim and 404 for a non-owner (.../Authorization/OwnershipHelperGateTests.cs:18). ConcurrencyConflictMetricsTests pins the persistence.concurrency.conflict counter: one increment, tagged with the entity type, for every optimistic-concurrency conflict the API answers and nothing for any other database update failure, with real EF Core exceptions raised against an in-memory SQLite database and measurements filtered by the file's own probe entity types because the meter is a process-wide static (MMCA.Common/Tests/Presentation/MMCA.Common.API.Tests/Concurrency/ConcurrencyConflictMetricsTests.cs:27). Unit tests of middleware/filters/controllers in isolation.
MMCA.Common.Grpc.Tests 28 The gRPC transport boundary: Result to RpcException round-tripping, the JWT-forwarding client interceptor, and the Polly resilience pipeline on typed clients (retry, circuit-breaker, and fault-injection). GrpcResultExceptionInterceptor is a good example of a test written around a fixed defect: the error-carrying case keeps the shared ToRpcException mapping, while the empty-errors case (what the message-only constructors produce) used to discard the exception message entirely and answer a placeholder "Unspecified failure", leaving the caller with a failure and no cause; it now keeps StatusCode.Internal and carries the real message, because synthesizing an Error.Failure instead would have downgraded a server-side fault to InvalidArgument and blamed the caller (MMCA.Common/Tests/Presentation/MMCA.Common.Grpc.Tests/GrpcResultExceptionInterceptorTests.cs:24). Its newest file covers the decode leg, the client-side turn of an RpcException back into a Result with its errors intact (.../ResultGrpcExtensionsDecoderTests.cs:14). GrpcWireFormatTests pins the wire conventions the hand-written adapters shared by copy: both the Z-marked and the marker-less timestamp forms must land as DateTimeKind.Utc on the same instant (MMCA.Common/Tests/Presentation/MMCA.Common.Grpc.Tests/GrpcWireFormatTests.cs:18). The Authentication/ folder covers the service token end to end: ServiceTokenAuthenticationTests pins the receiving side, a non-default ServiceToken JwtBearer scheme (HS256 only, svc claim required) plus an InternalCaller policy that accepts either the user token or a service token, with the load-bearing negative that a valid service token never satisfies a plain [Authorize] endpoint even when it shares the user scheme's key and issuer (.../Authentication/ServiceTokenAuthenticationTests.cs:33); sibling classes cover the token provider (.../Authentication/ServiceTokenProviderTests.cs:18), the settings (.../Authentication/ServiceTokenSettingsTests.cs:13), the default-scheme guard (.../Authentication/ServiceTokenDefaultSchemeGuardTests.cs:16), the client registration and HttpClient builder extensions (.../Authentication/ServiceTokenClientRegistrationTests.cs:17, .../Authentication/ServiceTokenHttpClientBuilderExtensionsTests.cs:22), and the client interceptor (.../ServiceTokenClientInterceptorTests.cs:24). Unit tests asserting ADR-007 / ADR-009 behavior.
MMCA.Common.Aspire.Tests 54 The service-defaults package: OutboxPollFilterProcessor (drops recurring outbox-poll spans from telemetry export), the SecurityHeadersMiddleware, the head-based trace-sampling cost knob (a ratio in (0,1) opts in, anything else samples everything so a typo cannot silently drop all telemetry, MMCA.Common/Tests/Hosting/MMCA.Common.Aspire.Tests/Telemetry/TracesSampleRatioTests.cs:11, §31), the metrics-instrumentation toggle (.../Telemetry/MetricsInstrumentationToggleTests.cs:16) and the Serilog host wiring (.../Logging/SerilogHostExtensionsTests.cs:19). It guards the one deliberate asymmetry in AddInfrastructureHealthChecks: a missing SQL connection string throws at startup when the host requires it, while absent Redis/RabbitMQ skip silently, and the optional checks carry HealthCheckTags.Optional so they never gate /health/ready (.../Health/InfrastructureHealthChecksTests.cs:16, §29). That optionality now has two files of its own after a production incident where readiness went hard-dependent on Redis: RedisPingHealthCheckTests covers the ping probe itself (.../Health/RedisPingHealthCheckTests.cs:16) and RedisReadinessSafetyTests pins that no Redis check can ever appear in the readiness set, because DistributedCacheService already degrades around a cache blip and an untagged check would instead take every replica out of rotation at once (.../Health/RedisReadinessSafetyTests.cs:24). A large half of the suite is §29 warm-up material: WarmupReadinessGate must stay closed until warm-up finishes, then latch open idempotently and safely under concurrency (.../Warmup/WarmupReadinessGateTests.cs:10), and the health-check face of that gate reports Unhealthy while the replica is still warming and Healthy the moment the gate opens, which is what actually keeps a cold replica out of the /health/ready rotation (.../Warmup/WarmupReadinessHealthCheckTests.cs:11); the warm-up hosted service must run every IWarmupTask once and open the gate even when a task fails or hangs (bounded by a per-task timeout), so a transient dependency outage cannot wedge a replica permanently out of rotation (.../Warmup/WarmupHostedServiceTests.cs:13, ADR-025); and SelfHttpWarmupTaskBase covers what the per-service copies each had to get right alone: port resolution under dynamic ports, the Testing short-circuit, the h2c version pin, and the non-fatal wrapper (.../Warmup/SelfHttpWarmupTaskBaseTests.cs:28). KestrelEndpointExtensionsTests pins deployment-shaped configuration with a technique worth borrowing, asserting on the plan a builder produced rather than on a running dependency: it reads the listener plan directly rather than through a bound server, because that plan decides whether a deployed revision answers its platform health probes at all (.../Kestrel/KestrelEndpointExtensionsTests.cs:14). The Azure-specific data-protection, Key Vault and Live Metrics tests live in MMCA.Common.Azure.Tests. Its gateway block covers the edge kit this package ships, the extension points ADC's Gateway host consumes rather than hand-writing (ADR-088): GatewayCorrelationMiddleware must leave a correlation ID on the request and the response, preserve a caller-supplied one end to end, and need nothing from DI, which is the whole reason it exists separately from the context-bound CorrelationIdMiddleware in MMCA.Common.API (.../Gateway/GatewayCorrelationMiddlewareTests.cs:15); the downstream health checks are pinned by name, tag and failure status, including that a repeated registration does not produce the duplicate check name the health-check service rejects at startup (.../Gateway/GatewayDownstreamHealthChecksTests.cs:17); the edge limiter covers its settings defaults, the bypass matcher, the fail-open posture on an unattributable client, and that registering twice does not throw (.../Gateway/GatewayRateLimitingTests.cs:21); and the CORS extension covers the allowed-origin policy a browser-facing edge needs (.../Gateway/GatewayCorsExtensionsTests.cs:19). ServiceDefaultsRetryTests pins the retry predicate every factory HttpClient gets, one retry for the idempotent verbs and none for POST and PATCH (MMCA.Common/Tests/Hosting/MMCA.Common.Aspire.Tests/Resilience/ServiceDefaultsRetryTests.cs:19). Its newest files are edge and probe guards: GatewayRetryAfterTests pins the Retry-After the edge limiter's 429 carries, written by an OnRejected that keeps any callback the host configured before it (.../Gateway/GatewayRetryAfterTests.cs:18); CachedHealthEndpointHeadersTests holds the cached probe endpoints to the three no-cache headers the stock middleware writes, asserted against fixed values and against the stock /alive endpoint from the same host (.../Health/CachedHealthEndpointHeadersTests.cs:13); PostgreSqlHealthCheckDataSourceTests pins that the PostgreSQL readiness check probes the container's configured data source when there is one, because a check built from the bare connection string would authenticate without the password provider and fail against a healthy database (.../Health/PostgreSqlHealthCheckDataSourceTests.cs:21); and HostIntegrationTripwireTests covers the startup tripwire that fails a host whose configuration asks for a vendor integration with no adapter call registered, naming the missing call (.../Integrations/HostIntegrationTripwireTests.cs:15). Unit suite over the Aspire service-defaults package.
MMCA.Common.Aspire.Hosting.Tests 6 The AppHost-tier package, tested without ever starting a distributed application: every case builds a DistributedApplicationBuilder, applies the extension, and asserts on the resource model it produced. H2cHealthCheckExtensionsTests covers WithH2cHealthCheck, the probe an AppHost needs because an Http2-only cleartext service cannot answer an ordinary HTTP/1.1 health request: the probe sends HTTP/2 with prior knowledge at the configured path, the check is keyed by endpoint so one resource can gate two, registration is idempotent per builder but its duplicate guard is not shared across builders, an undeclared endpoint name registers lazily rather than throwing, and unreachable/non-success/timeout all report Unhealthy with the reason (MMCA.Common/Tests/Hosting/MMCA.Common.Aspire.Hosting.Tests/H2cHealthCheckExtensionsTests.cs:17). ServiceBusEmulatorBrokerTests covers the local broker swap: the emulator image is pinned to a 2.x tag, both planes (AMQP and management) are published, host ports are left to Aspire, the emulator waits for the SQL Server it needs, the connection string and admin endpoint are derived from the allocated endpoints, and WithBroker selects the Azure Service Bus transport while the RabbitMQ overload stays unchanged (.../ServiceBusEmulatorBrokerTests.cs:16). E2eLiftTests covers the rate-limit lift the E2E stack asks for, both triggers (environment variable and call-site flag), the write-nothing default, and that the emitted keys are derived from the section-name constants owned by the settings types rather than spelled by hand, which is why the two settings packages are referenced from the test project and deliberately not from the AppHost package itself (.../E2eLiftTests.cs:24, and the reasoning is written into .../MMCA.Common.Aspire.Hosting.Tests.csproj). Unit style, no Docker.
MMCA.Common.Gateway.Tests 10 The reusable gateway package extracted so ADC's Gateway host stops hand-writing its edge. AddMmcaGatewayTests pins the registration surface (both config filters and the transform provider), settings binding from the MmcaGateway section, the documented defaults when no section exists, and, as a fitness-style assertion inside the unit suite, that the Gateway assembly does not depend on MMCA.Common.Aspire, keeping the two hosting packages independently consumable (MMCA.Common/Tests/Hosting/MMCA.Common.Gateway.Tests/AddMmcaGatewayTests.cs:16). The two config filters are where the package earns its keep, because they are what let a route table live in configuration (ADR-089): GatewayClusterProfileConfigFilterTests covers defaults applied to a cluster that declares nothing, per-cluster overrides merged property by property, a cluster's own config beating both, and an invalid HTTP version or version policy failing with the cluster named (.../Configuration/GatewayClusterProfileConfigFilterTests.cs:14); GatewayHealthCheckDefaultsConfigFilterTests covers the passive defaults filled into a cluster with no health-check block, active checks staying off unless the host turns them on, and every explicitly written block being left exactly as written (.../Configuration/GatewayHealthCheckDefaultsConfigFilterTests.cs:13). Both filters assert that routes are left untouched, since a filter that quietly rewrote a route would be invisible until traffic misrouted. GatewayRoutePolicyTests covers the named per-route limiters: partitioning on the caller address for a client-IP policy, one shared bucket for a global one, fail-open on an unresolvable IP, a 429 rejection, an out-of-range budget throwing at registration rather than at the first request, and a real window that exhausts and then rejects (.../RateLimiting/GatewayRoutePolicyTests.cs:17). GatewayTraceHeaderTransformProviderTests covers the route/cluster trace headers stamped on every proxied request, including that a client-supplied value is replaced rather than appended to (.../Transforms/GatewayTraceHeaderTransformProviderTests.cs:15), and ForwardedHeadersExtensionsTests covers the three forwarded headers, the cleared known-proxy allow lists a container platform needs, and that the options are fresh per call so one host's customization cannot leak into another (.../ForwardedHeadersExtensionsTests.cs:16). GatewayRetryAfterTests pins the Retry-After on a route-policy 429 and that the package's internal copy of the header logic (the package references no MMCA package) produces exactly what the Shared helper the other MMCA limiters use produces, so the two cannot drift, with HintlessLease as its in-file lease fixture (MMCA.Common/Tests/Hosting/MMCA.Common.Gateway.Tests/RateLimiting/GatewayRetryAfterTests.cs:18, :95). Unit style.
MMCA.Common.Testing.Tests 36 The suite that tests the test framework itself, so a regression in the shared scaffolding fails here rather than silently weakening every consumer suite. HandlerTestBaseTests drives HandlerTestBase<THandler> exactly as a consumer handler test would, registering repositories and relying on the pre-wired unit of work (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/HandlerTestBaseTests.cs:13), and DecoratorPipelineOrderTests runs DecoratorPipelineOrderTestsBase<...> against MMCA.Common's own registration sequence to prove the resolved pipelines nest in the ADR-014 order (.../DecoratorPipelineOrderTests.cs:21). Four files cover the helpers the cross-service tiers lean on, each one because the behavior would otherwise only be observable in a Docker-bound nightly: CrossServiceFixtureBase's container-free logic (connection-string composition and the first-write-wins environment snapshot, .../CrossServiceFixtureBaseTests.cs:13); JwtTokenGenerator.ConfigureInProcessTokenValidation, where what matters is that JWKS/OIDC discovery is switched off and the static committed key takes over, because a test host that still tries to discover fails at the first authenticated request with a network error rather than an auth error (.../JwtTokenGeneratorTests.cs:19); TestPolling.PollUntilAsync, which must stop at the first satisfying probe and on timeout return the last probed value so the caller's own assertion produces the failure message instead of a bare timeout (.../TestPollingTests.cs:11); and DependencyInjectionAssert.ReturnsSameCollection, which has to fail for a registration that hands back a different collection, the failure mode that silently drops everything chained after it (.../DependencyInjectionAssertTests.cs:12). Its newest files are the gateway scaffolding hoisted out of ADC: RecordingHttpForwarder, the fake that never proxies and echoes the destination, timeout, HTTP version and trace headers back so a consumer can assert its route table behaviorally (.../RecordingHttpForwarderTests.cs:16); MmcaGatewayHardeningTestsBase<TEntryPoint>, whose eight edge gates now live here once instead of per repo (.../MmcaGatewayHardeningTestsBaseTests.cs:20); and MiddlewarePipelineOrderTestsBase, applied to the framework's own pipeline through a body-less subclass (.../MiddlewarePipelineOrderTests.cs:10). The broker scaffolding, ServiceBusEmulatorFixtureBase, ships in MMCA.Common.Testing.Azure and is tested in MMCA.Common.Testing.Azure.Tests. Two small helper surfaces round it out, the rate-limiter test extensions that let a suite drive a limiter without wall-clock waits (.../RateLimiterTestExtensionsTests.cs:17) and the feature-management extensions that flip a flag inside a test host (.../FeatureManagementTestExtensionsTests.cs:16). Unit style.
MMCA.Common.UI.Tests 269 Shared Blazor components (delete-confirmation, empty-state, the mobile card/infinite-scroll lists, notification bell/inbox/list/send pages, primitives), the MudBlazor theme/provider harness, HTTP-resilience/service-exception helpers, list-page state/query-state services, the primitive markup snapshots, the auth-form view-model validation (§24), and the i18n globalization pair (the [!!...!!] bracket-sentinel pseudo-localizer and the ResxMudLocalizer MudBlazor-chrome boundary) plus the auth-aware nav menu and its mobile top-row. On i18n (ADR-027 / §27): CultureSwitcherTests guards that the switcher delegates to ICultureApplier instead of navigating to /culture/set itself, since that URL is a server endpoint and a hard-coded navigation left MAUI Blazor Hybrid heads (which host no ASP.NET pipeline) routing it through the Blazor router onto the not-found page (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Globalization/CultureSwitcherTests.cs:17); EndpointCultureApplier covers the web default, which must route through /culture/set with a force load because only that round trip writes the cookie SSR prerender and the WASM runtime both read (.../Globalization/EndpointCultureApplierTests.cs:15); and DocumentLanguageTests covers the component that makes a hybrid head's lang attribute follow a culture switch, asserted here precisely because no automated accessibility gate can catch a wrong lang (axe checks presence and syntax only, .../Components/DocumentLanguageTests.cs:13). On theming, ThemeToggleTests covers the app-bar Day/Dark switch including that disposal unsubscribes so the long-lived ThemeService never calls back into a dead component (.../Components/ThemeToggleTests.cs:16, ADR-028), and ApiUserPreferenceWriterTests pins the two guards that keep a signed-out or stale session from spending one 401 per theme/culture toggle, a write that is best effort and whose caller never learns it failed, making a doomed request pure cost (.../Services/Preferences/ApiUserPreferenceWriterTests.cs:16). On write safety, EntityServiceBaseIdempotencyRetryTests pins the Idempotency-Key EntityServiceBase<TEntityDTO, TIdentifierType> emits on creates and only on creates, the key staying identical across every retry of one logical operation, and the retry predicate's shape (5xx yes, 501 no, 429 yes) plus cancellation aborting the pipeline; the retried cases pay the real Polly backoff, so each is driven through the smallest attempt count that proves the behavior and asserts on captured attempts, never on wall-clock timing (.../Services/EntityServiceBaseIdempotencyRetryTests.cs:21). Its capability and JS boundary: LazyJsModule pins the single-flight import() the UI services delegate to, because an unguarded _module ??= await import(...) lets two concurrent callers each start an import so the browser holds two module instances and the later assignment leaks the earlier reference, while a failed import must not be cached and disposal must tolerate a disconnected circuit (.../Services/LazyJsModuleTests.cs:14); CapabilityFallbackTests sweeps every null/neutral capability default, each of which must degrade (report unsupported, no-op, or return empty) and never throw, because these are what shared components resolve on a head with no native or browser override (.../Services/Capabilities/CapabilityFallbackTests.cs:12, ADR-042); and QrCodeImageTests covers the managed QR render, a PNG data URI with required alt text, nothing at all for a blank payload, and a re-encode when the payload or error-correction level changes (.../Components/QrCodeImageTests.cs:11, ADR-071). Its newest work is the shared read/refresh plumbing and the account surface. LatestLoadGuard is the generation counter behind every "supersede an in-flight load" fix in both apps, pinned once here (.../Common/LatestLoadGuardTests.cs:11), and UiReadCache covers the opt-in client read cache pages share (.../Services/Caching/UiReadCacheTests.cs:15). ErrorSummaryTests covers the form-level error block that turns a failed Result into an accessible summary (.../Components/ErrorSummaryTests.cs:18, §24), ApiFileDownloadButtonTests the authenticated download path (.../Components/ApiFileDownloadButtonTests.cs:24), InfiniteScrollSentinelTests the intersection-observer sentinel behind the mobile lists (.../Components/InfiniteScrollSentinelTests.cs:14), HttpResultExecutorTests the single place a UI service turns an HTTP response into a Result (.../Services/HttpResultExecutorTests.cs:15), and AbsoluteUrlAttributeTests the validation attribute the share/QR surfaces depend on (.../Validation/AbsoluteUrlAttributeTests.cs:14). The multi-device session surface lands here as a page plus a parser: SessionsTests renders the sign-out-other-devices page in its loading, data, empty and error states (.../Pages/Auth/SessionsTests.cs:27) and UserAgentSummaryTests pins the user-agent-to-readable-device summary shown on each row, which is presentation over an attacker-controlled string and therefore must never throw on garbage (.../Services/Auth/UserAgentSummaryTests.cs:13, ADR-097). OfflineFirstPageSnapshotTests adds golden-markup coverage for the offline-first page shell (.../Pages/Common/OfflineFirstPageSnapshotTests.cs:14). The WebAssembly half of the same-origin API proxy sends no Authorization header and always X-CSRF: 1 once Api:SameOriginApiEndpoint is set (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Services/SameOriginProxyClientTests.cs:26); TokenRefreshPipelineTests drives the real APIClient pipeline so the refresh POST cannot re-enter its own in-flight hydrate (.../Services/Auth/TokenRefreshPipelineTests.cs:20); IdempotentReadRetryTests gives the anonymous lookup services the same transient-failure retry policy instance, GET only (.../Services/Api/IdempotentReadRetryTests.cs:16); PagedReadAllTests pins paging until the reported total (.../Services/Api/PagedReadAllTests.cs:14); NotificationPageGateTests hides the notification pages for an opted-in host that never registered the notification UI (.../Notifications/NotificationPageGateTests.cs:16); and two smaller pins cover the MAUI back-navigation module release (.../Services/Navigation/MauiBackNavigationBridgeTests.cs:12) and the cookie-sync write outcome (.../Services/Auth/JsFetchSessionCookieSyncTests.cs:13), with InMemoryHubServer as an in-process SignalR stand-in (.../Services/Notifications/InMemoryHubServer.cs:17). The newest block covers layout extension points and page lifetimes: MainLayoutFooterComponentTests pins the footer slot a module fills, rendered inside the one contentinfo footer rather than as an extra column in the desktop flex row (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Layout/MainLayoutFooterComponentTests.cs:25); PublicAppBarComponentTests covers the signed-out app-bar slot for a control such as site search or a cart, in the bar and in the mobile quick-actions row, while the signed-in slot keeps its gate (.../Layout/PublicAppBarComponentTests.cs:25); NotFoundContentTests pins that the first module supplying not-found content wins, a re-executed 403 still renders Access Denied, and with no module supplying one the page is unchanged (.../Pages/NotFoundContentTests.cs:20); AuthSubmitLifetimeTests pins that the sign-in and registration submits stop when the user navigates away or a render-mode transition tears the page down, instead of completing into a component that is gone (.../Pages/Auth/Lifetime/AuthSubmitLifetimeTests.cs:27); DetailLoadStateContentTests renders a record that does not exist and a record that could not be fetched differently, offering a retry only for the second (.../Components/PageState/DetailLoadStateContentTests.cs:13); StaleToastSweeperTests covers the sweep that clears toasts on screen longer than the grace period while keeping one raised in the same moment as the navigation (.../Components/Notifications/StaleToastSweeperTests.cs:15); and MmcaThemeProvidersColorModeTests pins the opt-in that stamps the color mode onto the document on the first interactive render and on every change, and never calls it when off (.../Theme/MmcaThemeProvidersColorModeTests.cs:16). Rendered with bUnit (component-render unit tests via BunitComponentTestBase).
MMCA.Common.UI.Web.Tests 38 The Blazor Server web-host pieces: ServerTokenStorageService (during SSR prerender tokens come from the HttpOnly session cookies; on the interactive circuit the access token is held in memory, hydrated single-flight, and refreshed proactively near expiry, while the refresh token is never readable, MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/Services/ServerTokenStorageServiceTests.cs), the server form-factor probe (.../Services/WebFormFactorTests.cs), and BlazorCspPolicyProvider, which pins the enforced production Content-Security-Policy verbatim (connect-src locked to the configured API/Gateway origin, no unsafe-eval, permissive Report-Only degradation on an unparseable endpoint, .../Security/BlazorCspPolicyProviderTests.cs, §26). Twelve of its types cover the same-origin API proxy, booted in-process against a fake gateway (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/SameOriginProxy/ProxyTestHarness.cs:148): the bearer is attached server-side from the HttpOnly cookie and a racing refresh runs once per session (.../SameOriginProxy/SameOriginApiProxyTokenTests.cs:15), every unsafe method needs X-CSRF: 1 (.../SameOriginApiProxyCsrfTests.cs:12), a foreign Origin or non-same-origin Sec-Fetch-Site is refused 403 (.../SameOriginApiProxyOriginTests.cs:14), an undecided refresh (5xx, 429) keeps the cookies and answers 503 with Retry-After (.../SameOriginApiProxyRefreshOutcomeTests.cs:14), sign-in, refresh and sign-out keep the token pair in the cookies (.../SameOriginApiProxyAuthFlowTests.cs:20), SignalR negotiate and WebSocket upgrades (HTTP/1.1 and RFC 8441 extended CONNECT) carry the cookie bearer (.../SameOriginApiProxyHubTests.cs:29), a host that does not opt in is unchanged (.../SameOriginApiProxyOptInTests.cs:21), and the Server circuit trades only protected handoffs (.../SessionHandoffServicesTests.cs:14). UiRateLimitingRetryAfterTests pins the Retry-After the Blazor host's limiter writes on a per-IP rejection through the OnRejected callback AddUiRateLimiting installs (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/Hardening/UiRateLimitingRetryAfterTests.cs:17), and ServiceWorkerEndpointTests covers MapCommonServiceWorker: a JavaScript content type that is never cached and carries the package version, the configuration kill switch that serves the inert worker instead, the anonymous, undocumented endpoint metadata the browser's update check depends on, and the navigations-only, never-cache-a-page rule pinned on the script text (.../Pwa/ServiceWorkerEndpointTests.cs:20). Unit and in-process integration tests.
MMCA.Common.UI.E2E.Tests 33 Playwright axe-core (WCAG 2.1 AA) + render-smoke over the backend-less Gallery host, driven from GalleryAxeTestBase: real Login/Register pages, the primitives/components showcase, and the shared Notification pages against stubbed collaborators, plus the dark-mode toggle, a Web-Vitals budget probe, and two i18n/mobile-parity gates, a qps-Ploc pseudo-locale round-trip asserting the [!! sentinel and no horizontal overflow under roughly 40% text expansion (ADR-027 / §27) and the culture+theme controls pinned into the mobile top-row below 1024px (§22). StickySidebarE2ETests illustrates why a rendered-behavior assertion beats a CSS assertion: it keeps the desktop sidebar pinned while the page scrolls, a behavior that broke because position: sticky resolves against the nearest ancestor scroll container and two innocuous rules (html, body { overflow-y: auto } and .page { overflow-x: hidden }, where a non-visible overflow-x forces the computed overflow-y from visible to auto) turned content-sized ancestors that never scroll into dead scrollports; the test asserts the pinning, so it catches any future ancestor that reintroduces one (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Layout/StickySidebarE2ETests.cs:23). Its newest pages are the password-recovery pair (.../ForgotPasswordPageE2ETests.cs, .../ResetPasswordPageE2ETests.cs:9, ADR-091), the sessions page (.../SessionsPageE2ETests.cs:13), a grid page added to the Gallery so the shared data grid is scanned like every other primitive (.../GridPageE2ETests.cs:16), and AuthOutcomeRulesTests, which pins the outcome vocabulary the auth flows assert on so the browser tests and the components cannot drift apart (.../AuthOutcomeRulesTests.cs:12). ServiceWorkerE2ETests covers the installable-web-app worker in a real browser: pwa-register.js registers it, and a navigation that cannot reach the network renders the framework offline page instead of the browser's error page, with the Gallery mapping the worker exactly as a consumer host does (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Pwa/ServiceWorkerE2ETests.cs:15). Deliberately outside MMCA.Common.slnx; runs in CI's ui-e2e job across chromium, firefox and webkit. E2E/accessibility style. [Rubric §21, Accessibility] (assesses automated a11y gating): this is where the framework proves zero axe violations before downstream apps consume the pages.
MMCA.Common.Benchmarks 6 A BenchmarkDotNet performance-smoke executable covering the two DB-free hot paths. SpecificationBenchmarks measures the per-instance compiled-expression cache behind Specification<TEntity, TIdentifierType>.IsSatisfiedBy (a cached-compile baseline versus the recompile-each-call anti-pattern) and the And/Or composition cost (MMCA.Common/Tests/Performance/MMCA.Common.Benchmarks/SpecificationBenchmarks.cs:14); QueryPipelineBenchmarks adds the read side, which runs on every list request in every consumer and regresses silently because the dynamic-LINQ predicate is re-parsed per call and the shaper reflects over the DTO: a single CONTAINS filter, a three-strategy mixed filter, dynamic sorting, and full-field versus sparse-fields= shaping of a 100-row page (.../QueryPipelineBenchmarks.cs:17). Deliberately outside MMCA.Common.slnx (like the Gallery), but not on-demand-only: CI's performance-smoke job (MMCA.Common/.github/workflows/ci.yml:332) runs the suite and then build/perfgate compares the results against the committed Tests/Performance/perf-baseline.json, failing on any violation of its allocation ceilings or machine-independent ratio floors, and on a rule naming a benchmark that produced no measurement, so the gate cannot pass vacuously (.../ci.yml:371). Moving a number deliberately means updating the baseline in the same PR. [Rubric §12, Performance & Scalability] (assesses measured, not assumed, hot-path cost): this is the evidence harness, and the baseline file turns it from a runs-clean smoke into a regression gate. Performance-smoke style.
MMCA.Common.LoadTests 14 Load tests over the framework's real persistence stack on one temp-file SQLite database, wired the way a host wires it (AddApplication() plus AddInfrastructure, a file database because each scope opens its own connection, MMCA.Common/Tests/Performance/MMCA.Common.LoadTests/Support/LoadStack.cs:25). PagedQueryLoadTests drives EntityQueryService.GetAllAsync against 100,000 seeded rows and asserts exact rows rather than a count (.../PagedQueryLoadTests.cs:13, seeded by .../Support/PagedQueryFixture.cs:16); OutboxThroughputLoadTests drains 10,000 events through the real OutboxProcessor to a counting transport, each delivered exactly once, above a rate floor (.../OutboxThroughputLoadTests.cs:20). LoadResults writes one JSON summary per scenario (.../Support/LoadResults.cs:11). Runs weekly from its own workflow (MMCA.Common/.github/workflows/load-tests.yml:16).

MMCA.ADC, Conference module (the largest application module)

Test project (assembly) Types What it covers · style
MMCA.ADC.Conference.Shared.Tests 9 Conference DTOs, requests, enums, and DTO/request mappers (the manual-mapping/Mapperly boundary, ADR-001). The DTO tests are deliberately shallow and cheap, pinning the required-value constructor, record equality, and the empty-collection defaults that keep a consumer from null-checking every navigation (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Shared.Tests/Events/EventDTOTests.cs:6). Pure unit tests.
MMCA.ADC.Conference.Domain.Tests 36 The Conference aggregates (Event, Session, Speaker, Room, Category, Question/Answer, Sponsor and now Activity): factory-method Result<T> outcomes, invariants, state transitions, and emitted domain events. SponsorTests is the model for the newer files: a database-generated id stays default after Create, exactly one SponsorChanged added-state event is raised, each length/blank invariant fails on its own, a booth number without the exhibitor flag is accepted and kept rather than silently dropped, and the tier values are pinned in package display order because the public roster groups by tier and reordering the enum would silently reorder the page (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Domain.Tests/Sponsors/SponsorTests.cs:11). The Activity aggregate (the non-session agenda items on the public schedule) follows the same shape, split across a behavior file and a dedicated invariants file (.../Activities/ActivityTests.cs:10, .../Invariants/ActivityInvariantsTests.cs:6), with a builder in .../Builders/ActivityBuilder.cs so the Application suite constructs one the same way. EventCascadeDeletionDomainServiceTests is the one to read for ordering: the cascade must soft-delete sessions, rooms, sponsors and activities before the event, must stop at the first child failure, and must leave the event undeleted when it does, so a partially cascaded delete is never committed (.../Services/EventCascadeDeletionDomainServiceTests.cs:9). Unit tests.
MMCA.ADC.Conference.Application.Tests 200 The command/query handlers for the Conference controllers, validators, navigation populators, the Sessionize import orchestrator + sync strategies (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Application.Tests/Events/UseCases/RefreshFromSessionize/RefreshFromSessionizeHandlerTests.cs:17), and the event/session live-window validation served to the live layer over gRPC (.../Events/EventLiveValidationServiceTests.cs:17, alongside GetPublicSessionFilterHandler and its cross-source filter query, ADR-018). It carries the durable AI-scoring pass (ADR-114) in three files. ScoreEventSessionsInternalCommandHandlerTests pins the handler's contract with the framework's at-least-once processor: it takes a per-event claim and skips rather than waits when another replica holds it (each pass is one paid Anthropic call per session), completes the row on a business refusal because replaying it would only pay for the same refusal again, lets a thrown fault propagate so the framework's backoff and attempt ceiling own the retry, and releases the claim on the way out (.../Sessions/UseCases/DecisionSupport/ScoreEventSessionsInternalCommandHandlerTests.cs:17, ScoreEventSessionsInternalCommandHandler). SessionScoringRunnerTests covers the pass itself, including the resume rule that skips every session scored since the command's request instant, which is what makes a queued duplicate or a retried attempt cheap (.../Sessions/UseCases/DecisionSupport/SessionScoringRunnerTests.cs:491, SessionScoringRunner). ScoreEventSessionsInternalCommandMarkerTests pins the opt-in pipeline markers on ScoreEventSessionsInternalCommand: the scoring feature flag, the session-selection capability, and a 12-minute execution budget that must stay below the handler's 15-minute claim time-to-live so the claim cannot expire mid-pass (.../Sessions/UseCases/DecisionSupport/ScoreEventSessionsInternalCommandMarkerTests.cs:51, MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/ScoreEventSessions/ScoreEventSessionsInternalCommandHandler.cs:63). It also carries the sessions-by-speaker filter handler, whose specification resolves the SessionSpeaker join down to an engine-portable ID-list criteria so the speaker pages and speaker dashboard filter server-side instead of pulling the whole catalog (.../Sessions/UseCases/GetSessionsBySpeakerFilter/GetSessionsBySpeakerFilterHandlerTests.cs:16, GetSessionsBySpeakerFilterHandler, §12). A family of public filter handlers (session, session-speaker, session-category-item, event-speaker, speaker, speaker-category-item, room, sponsor, and now activity) all defend the same anonymous-endpoint leak: a junction filter must select only rows whose parent is publicly visible, so a hidden session never leaks its existence through the speakers assigned to it (.../Sessions/UseCases/GetPublicSessionSpeakerFilter/GetPublicSessionSpeakerFilterHandlerTests.cs:18, and its activity twin at .../Activities/UseCases/GetPublicActivityFilter/GetPublicActivityFilterHandlerTests.cs:18, BR-49, §11). ExportEventCalendarHandler covers the whole-schedule .ics endpoint, which is anonymous, so unpublished and unknown events must collapse to the same NotFound (no existence oracle) and a legacy row carrying an unresolvable time zone must degrade rather than fault the request (.../Sessions/UseCases/ExportCalendar/ExportEventCalendarHandlerTests.cs:16). The sponsor and activity use cases are deliberately plain: create/update handlers assert the mapper-failure path, the repository add and the save, and nothing more, because the interesting behavior lives in the aggregate and in the public filter (.../Sponsors/UseCases/CreateSponsorHandlerTests.cs:14, .../Activities/UseCases/CreateActivityHandlerTests.cs:13). Its newest files are convention guards rather than behavior: SessionRoomFilterTests pins the public schedule's Room filter at the Application layer, since Session.RoomId is a real nullable column riding the generic filter pipeline with no dedicated handler, so the feature would break silently if the property were renamed or retyped (an unknown filter key is a validation failure and a mistyped one is ignored at apply time, .../Sessions/SessionRoomFilterTests.cs:15); ConferenceCrudRegistrationTests sweeps the module's CRUD registrations so a new aggregate cannot ship half-wired (.../ConferenceCrudRegistrationTests.cs:32); and BatchAddSessionQuestionAnswersHandlerTests covers the batched answer write behind the organizer feedback screens (.../Sessions/UseCases/BatchAddSessionQuestionAnswers/BatchAddSessionQuestionAnswersHandlerTests.cs:24). Its newest files pin the BR-71 category delete cascade, which only reaches loaded items, so the mock returns the category only when the handler declares the include (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Application.Tests/Categories/UseCases/DeleteConferenceCategoryHandlerTests.cs:17); the session-asset update's ownership re-check through the real access service (.../SessionAssets/UseCases/UpdateSessionAssetHandlerTests.cs:21); the erasure of a deleted account's feedback answers (.../Users/IntegrationEventHandlers/UserDeletedFeedbackHandlerTests.cs:16); and a speaker create that mints a fresh Guid rather than persisting Guid.Empty (.../Speakers/UseCases/Create/SpeakerCreateRequestMapperTests.cs:7). Its newest types are the in-file TestSessionUrlModel and TestSessionUrlValidator fixtures behind the session URL validation-rule tests (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Application.Tests/Sessions/Validation/SessionValidationRulesTests.cs:63, :65). The biggest application suite in ADC; fast unit tests with mocked repositories/services.
MMCA.ADC.Conference.Infrastructure.Tests 12 Conference-specific EF configurations, the module DB seeder, the Sessionize HTTP client, and SessionScoringService, the session-scoring service that now drives the framework's governed IChatClient (MMCA.Common.AI) rather than an Anthropic-specific handler, so its request/response shape and its cost counters are asserted through that shared pipeline (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/SessionScoringServiceTests.cs:13). The cross-replica claim and the retry policy around a scoring pass are not tested here: they live on the durable internal command in the Application layer (covered in MMCA.ADC.Conference.Application.Tests, above). What this project adds on the scoring side is the cache eviction the pass performs, pinned to the sessions tag only, because evicting the root conference tag instead emptied events, speakers, rooms, categories and questions onto the Basic-tier database while attendees were browsing (.../Services/OutputCacheSessionScoresCacheEvictorTests.cs:12, OutputCacheSessionScoresCacheEvictor). The scoring evaluation material is deliberately not here: the golden-replay, live-judge and prompt-contract tests sit in their own MMCA.ADC.Conference.Scoring.Evaluation.Tests project (the next row) so the paid, key-dependent tier can be gated on its own, which leaves this project the fast unit coverage of the clients, the scoring response guardrail and the cache evictor (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/SessionizeServiceTests.cs, .../Services/SessionScoreResponseGuardrailTests.cs, .../Services/OutputCacheSessionScoresCacheEvictorTests.cs) plus the module's EF configuration and seeder (.../Persistence/ConferenceEntityConfigurationTests.cs, .../Seeding/ConferenceModuleDbSeederTests.cs), whose in-file SampleDataHarness is the newest type (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Seeding/ConferenceModuleDbSeederTests.cs:191). Unit suite over faked HTTP handlers.
MMCA.ADC.Conference.API.Tests 28 Conference REST controllers (events, sessions, speakers, rooms, categories, questions/answers, session selection, sponsors, activities), the module's permission grants, and the Conference error-resource localization completeness check (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.API.Tests/Localization/ConferenceErrorResourcesTests.cs:15, §27). The controller tests follow one shape, visible in SponsorsControllerTests and repeated by the new ActivitiesControllerTests: success and each failure ErrorType map to their status code, and a successful mutation evicts the entity and conference cache tags while a failed one evicts nothing (.../Controllers/SponsorsControllerTests.cs:27, .../Controllers/ActivitiesControllerTests.cs:27). EntityExportAuthorizationTests is the cross-controller guard that came with ADR-078: an action inherited from EntityControllerBase<TEntity, TEntityDTO, TIdentifierType> carries no knowledge of the derived controller's read scoping, and every Conference read filter is applied in a method body rather than as an attribute, so the test pins that every scoped controller declares its own ExportAsync and that export on the bare-[Authorize] controllers carries its capability (.../Controllers/EntityExportAuthorizationTests.cs:18, §11). ConditionalWriteConventionTests pins wiring rather than behavior: the conditional-write behavior lives in the framework filter (covered in MMCA.Common.API.Tests), so what is worth pinning here is that the event lifecycle transitions keep [SupportsIfMatch] and keep a concurrency-aware request body, since an endpoint that quietly lost either would answer 200 to a caller who explicitly asked to write only against the version they read, with nothing failing to say so (.../Controllers/ConditionalWriteConventionTests.cs:14, ADR-035). Unit tests of the API layer.
MMCA.ADC.Conference.UI.Tests 129 Conference Blazor pages and components: the public event/session/speaker/sponsor/activity detail + filtered list pages, the management CRUD forms and management-route authorization, the organizer feedback dashboards, the speaker dashboard, the session-selection dashboard with its AI-score and speaker-overlap views (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Sessions/Selection/SessionSelectionAiScoresTests.cs:15), the home countdown (.../Pages/Home/ADCHomeTests.cs:20), and the share/QR/add-to-calendar buttons. One cluster is all stale-state guards, the failure class bUnit is uniquely good at catching: SessionDetailRoomCacheTests pins that navigating to a session in a different event refetches the per-event room lookup, since otherwise the page renders the previous event's room names and offers its rooms in the edit picker, while navigating within the same event must reuse the cache (.../Pages/Session/SessionDetailRoomCacheTests.cs:20); SessionSelectionStaleResponseTests covers M48, where the organizer dashboard fires a load per event selection and keeps a fire-and-forget score-polling loop running, and both used to write whatever came back into shared state without checking whether the selection had moved on, so a slow response for the previous event could overwrite the board or paint an error banner over it (a generation counter now supersedes in-flight work on every selection, .../Pages/SessionSelection/SessionSelectionStaleResponseTests.cs:23). The sponsor, activity and speaker-QR pages are the newest: PublicSponsorListTests pins the roster scoping to the current-or-next event, grouping by tier in package order with a sort-then-name order inside a tier, the booth badge for exhibitors, and the sponsorship-packet call to action that must appear when the roster is empty and disappear entirely when no packet URL is configured (.../Pages/Public/PublicSponsorListTests.cs:18); PublicActivityListTests does the same for the activity strip on the public schedule (.../Pages/Public/PublicActivityListTests.cs:16); and SpeakerQrTests pins that with the speaker_id claim the code encodes the absolute public profile URL (an app-relative route is useless to another device's camera) and that without the claim the page says so rather than rendering a code that points nowhere (.../Pages/Speaker/SpeakerQrTests.cs:17, ADR-071). Its newest technique is golden-markup: ComponentsSnapshotTests renders each reused Conference component from parameters alone and diffs its normalized markup against a committed baseline under Snapshots/, normalizing the per-render GUIDs MudBlazor injects so the comparison stays deterministic and OS-independent, and refreshed with UPDATE_SNAPSHOTS=1 after an intentional change (.../Components/ComponentsSnapshotTests.cs:27, §28). Alongside it, EventFilteredListPagePrerenderTests pins the prerender pass of the shared filtered-list page base (.../Pages/Common/EventFilteredListPagePrerenderTests.cs:20) and ClientUrlValidationTests pins the client-side URL validation the share and external-link surfaces depend on (.../Pages/ClientUrlValidationTests.cs:23). The newest files pin the shared privileged-reader check of the four public pages, where an auth fault degrades to the public audience but a cancellation propagates (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Public/PublicReadAudienceTests.cs:16); the My Schedule view's inline error and retry on a failed bookmark read (.../Pages/Public/Sessions/PublicSessionListMyScheduleTests.cs:21); that only the unfiltered programme overwrites the offline snapshot (.../Services/PublicSessionScheduleServiceTests.cs:15); the organizer session list's current-event default read from the injected TimeProvider at the local-midnight boundary (.../Pages/Sessions/SessionListCurrentEventClockTests.cs:23); and the client-side BR-122 schedule check (.../Pages/Sessions/SessionDetailScheduleTests.cs:25); a family of *DetailSavedRefreshFailedTests (activity, category, event, partner, question, sponsor) pins that a save the server committed counts as done even when the follow-up reload fails, leaving edit mode with a "saved, but not refreshed" warning and re-reading the row before the next edit so the next save posts the current row version (.../Pages/Activities/ActivityDetailSavedRefreshFailedTests.cs:20); and the public speaker list keeps one card per speaker id when offset paging repeats a row across two page fetches (.../Pages/Public/Speakers/PublicSpeakerListDuplicateAppendTests.cs:20), plus create/edit model and UI-service tests for rooms, sessions, speakers and session assets. Newer pins: ADCHomeTicketingPhaseTests shows the hero and workshop ticket buttons only while the featured event is still ahead, never while it loads, and drops them at the live flip without a reload, over a FakeTimeProvider pinned around a fixed 2030 edition (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Home/ADCHomeTests.cs:227); PublicSpeakerListSupersededLoadTests pins that two overlapping resets of the public speaker list end on the newest reset's rows and never paint "No speakers found." on the way there (.../Pages/Public/Speakers/PublicSpeakerListSupersededLoadTests.cs:22); and NoRecordsContentRecorder with RecordingComponent are fixtures in the session-list load-failure tests (.../Pages/Public/Sessions/PublicSessionListEventsLoadFailedTests.cs:354, :362). Rendered with bUnit (BunitTestBase over the shared BunitComponentTestBase). Component tests.
MMCA.ADC.Conference.IntegrationTests 47 Boots the Conference service host via WebApplicationFactory<Program> (gRPC peers faked, JWT re-pointed at an in-process test key) and drives real HTTP per role (Anonymous/Attendee/Speaker/Organizer), plus OpenAPI contract-snapshot, API-versioning, optimistic-concurrency, soft-delete + audit-stamp fidelity, idempotency replay, output-cache eviction, the includeChildren regression, and the in-process CrossServiceUserRegisteredTests (the Identity-to-Conference UserRegistered auto-link handler). SpeakerFeedbackAuthTests is a plain authorization matrix and a good example of what this tier is for: it covers GET /Speakers/{speakerId}/sessions/{sessionId}/feedback, a speaker's own read of free-text comments gated self-or-organizer, written as the regression for a finding where the endpoint was briefly [AllowAnonymous] with a public output cache, so any caller could read (and publicly cache) any speaker's feedback by URL manipulation (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Speaker/SpeakerFeedbackAuthTests.cs:15, §11). Its output-cache pair asserts eviction behaviorally, by the changed response body rather than by reading the cache store, and also pins that an authenticated read carries an Authorization header so the default policy skips the cache entirely (.../Reads/OutputCacheEvictionTests.cs:25). Integration style; needs a real SQL Server (ADC_TEST_SQL_BASE), runs in the integration-tests CI job.
MMCA.ADC.Conference.Scoring.Evaluation.Tests 10 The AI-evaluation suite for session scoring, in its own project so that the paid, key-dependent half can be gated apart from CI. A JSON corpus under Golden/ (seven recorded cases plus prompt-versions.json) feeds three files. GoldenReplayTests replays every case through the real SessionScoringService against a response recorded for that case, using a fresh replay client per case via the shared GoldenReplayTestsBase/ReplayChatClient harness, so no API key and no network are involved and it runs on every CI leg; it pins both directions at once, what goes out (the delimited envelope and the untrusted-input brief, including on the no-speakers case and the injection attempt) and what comes back (a recorded response still parses, still succeeds, and still produces the same weighted overall inside the band the case declares), so a change to prompt assembly or to the weighting math fails with the case that noticed it named (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/GoldenReplayTests.cs:35). LiveJudgeTests scores the same proposals through the real Anthropic API, which is what catches the failures a replay cannot (a model deprecation, a structured-output contract change, a prompt edit that reads fine but scores everything a point lower); it costs money and needs a key, so it is trait-gated (Category=AiEval.Live) and skips itself when ANTHROPIC_API_KEY is absent, and its bands are deliberately generous (roughly plus or minus 1.5 around the recorded value, clamped to the 1.0-10.0 range) because a judge model is not deterministic and a tight band would produce a flaky gate that gets ignored (.../LiveJudgeTests.cs:27, §31). PromptContractTests renders the system brief plus the user message for one canonical proposal fixed in the test file (not read from the corpus, so editing a case cannot move what the hash covers), hashes it, and compares against Golden/prompt-versions.json: a prompt edit without a version bump fails, and a version bump with no recorded hash fails too, which is what makes the PromptVersion persisted next to every score mean anything (.../PromptContractTests.cs:29). The corpus record and its nested input/speaker/expectation shapes are in GoldenCase.cs:18,60,76,92, and TestMeterFactory is a real IMeterFactory that exists only because the scoring service records its token counters through one (.../TestMeterFactory.cs:11). Unit (replay) plus a gated live-model evaluation tier.

MMCA.ADC, Engagement module (bookmarks, feedback, the conference-day live layer, and the badge/points wave)

Test project (assembly) Types What it covers · style
MMCA.ADC.Engagement.Shared.Tests 6 Bookmark/feedback/live DTOs, requests, and mappers, plus the contract types the ADR-072 wave added. BadgePayloadTests pins the QR payload both ways: the mmca-adc:badge:{credential} format, a case-insensitive parse that also accepts a bare or dash-less GUID and tolerates surrounding whitespace (a camera hands over whatever it read), and a clean false plus an empty credential for untrusted garbage (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Shared.Tests/CheckIns/BadgePayloadTests.cs:6). PointsSubjectKeysTests pins the ledger's subject keys, whose whole job is to not collide: event, session and sponsor keys built from the same numeric id must stay distinct, and each must be invariant-culture stable, because the key is a persisted uniqueness constraint and a culture-dependent number format would silently create a second awardable subject (.../Points/PointsSubjectKeysTests.cs:7). The settings and scope-name types round out the row. Unit.
MMCA.ADC.Engagement.Domain.Tests 12 The UserSessionBookmark, event/session feedback, and conference-day live-layer aggregates (LivePoll + SessionQuestion), joined by the badge/points aggregates: factory Result<T> outcomes, invariants, and domain events. CheckIn is the richest, because one aggregate carries three scopes and the scope decides which optional id is required and which is forbidden: a session scope needs a session id, an event scope must not carry one, a sponsor scope needs a sponsor id and no session, and exactly one AttendeeCheckedIn event is raised carrying (or deliberately not carrying) the sponsor payload (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Domain.Tests/CheckIns/CheckInTests.cs:8). AttendeeBadge pins the opaque credential: Create generates one, two calls for the same user never produce the same credential, Regenerate replaces it while preserving the owner, and no domain event is raised either way, since issuing a badge is not an engagement act (.../Badges/AttendeeBadgeTests.cs:6). LeaderboardOptIn pins the display-name snapshot taken at opt-in, the soft-delete leave, and the reactivate path that refreshes the name and raises the added state again, while a failed invariant leaves the entity untouched and raises nothing (.../Points/LeaderboardOptInTests.cs:8); PointsEntryInvariantsTests covers the ledger row's guards field by field (.../Points/PointsEntryInvariantsTests.cs:7). Unit.
MMCA.ADC.Engagement.Application.Tests 73 Bookmark, feedback, and live-layer (poll / session-question) add/remove/query handlers and validators, including the cross-module ISessionBookmarkValidationService / IBookmarkCountService / IEventLiveValidationService gRPC collaborators (stubbed), the poll-results builder (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Application.Tests/LivePolls/Services/LivePollResultsBuilderTests.cs:24), the read-side poll handlers added with the presenter surface (.../LivePolls/UseCases/GetPollResultsHandlerTests.cs:20), the question-upvote domain-event handler (.../SessionQuestions/DomainEventHandlers/SessionQuestionUpvoteChangedHandlerTests.cs:25), and the best-effort ILiveChannelPublisher ingress together with the queue that decouples it from the request path (.../Live/LiveChannelPublishQueueTests.cs:10, LiveChannelPublishQueue). The ADR-072 wave contributes three load-bearing files. PointsAwarder is where anti-farming and redelivery idempotency turn out to be the same rule: an award already present writes nothing, a rule configured to 0 (or an undefined activity) succeeds and writes nothing, an entry keeps the value it was awarded when the configuration later changes, and a save that loses the unique-index race reports success, including when the duplicate wording is buried in an inner exception, while any other save failure propagates (.../Points/Services/PointsAwarderTests.cs:13). CheckInAttendeeHandler pins the scan path: a repeat scan reports the original check-in and writes nothing, a session-scoped scan is filed under the session's own owning event (not the client's event context), an unknown and a malformed credential return the same BadgeNotFound (no probing oracle), and an unpublished event or a missing caller is refused (.../CheckIns/UseCases/CheckInAttendeeHandlerTests.cs:17). GetLeaderboardHandler pins the board as opt-in only (points without an opt-in never appear, and leaving removes you), ordering by total with an ordinal name tie-break, truncation to the configured size where 0 or a negative publishes nothing rather than throwing, and the snapshotted name being what is published (.../Points/UseCases/GetLeaderboardHandlerTests.cs:12). UserDeletedPointsHandlerTests closes the privacy loop: on UserDeleted the entry comes off the board and the published name is erased even for an already-left opt-in, the read deliberately looks past the soft-delete filter and tracks, and a redelivery reaches the same state while writing once (.../Points/IntegrationEventHandlers/UserDeletedPointsHandlerTests.cs:14, §30). Its newest file is the cross-service cache edge: bookmark counts are owned here but served by Conference, in another process whose output cache this side cannot touch directly, so UserSessionBookmarkCacheEvictionHandlerTests pins the one thing that closes that distance, that every bookmark change publishes an eviction request carrying the tag Conference actually registered, and that a broker which is down still cannot fail a bookmark the attendee already saved (.../UserSessionBookmarks/DomainEventHandlers/UserSessionBookmarkCacheEvictionHandlerTests.cs:20, ADR-026). The poll delete's soft-delete cascade only reaches loaded options, so the mock returns the poll only when the handler declares the include (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Application.Tests/LivePolls/UseCases/DeleteLivePollHandlerTests.cs:21), and four UserDeleted...HandlerTests pin account erasure of badges, bookmarks, session questions and votes (.../UserSessionBookmarks/IntegrationEventHandlers/UserDeletedBookmarksHandlerTests.cs:14). Unit.
MMCA.ADC.Engagement.Infrastructure.Tests 8 Engagement EF configuration plus the live-channel publish processor that fans domain changes out to the SignalR hub (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Infrastructure.Tests/Live/LiveChannelPublishProcessorTests.cs:10). Unit.
MMCA.ADC.Engagement.API.Tests 12 The Bookmarks/Feedback/Live/CheckIns/Points REST controllers in isolation. CheckInsControllerTests shows the two rules that matter at this edge: GetMyBadgeAsync takes no parameter other than the cancellation token, so the identity can only come from the token and never from the request body (asserted by reflection, because adding a parameter would break nothing loudly), and a repeat scan is Ok, not Conflict, because the organizer scanning a badge twice is a normal event and an error status would train them to ignore it (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.API.Tests/Controllers/CheckInsControllerTests.cs:23). ConditionalWriteConventionTests does for Engagement what its Conference twin does for events, on the endpoints two people actually race in a live room: poll open/close and the three question moderation decisions, where an endpoint that quietly lost [SupportsIfMatch] would let a moderator's explicit precondition pass unenforced (.../Controllers/ConditionalWriteConventionTests.cs:13, ADR-035). Its newest file pins the owner-or-admin filter vocabulary, the exact policy and claim names the ownership filters are constructed with, because a typo there fails open rather than loud (.../Authorization/OwnerOrAdminFilterVocabularyTests.cs:23, §11). Unit.
MMCA.ADC.Engagement.UI.Tests 54 Engagement Blazor renders and their UI services: the bookmark UI, the session/event feedback pages (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Pages/Feedback/SessionFeedbackTests.cs:26), the conference-day live/presenter surfaces (Happening Now, live poll, session Q&A, the moderation panel), the live-channel join/reconnect path (.../Pages/LiveChannelJoinTests.cs:40), and the session-reminder planner/coordinator. LiveEventListenerResilienceTests covers H18, where an invisible layout component rendered on every authenticated page sits under a shell with no ErrorBoundary above it, so anything escaping its render lifecycle tears down the Blazor circuit (a full page reload) on whatever page the user happens to be on, and the shared resilience handler raises TimeoutRejectedException/BrokenCircuitException that the service layer's narrow HttpRequestException catch deliberately does not cover (.../Components/LiveEventListenerResilienceTests.cs:34, §29); NowNextService covers the anonymous now-next endpoint contract plus its not-found degradation. The badge/points surfaces are the newest. CheckInScanTests is the load-bearing one: on a head whose IBarcodeScannerService.IsSupported is false (web and Windows) the scan affordance must not render at all while the manual name/email search stays, because that search is the whole check-in surface there, and the scope toggle must default to Session (.../Pages/CheckIn/CheckInScanTests.cs:20, ADR-042 / ADR-072). MyBadgeTests and MyPointsTests pin the four-state render (loading, data, empty, error) for the attendee's own badge and ledger, including the leaderboard rank block and the participation switch reflecting the opt-in state on load (.../Pages/CheckIn/MyBadgeTests.cs:19, .../Pages/Points/MyPointsTests.cs:19), SponsorVisitTests pins that a repeat visit reports the earlier one rather than an error, with distinct states for an unpublished event, an unrecognized server answer, and a failed post (.../Pages/Sponsors/SponsorVisitTests.cs:19), and the organizer rollups plus the room-code page have their own files (.../Pages/CheckIn/OrganizerAttendanceTests.cs:17, .../Pages/Rooms/RoomCheckInTests.cs:18). CurrentEventNotificationScopeProvider covers the event:{EventId} key shape, the never-throw degradation to unscoped, and the five-minute cache that keeps the notification bell's 30-second poll from costing an events fetch every time (.../Services/CurrentEventNotificationScopeProviderTests.cs:15, §31). Its newest three are the check-in service the scan pages call (.../Services/CheckInServiceTests.cs:15), the app-action route map that turns a notification payload into a destination route (.../Services/AppActionRouteMapTests.cs:20), and a golden-markup snapshot suite over the live-room components, where the committed baselines under Snapshots/ cover the poll card, the moderation panel, the question panel and the attendee search panel (.../Components/ComponentsSnapshotTests.cs:35, §28). LiveEventListenerTests pins that the circuit-lifetime listener still joins the live channel when the event's live window opens after the shell first rendered, not only when the event is live at the first render (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Components/LiveEventListenerTests.cs:27), and PresenterLayoutTests pins that the projector layout, which replaces MainLayout entirely, renders the layout components the UI modules register itself, so a deep link opened while the presenter view is on screen is not left buffered and the app-lock re-arm still runs there; its marker components and LayoutComponentModule are in-file fixtures (.../Layouts/PresenterLayoutTests.cs:21, :46). Component (bUnit).
MMCA.ADC.Engagement.IntegrationTests 22 Boots the Engagement service host via WebApplicationFactory<Program> and exercises the bookmark/feedback/live workflows + authorization over real HTTP, with the badge/points tier alongside. CheckInScanRoundTripTests proves over a real database what the handler tests prove in memory: a second scan reports the repeat and leaves one row and one enqueued AttendeeCheckedIn, a stale event context is corrected to the session's own event, and MyBadge returns the same credential on every call (MMCA.ADC/Tests/Integration/MMCA.ADC.Engagement.IntegrationTests/CheckIns/CheckInScanRoundTripTests.cs:24). PointsAwardRoundTripTests is the one to read for the outbox idiom: the check-in and its event commit in one transaction, so each test then drains the outbox the way OutboxProcessor does, reading the persisted payload back out of dbo.OutboxMessages, deserializing it, and handing it to the registered handler, which keeps the assertions deterministic instead of racing a background service, and it pins that a redelivery still leaves exactly one entry and that the unique index rejects a second award for the same subject (.../Points/PointsAwardRoundTripTests.cs:41). CheckInAuthorizationTests states the authorization contract plainly: every attendee may fetch their own badge (identity from the token, never the request), while writing a check-in and reading the attendance rollup require engagement:checkin:manage, held only by Organizer and Admin, and anonymous callers get 401 everywhere (.../CheckIns/CheckInAuthorizationTests.cs:14, §11). Integration; real SQL Server, integration-tests CI job.

MMCA.ADC, Identity module (User aggregate + JWT/JWKS + external OAuth)

Test project (assembly) Types What it covers · style
MMCA.ADC.Identity.Shared.Tests 2 Two Shared-project contracts: IdentityPermissionGrantsTests pins the Identity module's effective role-to-permission map (ADR-020) with literal expected sets, so a change to how a grant is declared can never change what a role is granted (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Shared.Tests/Authorization/IdentityPermissionGrantsTests.cs:14), and DisabledAttendeeQueryServiceTests checks the disabled attendee query service returns an empty list without throwing (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Shared.Tests/Users/DisabledAttendeeQueryServiceTests.cs:6). The JWT audience guard tests now live in MMCA.Common.API.Tests (MMCA.Common/Tests/Presentation/MMCA.Common.API.Tests/Startup/Auth/JwtAudienceTests.cs:12). Unit.
MMCA.ADC.Identity.Domain.Tests 6 The User/UserRole aggregate factories, invariants, anonymization, and speaker-linking domain events. Unit.
MMCA.ADC.Identity.Application.Tests 36 Registration/login/profile/role/preferences handlers and validators, the external-OAuth (Google/GitHub) exchange, the SpeakerLinkedToUser/SpeakerUnlinkedFromUser integration-event handlers, and the attendee query service the Notification module reads recipients through. The data-subject export block (ADR-076 / §30, ADC still runs its own handler rather than the framework's endpoint): ExportUserDataHandlerTests pins the owner-or-organizer gate with a case-insensitive role claim, the aggregation of the Engagement and Notification sections when the peers answer, the per-section unavailable degradation when a peer is unreachable or throws so the rest of the package still travels, and UTC-kind timestamps out of DateTimeKind.Unspecified audit columns (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Application.Tests/Users/UseCases/ExportUserData/ExportUserDataHandlerTests.cs:16); EngagementUserDataExportSectionTests pins what the Engagement section actually carries, including bookmarks, submitted questions, points entries with the readable activity name, leaderboard participation, and check-ins that earned no points (a check-in is personal data whether or not it scored, .../Users/UseCases/EngagementUserDataExportSectionTests.cs:11); and ExportUserDataRegistrationTests pins that the handler is discovered through its base class interface by the module scan and that section registration order is preserved (.../Users/UseCases/ExportUserDataRegistrationTests.cs:16). Its newest files are the app-side halves of two framework waves: the password-recovery handlers over the shared bases (.../Users/UseCases/ForgotPasswordHandlerTests.cs:22, .../Users/UseCases/ResetPasswordHandlerTests.cs:19, ADR-091), an in-memory refresh-session store used as the test double for the multi-device surface (.../Support/InMemoryRefreshSessionStore.cs, ADR-097), and the avatar read/remove handlers (.../Users/UseCases/GetUserAvatarHandlerTests.cs:15, .../Users/UseCases/RemoveUserAvatarHandlerTests.cs:16, ADR-045). Unit.
MMCA.ADC.Identity.Infrastructure.Tests 12 Identity EF config/repository, RS256 token issuance, and the JWKS provider. Its newest file is RefreshSessionModelGateTests, the consumer-side proof that the framework's refresh-session table appears in this host's model only when the feature is enabled, so a host that never opted in keeps exactly the migrations it had (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Infrastructure.Tests/Persistence/RefreshSessionModelGateTests.cs:46, ADR-097). SkiaSharpImageProcessor, the avatar normalizer, has its own file: untrusted uploads come out as exact-size square JPEGs, oriented, center-cropped to cover and with all metadata stripped (EXIF GPS is PII), only JPEG, PNG, WebP and GIF are accepted, and anything else, or a header declaring an oversized frame, fails as a validation error rather than an exception (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Infrastructure.Tests/Services/Users/SkiaSharpImageProcessorTests.cs:14, ADR-045), with ImageFixtures and NonSeekableStream as its fixtures (.../Services/Users/ImageFixtures.cs:11, .../Services/Users/SkiaSharpImageProcessorTests.cs:382). Unit.
MMCA.ADC.Identity.API.Tests 11 The Auth REST controller, the Users controller, the JWKS endpoint, and identity middleware in isolation, plus the data-export controller that fronts ADC's own DSAR handler (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.API.Tests/Controllers/UsersDataExportControllerTests.cs:20, §30). Unit.
MMCA.ADC.Identity.UI.Tests 18 Identity Blazor pages (login/register/profile/user-management) rendered with bUnit, now with a golden-markup snapshot suite over the user-claims component (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Components/ComponentsSnapshotTests.cs:15, §28). Component.
MMCA.ADC.Identity.IntegrationTests 40 Boots the Identity service host via WebApplicationFactory<Program> and drives the full auth surface over real HTTP: registration, login and its anonymous edge cases, claims, profile, user preferences, soft-deleted-user handling, the external-OAuth challenge/exchange, GDPR user export against faked Engagement and Notification peers (MMCA.ADC/Tests/Integration/MMCA.ADC.Identity.IntegrationTests/Attendee/UserExportTests.cs:24), and JWKS discovery. It also carries the two contract guards (OpenAPI snapshot and the RFC 9457 Problem Details subclass over ProblemDetailsContractTestsBase<TFixture>, .../Contract/ProblemDetailsContractTests.cs:16, §9), the compliance pair that proves erasure works end to end and that PII never reaches the log pipeline (.../Compliance/ErasureAndPiiLoggingTests.cs:19, ADR-005 / §30), an outbox-fidelity guard asserting registration atomically enqueues UserRegistered into [dbo].[OutboxMessages] (.../Data/OutboxFidelityTests.cs:17), the in-process CrossServiceSpeakerLinkTests, and the newest addition, PasswordResetFlowTests, which walks forgot-then-reset end to end against a real database and a real token store so the anti-enumeration answers and the single-use token are proved where they actually ship (.../Auth/PasswordResetFlowTests.cs:18, ADR-091). Integration; real SQL Server, integration-tests CI job.

MMCA.ADC, Notification module (push + inbox on top of the framework's notification types)

Test project (assembly) Types What it covers · style
MMCA.ADC.Notification.API.Tests 1 NotificationModuleTests pins the module contract itself, and it is a five-member subclass of the shared ModuleConformanceTestsBase<TModule>: it declares only ExpectedName ("Notification"), ExpectedDependencies (["Identity"]), ExpectedRequiresDependencies (true), and an AssertDisabledStubs override that proves RegisterDisabledStubs keeps the cross-module IUserNotificationExportService resolvable as a singleton DisabledUserNotificationExportService when the module is switched off (MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.API.Tests/NotificationModuleTests.cs:8). The conformance assertions themselves live in the base. Unit.
MMCA.ADC.Notification.Application.Tests 4 The module's two application services plus its DI registration: AttendeeNotificationRecipientProvider resolving broadcast recipients through the Identity IAttendeeQueryService gRPC contract (MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.Application.Tests/AttendeeNotificationRecipientProviderTests.cs:7), UserNotificationExportService assembling a data-subject export from the user-notification and push-notification repositories over InMemoryQueryableExecutor on top of HandlerTestBase<THandler> (.../UserNotificationExportServiceTests.cs:12, §30), and AddModuleNotificationApplication proving both are registered against their interfaces (.../DependencyInjectionTests.cs:10). Unit.
MMCA.ADC.Notification.IntegrationTests 10 Boots the Notification service host via WebApplicationFactory<Program> (the Identity recipient-lookup gRPC client faked by FakeAttendeeQueryService) and exercises the push-notification REST endpoints + inbox (NotificationsController/InboxController from MMCA.Common.API, MMCA.ADC/Tests/Integration/MMCA.ADC.Notification.IntegrationTests/Notifications/NotificationControllerTests.cs:16), the two contract guards (an OpenAPI snapshot at .../Contract/OpenApiContractTests.cs:16 and the Problem Details subclass at .../Contract/ProblemDetailsContractTests.cs:15, §9), and the real-time SignalR NotificationHub: a live HubConnection asserts authenticated connect, anonymous rejection (the hub carries [Authorize]), and a POST-triggered broadcast reaching the connected recipient (.../Notifications/NotificationHubTests.cs:15). Integration; real SQL Server (ADC_TEST_SQL_BASE), integration-tests CI job.

MMCA.ADC, host, service-adapter, cross-service, and end-to-end suites

Test project (assembly) Types What it covers · style
MMCA.ADC.Gateway.Tests 11 Boots the real YARP Gateway host in-process and asserts its operational guarantees, most of them with no test body in this repo at all: the host fixture and the gate bodies come from MMCA.Common.Testing, so ADC supplies only the entry point and its own route-table facts. SecurityHeadersTests is a four-line subclass of SecurityHeadersTestsBase that hands it a client from the shared ProductionHostApplicationFactory<Program> class fixture (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/SecurityHeadersTests.cs:12); the base probes /alive, which always answers regardless of backend reachability, and pins X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, frame-ancestors 'none' and HSTS (§26). GracefulShutdownTests is a body-less subclass of GracefulShutdownTestsBase<TEntryPoint> closed over the Gateway's Program (.../GracefulShutdownTests.cs:9): the base boots the host, stops it under a bounded token, and asserts ApplicationStopping then ApplicationStopped fired, so a hosted service that refuses to drain fails here instead of silently wedging a rolling deploy (§29). RouteMapTests is the one to read. Program.cs no longer hand-maps forwarders: it loads the ReverseProxy configuration section and applies the MMCA.Common.Gateway config filters over it (ADR-089), so adding or repointing a route is an appsettings.json edit. A config-owned table can drift in two directions, so the suite has two gates over one pinned table (.../RouteMapTests.cs:118). Behavioral: every pinned route is driven through the real proxy pipeline with IHttpForwarder replaced by RecordingHttpForwarder, which echoes the destination prefix, the activity timeout, and the forwarded HTTP version and version policy into response headers, so a removed or repointed route, a dropped timeout or a lost h2c setting fails there (.../RouteMapTests.cs:159), and an unmapped path must 404 at the gateway rather than be swallowed by a catch-all (:370). Completeness: the loaded IProxyConfig is compared against the same table, routes (:192) and clusters (:214), so a route ADDED to configuration without a test fails too. The table today is 27 routes across 5 clusters: four identity, sixteen conference, five engagement, two notification. Five clusters rather than four precisely because a YARP cluster owns the request config, so the REST 100-second activity timeout (MMCA.Common's shared 90-second request budget plus a deliberate 10-second margin, so the forwarder outlives the backend's own budget and the client sees the backend's error rather than a gateway abort, .../RouteMapTests.cs:81), the SignalR hub's long timeout, and the h2c prior-knowledge version pin on the three Http2-only backends cannot share one; the REST budget is now declared once as MmcaGateway:ClusterRequestDefaults instead of copied into five HttpRequest blocks, and the test asserts the effective cluster picks it up (:250). Two newer facts live here too: /Auth/{**catch-all} carries a tighter named limiter (auth-tight) than the edge global one, because everything under /Auth is credential handling and a burst from one IP there is far more likely to be an attack than a user, and every other route is asserted to carry no named policy (:307, :69-72); and every proxied request is stamped with route and cluster trace headers (:327). GatewayHardeningTests is now a subclass of MmcaGatewayHardeningTestsBase<TEntryPoint> that supplies only ADC's numbers: the per-client-IP allowance of 120 (.../GatewayHardeningTests.cs:38), the tighter named policy at 30 (:80), a representative limited path, and the downstream service names, while the eight gates themselves (bypass list, correlation ID generated or echoed, partitioning by the forwarded client IP, one readiness check per downstream service tagged ready and never live) live once in the base (ADR-088). AppHostBicepParityTests is the newest and guards a boundary nothing else does: Aspire's WithReference(peer) injects services__{peer}__{endpoint}__0 environment variables and infra/main.bicep hand-writes the same variables onto each container app, with nothing connecting the two, so a new cross-service call boots locally, passes every test and then fails in production with an unresolvable service name (and a Bicep entry for a removed edge is dead configuration nobody notices); both files are read as text rather than executed, because the AppHost hangs when started headless and Bicep is not executable here at all (.../AppHostBicepParityTests.cs:24, §17). The Gateway is a pure reverse proxy (no DbContext or broker) so the boot needs no SQL. Integration style.
MMCA.ADC.Services.Tests 23 The only project that tests the service-host gRPC adapters directly rather than through a booted host. Its first subject is an easily-missed wire contract in the data-subject export named by ADC's private PRIVACY.md §7 (ADR-005 / §30): SQL Server returns DateTimeKind.Unspecified values, and the "O" format omits the Z marker for them, so a timestamp that is genuinely UTC crosses the wire looking like a local time. UserEngagementExportGrpcServiceTests and UserNotificationExportGrpcServiceTests assert the emit leg (every timestamp ends with Z and parses back to the same instant, MMCA.ADC/Tests/Services/MMCA.ADC.Services.Tests/Exports/UserEngagementExportGrpcServiceTests.cs:17); the two ...GrpcAdapterTests assert the parse leg, which has to survive a rolling deploy where new Identity code talks to a Notification replica still emitting the marker-less form, so both wire forms must land as DateTimeKind.Utc on identical ticks (.../Exports/UserNotificationExportServiceGrpcAdapterTests.cs:19). FakeServerCallContext is a minimal ServerCallContext that lets a server implementation be invoked directly: the export services read only the cancellation token, so nothing else has to behave (.../Support/FakeServerCallContext.cs:10). The rest round-trip each cross-module gRPC server through its client adapter, resolved via the public composition with the typed client mocked (GrpcCalls builds the call a mocked client returns, MMCA.ADC/Tests/Services/MMCA.ADC.Services.Tests/Helpers/GrpcCalls.cs:9): attendee ids keep their order (.../Attendees/AttendeesGrpcServiceTests.cs:23), bookmark counts and bookmark validation survive the wire (.../Bookmarks/BookmarkCountsGrpcServiceTests.cs:17, .../Bookmarks/SessionBookmarksGrpcServiceTests.cs:18), and the adapters turn an RpcException or unmappable wire data into a failure Result rather than an escaping exception (.../Bookmarks/SessionBookmarkValidationServiceGrpcAdapterTests.cs:23, .../Live/EventLiveValidationServiceGrpcAdapterTests.cs:24, .../Live/EventLiveValidationGrpcServiceTests.cs:23). The anonymous live-channel ingress refuses a malformed key or event name or an oversized payload with InvalidArgument before the SignalR publisher (.../Live/LiveChannelGrpcServiceTests.cs:27), its publisher adapter is best-effort and swallows a failed push (.../Live/LiveChannelPublisherGrpcAdapterTests.cs:16), and the Conference warm-up's literal paths are pinned to PagedReadAll.LookupPageUrl so a warmed OutputCache key is one the UI actually requests (.../Warmup/SelfHttpOutputCacheWarmupTaskTests.cs:15); the Identity warm-up pins each path to a GET template on UsersController, because a path routing does not match answers 404 before the authentication, authorization and controller pipeline is JIT-compiled and RequireSuccessStatusCode = false hides the miss (.../Warmup/IdentitySelfHttpWarmupTaskTests.cs:15). The Conference half of the data-subject export is covered on both sides of the wire: the gRPC service puts the user's authored feedback answers on the wire with UTC-marked timestamps (.../Exports/UserConferenceExportGrpcServiceTests.cs:15), and Identity's adapter lands both timestamp forms as DateTimeKind.Utc and lets a transport fault propagate so the export handler can mark the section unavailable (.../Exports/UserConferenceExportServiceGrpcAdapterTests.cs:18). In MMCA.ADC.CI.slnf (MMCA.ADC/MMCA.ADC.CI.slnf:62), so it runs on every PR with no DB. Unit style.
MMCA.ADC.CrossService.IntegrationTests 13 The real-broker + real-gRPC tier: boots the REST hosts in one process against a Testcontainers SQL Server and a Testcontainers RabbitMQ, so the genuine MassTransit outbox to broker to consumer round-trip (UserRegistered auto-link, SpeakerLinked/SpeakerUnlinked back-link) and the real Conference to Engagement bookmark-count gRPC read run end to end, over a sequential env-boot fixture (a subclass of the shared CrossServiceFixtureBase) and a smoke gate that fails first if the container/host wiring is wrong. Its newest test is the one that needs this tier most: TwoReplicaHubFanOutTests boots the Notification host twice and proves a notification published on one replica reaches a SignalR client connected to the other, which is precisely the guarantee a single-host integration test cannot observe (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/CrossService/TwoReplicaHubFanOutTests.cs:35, §7), supported by a Notification-specific factory and a faked attendee query service in .../Infrastructure/. Integration style; needs Docker, runs in the weekday-nightly cross-service job (MMCA.ADC/.github/workflows/cross-service-tests.yml:75, scheduled by the 0 6 * * 1-5 cron at .../cross-service-tests.yml:31, behind a should-run guard that skips a night with no new commits, .../cross-service-tests.yml:50), not in Integration.slnf. This is one of the two jobs whose recency the cross-service-freshness deploy gate keys off.
MMCA.ADC.ServiceBusEmulator.IntegrationTests 3 Broker-parity smoke (§33): production runs on Azure Service Bus while local development runs RabbitMQ, so Service-Bus-specific transport behavior is otherwise observable only in the deployed environment. This tier runs MassTransit v8 against the official Service Bus emulator container with ADC's real integration-event contracts and proves the two transport-specific behaviors: admin-plane topology provisioning (topic per message type, subscription, receive-endpoint queue) and the AMQP publish to topic to subscription to consume round-trip (MMCA.ADC/Tests/Integration/MMCA.ADC.ServiceBusEmulator.IntegrationTests/ServiceBusRoundTripSmokeTests.cs:26). The fixture is the whole lesson here. It was dispatch-only from 2026-07-24 after hanging and being killed at its timeout on 7 of 7 runs, and the cause was read at the time as the emulator's floating companion SQL image; it was not. The real cause was provisioning volume: the test class implemented IAsyncLifetime, xUnit re-instantiates a test class per [Fact], and every bus start re-provisions the whole topology through an admin plane throttled at roughly one operation per second. The bus now lives on a collection fixture and starts once, and both startup phases are wall-clock bounded so a future hang fails with a phase-named error instead of being killed at the job timeout (which discards the step log, and is why this went unlocalized for a week): ServiceBusEmulatorFixture is now a thin subclass of the shared ServiceBusEmulatorFixtureBase (.../Infrastructure/ServiceBusEmulatorFixture.cs:22) with its [CollectionDefinition] at :51. Since 2026-08-31 the servicebus-emulator-smoke job is authoritative rather than advisory: it carries no continue-on-error, runs under a 10-minute spend cap (MMCA.ADC/.github/workflows/cross-service-tests.yml:153), and the cross-service-freshness deploy gate now requires a run in which both it and cross-service concluded success. (The third nightly job, apphost-smoke, is the one that stays advisory under ADR-098, .../cross-service-tests.yml:199.) Integration style; needs Docker.
MMCA.ADC.E2E.Tests 97 Playwright end-to-end against the running Aspire stack, using a Page-Object model (PageObjects/, whose page classes count toward the 96 alongside the workflow classes) and E2ETestBase login helpers, organized by actor workflow (Organizer/Speaker/Attendee/Identity/Preferences) plus the Engagement live-poll, session-Q&A and feedback flows, real-time notification push, a Web-Vitals budget check, a pseudo-locale sweep, and an AccessibilityTests axe sweep that runs 31 WCAG 2.1 AA scans across the public browse pages (events, sessions, speakers, sponsors), the organizer management lists and create forms, the speaker dashboard and QR page, Happening Now as both actors, and the badge/points/check-in/attendance surfaces (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/AccessibilityTests.cs:29); it complements the Identity-flow scans shipped in the MMCA.Common.Testing.E2E workflow bases. Those badge/points surfaces are no longer axe-only: CheckInAndPointsTests drives the attendee badge, the organizer manual check-in fallback, the attendee-driven room and booth codes, the two organizer rollups, and the ledger every one of them feeds (.../Workflows/Engagement/CheckInAndPointsTests.cs:29). It is also the file to read for two techniques the rest of the suite reuses. Three of its journeys need an event that is LIVE right now and no seeded event ever is (their dates are fixed), so each mints its own same-day published event, rooms and a session spanning the current moment, and DELETES the event in a finally block, because a leftover same-day published event becomes the app-wide current event (home phase, Happening Now, check-in, attendance, notification scope) for every later test in the suite; and the minted event is created in UTC rather than the seeded America/New_York, because session times are stored as event-zone wall clock, so with a UTC event the wall clock IS DateTime.UtcNow and there is no zone conversion to get wrong on a runner in any region. Points are awarded asynchronously (the check-in writes its row, the outbox publishes AttendeeCheckedIn, and the Engagement points handler awards on the way back), so every ledger assertion re-navigates in a bounded loop instead of expecting the first read to show it. The same wave added the attendee share/export journey (.../Workflows/Engagement/AttendeeShareAndExportTests.cs), live session Q&A (.../Workflows/Engagement/LiveSessionQaTests.cs:22), a data-integrity sweep (.../Workflows/Conference/DataIntegrityTests.cs:11), the public sponsor browse and organizer sponsor management flows (.../Workflows/Conference/PublicSponsorBrowseTests.cs:13), and two suites that are pure subclasses of shared bases: AuthorizationTests over AuthorizationTestsBase (.../Workflows/Identity/AuthorizationTests.cs:11) and a body-less PasswordResetTests over PasswordResetTestsBase (.../Workflows/Identity/PasswordResetTests.cs:5). The live-window arrangement the Q&A and moderation journeys share is one helper, LiveEventArrangement, which creates a published event plus a session spanning now and deletes the event afterwards, because a leftover live event becomes the app-wide current event (.../Helpers/LiveEventArrangement.cs:19); LiveModerationKeyboardTests uses it to run the shared KeyboardOperabilityTestsBase over the session Live page and the focus trap and Escape return of the moderation panel's poll-delete dialog (.../Workflows/Engagement/LiveModerationKeyboardTests.cs:23), and PhoneViewportTests asserts that each anonymous attendee page renders its main heading without scrolling sideways at a 390x844 viewport (.../Workflows/Conference/Public/PhoneViewportTests.cs:18). Runs once per engine via E2E_BROWSER (MMCA.ADC/.github/workflows/e2e.yml:301); the chromium leg gates deploy through e2e-gate (MMCA.ADC/.github/workflows/deploy.yml:677) while firefox and webkit alternate on the Monday and Thursday crons (.../e2e.yml:49-50). InstallableWebAppTests pins what this host supplies for the installable web app: a manifest an installing browser accepts (name, standalone display, root start URL, theme color, and 192, 512 and maskable icons that all resolve) and the framework service worker served at the site root as uncached JavaScript (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Conference/Public/InstallableWebAppTests.cs:13). The largest single project here and the source of most of the chapter's recorded E2E debugging history. [Rubric §28, Front-End Testing] + [Rubric §22, Responsive/Cross-Browser]: this suite is the cross-browser, real-user-flow safety net. E2E style.

⬅ Common Cloud Provider Adapters (AWS, Azure, GCP) • Index • Coverage audit ➡