Skip to content

Pin the application assembly in the CoreTests harness (GH-3423)#3429

Merged
mysticmind merged 1 commit into
mainfrom
fix-3423-application-assembly
Jul 14, 2026
Merged

Pin the application assembly in the CoreTests harness (GH-3423)#3429
mysticmind merged 1 commit into
mainfrom
fix-3423-application-assembly

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #3423. Follows #3427 (the fixture-ownership half, already merged).

The actual root cause

It was never an ordering or fixture problem. Wolverine was resolving the wrong application assembly.

When nothing pins it, establishApplicationAssembly falls back to determineCallingAssembly() (WolverineOptions.Assemblies.cs:66) — a stack walk that finds the outermost Wolverine frame and takes the first assembly outward that isn't System*/Microsoft*. Under xUnit, that frame is not reliably the test assembly.

I instrumented a failing run rather than keep guessing. Ground truth:

# the class run alone — correct
ApplicationAssembly          = CoreTests
Discovery.Assemblies:  Module1, Wolverine.RuntimeCompilation, OrderExtension, CoreTests
CanHandle(CreateTeamAndPlayer) = True

# the same class run after another test class — wrong
ApplicationAssembly          = xunit.execution.dotnet     <-- !!
Discovery.Assemblies:  Module1, Wolverine.RuntimeCompilation, OrderExtension, xunit.execution.dotnet
CanHandle(CreateTeamAndPlayer) = False

Conventional discovery then scans xUnit, finds no handlers, and HandlerGraph.CanHandle returns false. WolverineRuntime.findInvoker falls through to routing and throws IndeterminateRoutesException — which is what the tests reported, and which is purely a downstream symptom. The real failure was swallowed and logged at WolverineRuntime.cs:315 ("Failed to create a message handler for {MessageType}"), which is why this was so opaque.

Which frame the walk lands on depends on what ran before it. Hence: passes alone, passes in the full assembly, fails as a filtered subset.

The fix

Pin ApplicationAssembly explicitly in DefaultApp and in the two ClaimCheck harnesses that build their own hosts inside an async InitializeAsync. This is exactly what Wolverine's own doc comments prescribe for test harnesses:

"You may use this to 'help' Wolverine in testing scenarios to force it to consider this assembly as the main application assembly rather than assuming that the IDE or test runner assembly is the application assembly"

opts.ApplicationAssembly = typeof(DefaultApp).Assembly;

Discovery is now deterministic regardless of call stack or test ordering.

Also: two races in my own #3422 tests

Both compared counters sampled at different instants while the sweeper kept advancing them in the background — my bug, not the product's:

  • one waited on the snapshot count and then asserted on fetch counts. The budget probe runs at the top of a pass, before the per-database walk, so the Nth probe can land while a store has only been fetched N−1 times.
  • one asserted an exact probes == snapshots equality across two samples; a pass completing in between made it 3 vs 2.

Both now assert the invariant that holds at any instant rather than a moment-in-time equality. 25 consecutive runs green (was failing ~1 in 3).

Verification

before after
CoreTests.Persistence subset 20 failures 178/178, stable over 5 runs
Full CoreTests assembly 1931/1931 1953/1953
connection_budget tests, 25x ~1 in 3 failed 0/25 failed

Worth noting separately

determineCallingAssembly() is a best-effort heuristic and is documented as such, with ApplicationAssembly as the escape hatch — so pinning it in the harness is the sanctioned fix, not a workaround. But it's worth being aware that any host built from an async context can silently get the wrong application assembly and then quietly discover zero handlers. The swallowing at WolverineRuntime.cs:315 makes that failure mode much harder to diagnose than it needs to be. Happy to open a separate issue if you think either deserves attention.

🤖 Generated with Claude Code

Closes the rest of #3423. The remaining 19 failures were not an ordering or fixture
problem at all: Wolverine was resolving the wrong application assembly.

When nothing pins it, WolverineOptions.establishApplicationAssembly falls back to
determineCallingAssembly(), a stack walk that takes the first non-System/Microsoft
frame outside Wolverine. Under xUnit that frame is not reliably the test assembly —
instrumenting a failing run showed ApplicationAssembly resolving to
"xunit.execution.dotnet". Conventional discovery then scans xUnit, finds no handlers,
and HandlerGraph.CanHandle returns false. The IndeterminateRoutesException the tests
reported was only the downstream symptom; the real failure was swallowed and logged at
WolverineRuntime.cs:315.

Which frame the walk lands on depends on what ran before, which is why these tests
passed alone and in the full assembly but failed as a filtered subset.

Pin ApplicationAssembly explicitly in DefaultApp and in the two ClaimCheck harnesses
that build their own hosts inside an async InitializeAsync. This is what Wolverine's
own docs prescribe for test harnesses (see the ApplicationAssembly /
RememberedApplicationAssembly doc comments) and it makes discovery deterministic
regardless of call stack.

Also fixes two races in the connection-budget tests added in #3422, both mine. Each
compared counters sampled at different instants while the sweeper kept advancing them
in the background: one waited on the snapshot count and then asserted on fetch counts
(the budget probe runs at the top of a pass, before the per-database walk, so the Nth
probe can land while a store has only been fetched N-1 times), and one asserted an
exact probes == snapshots equality across two samples. Both now assert the invariant
that holds at any instant. 25 consecutive runs green.

Persistence subset: 20 failures -> 0 (178/178, stable over 5 runs).
Full CoreTests assembly: 1953/1953.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mysticmind
mysticmind merged commit 942a34a into main Jul 14, 2026
29 checks passed
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.

CoreTests: 20 tests fail when run as a filtered subset — IntegrationContext disposes a class fixture it doesn't own

2 participants