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));
}
}