using ExperionCrawler.Core.Application.Feedforward; using ExperionCrawler.Infrastructure.Control; using Xunit; namespace ExperionCrawler.Tests; public class FeedforwardFrontTests { [Fact] public void Front_stable_within_band() { var ind = new FrontPositionIndicator(bandwidth: 0.3); for (int i = 0; i < 50; i++) ind.Update(100.0, tsSec: 2, refTauSec: 60, strongSignal: true); var (state, trim, grade) = ind.Update(100.1, 2, 60, true); Assert.Contains("정상", state); Assert.Null(trim); Assert.Equal(Confidence.B, grade); } [Fact] public void Front_rise_triggers_reflux_advice() { var ind = new FrontPositionIndicator(bandwidth: 0.3); for (int i = 0; i < 200; i++) ind.Update(100.0, tsSec: 2, refTauSec: 60, strongSignal: false); var (state, trim, grade) = ind.Update(105.0, 2, 60, false); Assert.Contains("상승", state); Assert.Equal("환류↑ 권장", trim); Assert.Equal(Confidence.C, grade); } [Fact] public void Front_fall_triggers_boilup_advice() { var ind = new FrontPositionIndicator(bandwidth: 0.3); for (int i = 0; i < 200; i++) ind.Update(100.0, tsSec: 2, refTauSec: 60, strongSignal: true); var (state, trim, _) = ind.Update(95.0, 2, 60, true); Assert.Contains("하강", state); Assert.Contains("boilup", trim); } // ── ApplyFront 부호 규약 (브레인스토밍 §10.2-A 버그픽스 회귀) ────────── // temp_tags = [A하단 … D상단], 정상 A>B>C>D 단조감소. // front metric은 "상단−하단"이어야 함: 프론트 상승(heavies↑→상단 가열) 시 metric↑ → dev>0 → 상승/환류↑. private static ColumnConfig FrontCfg() => new() { Id = 1, Name = "C-FRONT", Enabled = true, FeedTag = "f", ProductKey = "P", ScanSec = 2, DTdP = 0.0, PRef = double.NaN, PressureTag = null, // 압력경로 비활성 → pUnstable 없음 FeedMoveThresholdPerMin = 0.0, // moving 비활성 → transient 없음 TempTags = new[] { "a", "b", "c", "d" }, Streams = new[] { new StreamConfig { Key = "P", FlowTag = "ficq-6118", Role = StreamRole.Commanded, Grade = Confidence.A, TargetCoeff = 0.95 } } }; private static PvSnapshot FrontSnap(double a, double b, double c, double d) => new( new TagSample("f", 100, true, DateTime.UtcNow), null, Array.Empty(), new Dictionary { ["P"] = new TagSample("ficq-6118", 95, true, DateTime.UtcNow) }) { Temps = new[] { new TagSample("a", a, true, DateTime.UtcNow), new TagSample("b", b, true, DateTime.UtcNow), new TagSample("c", c, true, DateTime.UtcNow), new TagSample("d", d, true, DateTime.UtcNow) } }; [Fact] public void ApplyFront_top_warming_is_front_rise_reflux_advice() { var engine = new FeedforwardEngine(); var st = new ColumnState(); // 정상 단조감소(A=110>B>C>D=90)로 baseline 시드 for (int i = 0; i < 5; i++) engine.Tick(FrontCfg(), FrontSnap(110, 105, 100, 90), st, DateTime.UtcNow); // 상단(D) 가열 = heavies 상승 = 프론트 상승 → 상승/환류↑ (버그였다면 하강/boilup로 반전) var res = engine.Tick(FrontCfg(), FrontSnap(110, 105, 100, 100), st, DateTime.UtcNow); Assert.Contains("상승", res.FrontPositionState); Assert.Equal("환류↑ 권장", res.FrontTrimAdvice); } [Fact] public void ApplyFront_top_cooling_is_front_fall_boilup_advice() { var engine = new FeedforwardEngine(); var st = new ColumnState(); for (int i = 0; i < 5; i++) engine.Tick(FrontCfg(), FrontSnap(110, 105, 100, 90), st, DateTime.UtcNow); // 상단(D) 추가 냉각 = 프론트 하강 → 하강/boilup var res = engine.Tick(FrontCfg(), FrontSnap(110, 105, 100, 80), st, DateTime.UtcNow); Assert.Contains("하강", res.FrontPositionState); Assert.Contains("boilup", res.FrontTrimAdvice); } }