using System.Reflection; using Furion.DependencyInjection; using System.Runtime.CompilerServices; using Admin.NET.Plugin.AiDOP.Entity.S8; using Admin.NET.Plugin.AiDOP.Service.S8; using Admin.NET.Plugin.AiDOP.Service.S8.Rules; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S8; /// /// S8-STEP6E-CFG-WATCH-ORCHESTRATION-DEDUPE-UI-CERT-1:钉住建单 seam 的 /// 「生产语义等价 + 纯委派」契约。 /// /// seam 的全部价值在于「认证时可替换、生产时透明」。一旦有人往正式实现里加逻辑, /// Watch 认证得到的就不再是生产行为——本文件就是防这个的。 /// public class S8ExceptionReportDispatcherTests { /// 接口方法签名必须与被委派的真实方法逐字一致,否则 seam 就偷偷改了契约。 [Fact] public void InterfaceSignature_MatchesRealService() { var iface = typeof(IS8ExceptionReportDispatcher).GetMethod("CreateFromHitAsync")!; var real = typeof(S8ManualReportService).GetMethod("CreateFromHitAsync")!; Assert.Equal(real.ReturnType, iface.ReturnType); Assert.Equal( real.GetParameters().Select(p => p.ParameterType).ToArray(), iface.GetParameters().Select(p => p.ParameterType).ToArray()); Assert.Equal( new[] { typeof(long), typeof(long), typeof(S8RuleHit) }, iface.GetParameters().Select(p => p.ParameterType).ToArray()); } /// 正式实现只能持有被委派的服务这一个依赖——多一个字段就说明它开始自己干活了。 [Fact] public void ProductionImplementation_HasOnlyTheDelegateDependency() { var fields = typeof(S8ExceptionReportDispatcher) .GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); Assert.Single(fields); Assert.Equal(typeof(S8ManualReportService), fields[0].FieldType); } /// /// 真正的委派证据:用未初始化的 S8ManualReportService 构造 dispatcher 并调用, /// 抛出的异常栈里必须出现 S8ManualReportService.CreateFromHitAsync—— /// 即调用确实**穿透到了真实服务**,而不是被 dispatcher 自己拦下或改写。 /// (不连 DB:真实服务在触仓储时抛 NRE,这正是我们要的穿透信号。) /// [Fact] public void ProductionImplementation_DelegatesIntoRealService() { var svc = (S8ManualReportService)RuntimeHelpers.GetUninitializedObject(typeof(S8ManualReportService)); var dispatcher = new S8ExceptionReportDispatcher(svc); var ex = Record.ExceptionAsync( () => dispatcher.CreateFromHitAsync(1L, 2L, new S8RuleHit())).GetAwaiter().GetResult(); Assert.NotNull(ex); var trace = ex!.ToString(); Assert.Contains(nameof(S8ManualReportService), trace); Assert.Contains("CreateFromHitAsync", trace); } /// /// 参数必须原样透传——tenantId / factoryId 不得被 dispatcher 改写或吞掉。 /// 用真实服务最外层的守卫(tenantId<=0 或 factoryId<=0 抛 S8BizException)作为探针: /// 若 dispatcher 篡改了入参,这个守卫就不会按预期触发。 /// [Theory] [InlineData(0L, 2L)] [InlineData(1L, 0L)] [InlineData(-1L, -1L)] public void ProductionImplementation_PassesArgumentsThroughUnchanged(long tenantId, long factoryId) { var svc = (S8ManualReportService)RuntimeHelpers.GetUninitializedObject(typeof(S8ManualReportService)); var dispatcher = new S8ExceptionReportDispatcher(svc); var ex = Record.ExceptionAsync( () => dispatcher.CreateFromHitAsync(tenantId, factoryId, new S8RuleHit())).GetAwaiter().GetResult(); Assert.IsType(ex); } /// seam 必须被 Furion 自动注册(ITransient),不需要改 Startup。 [Fact] public void ProductionImplementation_IsAutoRegistrable() { Assert.True(typeof(IS8ExceptionReportDispatcher).IsAssignableFrom(typeof(S8ExceptionReportDispatcher))); Assert.True(typeof(ITransient).IsAssignableFrom(typeof(S8ExceptionReportDispatcher))); Assert.True(typeof(S8ExceptionReportDispatcher).IsSealed); } /// 调度器必须消费 seam 接口而不是直接持有具体服务去自动建单。 [Fact] public void Scheduler_DependsOnTheSeamInterface() { var ctor = typeof(S8WatchSchedulerService).GetConstructors().Single(); Assert.Contains(ctor.GetParameters(), p => p.ParameterType == typeof(IS8ExceptionReportDispatcher)); } }