Skip to content

Stop IntegrationContext from disposing a class fixture it doesn't own (GH-3423)#3427

Merged
jeremydmiller merged 1 commit into
mainfrom
fix-integrationcontext-fixture-ownership
Jul 14, 2026
Merged

Stop IntegrationContext from disposing a class fixture it doesn't own (GH-3423)#3427
jeremydmiller merged 1 commit into
mainfrom
fix-integrationcontext-fixture-ownership

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Partial fix for #3423. This does not close the issue — see "What this doesn't fix" below. I'd rather land the correct-ownership change on its own merits than bundle it with a cause I haven't pinned yet.

The defect

DefaultApp is an xUnit class fixture — xUnit owns its lifetime and disposes it when the test class finishes. IntegrationContext.Dispose() disposed it anyway, after every test method:

public virtual void Dispose()
{
    _default.Dispose();   // disposing a fixture this class does not own
}

DefaultApp.Dispose() nulls the host, and the guard that papered over the resulting damage silently substituted a differently-configured host:

public void RecycleIfNecessary()
{
    if (Host == null) Host = WolverineHost.Basic();   // NOT the DefaultApp configuration
}

WolverineHost.Basic() has none of DefaultApp's IncludeType<MessageConsumer>(), IncludeType<InvokedMessageHandler>(), RegisterMessageType(...) calls, or the IAdditionService/IIdentityService/ITrackedTaskRepository registrations. So from the second test method in a class onward, tests could be running against a host they were never written for — and whether any given test passed came down to ordering.

The ownership was backwards in the other direction too: with(...) builds a host for the test, and that one was never disposed at all — a straight leak of a started IHost per call.

The fix

The test disposes what it built; the fixture is left to xUnit.

private IHost? _ownedHost;   // only set by with()

public virtual void Dispose() => _ownedHost?.Dispose();

protected async Task with(Action<WolverineOptions> configuration)
{
    _ownedHost?.Dispose();
    _ownedHost = await WolverineHost.ForAsync(configuration);
    Host = _ownedHost;
}

RecycleIfNecessary() and the Host = null! dance are deleted rather than kept as a band-aid — with the fixture no longer torn down mid-class, there's nothing to recycle. The one caller (find_handlers_with_the_default_handler_discovery) was already a no-op and is removed.

What this fixes

  • The NullReferenceExceptions out of DefaultApp.ChainFor<T>() (a null Host)
  • The leaked with() hosts
  • The xUnit fixture-lifetime violation itself

What this doesn't fix

19 of the 20 filtered-subset failures survive this change, with a separate cause. Running a StorageActionCompliance-derived class before another IntegrationContext class leaves the later host's HandlerGraph unable to handle conventionally-discovered messages — HandlerGraph.CanHandle(...) returns false, so findInvoker falls through to routing and throws IndeterminateRoutesException.

Deterministic repro:

# green
--filter "FullyQualifiedName~CoreTests.Persistence.using_multiple_storage_actions"
# 2 failures
--filter "...using_multiple_storage_actions|...using_storage_return_types_and_entity_attributes"

The obvious explanation — DisableConventionalDiscovery() on the compliance host leaking into hosts built later — is disproven: I built a host with DisableConventionalDiscovery(), disposed it, then built a plain host, and conventional discovery worked fine. Mechanism still unidentified. Written up on #3423 so the next person doesn't re-run the same dead end.

Verification

  • Full CoreTests assembly: 1931 passed, 0 failed (unchanged — this is what CI runs)
  • CoreTests.Persistence subset: 20 → 19 failures

🤖 Generated with Claude Code

…GH-3423)

DefaultApp is an IClassFixture, so xUnit owns its lifetime and disposes it when the
test class finishes. IntegrationContext.Dispose() disposed it anyway — after every
test method — and DefaultApp.Dispose() nulled the Host. The guard that papered over
that, RecycleIfNecessary(), silently rebuilt the shared host as WolverineHost.Basic(),
which carries none of DefaultApp's IncludeType/RegisterMessageType/service
registrations. So from the second test method in a class onward, tests could be running
against a differently-configured host than the one they were written for, and whether a
given test passed came down to ordering.

The ownership was also backwards in the other direction: with() builds a host *for the
test*, and that one was never disposed at all. Now the test disposes what it built and
leaves the fixture alone.

This removes the NullReferenceExceptions out of DefaultApp.ChainFor<T>() (a null Host)
and fixes the leaked with() hosts.

It does NOT close #3423. 19 of the 20 filtered-subset failures survive this change and
have a separate cause: running a StorageActionCompliance-derived class before another
IntegrationContext class leaves the later host's HandlerGraph unable to handle
conventionally-discovered messages (HandlerGraph.CanHandle returns false). The obvious
explanation — DisableConventionalDiscovery leaking across hosts — is disproven: a host
built after a DisableConventionalDiscovery host discovers handlers normally. Mechanism
still unidentified; details on the issue.

Full CoreTests assembly stays green (1931/1931). The Persistence subset goes 20 -> 19.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant