| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- using Admin.NET.Plugin.AiDOP.Entity;
- using Admin.NET.Plugin.AiDOP.SmartOps;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.SmartOps;
- public class S1KpiTargetSnapshotRefreshServiceTests
- {
- private static S1KpiTargetSnapshotRefreshService Svc(FakeS1KpiDailySnapshotStore daily, FakeKpiTargetStore configs) =>
- new(daily, new KpiTargetResolver(configs, new LegacyKpiTargetProvider(new FakeDailyReader(), Microsoft.Extensions.Logging.Abstractions.NullLogger<LegacyKpiTargetProvider>.Instance), Microsoft.Extensions.Logging.Abstractions.NullLogger<KpiTargetResolver>.Instance));
- [Fact]
- public async Task FormalFactory_OverwritesLegacySnapshot_OnMaxBizDateOnly()
- {
- var daily = new FakeS1KpiDailySnapshotStore();
- daily.Add(1, 9, 1, new DateTime(2026, 8, 13), "S1_L1_001", 20.3m, 20m);
- daily.Add(1, 9, 1, new DateTime(2026, 8, 14), "S1_L1_001", 20.3m, 20m);
- daily.Add(2, 9, 1, new DateTime(2026, 8, 14), "S1_L2_001", 1m, 3m);
- var configs = new FakeKpiTargetStore();
- configs.Configs.Add(Cfg(11, 1, 10m, new DateTime(2026, 8, 14), metricCode: "S1_L1_001"));
- configs.Configs.Add(Cfg(12, 1, 10m, new DateTime(2026, 8, 14), metricCode: "S1_L2_001"));
- var result = await Svc(daily, configs).RefreshAsync(9, 1);
- Assert.True(result.Ok);
- Assert.Equal(2, result.UpdatedRows);
- Assert.Equal(10m, daily.Get(1, 9, 1, new DateTime(2026, 8, 14), "S1_L1_001").TargetValue);
- Assert.Equal(KpiTargetSnapshotSource.FormalFactory, daily.Get(1, 9, 1, new DateTime(2026, 8, 14), "S1_L1_001").TargetSource);
- Assert.Equal(11, daily.Get(1, 9, 1, new DateTime(2026, 8, 14), "S1_L1_001").TargetConfigId);
- Assert.Equal(20m, daily.Get(1, 9, 1, new DateTime(2026, 8, 13), "S1_L1_001").TargetValue);
- Assert.Equal(20.3m, daily.Get(1, 9, 1, new DateTime(2026, 8, 14), "S1_L1_001").MetricValue);
- Assert.Equal(10m, daily.Get(2, 9, 1, new DateTime(2026, 8, 14), "S1_L2_001").TargetValue);
- }
- [Fact]
- public async Task TenantDefault_UsedWhenFactoryMissing()
- {
- var daily = new FakeS1KpiDailySnapshotStore();
- daily.Add(1, 9, 1, DateTime.Today, "S1_L1_001", 8m, 20m);
- var configs = new FakeKpiTargetStore();
- configs.Configs.Add(Cfg(7, 0, 5m, DateTime.Today.AddDays(-10), metricCode: "S1_L1_001"));
- var result = await Svc(daily, configs).RefreshAsync(9, 1);
- Assert.Equal(1, result.UpdatedRows);
- Assert.Equal(5m, daily.Get(1, 9, 1, DateTime.Today, "S1_L1_001").TargetValue);
- Assert.Equal(KpiTargetSnapshotSource.FormalTenantDefault, daily.Get(1, 9, 1, DateTime.Today, "S1_L1_001").TargetSource);
- }
- [Fact]
- public async Task Missing_DoesNotOverwrite()
- {
- var daily = new FakeS1KpiDailySnapshotStore();
- daily.Add(1, 9, 1, DateTime.Today, "S5_L1_001", 8m, 20m);
- var result = await Svc(daily, new FakeKpiTargetStore()).RefreshAsync(9, 1);
- Assert.Equal(0, result.UpdatedRows);
- Assert.True(result.MissingRows >= 1);
- Assert.Equal(20m, daily.Get(1, 9, 1, DateTime.Today, "S5_L1_001").TargetValue);
- Assert.Null(daily.Get(1, 9, 1, DateTime.Today, "S5_L1_001").TargetSource);
- }
- [Fact]
- public async Task DoesNotTouchOtherTenantOrFactory()
- {
- var daily = new FakeS1KpiDailySnapshotStore();
- daily.Add(1, 9, 1, DateTime.Today, "S1_L1_001", 8m, 20m);
- daily.Add(1, 8, 1, DateTime.Today, "S1_L1_001", 8m, 20m);
- daily.Add(1, 9, 2, DateTime.Today, "S1_L1_001", 8m, 20m);
- var configs = new FakeKpiTargetStore();
- configs.Configs.Add(Cfg(1, 1, 10m, DateTime.Today.AddDays(-1), metricCode: "S1_L1_001"));
- await Svc(daily, configs).RefreshAsync(9, 1);
- Assert.Equal(10m, daily.Get(1, 9, 1, DateTime.Today, "S1_L1_001").TargetValue);
- Assert.Equal(20m, daily.Get(1, 8, 1, DateTime.Today, "S1_L1_001").TargetValue);
- Assert.Equal(20m, daily.Get(1, 9, 2, DateTime.Today, "S1_L1_001").TargetValue);
- }
- [Fact]
- public async Task LevelFailure_RollsBackAllLevels()
- {
- var daily = new FakeS1KpiDailySnapshotStore { FailOnLevel = 3 };
- daily.Add(1, 9, 1, DateTime.Today, "S1_L1_001", 8m, 20m);
- daily.Add(3, 9, 1, DateTime.Today, "S1_L3_001", 8m, 20m);
- var configs = new FakeKpiTargetStore();
- configs.Configs.Add(Cfg(1, 1, 10m, DateTime.Today.AddDays(-1), metricCode: "S1_L1_001"));
- configs.Configs.Add(Cfg(2, 1, 10m, DateTime.Today.AddDays(-1), metricCode: "S1_L3_001"));
- await Assert.ThrowsAsync<InvalidOperationException>(() => Svc(daily, configs).RefreshAsync(9, 1));
- Assert.Equal(20m, daily.Get(1, 9, 1, DateTime.Today, "S1_L1_001").TargetValue);
- }
- [Fact]
- public async Task NoDailyRows_ReturnsZero()
- {
- var result = await Svc(new FakeS1KpiDailySnapshotStore(), new FakeKpiTargetStore()).RefreshAsync(9, 1);
- Assert.True(result.Ok);
- Assert.Equal(0, result.UpdatedRows);
- Assert.Contains("暂无", result.Message);
- }
- private static AdoSmartOpsKpiTargetConfig Cfg(long id, long factoryId, decimal value, DateTime from, DateTime? to = null, string metricCode = "S1_L1_001") =>
- new()
- {
- Id = id,
- TenantId = 9,
- FactoryId = factoryId,
- MetricCode = metricCode,
- TargetValue = value,
- EffectiveFrom = from,
- EffectiveTo = to,
- SourceType = "MANUAL",
- Status = 1,
- CreateTime = DateTime.Now,
- UpdateTime = DateTime.Now
- };
- }
- internal sealed class FakeS1KpiDailySnapshotStore : IS1KpiDailySnapshotStore
- {
- public int? FailOnLevel { get; set; }
- private readonly List<Row> _rows = new();
- public void Add(int level, long tenantId, long factoryId, DateTime bizDate, string metric, decimal metricValue, decimal? target)
- => _rows.Add(new Row
- {
- Level = level,
- TenantId = tenantId,
- FactoryId = factoryId,
- BizDate = bizDate.Date,
- MetricCode = metric,
- MetricValue = metricValue,
- TargetValue = target
- });
- public Row Get(int level, long tenantId, long factoryId, DateTime bizDate, string metric) =>
- _rows.Single(x => x.Level == level && x.TenantId == tenantId && x.FactoryId == factoryId && x.BizDate == bizDate.Date && x.MetricCode == metric);
- public async Task ExecuteInTransactionAsync(Func<Task> action, CancellationToken ct = default)
- {
- var backup = _rows.Select(x => x.Clone()).ToList();
- try
- {
- await action();
- }
- catch
- {
- _rows.Clear();
- _rows.AddRange(backup);
- throw;
- }
- }
- public Task<DateTime?> GetMaxBizDateAsync(int level, long tenantId, long factoryId, string moduleCode, CancellationToken ct = default)
- {
- _ = moduleCode;
- var dates = _rows.Where(x => x.Level == level && x.TenantId == tenantId && x.FactoryId == factoryId).Select(x => x.BizDate).ToList();
- DateTime? max = dates.Count == 0 ? null : dates.Max();
- return Task.FromResult(max);
- }
- public Task<List<S1KpiDailySnapshotRow>> ListLatestAsync(int level, long tenantId, long factoryId, DateTime bizDate, string moduleCode, CancellationToken ct = default)
- {
- _ = moduleCode;
- return Task.FromResult(_rows.Where(x => x.Level == level && x.TenantId == tenantId && x.FactoryId == factoryId && x.BizDate == bizDate.Date)
- .Select(x => new S1KpiDailySnapshotRow { MetricCode = x.MetricCode, BizDate = x.BizDate, MetricValue = x.MetricValue })
- .ToList());
- }
- public Task<int> UpdateTargetsAsync(int level, long tenantId, long factoryId, DateTime bizDate, string metricCode, KpiTargetSnapshot snap, string moduleCode, CancellationToken ct = default)
- {
- if (FailOnLevel == level)
- throw new InvalidOperationException($"fail level {level}");
- var hits = _rows.Where(x => x.Level == level && x.TenantId == tenantId && x.FactoryId == factoryId && x.BizDate == bizDate.Date && x.MetricCode == metricCode).ToList();
- foreach (var row in hits)
- {
- row.TargetValue = snap.TargetValue;
- row.TargetConfigId = snap.TargetConfigId;
- row.TargetSource = snap.TargetSource;
- row.TargetResolvedAt = snap.ResolvedAt;
- }
- return Task.FromResult(hits.Count);
- }
- internal sealed class Row
- {
- public int Level { get; set; }
- public long TenantId { get; set; }
- public long FactoryId { get; set; }
- public DateTime BizDate { get; set; }
- public string MetricCode { get; set; } = string.Empty;
- public decimal? MetricValue { get; set; }
- public decimal? TargetValue { get; set; }
- public long? TargetConfigId { get; set; }
- public string TargetSource { get; set; }
- public DateTime? TargetResolvedAt { get; set; }
- public Row Clone() => (Row)MemberwiseClone();
- }
- }
|