CHR0039: Assertion result is discarded and can never fail
Rule Description
Section titled “Rule Description”The event-sequence assertions in Cratis.Chronicle.Testing — ShouldHaveAppendedEvent<T>(...) and ShouldHaveTailSequenceNumber(...), in every overload — return a Task. They signal failure by throwing an EventSequenceAssertionException on that Task. The kernel-backed integration assertions in Cratis.Chronicle.XUnit.Integration.Events — ShouldHaveAppendedEvent<T>(...), ShouldHaveNextSequenceNumber(...) and ShouldHaveTailSequenceNumber(...) — work the same way.
Call one from a void-bodied fact and the awaitable is discarded, so the exception is never observed. The assertion becomes a no-op that passes no matter what the system under test does.
Await the assertion and declare the containing member async Task, or return the Task so the test runner awaits it.
Severity
Section titled “Severity”Warning
Example
Section titled “Example”using Cratis.Chronicle.Events;using Cratis.Chronicle.EventSequences;using Cratis.Chronicle.Testing.EventSequences;using Xunit;
[EventType]public record Chr0039AuthorRegistered(string Name);
public class Chr0039WhenRegisteringAnAuthor{ readonly IEventLog _eventLog = default!;
// Warning CHR0039: 'ShouldHaveAppendedEvent' returns an awaitable that is never awaited, so the // assertion can never fail. The fact is 'void', so the compiler's own CS4014 stays silent — // this spec passes even though no Chr0039AuthorRegistered carries that name. [Fact] void should_have_appended_the_event() => _eventLog.ShouldHaveAppendedEvent<Chr0039AuthorRegistered>(e => e.Name == "Jane Austen");
// Warning CHR0039: the same trap in a block body. [Fact] void should_have_appended_exactly_one_event() { _eventLog.ShouldHaveTailSequenceNumber(EventSequenceNumber.First); }}The fix:
using Cratis.Chronicle.Events;using Cratis.Chronicle.EventSequences;using Cratis.Chronicle.Testing.EventSequences;using Xunit;
[EventType]public record Chr0039AuthorRegisteredFixed(string Name);
public class Chr0039WhenRegisteringAnAuthorFixed{ readonly IEventLog _eventLog = default!;
// Declaring the fact 'async Task' and awaiting the assertion makes the exception it throws // observable, so the assertion can actually fail. [Fact] async Task should_have_appended_the_event() => await _eventLog.ShouldHaveAppendedEvent<Chr0039AuthorRegisteredFixed>(e => e.Name == "Jane Austen");
// Returning the Task works too — the test runner awaits it. [Fact] Task should_have_appended_exactly_one_event() => _eventLog.ShouldHaveTailSequenceNumber(EventSequenceNumber.First);}Why This Rule Exists
Section titled “Why This Rule Exists”The compiler already has a rule for a discarded Task — CS4014 — but it fires only inside an async method. A void-bodied fact is the natural and dominant shape of a spec, and it is exactly the shape where CS4014 says nothing. A zero-warning build is therefore blind to it.
What makes this a genuine trap rather than ordinary async carelessness is that the sibling assertions on the same surface are synchronous and void:
ShouldBeSuccessful()ShouldHaveValidationErrors()ShouldHaveConstraintViolation()/ShouldNotHaveConstraintViolation()ShouldHaveConcurrencyViolations()
Discarding “the result” of those is exactly how they are meant to be used. Nothing at the call site signals that the event-sequence assertions behave differently, so the two shapes sit side by side in the same file and only one of them works.
The failure mode is the worst one a test suite has: an assertion that appears to test something and tests nothing. The resulting false confidence survives review, CI, and time — and it hides real defects, because the assertion that would have caught a regression silently passes instead.
The rule is scoped to Should* methods on the Cratis testing surfaces — namespaces under Cratis. containing a .Testing. or .XUnit. segment — that return Task, Task<T>, ValueTask or ValueTask<T>, and only where the result is genuinely discarded: a statement on its own line, or the expression body of a void member. Awaiting, returning, assigning, or passing the awaitable along all count as observing it and are not flagged, and a Should* API outside those namespaces is never flagged.
Related Rules
Section titled “Related Rules”- Writing specs — the testing surfaces this rule covers.