From b9c76f3c7dc26ed5622bcac3bace1a72d921653e Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Wed, 29 Jul 2026 12:34:53 -0500 Subject: [PATCH] fix(naming): invalidate the message-name cache when naming strategies change (GH-3703) `WolverineMessageNaming.ToMessageTypeName` memoizes into a static `ImHashMap`, and `WolverineRuntime.HostService.StartAsync` pre-populates it from `Handlers.AllMessageTypes()`. Registering an interop assembly afterwards mutated the `InteropAssemblyInterfaces` strategy but left every already-resolved name in place, so any type named before the registration kept its old name forever and silently ignored the new strategy. `CoreTests` boots hundreds of hosts, so by the time `WolverineMessageNamingTests.use_interface_from_interop_message_naming` ran, `CoreTests.Util.ConcreteMessage` had already been cached under its full name and the test failed deterministically in the full suite while passing alone. `AddMessageInterfaceAssembly` and `InsertFirst` now clear the cache. Both are configuration-time calls -- the only product caller is `IPolicies.RegisterInteropMessageAssembly` -- and both no-op when the registration would not change anything, so the hundreds of host starts in a test run do not thrash the cache. The map is a pure memo, so discarding it only costs a recompute. Verified: CoreTests goes from 2101 passed / 1 failed to 2102 passed / 0 failed (2104 total, 2 skipped), matching the documented baseline delta exactly. `dotnet build wolverine.slnx -c Release` succeeded. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JMNKwGVHnyaBjiheC5k8KX --- .../Util/WolverineMessageNamingTests.cs | 4 ++++ src/Wolverine/Util/WolverineMessageNaming.cs | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/Testing/CoreTests/Util/WolverineMessageNamingTests.cs b/src/Testing/CoreTests/Util/WolverineMessageNamingTests.cs index e838311b8..0c403a211 100644 --- a/src/Testing/CoreTests/Util/WolverineMessageNamingTests.cs +++ b/src/Testing/CoreTests/Util/WolverineMessageNamingTests.cs @@ -44,6 +44,10 @@ public void use_the_version_if_it_exists() [Fact] public void use_interface_from_interop_message_naming() { + // GH-3703: this used to fail whenever any earlier test in the assembly had already booted a host + // that named ConcreteMessage -- WolverineMessageNaming memoizes into a static map, and registering + // the interop assembly did not invalidate what was already in it. AddMessageInterfaceAssembly now + // clears the cache, so the registration below takes effect regardless of what ran first. WolverineMessageNaming.AddMessageInterfaceAssembly(typeof(IInterfaceMessage).Assembly); typeof(ConcreteMessage).ToMessageTypeName().ShouldBe(typeof(IInterfaceMessage).ToMessageTypeName()); diff --git a/src/Wolverine/Util/WolverineMessageNaming.cs b/src/Wolverine/Util/WolverineMessageNaming.cs index 0b5686789..d00d94a09 100644 --- a/src/Wolverine/Util/WolverineMessageNaming.cs +++ b/src/Wolverine/Util/WolverineMessageNaming.cs @@ -153,6 +153,8 @@ public static class WolverineMessageNaming { if (_namingStrategies[0] is T) return; _namingStrategies.Insert(0, new T()); + + clearCache(); } /// @@ -163,7 +165,22 @@ public static class WolverineMessageNaming public static void AddMessageInterfaceAssembly(Assembly assembly) { var naming = _namingStrategies.OfType().Single(); + if (naming.Assemblies.Contains(assembly)) return; + naming.Assemblies.Fill(assembly); + + clearCache(); + } + + /// + /// Discard every memoized message type name. Changing the naming strategies invalidates any name + /// already resolved by the previous set -- a type cached under its full name by an earlier + /// call (or by during a host + /// start) would otherwise keep that name forever and silently ignore the new strategy. See GH-3703. + /// + private static void clearCache() + { + _typeNames = ImHashMap.Empty; } public static string GetPrettyName(this Type t)