perf(adminui): drift chip recomposes only when the config-edit watermark moves

This commit is contained in:
Joseph Doherty
2026-08-16 04:25:56 -04:00
parent d5f573b019
commit 684e59d871
5 changed files with 247 additions and 11 deletions
@@ -40,7 +40,9 @@ public class PageAuthorizationGuardTests
[typeof(NodeEdit)] = AdminUiPolicies.ConfigEditor,
[typeof(NamespaceEdit)] = AdminUiPolicies.ConfigEditor,
[typeof(AclEdit)] = AdminUiPolicies.ConfigEditor,
[typeof(Deployments)] = AdminUiPolicies.ConfigEditor,
// Fully qualified for the same reason as Certificates/Hosts below: the AdminUI.Deployments
// namespace (DeploymentDriftProbe) shadows the page's simple name from here.
[typeof(global::ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Deployments)] = AdminUiPolicies.ConfigEditor,
[typeof(Scripts)] = AdminUiPolicies.ConfigEditor,
[typeof(ScriptEdit)] = AdminUiPolicies.ConfigEditor,
@@ -0,0 +1,83 @@
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;
/// <summary>
/// Pins the <see cref="DeploymentDriftProbe"/> watermark contract: the expensive
/// <c>ConfigComposer</c> recomposition runs only when the <c>ConfigEdit</c> audit watermark (or the
/// sealed revision hash) has moved, and it definitely still runs when it has.
/// </summary>
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);
}
/// <summary>Stand-in for the <c>ConfigComposer</c> seam that counts how often it was invoked.</summary>
private sealed class CountingCompose(string result)
{
public string Result { get; set; } = result;
public int Count { get; private set; }
public Task<string> ComposeAsync(OtOpcUaConfigDbContext db)
{
Count++;
return Task.FromResult(Result);
}
}
}