#nullable enable using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; using NUnit.Framework; using ShrinkCommand.Integration; using ShrinkCommand.Integration.App; using ShrinkContext; using ShrinkDataSaver.Integration.App; using ShrinkNetwork; using ShrinkNetwork.Integration.App; namespace ShrinkCommand.Integration.Network.Tests { /// /// 阶段 3 集成组件模式(论文 6.5): /// 核心(command/network/datasaver)互不依赖,集成组件注入双方服务键; /// 全部提供者就绪才激活,任一退役即停用——依赖接线由响应式余效应完成。 /// public class IntegrationComponentsTests { private ShrinkContextRuntime _runtime = null!; [SetUp] public void SetUp() { _runtime = new ShrinkContextRuntime(); } [TearDown] public void TearDown() { _runtime.ShutdownAsync().GetAwaiter().GetResult(); } private static void Run(UniTask task) => task.GetAwaiter().GetResult(); [Test] public void Integration_ActivatesOnlyWhenBothProvidersActive() { var integration = _runtime.Use(new ShrinkCommandNetworkIntegrationComponent()); Assert.AreEqual(ShrinkFiberState.Inactive, integration.State, "两个服务键都缺失时等待"); var command = _runtime.Use(new ShrinkCommandAppComponent()); Assert.AreEqual(ShrinkFiberState.Active, command.State); Assert.AreEqual(ShrinkFiberState.Inactive, integration.State, "仅命令服务就绪仍等待网络服务"); var network = _runtime.Use(new ShrinkNetworkAppComponent()); Assert.AreEqual(ShrinkFiberState.Active, network.State); Assert.AreEqual(ShrinkFiberState.Active, integration.State, "双键齐备后集成组件激活并完成桥注册"); } [Test] public void EitherProviderRetire_DeactivatesIntegration_AndReactivatesOnReturn() { var command = _runtime.Use(new ShrinkCommandAppComponent()); var network = _runtime.Use(new ShrinkNetworkAppComponent()); var integration = _runtime.Use(new ShrinkCommandNetworkIntegrationComponent()); Assert.AreEqual(ShrinkFiberState.Active, integration.State); Run(_runtime.RetireAsync(network)); Assert.AreEqual(ShrinkFiberState.Inactive, integration.State, "网络提供者退役,集成组件自动停用(服务键撤回触发)"); Assert.AreEqual(ShrinkFiberState.Active, command.State, "命令核心不受影响"); var networkAgain = _runtime.Use(new ShrinkNetworkAppComponent()); Assert.AreEqual(ShrinkFiberState.Active, integration.State, "提供者回归后集成组件自动重新激活"); } [Test] public void CommandComponent_ProvidesDefaultServiceKey() { var fiber = _runtime.Use(new ShrinkCommandAppComponent()); Assert.AreEqual(ShrinkFiberState.Active, fiber.State); Assert.IsTrue(_runtime.TryGetRaw(_runtime.RootContext, ShrinkCommandAppComponent.ServiceKey, out var service)); Assert.AreSame(ShrinkCommandRuntime.Default, service); Run(_runtime.RetireAsync(fiber)); Assert.IsFalse(_runtime.TryGetRaw(_runtime.RootContext, ShrinkCommandAppComponent.ServiceKey, out _), "退役撤回服务键"); } [Test] public void DataSaverComponent_Activates_FlushesOnDeactivate_WithdrawsKey() { var fiber = _runtime.Use(new ShrinkDataSaverAppComponent()); Assert.AreEqual(ShrinkFiberState.Active, fiber.State); Assert.IsTrue(_runtime.TryGetRaw(_runtime.RootContext, ShrinkDataSaverAppComponent.ServiceKey, out _)); Assert.DoesNotThrow(() => Run(_runtime.RetireAsync(fiber)), "停用执行补偿式收尾(设置最终落盘;未初始化环境安全跳过)"); Assert.IsFalse(_runtime.TryGetRaw(_runtime.RootContext, ShrinkDataSaverAppComponent.ServiceKey, out _)); } [Test] public void DataSaverIntercept_ReadOnlyViewAllowsReadsAndDeniesWriterCapability() { var provider = _runtime.Use(new ShrinkDataSaverAppComponent()); Assert.AreEqual(ShrinkFiberState.Active, provider.State); var intercept = new Dictionary> { [ShrinkDataSaverAppComponent.ServiceKey] = new Dictionary { [ShrinkDataSaverAccess.MetadataKey] = ShrinkDataSaverAccess.ReadOnly } }; var consumerComponent = new DataSaverAccessConsumer(); var consumer = _runtime.Use(consumerComponent, intercept: intercept); Assert.AreEqual(ShrinkFiberState.Active, consumer.State); var reader = consumerComponent.GetReader(); Assert.IsNotNull(reader); Assert.IsFalse(reader is IShrinkDataSaverWriter, "只读上下文取得的对象不应再暴露写接口"); Assert.Throws(() => consumerComponent.GetWriter()); Assert.IsTrue(_runtime.TryGetRaw(_runtime.RootContext, ShrinkDataSaverAppComponent.ServiceKey, out _), "编排层 raw read 保留原始服务;intercept 只介导组件访问"); } private sealed class DataSaverAccessConsumer : IShrinkComponent { private ShrinkCtx? _ctx; public string Name => "test.datasaver.read-only-consumer"; public IReadOnlyList Inject { get; } = new[] { ShrinkDataSaverAppComponent.ServiceKey }; public IReadOnlyList Provide => Array.Empty(); public UniTask ApplyAsync(ShrinkCtx ctx, object? config) { _ctx = ctx; return UniTask.CompletedTask; } public IShrinkDataSaverReader GetReader() => Context.Get( ShrinkDataSaverAppComponent.ServiceKey); public IShrinkDataSaverWriter GetWriter() => Context.Get( ShrinkDataSaverAppComponent.ServiceKey); private ShrinkCtx Context => _ctx ?? throw new InvalidOperationException("Consumer is not active."); } } }