Brighter uses a test generation tool to produce consistent test suites across all provider implementations for both outbox (MSSQL, PostgreSQL, MySQL, SQLite, DynamoDB, MongoDB, GCP Spanner/Firestore) and messaging gateway (RabbitMQ, Kafka, AWS SNS/SQS, Redis, RocketMQ, GCP Pub/Sub, PostgreSQL, MSSQL) tests. See ADR 0035 for the design rationale.
Never edit generated test files directly. Always edit the Liquid templates, then regenerate. Generated files are overwritten each time the generator runs. Any hand-edits will be lost.
tools/Paramore.Brighter.Test.Generator/
├── Templates/
│ ├── DefaultMessageBuilder.cs.liquid ← Default message builder implementation
│ ├── DefaultMessageAssertion.cs.liquid ← Default message assertion implementation
│ ├── IAmAMessageBuilder.cs.liquid ← Message builder interface
│ ├── IAmAMessageAssertion.cs.liquid ← Message assertion interface
│ ├── Outbox/
│ │ ├── Sync/ ← 14 Liquid templates for sync outbox tests (13 tests + 1 interface)
│ │ ├── Async/ ← 14 Liquid templates for async outbox tests (13 tests + 1 interface)
│ │ └── Causation/ ← Liquid template for causation-tracking outbox tests (subclass of CausationTrackingOutboxBaseTests)
│ ├── MessagingGateway/
│ │ ├── Reactor/ ← 21 Liquid templates for sync messaging gateway tests (20 tests + 1 interface)
│ │ ├── Proactor/ ← 21 Liquid templates for async messaging gateway tests (20 tests + 1 interface)
│ │ └── Shared/ ← 3 Liquid templates shared by both variants (ConformanceDeferredPump, ConformanceHarnessMessageScheduler, RejectionMetadataKeys)
│ └── GatewayConformance/ ← 1 Liquid template for the channel factory declaration sweep (ADR 0073)
├── Generators/
│ ├── BaseGenerator.cs
│ ├── OutboxGenerator.cs
│ ├── MessagingGatewayGenerator.cs
│ ├── GatewayConformanceGenerator.cs
│ └── SharedGenerator.cs
├── Configuration/
│ ├── TestConfiguration.cs
│ ├── OutboxConfiguration.cs
│ ├── MessagingGatewayConfiguration.cs
│ └── GatewayConformanceConfiguration.cs
└── Program.cs
tests/Paramore.Brighter.*.Tests/
├── test-configuration.json ← Per-provider configuration
├── Outbox/
│ └── [Prefix]/Generated/ ← Output directory (do not hand-edit)
│ ├── Sync/*.cs
│ ├── Async/*.cs
│ └── Causation/*.cs
└── MessagingGateway/
├── [Prefix]/Generated/ ← Output directory (do not hand-edit)
│ ├── Reactor/*.cs
│ └── Proactor/*.cs
└── Generated/Conformance/ ← Output directory (do not hand-edit); one per project, never per [Prefix]
└── *.cs
| Project | Outbox | Messaging Gateway | Variants |
|---|---|---|---|
Paramore.Brighter.AWS.Tests |
✅ | SnsStandard, SnsFifo, SqsStandard, SqsFifo | |
Paramore.Brighter.AWS.V4.Tests |
✅ | SnsStandard, SnsFifo, SqsStandard, SqsFifo | |
Paramore.Brighter.AzureServiceBus.Tests |
✅ | single | |
Paramore.Brighter.DynamoDB.Tests |
✅ | single | |
Paramore.Brighter.DynamoDB.V4.Tests |
✅ | single | |
Paramore.Brighter.Gcp.Tests |
✅ | ✅ | Firestore, SpannerBinary, SpannerText (outbox); Pull, PullOrdering, Stream, StreamOrdering (gateway) |
Paramore.Brighter.Kafka.Tests |
✅ | Classic, Consumer, PartitionKey | |
Paramore.Brighter.MQTT.Tests |
✅ | single | |
Paramore.Brighter.MSSQL.Tests |
✅ | ✅ | Text, Binary (outbox); single (gateway) |
Paramore.Brighter.MongoDb.Tests |
✅ | single | |
Paramore.Brighter.MySQL.Tests |
✅ | Text, Binary | |
Paramore.Brighter.PostgresSQL.Tests |
✅ | ✅ | Text, Binary (outbox); single (gateway) |
Paramore.Brighter.RMQ.Async.Tests |
✅ | Classic, Quorum | |
Paramore.Brighter.RMQ.Sync.Tests |
✅ | single | |
Paramore.Brighter.Redis.Tests |
✅ | single | |
Paramore.Brighter.RocketMQ.Tests |
✅ | single | |
Paramore.Brighter.Sqlite.Tests |
✅ | Text, Binary |
All 17 projects carrying a
test-configuration.jsonare listed above, derived from those files. If you add a project or change its variants, rebuild the whole row set fromtests/*/test-configuration.jsonrather than editing one line — that is how this table came to be missing three projects and to describe Kafka's variants as they were two renames ago.
Templates use Liquid syntax (via the Fluid library). There are 56 Liquid templates total.
Generated into the test project root. These provide common helpers used by both outbox and messaging gateway tests:
DefaultMessageBuilder.cs.liquid— DefaultIAmAMessageBuilderimplementation that builds test messages with randomized propertiesDefaultMessageAssertion.cs.liquid— DefaultIAmAMessageAssertionimplementation for validating received messagesIAmAMessageBuilder.cs.liquid— Interface for building test messages (supports topic, partition key, correlation ID, etc.)IAmAMessageAssertion.cs.liquid— Interface for asserting message properties after receive
Sync (14 templates) and Async (14 templates) — each contains 1 provider interface + 13 test scenarios:
| Template | Description |
|---|---|
IAmAnOutboxProviderSync/Async |
Provider interface for managing outbox storage |
When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties |
Verifies all message properties survive round-trip |
When_Adding_A_Duplicate_Message_It_Should_Not_Throw |
Idempotent add behavior |
When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit |
Transaction commit behavior |
When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored |
Transaction rollback behavior |
When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox |
Single message deletion |
When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox |
Batch message deletion |
When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message |
Lookup by ID |
When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages |
Batch lookup by IDs |
When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message |
Missing message handling |
When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched |
Full message listing |
When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age |
Outstanding message age filtering |
When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age |
Dispatched message age filtering |
When_Storing_A_Message_With_A_Relative_DataSchema_It_Should_Be_Read_Back |
Relative DataSchema URI survives round-trip |
Templates containing Transaction in the filename are skipped when SupportsTransactions is false.
| Variable | Source | Description |
|---|---|---|
{{ Namespace }} |
test-configuration.json |
Test project namespace |
{{ OutboxProvider }} |
test-configuration.json |
Provider class name (e.g. MSSQLTextOutboxProvider) |
{{ MessageBuilder }} |
test-configuration.json |
Message builder class (defaults to DefaultMessageBuilder) |
{{ Transaction }} |
test-configuration.json |
Transaction type for the provider |
{{ Prefix }} |
Derived from outbox key | Namespace suffix (e.g. .Text, .Binary) |
{{ Category }} |
test-configuration.json |
Optional xUnit [Trait("Category", ...)] value |
{{ CollectionName }} |
test-configuration.json |
Optional xUnit [Collection(...)] value |
Reactor (21 templates) and Proactor (21 templates) — each contains 1 provider interface + 20 test scenarios:
| Template | Description |
|---|---|
IAmAMessageGatewayReactorProvider/ProactorProvider |
Provider interface for creating gateway components |
When_posting_a_message_via_the_messaging_gateway_should_be_received |
Basic send/receive round-trip |
When_posting_a_message_but_no_broker_created_should_throw_exception |
Missing broker error |
When_multiple_threads_try_to_post_a_message_at_the_same_time_should_not_throw_exception |
Thread-safety |
When_a_message_consumer_reads_multiple_messages_should_receive_all_messages |
Batch receive |
When_sending_a_message_should_propagate_activity_context |
OpenTelemetry context propagation |
When_confirming_posting_a_message_should_receive_publish_confirmation |
Publisher confirms |
When_infrastructure_missing_and_assume_channel_should_throw_exception |
Assume mode error |
When_infrastructure_missing_and_validate_channel_should_throw_exception |
Validate mode error |
When_nacking_a_message_it_should_be_redelivered |
Nack redelivery (FR-16) |
When_requeuing_a_failed_message_should_be_redelivered |
Basic requeue |
When_requeuing_a_failed_message_with_delay_should_redeliver_after_delay |
Delayed requeue (FR-2) |
When_requeuing_a_failed_message_with_zero_delay_should_redeliver_immediately |
Zero-delay requeue |
When_requeuing_a_message_too_many_times_should_move_to_dead_letter_queue |
Requeue budget exhausted to DLQ (FR-23) |
When_sending_a_delayed_message_should_deliver_after_delay |
Delayed send (FR-9) |
When_rejecting_message_should_include_metadata |
Rejection metadata stamped on the rejected message (FR-8) |
When_rejecting_message_with_delivery_error_should_send_to_dlq |
Delivery-error reject routes to the DLQ |
When_rejecting_message_with_unknown_reason_should_send_to_dlq |
Unknown reason defaults to the DLQ |
When_rejecting_message_with_unacceptable_reason_should_send_to_invalid_channel |
Unacceptable routes to the invalid channel |
When_rejecting_message_with_unacceptable_and_no_invalid_channel_should_fallback_to_dlq |
Unacceptable falls back to the DLQ when no invalid channel exists |
When_rejecting_message_with_no_channels_configured_should_acknowledge_and_log |
No DLQ and no invalid channel: acknowledge and continue |
Templates are skipped based on feature support flags (see Feature Flags below).
| Variable | Source | Description |
|---|---|---|
{{ Namespace }} |
test-configuration.json |
Test project namespace |
{{ Prefix }} |
Derived from gateway key | Namespace suffix (e.g. .Classic, .Quorum) |
{{ Publication }} |
test-configuration.json |
Publication type (e.g. RmqPublication) |
{{ Subscription }} |
test-configuration.json |
Subscription type (e.g. RmqSubscription) |
{{ MessageGatewayProvider }} |
test-configuration.json |
Provider implementation class |
{{ MessageBuilder }} |
test-configuration.json |
Message builder class (defaults to DefaultMessageBuilder) |
{{ MessageAssertion }} |
test-configuration.json |
Message assertion class (defaults to DefaultMessageAssertion) |
{{ Category }} |
test-configuration.json |
Optional xUnit [Trait("Category", ...)] value |
{{ CollectionName }} |
test-configuration.json |
Optional xUnit [Collection(...)] value |
{{ ReceiveMessageTimeoutInMilliseconds }} |
test-configuration.json |
Timeout for receive operations (default: 300ms) |
{{ DelayBetweenReceiveMessageInMilliseconds }} |
test-configuration.json |
Optional delay before reading messages |
Each test project has a test-configuration.json. Two forms are supported:
Single outbox (e.g. DynamoDB, MongoDB):
{
"Namespace": "Paramore.Brighter.DynamoDB.Tests",
"Outbox": {
"Transaction": "Amazon.DynamoDBv2.Model.TransactWriteItemsRequest",
"OutboxProvider": "DynamoDBOutboxProvider",
"CollectionName": "DynamoDBOutbox"
}
}Multiple outboxes (e.g. MSSQL with Text/Binary, GCP with Firestore/Spanner):
{
"Namespace": "Paramore.Brighter.MSSQL.Tests",
"Outboxes": {
"Text": {
"Transaction": "System.Data.Common.DbTransaction",
"OutboxProvider": "MSSQLTextOutboxProvider",
"Category": "MSSQL",
"CollectionName": "MSSQLTextOutbox"
},
"Binary": {
"Transaction": "System.Data.Common.DbTransaction",
"OutboxProvider": "MSSQLBinaryOutboxProvider",
"Category": "MSSQL",
"CollectionName": "MSSQLBinaryOutbox"
}
}
}To disable transaction tests for providers that don't support them (e.g. MongoDB):
{
"Outbox": {
"SupportsTransactions": false
}
}Single messaging gateway (one transport type):
{
"Namespace": "Paramore.Brighter.Redis.Tests",
"MessagingGateway": {
"Prefix": "Redis",
"Publication": "Paramore.Brighter.MessagingGateway.Redis.RedisPublication",
"Subscription": "Paramore.Brighter.MessagingGateway.Redis.RedisSubscription",
"MessageGatewayProvider": "Paramore.Brighter.Redis.Tests.MessagingGateway.RedisMessageGatewayProvider",
"Category": "Redis",
"CollectionName": "RedisMessagingGateway",
"HasSupportToValidateInfrastructure": false
}
}This generates tests into:
MessagingGateway/Generated/Reactor/*.csMessagingGateway/Generated/Proactor/*.cs
Multiple messaging gateways (e.g. RabbitMQ Classic vs Quorum queues):
{
"Namespace": "Paramore.Brighter.RMQ.Async.Tests",
"MessageAssertion": "RmqMessageAssertion",
"MessagingGateways": {
"Classic": {
"Publication": "Paramore.Brighter.MessagingGateway.RMQ.Async.RmqPublication",
"Subscription": "Paramore.Brighter.MessagingGateway.RMQ.Async.RmqSubscription",
"MessageGatewayProvider": "Paramore.Brighter.RMQ.Async.Tests.MessagingGateway.RmqClassicMessageGatewayProvider",
"Category": "RMQ",
"CollectionName": "Classic",
"ReceiveMessageTimeoutInMilliseconds": 4000,
"HasSupportToPublishConfirmation": true,
"HasSupportToValidateBrokerExistence": true
},
"Quorum": {
"Publication": "Paramore.Brighter.MessagingGateway.RMQ.Async.RmqPublication",
"Subscription": "Paramore.Brighter.MessagingGateway.RMQ.Async.RmqSubscription",
"MessageGatewayProvider": "Paramore.Brighter.RMQ.Async.Tests.MessagingGateway.RmqQuorumMessageGatewayProvider",
"Category": "RMQ",
"CollectionName": "Quorum",
"ReceiveMessageTimeoutInMilliseconds": 4000,
"HasSupportToPublishConfirmation": true,
"HasSupportToValidateBrokerExistence": true
}
}
}Each key in MessagingGateways becomes a subfolder and namespace segment. This generates:
MessagingGateway/Classic/Generated/Reactor/*.cs(namespace...MessagingGateway.Classic.Reactor)MessagingGateway/Classic/Generated/Proactor/*.cs(namespace...MessagingGateway.Classic.Proactor)MessagingGateway/Quorum/Generated/Reactor/*.cs(namespace...MessagingGateway.Quorum.Reactor)MessagingGateway/Quorum/Generated/Proactor/*.cs(namespace...MessagingGateway.Quorum.Proactor)
Each variant gets its own IAmAMessageGatewayReactorProvider and IAmAMessageGatewayProactorProvider interfaces in its namespace. The MessageGatewayProvider must implement both interfaces for its variant.
Root-level MessageBuilder and MessageAssertion in the configuration serve as defaults. Each outbox or gateway entry can override them with its own values. Use custom message builders (e.g. FifoMessageBuilder) when the transport requires specific message properties like partition keys or ordering group IDs.
Feature flags control which test templates are generated. When a flag is false, templates matching specific filename patterns are skipped:
| Flag | Default | Skips templates containing | Use case |
|---|---|---|---|
HasSupportToPublishConfirmation |
false |
confirming_posting |
Transport doesn't support publisher confirms |
HasSupportToValidateBrokerExistence |
false |
no_broker_created |
Transport can't validate broker existence |
HasSupportToValidateInfrastructure |
true |
assume_channel, validate_channel |
Transport can't validate infrastructure existence |
HasSupportToDetectMissingInfrastructureOnAssume |
true |
assume_channel |
Transport honours an explicit Validate but completes silently against infrastructure that is absent under OnMissingChannel.Assume. Kafka's KIP-848 Consumer configuration keeps this false: a configuration-only startup warning documents the limitation without adding a broker check. |
Each variant needs a provider class that implements the generated interfaces. The provider encapsulates transport-specific setup:
public class RmqClassicMessageGatewayProvider
: Classic.Proactor.IAmAMessageGatewayProactorProvider,
Classic.Reactor.IAmAMessageGatewayReactorProvider
{
// Creates Publication, Subscription, Producer, Channel
// Handles cleanup and dead letter queue access
}Key provider responsibilities:
GetOrCreateRoutingKey()/GetOrCreateChannelName()— generate unique routing keys and channel names per test (uses[CallerMemberName])CreatePublication()— create a publication with transport-specific optionsCreateSubscription()— configure transport-specific options (e.g.queueType: QueueType.Quorum,isDurable: true)CreateProducer()/CreateProducerAsync()— create message producers, with special handling for validate-mode testsCreateChannel()/CreateChannelAsync()— create channels, optionally wrapping with requeue tracking for DLQ testsGetMessageFromDeadLetterQueue()/GetMessageFromDeadLetterQueueAsync()— read from DLQ for retry limit testsCleanUp()/CleanUpAsync()— dispose producers, channels, and clean up sent messages
Each outbox variant needs a provider class implementing the generated sync/async interfaces:
public class MSSQLTextOutboxProvider
: Outbox.Text.Generated.Sync.IAmAnOutboxProviderSync,
Outbox.Text.Generated.Async.IAmAnOutboxProviderAsync
{
// Creates/deletes data store, creates outbox instances, manages transactions
}Key provider responsibilities:
CreateStore()/CreateStoreAsync()— initialize the outbox data store (create tables, etc.)DeleteStore()/DeleteStoreAsync()— tear down the store and clean up test messagesCreateOutbox()/CreateOutboxAsync()— create an outbox instance for the providerCreateTransactionProvider()— create a transaction provider for transaction testsGetAllMessages()/GetAllMessagesAsync()— retrieve all stored messages for assertions
Every shipped messaging gateway carries a channel factory declaration conformance sweep (ADR 0073),
which guards against the defect class ADR 0072 corrects: a Subscription whose declared
ChannelFactoryType doesn't actually implement IAmAChannelFactory, or inherits the in-memory
default rather than declaring its own. A GatewayConformance section opts a project's assembly into
this sweep:
{
"Namespace": "Paramore.Brighter.Redis.Tests",
"GatewayConformance": {
"SubscriptionType": "Paramore.Brighter.MessagingGateway.Redis.RedisSubscription"
}
}SubscriptionType(required) — the fully-qualified name of the subscription type the sweep is expected to report as a subject. Rendered both as a locator (typeof(SubscriptionType).Assemblypicks out the gateway assembly the sweep examines) and as an expected subject. Must name a type that survives subsumption — one that declares its ownChannelFactoryType, or a root candidate with no ancestor that also declares one.AdditionalExpectedSubjects(optional) — further fully-qualified type names the sweep is expected to report, beyondSubscriptionType. A generic entry is written with its arity backtick exactly asSystem.Type.FullNamereports it (e.g.Ns.Foo`1), not with angle brackets; the generator converts it to an open-generictypeof(Ns.Foo<>)literal when rendering.Category(optional) — unused by the sweep guard itself; present only for symmetry with the other configuration sections.
This generates one file into MessagingGateway/Generated/Conformance/ —
When_sweeping_the_gateway_assembly_should_find_no_invalid_channel_factory_declaration.cs — however
many MessagingGateway/MessagingGateways variants the same project also configures: one sweep
per project, never per gateway variant, since the sweep examines the whole assembly regardless of
how many transport variants are tested against it.
Three projects carry only a GatewayConformance section, with no Outbox or MessagingGateway
section of their own — Paramore.Brighter.AzureServiceBus.Tests, Paramore.Brighter.MQTT.Tests and
Paramore.Brighter.RMQ.Sync.Tests. Their gateways had no generated test suite before this sweep
existed, so their test-configuration.json files are new, added purely so the thirteenth-gateway
audit (which every shipped gateway must be named by exactly once) has a configuration to find each
of them by.
Templates should be created or updated when a change affects behavior shared across all providers. Common scenarios include:
- Adding a new test scenario that all providers must satisfy (e.g. a new outbox operation, a new messaging gateway pattern)
- Fixing a bug in shared test logic that applies to every provider
- Changing a shared interface or base class that generated tests depend on
- Adding a new provider that needs the standard test suite — create a
test-configuration.jsonand run the generator
After creating or updating templates, you must regenerate the tests. Use the convenience scripts at the repo root to regenerate all test projects at once:
# macOS/Linux
./generate-test.sh
# Windows
.\generate-test.ps1Or follow the manual steps below to regenerate specific projects.
Templates are in:
- Outbox:
tools/Paramore.Brighter.Test.Generator/Templates/Outbox/Sync/and.../Async/ - Messaging Gateway:
tools/Paramore.Brighter.Test.Generator/Templates/MessagingGateway/Reactor/and.../Proactor/
Find the template that corresponds to the test you want to change.
dotnet build tools/Paramore.Brighter.Test.GeneratorThe build copies templates to the bin/ output directory. Without rebuilding, the generator uses stale cached templates.
The generator uses the current working directory as the output root. You must run it from the test project directory:
# Correct - generates into the test project
cd tests/Paramore.Brighter.MSSQL.Tests
dotnet run --no-build --project ../../tools/Paramore.Brighter.Test.Generator
# Wrong - generates into the repo root
dotnet run --project tools/Paramore.Brighter.Test.Generator -- --file tests/Paramore.Brighter.MSSQL.Tests/test-configuration.jsonTemplate changes affect all providers. Regenerate every project that has a test-configuration.json:
dotnet build tools/Paramore.Brighter.Test.Generator
for testdir in \
tests/Paramore.Brighter.AWS.Tests \
tests/Paramore.Brighter.AWS.V4.Tests \
tests/Paramore.Brighter.DynamoDB.Tests \
tests/Paramore.Brighter.DynamoDB.V4.Tests \
tests/Paramore.Brighter.Gcp.Tests \
tests/Paramore.Brighter.Kafka.Tests \
tests/Paramore.Brighter.MSSQL.Tests \
tests/Paramore.Brighter.MongoDb.Tests \
tests/Paramore.Brighter.MySQL.Tests \
tests/Paramore.Brighter.PostgresSQL.Tests \
tests/Paramore.Brighter.Redis.Tests \
tests/Paramore.Brighter.RMQ.Async.Tests \
tests/Paramore.Brighter.RocketMQ.Tests \
tests/Paramore.Brighter.Sqlite.Tests; do
(cd "$testdir" && dotnet run --no-build --project ../../tools/Paramore.Brighter.Test.Generator)
doneOr use the convenience scripts which iterate all subdirectories under tests/ (projects without a test-configuration.json are silently skipped):
./generate-test.sh # macOS/Linux
.\generate-test.ps1 # WindowsSome test logic lives in non-generated base classes in tests/Paramore.Brighter.Base.Test/Outbox/:
OutboxTest.cs— base class for sync outbox testsOutboxAsyncTest.cs— base class for async outbox testsRelationDatabaseOutboxTest.cs— base class for relational database sync outbox testsRelationDatabaseOutboxAsyncTest.cs— base class for relational database async outbox tests
If you change a template pattern that also exists in these base classes, update them too.
The generator only creates or overwrites files — it never deletes existing generated files. If you change a feature flag from true to false (e.g. disabling HasSupportToPublishConfirmation), you must manually delete the previously-generated test files that are no longer wanted. Otherwise stale tests will remain and may fail.
Similarly, if you rename or remove a template, the old generated files remain on disk.
You no longer have to check by hand — CI does it, and it will fail the build. The build job runs
the generated-tree audit, which compares the files the checked-in configurations would produce with
the files actually on disk under a Generated/ directory, in both directions:
dotnet test tests/Paramore.Brighter.Test.Generator.Tests/Paramore.Brighter.Test.Generator.Tests.csproj -f net10.0- Missing — a file a configuration asks for that is not on disk. Run
./generate-test.shand commit the result. - Orphan — a file on disk that no configuration asks for. This is the flipped-flag and deleted-template case above. The audit cannot tell you which side is wrong: it reports that the configuration and the tree disagree, not whether the flag or the file is the mistake. Decide that first, then delete by hand — the generator will not do it for you.
The audit reports names, not contents. A template you edited without regenerating leaves every file present and every name correct, and the audit stays green — so a green audit means the tree holds the files the configuration names, not that it is up to date.
Two things to know when changing the generator itself:
- The expected set comes from
OutboxGenerator.Plan/MessagingGatewayGenerator.Plan, which walk the sameSuites(...)descriptionGenerateAsyncwalks. A new suite must be added toSuitesFor(...), not just to the generate path — a suite that only the generate path knows about is written to disk and then reported as an orphan by the audit. - Anything that prepares the model a template renders from belongs in
SuitesFor(...)too (seeWithPrefix/WithDefaultsFrom), so that planning and generating see the same model.
When writing or modifying test templates, avoid patterns that cause CI flakiness:
- Timestamp comparisons: Use
Assert.Equal(expected, actual, TimeSpan.FromSeconds(1))tolerance instead of string formatting. Database datetime precision varies across providers. - Age filter tests: Use explicit past timestamps (e.g.
DateTime.UtcNow.AddSeconds(-30)) instead of relying onDateTime.UtcNowfor recently-dispatched messages. This avoids races between .NET and database clocks. - Receive timeouts: Set
ReceiveMessageTimeoutInMillisecondsappropriately for the transport. Fast transports (MSSQL, PostgreSQL) can use the default 300ms; broker-based transports need higher values (RMQ: 4000ms, AWS: 4000ms, Kafka: 15000ms, GCP: 10000ms). - Kafka retry counts: Use
maxTries <= 10(not<= 3) for consumer read loops. Consumer group rebalancing in CI can take 5-10+ seconds. - SQLite concurrent access: The SQLite tests use WAL journal mode to handle parallel test execution against a shared
test.dbfile. - Collection attributes: Use
CollectionNamein configuration to control xUnit test collection grouping. This prevents parallel execution conflicts for tests sharing the same transport resources.