Переглянути джерело

feat(house-fault): StartupFaultPolicy 坏舱剔除/致命判定(TDD 真值表 6绿)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie 1 день тому
батько
коміт
761a996

+ 71 - 0
ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/StartupFaultPolicyTests.cs

@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Linq;
+using IvfTl.Control.Entity.InitEntitys;
+using ivf_tl_Control;
+using Xunit;
+
+namespace IvfTl.ControlHost.Tests
+{
+    public class StartupFaultPolicyTests
+    {
+        private static HouseFault Fault(int sn, HouseFaultType t = HouseFaultType.CcdSnMissing)
+            => new HouseFault { HouseSn = sn, Type = t, Reason = "x", Stage = "扫口握手" };
+
+        [Fact]
+        public void BadHouseSns_Only_Counts_Known_Houses()
+        {
+            var faults = new List<HouseFault>
+            {
+                Fault(6),
+                Fault(-1, HouseFaultType.CameraDuplicateSn), // 舱号未知 → 不算坏舱
+                Fault(8),
+            };
+            var bad = StartupFaultPolicy.BadHouseSns(faults);
+            Assert.Equal(new[] { 6, 8 }, bad.OrderBy(x => x).ToArray());
+        }
+
+        [Fact]
+        public void RunnableHouses_Excludes_Bad_And_Sorts()
+        {
+            var discovered = new[] { 2, 4, 6, 7, 8, 9, 11 };
+            var faults = new List<HouseFault> { Fault(6), Fault(8) };
+            var run = StartupFaultPolicy.RunnableHouses(discovered, faults);
+            Assert.Equal(new[] { 2, 4, 7, 9, 11 }, run.ToArray());
+        }
+
+        [Fact]
+        public void RunnableHouses_With_No_Faults_Returns_All_Discovered()
+        {
+            var discovered = new[] { 2, 4, 6 };
+            var run = StartupFaultPolicy.RunnableHouses(discovered, new List<HouseFault>());
+            Assert.Equal(new[] { 2, 4, 6 }, run.ToArray());
+        }
+
+        [Fact]
+        public void RunnableHouses_UnknownHouseFault_Does_Not_Exclude_Anyone()
+        {
+            var discovered = new[] { 2, 4 };
+            var faults = new List<HouseFault> { Fault(-1, HouseFaultType.CameraReadFailed) };
+            var run = StartupFaultPolicy.RunnableHouses(discovered, faults);
+            Assert.Equal(new[] { 2, 4 }, run.ToArray());
+        }
+
+        [Fact]
+        public void IsFatal_True_Only_When_No_Runnable_House()
+        {
+            Assert.True(StartupFaultPolicy.IsFatal(new List<int>()));
+            Assert.False(StartupFaultPolicy.IsFatal(new List<int> { 11 }));
+            Assert.False(StartupFaultPolicy.IsFatal(new List<int> { 2, 4 }));
+        }
+
+        [Fact]
+        public void Partial_Failure_Is_Never_Fatal()
+        {
+            // 7 个舱里坏 2 个 → 仍 5 个可跑 → 不致命(核心需求)
+            var discovered = new[] { 2, 4, 6, 7, 8, 9, 11 };
+            var faults = new List<HouseFault> { Fault(6), Fault(8) };
+            var run = StartupFaultPolicy.RunnableHouses(discovered, faults);
+            Assert.False(StartupFaultPolicy.IsFatal(run));
+        }
+    }
+}

+ 26 - 0
ivf_tl_operate_2.0/control/ivf_tl_Control/StartupFaultPolicy.cs

@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Linq;
+using IvfTl.Control.Entity.InitEntitys;
+
+namespace ivf_tl_Control
+{
+    /// <summary>
+    /// 启动期坏舱决策(无 I/O,纯函数,可单测)。
+    /// 规则:坏舱=故障清单中已知舱号(HouseSn>0);可跑舱=发现的舱−坏舱;致命=零可跑舱。
+    /// 单舱/部分舱坏绝不致命 —— 落实 spec §34。
+    /// </summary>
+    public static class StartupFaultPolicy
+    {
+        public static HashSet<int> BadHouseSns(IEnumerable<HouseFault> faults)
+            => faults.Where(f => f.HouseSn > 0).Select(f => f.HouseSn).ToHashSet();
+
+        public static List<int> RunnableHouses(IEnumerable<int> discovered, IEnumerable<HouseFault> faults)
+        {
+            var bad = BadHouseSns(faults);
+            return discovered.Where(sn => !bad.Contains(sn)).Distinct().OrderBy(x => x).ToList();
+        }
+
+        public static bool IsFatal(IReadOnlyList<int> runnableHouses)
+            => runnableHouses == null || runnableHouses.Count == 0;
+    }
+}