using NSubstitute; using ZB.MOM.WW.ScadaBridge.Commons.Types; using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening; using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates; using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening; namespace ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests; /// /// The staleness fast path: memoises a computed /// revision hash against the readings it was /// computed under, and the memo is PROCESS-STATIC. That makes the watermark the /// only thing standing between a cached hash and a wrong answer — so these tests /// pin that the memo is served only while the watermark has not moved, and is /// dropped the moment it has. /// /// /// The failure this guards against is a real one: a data-connection edit, or a /// bundle import committing through the raw DbContext, changes the flattened /// output without going through the repository path that derives bumps from the /// change tracker. If that write does not bump, the memo below keeps answering with /// the old hash forever. /// /// public class StaleInstanceProbeTests { private const int InstanceId = 1; private readonly TemplateGraphWatermark _watermark = new(); private readonly IFlatteningPipeline _pipeline = Substitute.For(); private string _currentHash = "sha256:first"; private int _flattenCount; public StaleInstanceProbeTests() { // Process-static memo: start every test from a clean slate. StaleInstanceProbe.ClearMemos(); _pipeline.CreateSession().Returns(_ => new FlattenSession(_watermark)); _pipeline .FlattenAndValidateAsync(InstanceId, Arg.Any(), Arg.Any(), Arg.Any()) .Returns(_ => { _flattenCount++; var config = new FlattenedConfiguration { InstanceUniqueName = "Inst-01" }; return Result.Success( new FlatteningPipelineResult(config, _currentHash, ValidationResult.Success())); }); } private StaleInstanceProbe CreateProbe() => new(_pipeline, _watermark); [Fact] public async Task UnchangedWatermark_ServesMemoisedHash_WithoutReflattening() { var probe = CreateProbe(); Assert.Equal("sha256:first", await probe.GetCurrentRevisionHashAsync(InstanceId)); Assert.Equal(1, _flattenCount); Assert.Equal("sha256:first", await probe.GetCurrentRevisionHashAsync(InstanceId)); Assert.Equal(1, _flattenCount); } /// /// BumpAll is the unattributed fallback used by everything that changes a /// flattening input without an owning template id — data-connection saves and /// bundle imports both rely on it. It must invalidate the memo. /// [Fact] public async Task BumpAll_InvalidatesTheMemo() { var probe = CreateProbe(); await probe.GetCurrentRevisionHashAsync(InstanceId); // The underlying config drifted (e.g. a data connection was repointed). _currentHash = "sha256:second"; // Without a bump the stale hash would still be served... Assert.Equal("sha256:first", await probe.GetCurrentRevisionHashAsync(InstanceId)); _watermark.BumpAll(); Assert.Equal("sha256:second", await probe.GetCurrentRevisionHashAsync(InstanceId)); Assert.Equal(2, _flattenCount); } [Fact] public async Task BumpInstance_InvalidatesTheMemoForThatInstance() { var probe = CreateProbe(); await probe.GetCurrentRevisionHashAsync(InstanceId); _currentHash = "sha256:second"; _watermark.BumpInstance(InstanceId); Assert.Equal("sha256:second", await probe.GetCurrentRevisionHashAsync(InstanceId)); } }