Phase II:
- FfOperatorAction entity + ff_operator_action DDL/DbSet
- IFeedforwardWriteGuard + FeedforwardWriteGuard (SP bounds, grade C, transient, NaN)
- IFeedforwardAuditService + FeedforwardAuditService (raw ADO insert/query)
- FeedforwardSupervisor.AutoWriteAsync (per-stream OPC UA after Tick, rate-limited)
- FeedforwardConfigStore: advisory_only now read/writes DB, sp_node_id column
- FeedforwardController: auth (X-Kb-Token) on config/delete/write/audit;
POST write/{id}/{key} manual SP write; GET audit; write results in MapColumn
- ff.js: token header, auto-write badge, per-stream write result, spNodeId, advisoryOnly
- ff.css: .ff-write-badge, .ff-write, .ff-write-err, .ff-wg-blocked
- Program.cs: register audit (Scoped) + write guard (Singleton)
WO-2~7 (build 0W/0E, test 22/22):
- PCT monitor, θ auto-tune, slow bias, front position indicator,
total reflux recovery, config form expansion
60 lines
2.6 KiB
C#
60 lines
2.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using ExperionCrawler.Core.Application.Feedforward;
|
|
using ExperionCrawler.Infrastructure.Control;
|
|
using Xunit;
|
|
|
|
namespace ExperionCrawler.Tests;
|
|
|
|
/// <summary>WO-1 (P-5 confidence 자동강등) 엔진 통합 검증. Downgrade는 private이라 Tick 경유로 관측.</summary>
|
|
public class FeedforwardEngineTests
|
|
{
|
|
private static ColumnConfig Cfg(double feedFilterTau = 300, double moveThr = 0) => new()
|
|
{
|
|
Id = 1, Name = "C-TEST", Enabled = true, FeedTag = "f", ProductKey = "P",
|
|
ScanSec = 2, FeedFilterTauSec = feedFilterTau, FeedMoveThresholdPerMin = moveThr,
|
|
Streams = new[]
|
|
{
|
|
new StreamConfig { Key = "P", FlowTag = "ficq-6118", Role = StreamRole.Commanded,
|
|
Grade = Confidence.A, TargetCoeff = 0.95 }
|
|
}
|
|
};
|
|
|
|
private static PvSnapshot Snap(double feed, double streamVal, bool streamGood) => new(
|
|
new TagSample("f", feed, true, DateTime.UtcNow),
|
|
null,
|
|
Array.Empty<TagSample>(),
|
|
new Dictionary<string, TagSample> { ["P"] = new TagSample("ficq-6118", streamVal, streamGood, DateTime.UtcNow) });
|
|
|
|
[Fact] // 정상·신선 → config 등급(A) 유지, 사유 없음
|
|
public void Fresh_normal_keeps_config_grade()
|
|
{
|
|
var res = new FeedforwardEngine().Tick(Cfg(), Snap(100, 95, true), new ColumnState(), DateTime.UtcNow);
|
|
var p = res.Streams[0];
|
|
Assert.Equal(Confidence.A, p.Grade);
|
|
Assert.Null(p.GradeReason);
|
|
}
|
|
|
|
[Fact] // PV 신선도 불량 → 한 단계 강등(A→B) + 사유
|
|
public void Stale_pv_downgrades_one_level()
|
|
{
|
|
var res = new FeedforwardEngine().Tick(Cfg(), Snap(100, 80, false), new ColumnState(), DateTime.UtcNow);
|
|
var p = res.Streams[0];
|
|
Assert.Equal(Confidence.B, p.Grade);
|
|
Assert.Contains("신선도", p.GradeReason);
|
|
}
|
|
|
|
[Fact] // stale + 과도 동시 → 두 단계 강등(A→C) Clamp 확인
|
|
public void Stale_plus_transient_clamps_to_C()
|
|
{
|
|
var cfg = Cfg(feedFilterTau: 0, moveThr: 1); // 필터 off + 작은 임계 → 큰 피드점프가 과도 유발
|
|
var engine = new FeedforwardEngine();
|
|
var st = new ColumnState();
|
|
engine.Tick(cfg, Snap(100, 80, false), st, DateTime.UtcNow); // tick1: 시드
|
|
var res = engine.Tick(cfg, Snap(1000, 80, false), st, DateTime.UtcNow); // tick2: 피드 급변 → 과도
|
|
var p = res.Streams[0];
|
|
Assert.True(res.Transient);
|
|
Assert.Equal(Confidence.C, p.Grade); // A→(stale)B→(transient)C, C에서 클램프
|
|
}
|
|
}
|