using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Deployments; using ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Deployments; /// /// Pins the watermark contract: the expensive /// ConfigComposer recomposition runs only when the ConfigEdit audit watermark (or the /// sealed revision hash) has moved, and it definitely still runs when it has. /// public sealed class DeploymentDriftProbeTests { private const string SealedHash = "sealed-hash-aaaa"; [Fact] public async Task An_unchanged_watermark_reuses_the_cached_verdict() { var name = $"drift-{Guid.NewGuid():N}"; var factory = UnsTreeTestDb.Factory(name); var compose = new CountingCompose(SealedHash); var probe = new DeploymentDriftProbe(factory, compose.ComposeAsync); var first = await probe.GetDriftAsync(SealedHash, CancellationToken.None); var second = await probe.GetDriftAsync(SealedHash, CancellationToken.None); // Nothing mutated between the calls, so the second call must be served from the cache. compose.Count.ShouldBe(1); first.ShouldBe(false); second.ShouldBe(false); } [Fact] public async Task A_new_config_edit_recomposes() { var name = $"drift-{Guid.NewGuid():N}"; var factory = UnsTreeTestDb.Factory(name); var compose = new CountingCompose(SealedHash); var probe = new DeploymentDriftProbe(factory, compose.ComposeAsync); var before = await probe.GetDriftAsync(SealedHash, CancellationToken.None); before.ShouldBe(false); // An edit lands: the audit row moves the watermark, and the (faked) composer now reports a // different revision — the probe must recompose and flip the verdict. await using (var db = UnsTreeTestDb.CreateNamed(name)) { db.ConfigEdits.Add(new ConfigEdit { EntityType = "Equipment", EntityId = Guid.NewGuid(), FieldsJson = "{}", EditedBy = "test", SourceNode = "node-1", EditedAtUtc = DateTime.UtcNow, }); await db.SaveChangesAsync(); } compose.Result = "a-different-hash"; var after = await probe.GetDriftAsync(SealedHash, CancellationToken.None); compose.Count.ShouldBe(2); after.ShouldBe(true); } /// Stand-in for the ConfigComposer seam that counts how often it was invoked. private sealed class CountingCompose(string result) { public string Result { get; set; } = result; public int Count { get; private set; } public Task ComposeAsync(OtOpcUaConfigDbContext db) { Count++; return Task.FromResult(Result); } } }