diff --git a/.github/workflows/topology-equivalence.yml b/.github/workflows/topology-equivalence.yml
new file mode 100644
index 00000000..6dc7feac
--- /dev/null
+++ b/.github/workflows/topology-equivalence.yml
@@ -0,0 +1,35 @@
+name: Topology Equivalence Gate
+
+# C143c D9:跨 Role 消息链路语义等价 CI 门禁。
+# 4 条代表性链路 × 2 拓扑(All-in-One 单进程 / 三进程)在同一套固定预期下运行,
+# 任一用例失败即阻塞合并。这是 DynamicPhase-0 核心契约(AC-2)的守护门。
+
+on:
+ pull_request:
+ branches:
+ - main
+ workflow_dispatch:
+
+jobs:
+ topology-equivalence:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v3
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Restore
+ run: dotnet restore Server.slnx
+
+ - name: Build
+ run: dotnet build Server.slnx --no-restore
+
+ - name: Run Topology Equivalence Suite
+ run: >
+ dotnet test Tests/GameFrameX.Tests/GameFrameX.Tests.csproj --no-build
+ --filter "FullyQualifiedName~GameFrameX.Tests.Topology.Equivalence"
+ --logger GitHubActions
diff --git a/Directory.Build.props b/Directory.Build.props
index 1f39ea25..3018e20b 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -40,6 +40,16 @@
false
+
+
+ Debug
+ AnyCPU
+
+
$(MSBuildProjectDirectory)/../bin/app_debug
diff --git a/GameFrameX.NetWork.RemoteMessaging/GlobalUsings.cs b/GameFrameX.NetWork.RemoteMessaging/GlobalUsings.cs
index 261b987e..1e0c3920 100644
--- a/GameFrameX.NetWork.RemoteMessaging/GlobalUsings.cs
+++ b/GameFrameX.NetWork.RemoteMessaging/GlobalUsings.cs
@@ -37,4 +37,5 @@
global using GameFrameX.NetWork.RemoteMessaging.Resilience;
global using GameFrameX.NetWork.RemoteMessaging.Transport;
global using GameFrameX.NetWork.RemoteMessaging.Versioning;
-global using GameFrameX.Utility;
\ No newline at end of file
+global using GameFrameX.Utility;
+global using GameFrameX.NetWork.RemoteMessaging.Routing;
diff --git a/GameFrameX.NetWork.RemoteMessaging/Routing/ILocalRoleMessageDispatcher.cs b/GameFrameX.NetWork.RemoteMessaging/Routing/ILocalRoleMessageDispatcher.cs
new file mode 100644
index 00000000..d99b47b7
--- /dev/null
+++ b/GameFrameX.NetWork.RemoteMessaging/Routing/ILocalRoleMessageDispatcher.cs
@@ -0,0 +1,57 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+namespace GameFrameX.NetWork.RemoteMessaging.Routing;
+
+///
+/// 本地 Role 消息投递缝(C143c D3 case 1)。
+///
+///
+/// The local in-process delivery seam behind D3 case 1.
+/// Production delivery goes through the actor pipeline (Actor.Tell/SendAsync semantics);
+/// this interface is the explicit seam so the routing decision itself stays free of
+/// actor-world dependencies (GameFrameX.Core is outside this assembly's reference closure).
+/// The production actor-backed dispatcher arrives with C143e; until then the router is
+/// wired without one and a case 1 hit fails loudly with .
+///
+public interface ILocalRoleMessageDispatcher
+{
+ ///
+ /// 将信封投递给本进程目标 Role 的消息处理队列。
+ ///
+ ///
+ /// Delivers the envelope to the target role's in-process message handling queue.
+ /// The returned task completes when the message has been handed to (or processed by)
+ /// the target queue, preserving per-role FIFO ordering for messages routed in order.
+ ///
+ /// 路由信封(目标 Role 必属于本进程角色集)/ The routing envelope (target role is hosted by this process)
+ /// 取消操作的令牌 / The cancellation token
+ Task DispatchAsync(MessageEnvelope envelope, CancellationToken cancellationToken = default);
+}
diff --git a/GameFrameX.NetWork.RemoteMessaging/Routing/IRemoteRoleRouter.cs b/GameFrameX.NetWork.RemoteMessaging/Routing/IRemoteRoleRouter.cs
new file mode 100644
index 00000000..5404aadf
--- /dev/null
+++ b/GameFrameX.NetWork.RemoteMessaging/Routing/IRemoteRoleRouter.cs
@@ -0,0 +1,58 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+namespace GameFrameX.NetWork.RemoteMessaging.Routing;
+
+///
+/// 跨进程 Role 消息转发缝(C143c D3 case 2/3)。
+///
+///
+/// The remote forwarding seam behind D3 case 2/3.
+/// Case 2 forwards to a known target instance id; case 3 picks any active instance of
+/// the target role from the reachability table. The real implementation (endpoint
+/// reachability table + ForwardToRemoteServerAsync) is delivered by C143d; until then
+/// is the placeholder and throws
+/// . Topology equivalence tests substitute a
+/// loopback forwarder to simulate the remote hop inside a single test process.
+///
+public interface IRemoteRoleRouter
+{
+ ///
+ /// 将信封转发给目标 Role 所在的远端进程。
+ ///
+ ///
+ /// Forwards the envelope to the remote process hosting the target role
+ /// (a known instance for D3 case 2, or any active instance for case 3).
+ ///
+ /// 路由信封(目标 Role 不属于本进程角色集)/ The routing envelope (target role is hosted by another process)
+ /// 取消操作的令牌 / The cancellation token
+ /// 投递分支恒为 / Always returns
+ Task ForwardAsync(MessageEnvelope envelope, CancellationToken cancellationToken = default);
+}
diff --git a/GameFrameX.NetWork.RemoteMessaging/Routing/IRoleRouter.cs b/GameFrameX.NetWork.RemoteMessaging/Routing/IRoleRouter.cs
new file mode 100644
index 00000000..a052efa9
--- /dev/null
+++ b/GameFrameX.NetWork.RemoteMessaging/Routing/IRoleRouter.cs
@@ -0,0 +1,57 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+namespace GameFrameX.NetWork.RemoteMessaging.Routing;
+
+///
+/// 跨 Role 消息路由缝(C143c D3)。
+///
+///
+/// The cross-role message routing seam (C143c D3).
+/// Implementations apply the three-step decision to every envelope:
+/// target role hosted by this process goes to local in-process delivery (case 1);
+/// everything else is handed to the remote forwarding seam (case 2/3, delivered by C143d).
+/// Business code reaches the process-wide instance through .
+///
+public interface IRoleRouter
+{
+ ///
+ /// 路由一封跨 Role 消息信封。
+ ///
+ ///
+ /// Routes one cross-role message envelope.
+ ///
+ /// 路由信封 / The routing envelope
+ /// 取消操作的令牌 / The cancellation token
+ /// 实际命中的投递分支 / The delivery branch that was actually hit
+ /// 当 为 null 时抛出 / Thrown when is null
+ /// 当路由决策失败(空目标 Role / 缺本地投递器 / 缺远程转发器)时抛出 / Thrown when routing cannot decide a route
+ Task RouteAsync(MessageEnvelope envelope, CancellationToken cancellationToken = default);
+}
diff --git a/GameFrameX.NetWork.RemoteMessaging/Routing/InProcessRoleRouter.cs b/GameFrameX.NetWork.RemoteMessaging/Routing/InProcessRoleRouter.cs
new file mode 100644
index 00000000..00952509
--- /dev/null
+++ b/GameFrameX.NetWork.RemoteMessaging/Routing/InProcessRoleRouter.cs
@@ -0,0 +1,147 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+namespace GameFrameX.NetWork.RemoteMessaging.Routing;
+
+///
+/// 进程内跨 Role 路由器(C143c D3 三步判定)。
+///
+///
+/// In-process cross-role router implementing the D3 three-step decision.
+/// The hosted role names are captured as a defensive snapshot at construction time
+/// (mirroring the RoleSet snapshot semantics of C143b), so this instance always routes
+/// against the process shape it was built for. The constructor accepts plain role names
+/// instead of the StartUp RoleSet type because this assembly must not depend on the
+/// startup module; the launch flow passes the published role snapshot when wiring
+/// .
+/// Failure semantics: every undecidable route throws —
+/// the router never drops a message and never falls back to another branch silently.
+///
+public sealed class InProcessRoleRouter : IRoleRouter
+{
+ ///
+ /// 本进程承载的 Role 名快照。
+ ///
+ ///
+ /// The defensive snapshot of role names hosted by this process.
+ /// Uses the default ordinal comparer, matching the RoleSet snapshot semantics of C143b.
+ ///
+ private readonly HashSet _hostedRoleNames;
+
+ ///
+ /// 本地投递缝(case 1;可为 null,为 null 时 case 1 命中即显式失败)。
+ ///
+ ///
+ /// The local delivery seam (case 1); may be null, in which case a case 1 hit fails loudly.
+ ///
+ private readonly ILocalRoleMessageDispatcher _localDispatcher;
+
+ ///
+ /// 远程转发缝(case 2/3;可为 null,为 null 时非本进程目标即显式失败)。
+ ///
+ ///
+ /// The remote forwarding seam (case 2/3); may be null, in which case any non-local target fails loudly.
+ ///
+ private readonly IRemoteRoleRouter _remoteRouter;
+
+ ///
+ /// 初始化进程内跨 Role 路由器。
+ ///
+ ///
+ /// Initializes the router with a defensive copy of the hosted role names
+ /// and the optional delivery seams. Both seams are optional on purpose:
+ /// production wiring (GameApp) installs the router before the actor-backed local
+ /// dispatcher exists (C143e) and before remote forwarding exists (C143d), so a
+ /// premature route attempt fails with instead
+ /// of silently succeeding against a half-built pipeline.
+ ///
+ /// 本进程承载的 Role 名集合(构造时防御性拷贝)/ The role names hosted by this process (defensively copied)
+ /// 本地投递缝;null 表示 case 1 命中时显式失败 / The local delivery seam; null makes case 1 hits fail loudly
+ /// 远程转发缝;null 表示 case 2/3 显式失败 / The remote forwarding seam; null makes case 2/3 fail loudly
+ /// 当 为 null 时抛出 / Thrown when is null
+ public InProcessRoleRouter(IEnumerable hostedRoleNames, ILocalRoleMessageDispatcher localDispatcher = null, IRemoteRoleRouter remoteRouter = null)
+ {
+ ArgumentNullException.ThrowIfNull(hostedRoleNames, nameof(hostedRoleNames));
+
+ _hostedRoleNames = new HashSet(hostedRoleNames);
+ _localDispatcher = localDispatcher;
+ _remoteRouter = remoteRouter;
+ }
+
+ ///
+ /// 路由一封跨 Role 消息信封(D3 三步判定)。
+ ///
+ ///
+ /// Applies the D3 three-step decision to the envelope:
+ /// target role hosted by this process goes to the local dispatcher (case 1);
+ /// anything else goes to the remote forwarding seam (case 2/3).
+ /// An empty target role, a case 1 hit without a local dispatcher, or a non-local
+ /// target without a remote forwarder all throw —
+ /// never a silent fallback.
+ ///
+ /// 路由信封 / The routing envelope
+ /// 取消操作的令牌 / The cancellation token
+ /// 实际命中的投递分支 / The delivery branch that was actually hit
+ /// 当 为 null 时抛出 / Thrown when is null
+ /// 当路由决策失败时抛出 / Thrown when routing cannot decide a route
+ public async Task RouteAsync(MessageEnvelope envelope, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(envelope, nameof(envelope));
+
+ if (string.IsNullOrWhiteSpace(envelope.TargetRole))
+ {
+ throw new RouteNotFoundException(envelope.TargetRole, "Cannot route an envelope without a target role.");
+ }
+
+ // D3 case 1:目标 Role 属于本进程角色集 → 本地投递
+ if (_hostedRoleNames.Contains(envelope.TargetRole))
+ {
+ if (_localDispatcher == null)
+ {
+ throw new RouteNotFoundException(
+ envelope.TargetRole,
+ $"Target role '{envelope.TargetRole}' is hosted by this process but no local message dispatcher is configured (the actor-backed dispatcher arrives with C143e).");
+ }
+
+ await _localDispatcher.DispatchAsync(envelope, cancellationToken);
+ return RoleRouteDelivery.LocalActor;
+ }
+
+ // D3 case 2/3:目标 Role 不属于本进程角色集 → 远程转发缝(真实实现随 C143d 交付)
+ if (_remoteRouter == null)
+ {
+ throw new RouteNotFoundException(
+ envelope.TargetRole,
+ $"Target role '{envelope.TargetRole}' is not hosted by this process and no remote role router is configured (remote forwarding arrives with C143d).");
+ }
+
+ return await _remoteRouter.ForwardAsync(envelope, cancellationToken);
+ }
+}
diff --git a/GameFrameX.NetWork.RemoteMessaging/Routing/MessageEnvelope.cs b/GameFrameX.NetWork.RemoteMessaging/Routing/MessageEnvelope.cs
new file mode 100644
index 00000000..0d2478b8
--- /dev/null
+++ b/GameFrameX.NetWork.RemoteMessaging/Routing/MessageEnvelope.cs
@@ -0,0 +1,106 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and relevant international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+namespace GameFrameX.NetWork.RemoteMessaging.Routing;
+
+///
+/// 跨 Role 路由信封(C143c D3)。
+///
+///
+/// Envelope for cross-role message routing (C143c D3).
+/// Carries everything the role routing seam needs to make its three-step decision:
+/// the target role name (process locality check), an optional target instance id
+/// (distinguishes D3 case 2 "known instance" from case 3 "any active instance"),
+/// the target actor id used by local in-process delivery, and the message payload itself.
+/// The envelope is immutable after construction; routing never mutates it.
+///
+public sealed class MessageEnvelope
+{
+ ///
+ /// 初始化路由信封。
+ ///
+ ///
+ /// Initializes the routing envelope.
+ /// An empty or whitespace target role is intentionally not rejected here:
+ /// role-name validity is a routing decision, and the router fails it loudly with
+ /// instead of a silent fallback (C143c risk mitigation).
+ ///
+ /// 目标 Role 的服务器类型名 / The target role server type name
+ /// 要路由的消息 / The message to route
+ /// 本地投递目标 ActorId(跨进程跳时忽略)/ The local delivery target actor id (ignored on remote hops)
+ /// 可选的目标实例 Id(非空走 D3 case 2,空走 case 3)/ Optional target instance id (non-null selects D3 case 2, null selects case 3)
+ /// 当 为 null 时抛出 / Thrown when is null
+ public MessageEnvelope(string targetRole, MessageObject message, long targetActorId = 0, string targetInstanceId = null)
+ {
+ ArgumentNullException.ThrowIfNull(message, nameof(message));
+
+ TargetRole = targetRole;
+ Message = message;
+ TargetActorId = targetActorId;
+ TargetInstanceId = targetInstanceId;
+ }
+
+ ///
+ /// 获取目标 Role 的服务器类型名。
+ ///
+ ///
+ /// Gets the target role server type name.
+ ///
+ /// 目标 Role 名 / The target role name
+ public string TargetRole { get; }
+
+ ///
+ /// 获取要路由的消息。
+ ///
+ ///
+ /// Gets the message to route.
+ ///
+ /// 消息负载 / The message payload
+ public MessageObject Message { get; }
+
+ ///
+ /// 获取本地投递目标 ActorId。
+ ///
+ ///
+ /// Gets the local delivery target actor id; ignored on remote hops.
+ ///
+ /// 本地投递目标 ActorId / The local delivery target actor id
+ public long TargetActorId { get; }
+
+ ///
+ /// 获取可选的目标实例 Id。
+ ///
+ ///
+ /// Gets the optional target instance id. A non-null value selects D3 case 2
+ /// (forward to a known instance); null selects D3 case 3 (any active instance of the role).
+ ///
+ /// 目标实例 Id;未指定时为 null / The target instance id, or null when unspecified
+ public string TargetInstanceId { get; }
+}
diff --git a/GameFrameX.NetWork.RemoteMessaging/Routing/RemoteRoleRouter.cs b/GameFrameX.NetWork.RemoteMessaging/Routing/RemoteRoleRouter.cs
new file mode 100644
index 00000000..5b64179f
--- /dev/null
+++ b/GameFrameX.NetWork.RemoteMessaging/Routing/RemoteRoleRouter.cs
@@ -0,0 +1,64 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+namespace GameFrameX.NetWork.RemoteMessaging.Routing;
+
+///
+/// 跨进程 Role 转发占位实现(C143c → C143d)。
+///
+///
+/// Placeholder implementation of the D3 case 2/3 remote forwarding seam.
+/// Every forward attempt throws on purpose:
+/// real forwarding needs the endpoint reachability table and ForwardToRemoteServerAsync,
+/// which are delivered by change C143d. Keeping the placeholder as the production default
+/// makes premature cross-process routing fail loudly at the seam instead of silently
+/// dead-lettering messages. Topology equivalence tests replace it with a loopback forwarder.
+///
+public sealed class RemoteRoleRouter : IRemoteRoleRouter
+{
+ ///
+ /// 转发占位:恒抛 。
+ ///
+ ///
+ /// Placeholder forward: always throws .
+ ///
+ /// 路由信封 / The routing envelope
+ /// 取消操作的令牌 / The cancellation token
+ /// 恒不返回 / Never returns
+ /// 当 为 null 时抛出 / Thrown when is null
+ /// 恒抛出,等待 C143d 实现可达表转发 / Always thrown until C143d implements reachability-table forwarding
+ public Task ForwardAsync(MessageEnvelope envelope, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(envelope, nameof(envelope));
+
+ throw new NotImplementedException(
+ $"Remote role forwarding for target role '{envelope.TargetRole}' (D3 case 2/3) is not implemented yet; it arrives with change C143d together with the endpoint reachability table.");
+ }
+}
diff --git a/GameFrameX.NetWork.RemoteMessaging/Routing/RoleRouteDelivery.cs b/GameFrameX.NetWork.RemoteMessaging/Routing/RoleRouteDelivery.cs
new file mode 100644
index 00000000..5654ab07
--- /dev/null
+++ b/GameFrameX.NetWork.RemoteMessaging/Routing/RoleRouteDelivery.cs
@@ -0,0 +1,60 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+namespace GameFrameX.NetWork.RemoteMessaging.Routing;
+
+///
+/// 跨 Role 路由投递结果(C143c D3)。
+///
+///
+/// Delivery result of cross-role routing (C143c D3).
+/// Tells the caller which branch of the three-step routing decision actually delivered
+/// the message, so topology equivalence tests can assert that the All-in-One topology
+/// hits the local in-process path 100% of the time.
+/// Values start at 1 on purpose: an uninitialized field must never read as a valid delivery.
+///
+public enum RoleRouteDelivery
+{
+ ///
+ /// D3 case 1:目标 Role 属于本进程角色集,经本地投递缝送达。
+ ///
+ ///
+ /// D3 case 1: the target role belongs to this process and was delivered through the local dispatcher.
+ ///
+ LocalActor = 1,
+
+ ///
+ /// D3 case 2/3:目标 Role 不属于本进程角色集,经远程转发缝投出。
+ ///
+ ///
+ /// D3 case 2/3: the target role is hosted by another process and was handed to the remote forwarding seam.
+ ///
+ RemoteForwarded = 2,
+}
diff --git a/GameFrameX.NetWork.RemoteMessaging/Routing/RoleRouterHolder.cs b/GameFrameX.NetWork.RemoteMessaging/Routing/RoleRouterHolder.cs
new file mode 100644
index 00000000..be27d074
--- /dev/null
+++ b/GameFrameX.NetWork.RemoteMessaging/Routing/RoleRouterHolder.cs
@@ -0,0 +1,102 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+namespace GameFrameX.NetWork.RemoteMessaging.Routing;
+
+///
+/// 跨 Role 路由器全局持有者(C143c)。
+///
+///
+/// Global holder for the process-wide instance,
+/// following the UnifiedMessageSenderHolder pattern (this stack has no DI container;
+/// global infrastructure is reached through typed holders).
+/// is called by the launch flow (GameApp) right after the
+/// process role snapshot is published; business code reads .
+///
+public static class RoleRouterHolder
+{
+ ///
+ /// 全局路由器实例。
+ ///
+ ///
+ /// The global router instance.
+ ///
+ private static IRoleRouter _router;
+
+ ///
+ /// 初始化互斥锁。
+ ///
+ ///
+ /// The initialization lock.
+ ///
+ private static readonly object InitializeLock = new object();
+
+ ///
+ /// 获取全局跨 Role 路由器实例。必须在调用 之后使用。
+ ///
+ ///
+ /// Gets the process-wide role router. Must be used only after .
+ /// Reading before initialization throws instead of returning null: routing against a
+ /// missing router must fail loudly at the call site (never a silent drop).
+ ///
+ /// 全局路由器实例 / The process-wide router instance
+ /// 当尚未调用 时抛出 / Thrown when has not been called
+ public static IRoleRouter Current
+ {
+ get
+ {
+ if (_router == null)
+ {
+ throw new InvalidOperationException("RoleRouterHolder has not been initialized; call RoleRouterHolder.Initialize during launch before routing.");
+ }
+
+ return _router;
+ }
+ }
+
+ ///
+ /// 初始化全局路由器。启动流程调用一次。
+ ///
+ ///
+ /// Initializes the process-wide router. Called once during launch
+ /// (after the role snapshot is published, before any host starts).
+ ///
+ /// 路由器实例 / The router instance
+ /// 当 为 null 时抛出 / Thrown when is null
+ public static void Initialize(IRoleRouter router)
+ {
+ ArgumentNullException.ThrowIfNull(router, nameof(router));
+
+ lock (InitializeLock)
+ {
+ _router = router;
+ }
+ }
+}
diff --git a/GameFrameX.NetWork.RemoteMessaging/Routing/RouteNotFoundException.cs b/GameFrameX.NetWork.RemoteMessaging/Routing/RouteNotFoundException.cs
new file mode 100644
index 00000000..810cf811
--- /dev/null
+++ b/GameFrameX.NetWork.RemoteMessaging/Routing/RouteNotFoundException.cs
@@ -0,0 +1,82 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+namespace GameFrameX.NetWork.RemoteMessaging.Routing;
+
+///
+/// 跨 Role 路由决策失败异常(C143c D3)。
+///
+///
+/// Thrown when the cross-role routing seam cannot decide where a message must go
+/// (empty target role, target role not hosted by this process with no remote forwarder
+/// configured, or a local role hit with no local dispatcher configured).
+/// The router never falls back silently: an undecidable route is always a loud failure (C143c risk mitigation).
+///
+public sealed class RouteNotFoundException : Exception
+{
+ ///
+ /// 初始化路由决策失败异常。
+ ///
+ ///
+ /// Initializes the exception with the target role that could not be routed.
+ ///
+ /// 无法路由的目标 Role 名(可能为 null 或空白)/ The target role that could not be routed (may be null or whitespace)
+ /// 失败原因描述 / The failure description
+ public RouteNotFoundException(string targetRole, string message)
+ : base(message)
+ {
+ TargetRole = targetRole;
+ }
+
+ ///
+ /// 初始化路由决策失败异常(含内部异常)。
+ ///
+ ///
+ /// Initializes the exception with the target role, a failure description and an inner exception.
+ ///
+ /// 无法路由的目标 Role 名(可能为 null 或空白)/ The target role that could not be routed (may be null or whitespace)
+ /// 失败原因描述 / The failure description
+ /// 内部异常 / The inner exception
+ public RouteNotFoundException(string targetRole, string message, Exception innerException)
+ : base(message, innerException)
+ {
+ TargetRole = targetRole;
+ }
+
+ ///
+ /// 获取无法路由的目标 Role 名。
+ ///
+ ///
+ /// Gets the target role that could not be routed; may be null or whitespace
+ /// when the failure was an empty role name.
+ ///
+ /// 目标 Role 名 / The target role name
+ public string TargetRole { get; }
+}
diff --git a/GameFrameX.StartUp/GameApp.cs b/GameFrameX.StartUp/GameApp.cs
index 13a93786..9c96022c 100644
--- a/GameFrameX.StartUp/GameApp.cs
+++ b/GameFrameX.StartUp/GameApp.cs
@@ -37,6 +37,7 @@
using GameFrameX.Foundation.Options.Attributes;
using GameFrameX.Foundation.Utility;
using GameFrameX.Localization;
+using GameFrameX.NetWork.RemoteMessaging.Routing;
using GameFrameX.StartUp.Abstractions;
using GameFrameX.StartUp.Options;
using GameFrameX.Utility;
@@ -325,6 +326,10 @@ private static void Launcher(string[] args, IReadOnlyList
+
diff --git a/Tests/GameFrameX.Tests/GameFrameX.Tests.csproj b/Tests/GameFrameX.Tests/GameFrameX.Tests.csproj
index a199662d..5a866aa0 100644
--- a/Tests/GameFrameX.Tests/GameFrameX.Tests.csproj
+++ b/Tests/GameFrameX.Tests/GameFrameX.Tests.csproj
@@ -27,6 +27,11 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Tests/GameFrameX.Tests/Topology/Equivalence/ChainExecutionResult.cs b/Tests/GameFrameX.Tests/Topology/Equivalence/ChainExecutionResult.cs
new file mode 100644
index 00000000..6306b3bb
--- /dev/null
+++ b/Tests/GameFrameX.Tests/Topology/Equivalence/ChainExecutionResult.cs
@@ -0,0 +1,54 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+using GameFrameX.NetWork.RemoteMessaging.Routing;
+
+namespace GameFrameX.Tests.Topology.Equivalence;
+
+///
+/// 语义等价用例集的链路执行结果快照(C143c D9)。
+///
+///
+/// Deterministic snapshot of one chain execution: the delivery branch of every hop,
+/// the per-role receive order, and the final business state. Both topologies capture
+/// the same shape so tests can assert against fixed expectations (and, transitively,
+/// equivalence between topologies).
+///
+public sealed class ChainExecutionResult
+{
+ /// 每一跳的投递分支(含处理器内发起的应答跳)/ The delivery branch of every hop, including reply hops initiated inside handlers
+ public IReadOnlyList Deliveries { get; set; }
+
+ /// 每 Role 的到达序(消息类型名)/ The per-role receive order (message type names)
+ public IReadOnlyDictionary> ArrivalOrderByRole { get; set; }
+
+ /// 最终状态快照(确定性拼接)/ The final state snapshot (deterministic concatenation)
+ public string StateSnapshot { get; set; }
+}
diff --git a/Tests/GameFrameX.Tests/Topology/Equivalence/CrossRoleChains.cs b/Tests/GameFrameX.Tests/Topology/Equivalence/CrossRoleChains.cs
new file mode 100644
index 00000000..bb484af2
--- /dev/null
+++ b/Tests/GameFrameX.Tests/Topology/Equivalence/CrossRoleChains.cs
@@ -0,0 +1,286 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+using GameFrameX.NetWork.RemoteMessaging.Routing;
+
+namespace GameFrameX.Tests.Topology.Equivalence;
+
+///
+/// 语义等价用例集的 4 条代表性跨 Role 消息链路(C143c D9)。
+///
+///
+/// The four representative cross-role chains of the equivalence suite (C143c D9):
+/// Gate→Game session establishment, Game→Social friend query, Game→Match match request
+/// and Match→Game settlement callback. Every chain installs its role handlers on the
+/// world, then issues three sequential request round trips (deterministic player ids),
+/// so receive order and final state are fully deterministic and comparable across
+/// topologies. Reply hops are routed from inside the request handler — the same
+/// request/reply shape business code will use through the routing seam.
+/// All state values are derived deterministically from the request ids: equivalence
+/// assertions compare fixed strings, never random values.
+///
+public static class CrossRoleChains
+{
+ ///
+ /// Gate Role 名。
+ ///
+ public const string GateRoleName = "Gate";
+
+ ///
+ /// Game Role 名。
+ ///
+ public const string GameRoleName = "Game";
+
+ ///
+ /// Social Role 名。
+ ///
+ public const string SocialRoleName = "Social";
+
+ ///
+ /// Match Role 名。
+ ///
+ public const string MatchRoleName = "Match";
+
+ ///
+ /// 链路使用的玩家 Id 序列(确定性)。
+ ///
+ public static readonly long[] ChainPlayerIds = new long[] { 1001, 1002, 1003 };
+
+ ///
+ /// 链路使用的对局 Id 序列(确定性)。
+ ///
+ public static readonly long[] ChainMatchIds = new long[] { 5001, 5002, 5003 };
+
+ ///
+ /// 链路一:Gate→Game 会话建立(Game 应答回 Gate)。
+ ///
+ /// 拓扑世界 / The topology world
+ /// 跳超时(毫秒)/ The hop timeout in milliseconds
+ /// Game 处理延时(毫秒,超时等价对用)/ The Game handler delay in milliseconds (for the timeout equivalence pair)
+ /// 链路执行结果 / The chain execution result
+ public static async Task RunGateToGameSessionEstablishAsync(TopologyWorld world, int hopTimeoutMilliseconds, int gameHandlerDelayMilliseconds = 0)
+ {
+ var establishedPlayerIds = new List();
+ var acknowledgedPlayerIds = new List();
+
+ world.GetMailbox(GameRoleName).SetHandler(async envelope =>
+ {
+ if (envelope.Message is EquivalenceTestMessages.SessionEstablishRequest request)
+ {
+ if (gameHandlerDelayMilliseconds > 0)
+ {
+ await Task.Delay(gameHandlerDelayMilliseconds);
+ }
+
+ establishedPlayerIds.Add(request.PlayerId);
+ world.SetRoleState(GameRoleName, "SessionEstablished", string.Join(",", establishedPlayerIds));
+ await world.RouteAsync(
+ GameRoleName,
+ GateRoleName,
+ new EquivalenceTestMessages.SessionEstablishResponse { PlayerId = request.PlayerId, Accepted = true },
+ hopTimeoutMilliseconds);
+ }
+ });
+
+ world.GetMailbox(GateRoleName).SetHandler(envelope =>
+ {
+ if (envelope.Message is EquivalenceTestMessages.SessionEstablishResponse response)
+ {
+ acknowledgedPlayerIds.Add(response.PlayerId);
+ world.SetRoleState(GateRoleName, "SessionAcknowledged", string.Join(",", acknowledgedPlayerIds));
+ }
+
+ return Task.CompletedTask;
+ });
+
+ foreach (var playerId in ChainPlayerIds)
+ {
+ await world.RouteAsync(
+ GateRoleName,
+ GameRoleName,
+ new EquivalenceTestMessages.SessionEstablishRequest { PlayerId = playerId },
+ hopTimeoutMilliseconds);
+ }
+
+ return world.CaptureResult();
+ }
+
+ ///
+ /// 链路二:Game→Social 好友查询(Social 应答回 Game)。
+ ///
+ /// 拓扑世界 / The topology world
+ /// 跳超时(毫秒)/ The hop timeout in milliseconds
+ /// Social 处理延时(毫秒,超时等价对用)/ The Social handler delay in milliseconds (for the timeout equivalence pair)
+ /// 链路执行结果 / The chain execution result
+ public static async Task RunGameToSocialFriendQueryAsync(TopologyWorld world, int hopTimeoutMilliseconds, int socialHandlerDelayMilliseconds = 0)
+ {
+ var servedPlayerIds = new List();
+ var receivedFriendLists = new List();
+
+ world.GetMailbox(SocialRoleName).SetHandler(async envelope =>
+ {
+ if (envelope.Message is EquivalenceTestMessages.FriendListRequest request)
+ {
+ if (socialHandlerDelayMilliseconds > 0)
+ {
+ await Task.Delay(socialHandlerDelayMilliseconds);
+ }
+
+ servedPlayerIds.Add(request.PlayerId);
+ world.SetRoleState(SocialRoleName, "FriendListServed", string.Join(",", servedPlayerIds));
+ var friendPlayerIds = new long[] { request.PlayerId + 1000, request.PlayerId + 2000 };
+ await world.RouteAsync(
+ SocialRoleName,
+ GameRoleName,
+ new EquivalenceTestMessages.FriendListResponse { PlayerId = request.PlayerId, FriendPlayerIds = friendPlayerIds },
+ hopTimeoutMilliseconds);
+ }
+ });
+
+ world.GetMailbox(GameRoleName).SetHandler(envelope =>
+ {
+ if (envelope.Message is EquivalenceTestMessages.FriendListResponse response)
+ {
+ receivedFriendLists.Add($"{response.PlayerId}:{string.Join("+", response.FriendPlayerIds)}");
+ world.SetRoleState(GameRoleName, "FriendQueryResult", string.Join(",", receivedFriendLists));
+ }
+
+ return Task.CompletedTask;
+ });
+
+ foreach (var playerId in ChainPlayerIds)
+ {
+ await world.RouteAsync(
+ GameRoleName,
+ SocialRoleName,
+ new EquivalenceTestMessages.FriendListRequest { PlayerId = playerId },
+ hopTimeoutMilliseconds);
+ }
+
+ return world.CaptureResult();
+ }
+
+ ///
+ /// 链路三:Game→Match 匹配请求(Match 应答回 Game)。
+ ///
+ /// 拓扑世界 / The topology world
+ /// 跳超时(毫秒)/ The hop timeout in milliseconds
+ /// 链路执行结果 / The chain execution result
+ public static async Task RunGameToMatchJoinRequestAsync(TopologyWorld world, int hopTimeoutMilliseconds)
+ {
+ var issuedTickets = new List();
+ var receivedTickets = new List();
+
+ world.GetMailbox(MatchRoleName).SetHandler(async envelope =>
+ {
+ if (envelope.Message is EquivalenceTestMessages.MatchJoinRequest request)
+ {
+ var ticketId = $"ticket-{request.PlayerId}";
+ issuedTickets.Add($"{request.PlayerId}:{ticketId}");
+ world.SetRoleState(MatchRoleName, "MatchTicketIssued", string.Join(",", issuedTickets));
+ await world.RouteAsync(
+ MatchRoleName,
+ GameRoleName,
+ new EquivalenceTestMessages.MatchJoinResponse { PlayerId = request.PlayerId, TicketId = ticketId },
+ hopTimeoutMilliseconds);
+ }
+ });
+
+ world.GetMailbox(GameRoleName).SetHandler(envelope =>
+ {
+ if (envelope.Message is EquivalenceTestMessages.MatchJoinResponse response)
+ {
+ receivedTickets.Add($"{response.PlayerId}:{response.TicketId}");
+ world.SetRoleState(GameRoleName, "MatchTicketReceived", string.Join(",", receivedTickets));
+ }
+
+ return Task.CompletedTask;
+ });
+
+ foreach (var playerId in ChainPlayerIds)
+ {
+ await world.RouteAsync(
+ GameRoleName,
+ MatchRoleName,
+ new EquivalenceTestMessages.MatchJoinRequest { PlayerId = playerId },
+ hopTimeoutMilliseconds);
+ }
+
+ return world.CaptureResult();
+ }
+
+ ///
+ /// 链路四:Match→Game 结算回调(Game 确认回 Match)。
+ ///
+ /// 拓扑世界 / The topology world
+ /// 跳超时(毫秒)/ The hop timeout in milliseconds
+ /// 链路执行结果 / The chain execution result
+ public static async Task RunMatchToGameSettlementCallbackAsync(TopologyWorld world, int hopTimeoutMilliseconds)
+ {
+ var appliedSettlements = new List();
+ var acknowledgedSettlements = new List();
+
+ world.GetMailbox(GameRoleName).SetHandler(async envelope =>
+ {
+ if (envelope.Message is EquivalenceTestMessages.SettlementCallbackMessage callback)
+ {
+ appliedSettlements.Add($"{callback.MatchId}:{callback.WinnerPlayerId}");
+ world.SetRoleState(GameRoleName, "SettlementApplied", string.Join(",", appliedSettlements));
+ await world.RouteAsync(
+ GameRoleName,
+ MatchRoleName,
+ new EquivalenceTestMessages.SettlementAckMessage { MatchId = callback.MatchId, WinnerPlayerId = callback.WinnerPlayerId },
+ hopTimeoutMilliseconds);
+ }
+ });
+
+ world.GetMailbox(MatchRoleName).SetHandler(envelope =>
+ {
+ if (envelope.Message is EquivalenceTestMessages.SettlementAckMessage acknowledgement)
+ {
+ acknowledgedSettlements.Add($"{acknowledgement.MatchId}:{acknowledgement.WinnerPlayerId}");
+ world.SetRoleState(MatchRoleName, "SettlementAcknowledged", string.Join(",", acknowledgedSettlements));
+ }
+
+ return Task.CompletedTask;
+ });
+
+ for (var index = 0; index < ChainMatchIds.Length; index++)
+ {
+ await world.RouteAsync(
+ MatchRoleName,
+ GameRoleName,
+ new EquivalenceTestMessages.SettlementCallbackMessage { MatchId = ChainMatchIds[index], WinnerPlayerId = 2000 + index },
+ hopTimeoutMilliseconds);
+ }
+
+ return world.CaptureResult();
+ }
+}
diff --git a/Tests/GameFrameX.Tests/Topology/Equivalence/EquivalenceTestMessages.cs b/Tests/GameFrameX.Tests/Topology/Equivalence/EquivalenceTestMessages.cs
new file mode 100644
index 00000000..dad2e214
--- /dev/null
+++ b/Tests/GameFrameX.Tests/Topology/Equivalence/EquivalenceTestMessages.cs
@@ -0,0 +1,187 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+using GameFrameX.NetWork.Messages;
+
+namespace GameFrameX.Tests.Topology.Equivalence;
+
+///
+/// 语义等价用例集的测试消息定义(C143c D9)。
+///
+///
+/// Test messages for the topology equivalence suite (C143c D9).
+/// Four representative cross-role chains need request/response pairs; they are nested
+/// in one static class because they are pure test payloads with no behavior of their own.
+/// Values are deterministic (derived from player ids) so both topologies can be compared
+/// through identical state snapshots. The messages are never serialized in this suite,
+/// so no protobuf contracts are required.
+///
+public static class EquivalenceTestMessages
+{
+ ///
+ /// 会话建立请求(Gate→Game)。
+ ///
+ public sealed class SessionEstablishRequest : MessageObject
+ {
+ /// 玩家 Id / The player id
+ public long PlayerId { get; set; }
+
+ ///
+ public override void Clear()
+ {
+ PlayerId = 0;
+ }
+ }
+
+ ///
+ /// 会话建立应答(Game→Gate)。
+ ///
+ public sealed class SessionEstablishResponse : MessageObject
+ {
+ /// 玩家 Id / The player id
+ public long PlayerId { get; set; }
+
+ /// 是否已接受 / Whether the session was accepted
+ public bool Accepted { get; set; }
+
+ ///
+ public override void Clear()
+ {
+ PlayerId = 0;
+ Accepted = false;
+ }
+ }
+
+ ///
+ /// 好友列表查询请求(Game→Social)。
+ ///
+ public sealed class FriendListRequest : MessageObject
+ {
+ /// 玩家 Id / The player id
+ public long PlayerId { get; set; }
+
+ ///
+ public override void Clear()
+ {
+ PlayerId = 0;
+ }
+ }
+
+ ///
+ /// 好友列表查询应答(Social→Game)。
+ ///
+ public sealed class FriendListResponse : MessageObject
+ {
+ /// 玩家 Id / The player id
+ public long PlayerId { get; set; }
+
+ /// 好友 Id 列表(确定性构造)/ The friend id list (deterministically built)
+ public long[] FriendPlayerIds { get; set; }
+
+ ///
+ public override void Clear()
+ {
+ PlayerId = 0;
+ FriendPlayerIds = null;
+ }
+ }
+
+ ///
+ /// 匹配加入请求(Game→Match)。
+ ///
+ public sealed class MatchJoinRequest : MessageObject
+ {
+ /// 玩家 Id / The player id
+ public long PlayerId { get; set; }
+
+ ///
+ public override void Clear()
+ {
+ PlayerId = 0;
+ }
+ }
+
+ ///
+ /// 匹配加入应答(Match→Game)。
+ ///
+ public sealed class MatchJoinResponse : MessageObject
+ {
+ /// 玩家 Id / The player id
+ public long PlayerId { get; set; }
+
+ /// 匹配票据(由玩家 Id 确定性生成)/ The match ticket (deterministically derived from the player id)
+ public string TicketId { get; set; }
+
+ ///
+ public override void Clear()
+ {
+ PlayerId = 0;
+ TicketId = null;
+ }
+ }
+
+ ///
+ /// 结算回调(Match→Game)。
+ ///
+ public sealed class SettlementCallbackMessage : MessageObject
+ {
+ /// 对局 Id / The match id
+ public long MatchId { get; set; }
+
+ /// 胜者玩家 Id / The winner player id
+ public long WinnerPlayerId { get; set; }
+
+ ///
+ public override void Clear()
+ {
+ MatchId = 0;
+ WinnerPlayerId = 0;
+ }
+ }
+
+ ///
+ /// 结算确认(Game→Match)。
+ ///
+ public sealed class SettlementAckMessage : MessageObject
+ {
+ /// 对局 Id / The match id
+ public long MatchId { get; set; }
+
+ /// 已落地的胜者玩家 Id / The applied winner player id
+ public long WinnerPlayerId { get; set; }
+
+ ///
+ public override void Clear()
+ {
+ MatchId = 0;
+ WinnerPlayerId = 0;
+ }
+ }
+}
diff --git a/Tests/GameFrameX.Tests/Topology/Equivalence/InProcessRoleRouterTests.cs b/Tests/GameFrameX.Tests/Topology/Equivalence/InProcessRoleRouterTests.cs
new file mode 100644
index 00000000..f7599bf8
--- /dev/null
+++ b/Tests/GameFrameX.Tests/Topology/Equivalence/InProcessRoleRouterTests.cs
@@ -0,0 +1,191 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+using GameFrameX.NetWork.RemoteMessaging.Routing;
+
+namespace GameFrameX.Tests.Topology.Equivalence;
+
+///
+/// InProcessRoleRouter D3 三步判定单元测试(C143c)。
+///
+///
+/// Unit tests for the InProcessRoleRouter three-step decision (C143c):
+/// case 1 goes through the local dispatcher (mock), case 2/3 delegates to the remote
+/// seam — with the production placeholder that throws NotImplementedException — and
+/// every undecidable route fails loudly with RouteNotFoundException.
+///
+public class InProcessRoleRouterTests
+{
+ ///
+ /// 记录型本地投递器(mock Actor 投递缝)。
+ ///
+ private sealed class RecordingLocalDispatcher : ILocalRoleMessageDispatcher
+ {
+ /// 已收到的信封 / The received envelopes
+ public List ReceivedEnvelopes { get; } = new List();
+
+ ///
+ public Task DispatchAsync(MessageEnvelope envelope, CancellationToken cancellationToken = default)
+ {
+ ReceivedEnvelopes.Add(envelope);
+ return Task.CompletedTask;
+ }
+ }
+
+ ///
+ /// 固定应答型远程转发器(mock D3 case 2/3 缝)。
+ ///
+ private sealed class StubRemoteRoleRouter : IRemoteRoleRouter
+ {
+ /// 已收到的信封 / The received envelopes
+ public List ReceivedEnvelopes { get; } = new List();
+
+ ///
+ public Task ForwardAsync(MessageEnvelope envelope, CancellationToken cancellationToken = default)
+ {
+ ReceivedEnvelopes.Add(envelope);
+ return Task.FromResult(RoleRouteDelivery.RemoteForwarded);
+ }
+ }
+
+ ///
+ /// case 1:目标 Role 属于本进程角色集 → 经本地投递器送达并返回 LocalActor。
+ ///
+ [Fact]
+ public async Task RouteAsync_TargetRoleHostedLocally_DispatchesThroughLocalDispatcher()
+ {
+ var localDispatcher = new RecordingLocalDispatcher();
+ var router = new InProcessRoleRouter(new List { "Game", "Social" }, localDispatcher);
+ var envelope = new MessageEnvelope("Game", new EquivalenceTestMessages.SessionEstablishRequest { PlayerId = 1001 });
+
+ var delivery = await router.RouteAsync(envelope);
+
+ Assert.Equal(RoleRouteDelivery.LocalActor, delivery);
+ Assert.Single(localDispatcher.ReceivedEnvelopes);
+ Assert.Same(envelope, localDispatcher.ReceivedEnvelopes[0]);
+ }
+
+ ///
+ /// case 1 命中但未配置本地投递器 → 显式抛 RouteNotFoundException。
+ ///
+ [Fact]
+ public async Task RouteAsync_LocalTargetWithoutDispatcher_ThrowsRouteNotFound()
+ {
+ var router = new InProcessRoleRouter(new List { "Game" }, null);
+ var envelope = new MessageEnvelope("Game", new EquivalenceTestMessages.SessionEstablishRequest());
+
+ var exception = await Assert.ThrowsAsync(() => router.RouteAsync(envelope));
+
+ Assert.Equal("Game", exception.TargetRole);
+ }
+
+ ///
+ /// case 2/3:目标 Role 不属于本进程 → 委托远程缝;生产占位 RemoteRoleRouter 恒抛 NotImplementedException。
+ ///
+ [Fact]
+ public async Task RouteAsync_RemoteTargetWithPlaceholderForwarder_ThrowsNotImplemented()
+ {
+ var router = new InProcessRoleRouter(new List { "Gate" }, new RecordingLocalDispatcher(), new RemoteRoleRouter());
+ var envelope = new MessageEnvelope("Game", new EquivalenceTestMessages.SessionEstablishRequest());
+
+ await Assert.ThrowsAsync(() => router.RouteAsync(envelope));
+ }
+
+ ///
+ /// case 2/3:目标 Role 不属于本进程 → 委托远程缝并原样返回转发结果。
+ ///
+ [Fact]
+ public async Task RouteAsync_RemoteTargetWithForwarder_ForwardsAndReturnsRemoteForwarded()
+ {
+ var remoteRouter = new StubRemoteRoleRouter();
+ var router = new InProcessRoleRouter(new List { "Gate" }, new RecordingLocalDispatcher(), remoteRouter);
+ var envelope = new MessageEnvelope("Game", new EquivalenceTestMessages.SessionEstablishRequest(), targetInstanceId: "game-instance-01");
+
+ var delivery = await router.RouteAsync(envelope);
+
+ Assert.Equal(RoleRouteDelivery.RemoteForwarded, delivery);
+ Assert.Single(remoteRouter.ReceivedEnvelopes);
+ Assert.Same(envelope, remoteRouter.ReceivedEnvelopes[0]);
+ }
+
+ ///
+ /// case 2/3 未配置远程缝 → 显式抛 RouteNotFoundException(不静默)。
+ ///
+ [Fact]
+ public async Task RouteAsync_RemoteTargetWithoutForwarder_ThrowsRouteNotFound()
+ {
+ var router = new InProcessRoleRouter(new List { "Gate" }, new RecordingLocalDispatcher());
+ var envelope = new MessageEnvelope("Game", new EquivalenceTestMessages.SessionEstablishRequest());
+
+ var exception = await Assert.ThrowsAsync(() => router.RouteAsync(envelope));
+
+ Assert.Equal("Game", exception.TargetRole);
+ }
+
+ ///
+ /// 空白目标 Role → 路由决策失败抛 RouteNotFoundException。
+ ///
+ [Fact]
+ public async Task RouteAsync_EmptyTargetRole_ThrowsRouteNotFound()
+ {
+ var router = new InProcessRoleRouter(new List { "Game" }, new RecordingLocalDispatcher());
+ var envelope = new MessageEnvelope(" ", new EquivalenceTestMessages.SessionEstablishRequest());
+
+ await Assert.ThrowsAsync(() => router.RouteAsync(envelope));
+ }
+
+ ///
+ /// null 信封 → ArgumentNullException。
+ ///
+ [Fact]
+ public async Task RouteAsync_NullEnvelope_ThrowsArgumentNullException()
+ {
+ var router = new InProcessRoleRouter(new List { "Game" }, new RecordingLocalDispatcher());
+
+ await Assert.ThrowsAsync(() => router.RouteAsync(null));
+ }
+
+ ///
+ /// 构造后源集合变更不影响路由判定(角色集快照防御性拷贝)。
+ ///
+ [Fact]
+ public async Task Constructor_DefensivelyCopiesHostedRoleNames_LaterMutationDoesNotAffectRouting()
+ {
+ var hostedRoleNames = new List { "Game" };
+ var localDispatcher = new RecordingLocalDispatcher();
+ var router = new InProcessRoleRouter(hostedRoleNames, localDispatcher);
+
+ hostedRoleNames.Clear();
+
+ var delivery = await router.RouteAsync(new MessageEnvelope("Game", new EquivalenceTestMessages.SessionEstablishRequest()));
+
+ Assert.Equal(RoleRouteDelivery.LocalActor, delivery);
+ }
+}
diff --git a/Tests/GameFrameX.Tests/Topology/Equivalence/LoopbackRemoteRoleRouter.cs b/Tests/GameFrameX.Tests/Topology/Equivalence/LoopbackRemoteRoleRouter.cs
new file mode 100644
index 00000000..887914c2
--- /dev/null
+++ b/Tests/GameFrameX.Tests/Topology/Equivalence/LoopbackRemoteRoleRouter.cs
@@ -0,0 +1,86 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+using GameFrameX.NetWork.RemoteMessaging.Routing;
+
+namespace GameFrameX.Tests.Topology.Equivalence;
+
+///
+/// 多进程拓扑的环回远程转发器(C143c D9 等价用例集)。
+///
+///
+/// Loopback remote forwarder for the MultiProcess topology fixture (C143c D9).
+/// Inside one test process the D3 case 2/3 remote hop cannot use real RemoteMessaging
+/// (the production forwarder is the C143d placeholder that throws NotImplementedException),
+/// so the fixture substitutes this loopback: it resolves the target role to the simulated
+/// process cell that hosts it and routes the envelope into that cell's router — the same
+/// decision C143d's reachability table will make, minus the physical transport.
+/// Cancellation flows through the whole path so timeout behavior stays comparable
+/// across topologies.
+///
+public sealed class LoopbackRemoteRoleRouter : IRemoteRoleRouter
+{
+ ///
+ /// 目标 Role 名 → 承载进程 cell 的路由器。
+ ///
+ private readonly IReadOnlyDictionary _cellRoutersByRole;
+
+ ///
+ /// 初始化环回转发器。
+ ///
+ /// 目标 Role 名 → cell 路由器映射 / The map of target role name to the hosting cell's router
+ public LoopbackRemoteRoleRouter(IReadOnlyDictionary cellRoutersByRole)
+ {
+ _cellRoutersByRole = cellRoutersByRole ?? throw new ArgumentNullException(nameof(cellRoutersByRole));
+ }
+
+ ///
+ /// 将信封环回路由进承载目标 Role 的 cell。
+ ///
+ /// 路由信封 / The routing envelope
+ /// 取消操作的令牌 / The cancellation token
+ /// 恒为 / Always
+ /// 当目标 Role 不在任何 cell 时抛出 / Thrown when no cell hosts the target role
+ public async Task ForwardAsync(MessageEnvelope envelope, CancellationToken cancellationToken = default)
+ {
+ if (envelope == null)
+ {
+ throw new ArgumentNullException(nameof(envelope));
+ }
+
+ if (!_cellRoutersByRole.TryGetValue(envelope.TargetRole, out var cellRouter))
+ {
+ throw new RouteNotFoundException(envelope.TargetRole, $"No simulated process cell hosts target role '{envelope.TargetRole}'.");
+ }
+
+ await cellRouter.RouteAsync(envelope, cancellationToken);
+ return RoleRouteDelivery.RemoteForwarded;
+ }
+}
diff --git a/Tests/GameFrameX.Tests/Topology/Equivalence/RoleMailbox.cs b/Tests/GameFrameX.Tests/Topology/Equivalence/RoleMailbox.cs
new file mode 100644
index 00000000..b793e2f4
--- /dev/null
+++ b/Tests/GameFrameX.Tests/Topology/Equivalence/RoleMailbox.cs
@@ -0,0 +1,193 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+using System.Threading.Channels;
+using GameFrameX.NetWork.RemoteMessaging.Routing;
+
+namespace GameFrameX.Tests.Topology.Equivalence;
+
+///
+/// 等价用例集的单 Role 邮箱(C143c D9)。
+///
+///
+/// Per-role mailbox for the topology equivalence suite (C143c D9).
+/// Simulates one role's in-process message queue with the same guarantees the actor
+/// pipeline provides in production: strict per-role FIFO processing, one message at a
+/// time, and senders may either fire-and-forget or await the handler's completion
+/// (the Actor.Tell/SendAsync semantics behind D3 case 1).
+/// Known ceiling: this is a lightweight queue, not the real Actor; the real actor-backed
+/// local dispatcher arrives with C143e and is exercised there.
+/// The arrival log records the message type name of every processed message so the
+/// equivalence tests can assert identical receive order across topologies.
+///
+public sealed class RoleMailbox
+{
+ ///
+ /// 邮箱工作项:信封 + 完成源。
+ ///
+ private sealed class MailboxWorkItem
+ {
+ ///
+ /// 初始化工作项。
+ ///
+ public MailboxWorkItem(MessageEnvelope envelope)
+ {
+ Envelope = envelope;
+ Completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ }
+
+ /// 路由信封 / The routing envelope
+ public MessageEnvelope Envelope { get; }
+
+ /// 处理完成源 / The handler completion source
+ public TaskCompletionSource Completion { get; }
+ }
+
+ ///
+ /// Role 名。
+ ///
+ private readonly string _roleName;
+
+ ///
+ /// FIFO 工作项通道(单消费者)。
+ ///
+ private readonly Channel _channel = Channel.CreateUnbounded(
+ new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });
+
+ ///
+ /// 单消费者处理任务。
+ ///
+ private readonly Task _processingTask;
+
+ ///
+ /// 到达日志锁。
+ ///
+ private readonly object _arrivalLogLock = new object();
+
+ ///
+ /// 按处理顺序记录的消息类型名日志。
+ ///
+ private readonly List _arrivalLog = new List();
+
+ ///
+ /// 当前消息处理器。
+ ///
+ private Func _handler = _ => Task.CompletedTask;
+
+ ///
+ /// 初始化单 Role 邮箱并启动 FIFO 消费者。
+ ///
+ /// Role 名 / The role name
+ public RoleMailbox(string roleName)
+ {
+ _roleName = roleName;
+ _processingTask = Task.Run(ProcessAsync);
+ }
+
+ ///
+ /// 获取 Role 名。
+ ///
+ public string RoleName
+ {
+ get { return _roleName; }
+ }
+
+ ///
+ /// 设置消息处理器(每个测试安装一次)。
+ ///
+ /// 消息处理器 / The message handler
+ public void SetHandler(Func handler)
+ {
+ _handler = handler ?? throw new ArgumentNullException(nameof(handler));
+ }
+
+ ///
+ /// 投递一封信封:入队后等待处理完成,等待期尊重取消令牌(超时语义)。
+ ///
+ ///
+ /// Enqueues the envelope and waits for its handler to complete.
+ /// The enqueue itself is never cancelled by the hop timeout (the message has been
+ /// committed to the queue once routing accepted it); only the wait observes the
+ /// cancellation token, which is exactly the SendAsync-with-timeout semantics both
+ /// topologies are compared on.
+ ///
+ /// 路由信封 / The routing envelope
+ /// 取消操作的令牌 / The cancellation token
+ public async Task DeliverAsync(MessageEnvelope envelope, CancellationToken cancellationToken)
+ {
+ var item = new MailboxWorkItem(envelope);
+ await _channel.Writer.WriteAsync(item, CancellationToken.None);
+ await item.Completion.Task.WaitAsync(cancellationToken);
+ }
+
+ ///
+ /// 获取到达日志快照(按处理顺序的消息类型名)。
+ ///
+ /// 消息类型名列表 / The ordered message type names
+ public IReadOnlyList GetArrivalLog()
+ {
+ lock (_arrivalLogLock)
+ {
+ return _arrivalLog.ToList();
+ }
+ }
+
+ ///
+ /// 关闭邮箱(停止接收新消息,测试收尾用)。
+ ///
+ public void Close()
+ {
+ _channel.Writer.TryComplete();
+ }
+
+ ///
+ /// 单消费者处理循环:严格按 FIFO 逐条调用处理器并记录到达日志。
+ ///
+ private async Task ProcessAsync()
+ {
+ await foreach (var item in _channel.Reader.ReadAllAsync())
+ {
+ lock (_arrivalLogLock)
+ {
+ _arrivalLog.Add(item.Envelope.Message.GetType().Name);
+ }
+
+ try
+ {
+ await _handler(item.Envelope);
+ item.Completion.SetResult(true);
+ }
+ catch (Exception exception)
+ {
+ item.Completion.SetException(exception);
+ }
+ }
+ }
+}
diff --git a/Tests/GameFrameX.Tests/Topology/Equivalence/TopologyEquivalenceTests.cs b/Tests/GameFrameX.Tests/Topology/Equivalence/TopologyEquivalenceTests.cs
new file mode 100644
index 00000000..99d2d201
--- /dev/null
+++ b/Tests/GameFrameX.Tests/Topology/Equivalence/TopologyEquivalenceTests.cs
@@ -0,0 +1,293 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+using GameFrameX.NetWork.RemoteMessaging.Routing;
+
+namespace GameFrameX.Tests.Topology.Equivalence;
+
+///
+/// 跨 Role 消息链路语义等价测试(C143c D9,AC-2)。
+///
+///
+/// Topology equivalence suite (C143c D9, AC-2): every representative chain runs in the
+/// All-in-One topology and in the design-source §D9 three-process topology, and both
+/// assert the SAME fixed expectations — per-role receive order, final business state
+/// snapshot, and the D3 delivery branch sequence — so semantic equivalence between the
+/// topologies is enforced by the CI gate (topology-equivalence.yml).
+/// The All-in-One cases additionally prove the D3 acceptance property: case 1 local
+/// delivery is hit by 100% of the hops.
+///
+public class TopologyEquivalenceTests
+{
+ ///
+ /// 等价链路种类。
+ ///
+ public enum EquivalenceChainKind
+ {
+ /// Gate→Game 会话建立 / Gate→Game session establishment
+ GateToGameSessionEstablish = 1,
+
+ /// Game→Social 好友查询 / Game→Social friend query
+ GameToSocialFriendQuery = 2,
+
+ /// Game→Match 匹配请求 / Game→Match match request
+ GameToMatchJoinRequest = 3,
+
+ /// Match→Game 结算回调 / Match→Game settlement callback
+ MatchToGameSettlementCallback = 4,
+ }
+
+ ///
+ /// 等价拓扑种类。
+ ///
+ public enum EquivalenceTopologyKind
+ {
+ /// All-in-One 单进程 / All-in-One single process
+ AllInOne = 1,
+
+ /// 三进程(设计源 §D9:Gate | Game+Social | Match)/ Three processes (§D9)
+ MultiProcess = 2,
+ }
+
+ ///
+ /// 常规链路跳超时(毫秒)——远大于处理耗时,两拓扑都应成功。
+ ///
+ private const int ComfortableHopTimeoutMilliseconds = 5000;
+
+ ///
+ /// 紧跳超时(毫秒)——远小于处理器延时,两拓扑都应失败。
+ ///
+ private const int TightHopTimeoutMilliseconds = 80;
+
+ ///
+ /// 处理器延时(毫秒)——制造必超时的处理耗时。
+ ///
+ private const int HandlerDelayMilliseconds = 400;
+
+ ///
+ /// 四条代表性链路 × 两拓扑:消息序、最终状态、投递分支全部符合固定预期(8 用例)。
+ ///
+ [Theory]
+ [InlineData(EquivalenceChainKind.GateToGameSessionEstablish, EquivalenceTopologyKind.AllInOne)]
+ [InlineData(EquivalenceChainKind.GateToGameSessionEstablish, EquivalenceTopologyKind.MultiProcess)]
+ [InlineData(EquivalenceChainKind.GameToSocialFriendQuery, EquivalenceTopologyKind.AllInOne)]
+ [InlineData(EquivalenceChainKind.GameToSocialFriendQuery, EquivalenceTopologyKind.MultiProcess)]
+ [InlineData(EquivalenceChainKind.GameToMatchJoinRequest, EquivalenceTopologyKind.AllInOne)]
+ [InlineData(EquivalenceChainKind.GameToMatchJoinRequest, EquivalenceTopologyKind.MultiProcess)]
+ [InlineData(EquivalenceChainKind.MatchToGameSettlementCallback, EquivalenceTopologyKind.AllInOne)]
+ [InlineData(EquivalenceChainKind.MatchToGameSettlementCallback, EquivalenceTopologyKind.MultiProcess)]
+ public async Task CrossRoleChain_BothTopologies_PreserveOrderStateAndDeliveryBranch(
+ EquivalenceChainKind chainKind,
+ EquivalenceTopologyKind topologyKind)
+ {
+ using (var world = CreateWorld(topologyKind))
+ {
+ var result = await RunChainAsync(world, chainKind, ComfortableHopTimeoutMilliseconds);
+
+ AssertExpectedArrivalOrder(chainKind, result);
+ AssertExpectedStateSnapshot(chainKind, result);
+ AssertExpectedDeliveryBranches(chainKind, topologyKind, result);
+ }
+ }
+
+ ///
+ /// 超时行为等价:相同紧超时参数下,两拓扑对本地跳(Game→Social)与跨进程跳(Gate→Game)都同样失败。
+ ///
+ [Fact]
+ public async Task CrossRoleChain_WithTightTimeout_FailsInBothTopologies()
+ {
+ using (var allInOneWorld = CreateWorld(EquivalenceTopologyKind.AllInOne))
+ using (var multiProcessWorld = CreateWorld(EquivalenceTopologyKind.MultiProcess))
+ {
+ // 本地跳(两拓扑内 Game→Social 均为 case 1)
+ await Assert.ThrowsAnyAsync(() =>
+ CrossRoleChains.RunGameToSocialFriendQueryAsync(allInOneWorld, TightHopTimeoutMilliseconds, HandlerDelayMilliseconds));
+ await Assert.ThrowsAnyAsync(() =>
+ CrossRoleChains.RunGameToSocialFriendQueryAsync(multiProcessWorld, TightHopTimeoutMilliseconds, HandlerDelayMilliseconds));
+
+ // 跨进程跳(三进程拓扑内 Gate→Game 经环回转发缝)
+ await Assert.ThrowsAnyAsync(() =>
+ CrossRoleChains.RunGateToGameSessionEstablishAsync(allInOneWorld, TightHopTimeoutMilliseconds, HandlerDelayMilliseconds));
+ await Assert.ThrowsAnyAsync(() =>
+ CrossRoleChains.RunGateToGameSessionEstablishAsync(multiProcessWorld, TightHopTimeoutMilliseconds, HandlerDelayMilliseconds));
+ }
+ }
+
+ ///
+ /// 创建指定拓扑的世界。
+ ///
+ private static TopologyWorld CreateWorld(EquivalenceTopologyKind topologyKind)
+ {
+ if (topologyKind == EquivalenceTopologyKind.AllInOne)
+ {
+ return TopologyWorld.CreateAllInOne(new List
+ {
+ CrossRoleChains.GateRoleName,
+ CrossRoleChains.GameRoleName,
+ CrossRoleChains.SocialRoleName,
+ CrossRoleChains.MatchRoleName,
+ });
+ }
+
+ return TopologyWorld.CreateMultiProcess(
+ CrossRoleChains.GateRoleName,
+ CrossRoleChains.GameRoleName,
+ CrossRoleChains.SocialRoleName,
+ CrossRoleChains.MatchRoleName);
+ }
+
+ ///
+ /// 运行指定链路。
+ ///
+ private static Task RunChainAsync(TopologyWorld world, EquivalenceChainKind chainKind, int hopTimeoutMilliseconds)
+ {
+ if (chainKind == EquivalenceChainKind.GateToGameSessionEstablish)
+ {
+ return CrossRoleChains.RunGateToGameSessionEstablishAsync(world, hopTimeoutMilliseconds);
+ }
+
+ if (chainKind == EquivalenceChainKind.GameToSocialFriendQuery)
+ {
+ return CrossRoleChains.RunGameToSocialFriendQueryAsync(world, hopTimeoutMilliseconds);
+ }
+
+ if (chainKind == EquivalenceChainKind.GameToMatchJoinRequest)
+ {
+ return CrossRoleChains.RunGameToMatchJoinRequestAsync(world, hopTimeoutMilliseconds);
+ }
+
+ return CrossRoleChains.RunMatchToGameSettlementCallbackAsync(world, hopTimeoutMilliseconds);
+ }
+
+ ///
+ /// 断言每 Role 到达序符合发送序,且无关 Role 零到达(无误路由)。
+ ///
+ private static void AssertExpectedArrivalOrder(EquivalenceChainKind chainKind, ChainExecutionResult result)
+ {
+ if (chainKind == EquivalenceChainKind.GateToGameSessionEstablish)
+ {
+ Assert.Equal(Repeat("SessionEstablishRequest", 3), result.ArrivalOrderByRole[CrossRoleChains.GameRoleName]);
+ Assert.Equal(Repeat("SessionEstablishResponse", 3), result.ArrivalOrderByRole[CrossRoleChains.GateRoleName]);
+ Assert.Empty(result.ArrivalOrderByRole[CrossRoleChains.SocialRoleName]);
+ Assert.Empty(result.ArrivalOrderByRole[CrossRoleChains.MatchRoleName]);
+ }
+ else if (chainKind == EquivalenceChainKind.GameToSocialFriendQuery)
+ {
+ Assert.Equal(Repeat("FriendListRequest", 3), result.ArrivalOrderByRole[CrossRoleChains.SocialRoleName]);
+ Assert.Equal(Repeat("FriendListResponse", 3), result.ArrivalOrderByRole[CrossRoleChains.GameRoleName]);
+ Assert.Empty(result.ArrivalOrderByRole[CrossRoleChains.GateRoleName]);
+ Assert.Empty(result.ArrivalOrderByRole[CrossRoleChains.MatchRoleName]);
+ }
+ else if (chainKind == EquivalenceChainKind.GameToMatchJoinRequest)
+ {
+ Assert.Equal(Repeat("MatchJoinRequest", 3), result.ArrivalOrderByRole[CrossRoleChains.MatchRoleName]);
+ Assert.Equal(Repeat("MatchJoinResponse", 3), result.ArrivalOrderByRole[CrossRoleChains.GameRoleName]);
+ Assert.Empty(result.ArrivalOrderByRole[CrossRoleChains.GateRoleName]);
+ Assert.Empty(result.ArrivalOrderByRole[CrossRoleChains.SocialRoleName]);
+ }
+ else
+ {
+ Assert.Equal(Repeat("SettlementCallbackMessage", 3), result.ArrivalOrderByRole[CrossRoleChains.GameRoleName]);
+ Assert.Equal(Repeat("SettlementAckMessage", 3), result.ArrivalOrderByRole[CrossRoleChains.MatchRoleName]);
+ Assert.Empty(result.ArrivalOrderByRole[CrossRoleChains.GateRoleName]);
+ Assert.Empty(result.ArrivalOrderByRole[CrossRoleChains.SocialRoleName]);
+ }
+ }
+
+ ///
+ /// 断言最终业务状态快照等于固定预期(两拓扑共用同一常量 ⇒ 等价)。
+ ///
+ private static void AssertExpectedStateSnapshot(EquivalenceChainKind chainKind, ChainExecutionResult result)
+ {
+ if (chainKind == EquivalenceChainKind.GateToGameSessionEstablish)
+ {
+ Assert.Equal(
+ "Game|SessionEstablished=1001,1002,1003;Gate|SessionAcknowledged=1001,1002,1003",
+ result.StateSnapshot);
+ }
+ else if (chainKind == EquivalenceChainKind.GameToSocialFriendQuery)
+ {
+ Assert.Equal(
+ "Game|FriendQueryResult=1001:2001+3001,1002:2002+3002,1003:2003+3003;Social|FriendListServed=1001,1002,1003",
+ result.StateSnapshot);
+ }
+ else if (chainKind == EquivalenceChainKind.GameToMatchJoinRequest)
+ {
+ Assert.Equal(
+ "Game|MatchTicketReceived=1001:ticket-1001,1002:ticket-1002,1003:ticket-1003;Match|MatchTicketIssued=1001:ticket-1001,1002:ticket-1002,1003:ticket-1003",
+ result.StateSnapshot);
+ }
+ else
+ {
+ Assert.Equal(
+ "Game|SettlementApplied=5001:2000,5002:2001,5003:2002;Match|SettlementAcknowledged=5001:2000,5002:2001,5003:2002",
+ result.StateSnapshot);
+ }
+ }
+
+ ///
+ /// 断言投递分支序列:All-in-One 恒 case 1(D3 验收:100% 命中);三进程拓扑按 cell 划分分支。
+ ///
+ private static void AssertExpectedDeliveryBranches(
+ EquivalenceChainKind chainKind,
+ EquivalenceTopologyKind topologyKind,
+ ChainExecutionResult result)
+ {
+ // 3 次请求 + 3 次应答
+ Assert.Equal(6, result.Deliveries.Count);
+
+ if (topologyKind == EquivalenceTopologyKind.AllInOne)
+ {
+ // D3 验收:All-in-One 形态下 case 1 本地直投被 100% 命中
+ Assert.All(result.Deliveries, delivery => Assert.Equal(RoleRouteDelivery.LocalActor, delivery));
+ return;
+ }
+
+ // 三进程拓扑:Gate | Game+Social | Match —— 链路二的全部跳都在 Game+Social cell 内,其余链路全部跨 cell
+ var expectedBranch = chainKind == EquivalenceChainKind.GameToSocialFriendQuery
+ ? RoleRouteDelivery.LocalActor
+ : RoleRouteDelivery.RemoteForwarded;
+ Assert.All(result.Deliveries, delivery => Assert.Equal(expectedBranch, delivery));
+ }
+
+ ///
+ /// 构造重复的消息名序列。
+ ///
+ private static List Repeat(string messageName, int count)
+ {
+ var names = new List(count);
+ for (var index = 0; index < count; index++)
+ {
+ names.Add(messageName);
+ }
+
+ return names;
+ }
+}
diff --git a/Tests/GameFrameX.Tests/Topology/Equivalence/TopologyWorld.cs b/Tests/GameFrameX.Tests/Topology/Equivalence/TopologyWorld.cs
new file mode 100644
index 00000000..67a94fe0
--- /dev/null
+++ b/Tests/GameFrameX.Tests/Topology/Equivalence/TopologyWorld.cs
@@ -0,0 +1,273 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
+// 均受中华人民共和国及相关国际法律法规保护。
+// are protected by the laws of the People's Republic of China and related international regulations.
+// 使用本项目须严格遵守相应法律法规与开源许可证之规定。
+// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// please see the LICENSE file in the root directory of the source code for the full license text.
+// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
+// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
+// 侵犯他人合法权益等法律法规所禁止的行为!
+// or infringe upon the legitimate rights and interests of others as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or liabilities arising from secondary development based on this project
+// 本组织与贡献者概不承担。
+// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
+// GitHub 仓库:https://github.com/GameFrameX
+// GitHub Repository: https://github.com/GameFrameX
+// Gitee 仓库:https://gitee.com/GameFrameX
+// Gitee Repository: https://gitee.com/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+
+using System.Collections.Concurrent;
+using GameFrameX.NetWork.Messages;
+using GameFrameX.NetWork.RemoteMessaging.Routing;
+
+namespace GameFrameX.Tests.Topology.Equivalence;
+
+///
+/// 等价用例集的拓扑世界:把若干模拟进程 cell 组装成一种可路由的拓扑(C143c D9)。
+///
+///
+/// Topology world for the equivalence suite (C143c D9): assembles simulated process cells
+/// into a routable topology. AllInOne is one cell hosting every role (every hop must hit
+/// D3 case 1); MultiProcess follows the design source §D9 three-process split
+/// (Gate | Game+Social | Match) with a loopback forwarder standing in for the C143d
+/// remote hop. Handlers may route replies back through — a
+/// handler runs on its own mailbox consumer, so cross-role round trips never deadlock.
+///
+public sealed class TopologyWorld : IDisposable
+{
+ ///
+ /// 单个模拟进程 cell:角色集 + 路由器 + 邮箱。
+ ///
+ private sealed class TopologyCell
+ {
+ /// cell 承载的 Role 名 / The role names hosted by this cell
+ public List RoleNames { get; } = new List();
+
+ /// cell 路由器 / The cell router
+ public InProcessRoleRouter Router { get; set; }
+
+ /// Role → 邮箱 / Role to mailbox
+ public Dictionary Mailboxes { get; } = new Dictionary();
+ }
+
+ ///
+ /// cell 内本地投递器:按信封目标 Role 分发到对应邮箱。
+ ///
+ private sealed class CellMailboxDispatcher : ILocalRoleMessageDispatcher
+ {
+ /// cell 邮箱表 / The cell mailboxes
+ private readonly IReadOnlyDictionary _mailboxes;
+
+ ///
+ /// 初始化投递器。
+ ///
+ public CellMailboxDispatcher(IReadOnlyDictionary mailboxes)
+ {
+ _mailboxes = mailboxes;
+ }
+
+ ///
+ public Task DispatchAsync(MessageEnvelope envelope, CancellationToken cancellationToken = default)
+ {
+ if (envelope == null)
+ {
+ throw new ArgumentNullException(nameof(envelope));
+ }
+
+ if (!_mailboxes.TryGetValue(envelope.TargetRole, out var mailbox))
+ {
+ throw new RouteNotFoundException(envelope.TargetRole, $"No mailbox for target role '{envelope.TargetRole}' in this cell.");
+ }
+
+ return mailbox.DeliverAsync(envelope, cancellationToken);
+ }
+ }
+
+ ///
+ /// 拓扑内全部 cell。
+ ///
+ private readonly List _cells = new List();
+
+ ///
+ /// Role 名 → 承载 cell。
+ ///
+ private readonly Dictionary _cellByRole = new Dictionary();
+
+ ///
+ /// Role 名 → 邮箱。
+ ///
+ private readonly Dictionary _mailboxByRole = new Dictionary();
+
+ ///
+ /// 全部 Role 名(固定顺序)。
+ ///
+ private readonly List _allRoleNames;
+
+ ///
+ /// 每一跳投递分支记录(含处理器内应答跳)。
+ ///
+ private readonly ConcurrentQueue _deliveries = new ConcurrentQueue();
+
+ ///
+ /// Role 业务状态存储(确定性快照用)。
+ ///
+ private readonly ConcurrentDictionary _roleState = new ConcurrentDictionary();
+
+ ///
+ /// 初始化拓扑世界。
+ ///
+ /// 每个 cell 承载的 Role 名分组 / The role name group of every cell
+ private TopologyWorld(IEnumerable> cellRoleGroups)
+ {
+ var allRoleNames = new List();
+ var cellRoutersByRolePlaceholder = new Dictionary();
+ LoopbackRemoteRoleRouter loopbackForwarder = new LoopbackRemoteRoleRouter(cellRoutersByRolePlaceholder);
+
+ foreach (var roleGroup in cellRoleGroups)
+ {
+ var cell = new TopologyCell();
+ cell.RoleNames.AddRange(roleGroup);
+ foreach (var roleName in roleGroup)
+ {
+ var mailbox = new RoleMailbox(roleName);
+ cell.Mailboxes[roleName] = mailbox;
+ _mailboxByRole[roleName] = mailbox;
+ _cellByRole[roleName] = cell;
+ allRoleNames.Add(roleName);
+ }
+
+ var dispatcher = new CellMailboxDispatcher(cell.Mailboxes);
+ cell.Router = new InProcessRoleRouter(cell.RoleNames, dispatcher, loopbackForwarder);
+ _cells.Add(cell);
+ foreach (var roleName in roleGroup)
+ {
+ cellRoutersByRolePlaceholder[roleName] = cell.Router;
+ }
+ }
+
+ _allRoleNames = allRoleNames;
+ }
+
+ ///
+ /// 创建 All-in-One 拓扑:单 cell 承载全部 Role,每一跳都必须命中 D3 case 1。
+ ///
+ /// 全部 Role 名 / All role names
+ /// 拓扑世界 / The topology world
+ public static TopologyWorld CreateAllInOne(IReadOnlyList roleNames)
+ {
+ return new TopologyWorld(new List> { roleNames });
+ }
+
+ ///
+ /// 创建三进程拓扑(设计源 §D9):Gate 进程 / Game+Social 进程 / Match 进程。
+ ///
+ /// Gate Role 名 / The Gate role name
+ /// Game Role 名 / The Game role name
+ /// Social Role 名 / The Social role name
+ /// Match Role 名 / The Match role name
+ /// 拓扑世界 / The topology world
+ public static TopologyWorld CreateMultiProcess(string gateRoleName, string gameRoleName, string socialRoleName, string matchRoleName)
+ {
+ var cellRoleGroups = new List>
+ {
+ new List { gateRoleName },
+ new List { gameRoleName, socialRoleName },
+ new List { matchRoleName },
+ };
+ return new TopologyWorld(cellRoleGroups);
+ }
+
+ ///
+ /// 获取指定 Role 的邮箱。
+ ///
+ /// Role 名 / The role name
+ public RoleMailbox GetMailbox(string roleName)
+ {
+ return _mailboxByRole[roleName];
+ }
+
+ ///
+ /// 从源 Role 向目标 Role 路由一封消息(带跳超时),并记录投递分支。
+ ///
+ ///
+ /// Routes one message hop from the source role's cell router — the routing decision
+ /// belongs to the sending process (D3), so the source role selects the cell.
+ /// The hop timeout is applied through a cancellation token, identically in both topologies.
+ ///
+ /// 源 Role 名 / The source role name
+ /// 目标 Role 名 / The target role name
+ /// 消息 / The message
+ /// 跳超时(毫秒)/ The hop timeout in milliseconds
+ /// 投递分支 / The delivery branch
+ public async Task RouteAsync(string sourceRoleName, string targetRoleName, MessageObject message, int hopTimeoutMilliseconds)
+ {
+ var envelope = new MessageEnvelope(targetRoleName, message);
+ using (var cancellationSource = new CancellationTokenSource(hopTimeoutMilliseconds))
+ {
+ var delivery = await _cellByRole[sourceRoleName].Router.RouteAsync(envelope, cancellationSource.Token);
+ _deliveries.Enqueue(delivery);
+ return delivery;
+ }
+ }
+
+ ///
+ /// 写入一条 Role 业务状态(确定性快照的组成项)。
+ ///
+ /// Role 名 / The role name
+ /// 状态键 / The state key
+ /// 状态值 / The state value
+ public void SetRoleState(string roleName, string stateKey, string stateValue)
+ {
+ _roleState[$"{roleName}|{stateKey}"] = stateValue;
+ }
+
+ ///
+ /// 捕获链路执行结果快照。
+ ///
+ /// 链路执行结果 / The chain execution result
+ public ChainExecutionResult CaptureResult()
+ {
+ var arrivalOrderByRole = new Dictionary>();
+ foreach (var roleName in _allRoleNames)
+ {
+ arrivalOrderByRole[roleName] = _mailboxByRole[roleName].GetArrivalLog();
+ }
+
+ var stateSnapshot = string.Join(
+ ";",
+ _roleState.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key}={pair.Value}"));
+
+ return new ChainExecutionResult
+ {
+ Deliveries = _deliveries.ToList(),
+ ArrivalOrderByRole = arrivalOrderByRole,
+ StateSnapshot = stateSnapshot,
+ };
+ }
+
+ ///
+ /// 关闭全部邮箱。
+ ///
+ public void Dispose()
+ {
+ foreach (var cell in _cells)
+ {
+ foreach (var mailbox in cell.Mailboxes.Values)
+ {
+ mailbox.Close();
+ }
+ }
+ }
+}