feat(health): surface pending-updates counts on the Health dashboard and CLI (ScadaBridge#63)
Health page gains PendingUpdatesHealthTiles (stale instances / sites with stale artifacts, Deployer+Administrator only, site-scope narrowed, shared cache). New GetPendingUpdates management command (Deployer) and 'health pending-updates' CLI leaf. Flatten/cache computation untouched; the command reads IPendingUpdatesEvaluator directly because the UI cache is not reachable from ManagementService. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -1848,6 +1848,63 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
|
||||
}
|
||||
|
||||
private void RegisterPendingUpdates()
|
||||
{
|
||||
var evaluator = Substitute.For<ZB.MOM.WW.ScadaBridge.DeploymentManager.PendingUpdates.IPendingUpdatesEvaluator>();
|
||||
evaluator.EvaluateAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(
|
||||
new ZB.MOM.WW.ScadaBridge.DeploymentManager.PendingUpdates.PendingUpdatesSnapshot(
|
||||
DateTimeOffset.UtcNow,
|
||||
[
|
||||
new(1, "S1", "Site 1", ZB.MOM.WW.ScadaBridge.DeploymentManager.PendingUpdates.ArtifactDeploymentState.OutOfDate, null, 10, 4, 0),
|
||||
new(2, "S2", "Site 2", ZB.MOM.WW.ScadaBridge.DeploymentManager.PendingUpdates.ArtifactDeploymentState.Current, null, 10, 6, 0),
|
||||
])));
|
||||
_services.AddScoped(_ => evaluator);
|
||||
}
|
||||
|
||||
/// <summary>Gitea #63: a system-wide Deployer gets fleet totals.</summary>
|
||||
[Fact]
|
||||
public void GetPendingUpdates_SystemWideDeployer_ReturnsFleetTotals()
|
||||
{
|
||||
RegisterPendingUpdates();
|
||||
var actor = CreateActor();
|
||||
|
||||
actor.Tell(Envelope(new GetPendingUpdatesCommand(), "Deployer"));
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
using var doc = JsonDocument.Parse(response.JsonData);
|
||||
Assert.Equal(10, doc.RootElement.GetProperty("staleInstanceCount").GetInt32());
|
||||
Assert.Equal(1, doc.RootElement.GetProperty("staleArtifactSiteCount").GetInt32());
|
||||
Assert.Equal(2, doc.RootElement.GetProperty("sites").GetArrayLength());
|
||||
Assert.Equal("OutOfDate", doc.RootElement.GetProperty("sites")[0].GetProperty("artifactState").GetString());
|
||||
}
|
||||
|
||||
/// <summary>Gitea #63: a site-scoped Deployer's totals exclude other sites.</summary>
|
||||
[Fact]
|
||||
public void GetPendingUpdates_SiteScopedDeployer_SeesOnlyPermittedSites()
|
||||
{
|
||||
RegisterPendingUpdates();
|
||||
var actor = CreateActor();
|
||||
|
||||
actor.Tell(ScopedEnvelope(new GetPendingUpdatesCommand(), new[] { "1" }, "Deployer"));
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
using var doc = JsonDocument.Parse(response.JsonData);
|
||||
Assert.Equal(4, doc.RootElement.GetProperty("staleInstanceCount").GetInt32());
|
||||
Assert.Equal(1, doc.RootElement.GetProperty("sites").GetArrayLength());
|
||||
}
|
||||
|
||||
/// <summary>Gitea #63: a non-Deployer is refused.</summary>
|
||||
[Fact]
|
||||
public void GetPendingUpdates_Viewer_IsUnauthorized()
|
||||
{
|
||||
RegisterPendingUpdates();
|
||||
var actor = CreateActor();
|
||||
|
||||
actor.Tell(Envelope(new GetPendingUpdatesCommand(), "Viewer"));
|
||||
|
||||
ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that query parked messages, when out of scope for site scoped user, returns unauthorized.</summary>
|
||||
[Fact]
|
||||
public void QueryParkedMessages_OutOfScopeForSiteScopedUser_ReturnsUnauthorized()
|
||||
|
||||
@@ -148,6 +148,7 @@ public class RequiredRoleMatrixTests
|
||||
["SetInstanceNativeAlarmSourceOverrides"] = [Roles.Deployer],
|
||||
["DeleteInstanceNativeAlarmSourceOverride"] = [Roles.Deployer],
|
||||
["GetDeploymentDiff"] = [Roles.Deployer],
|
||||
["GetPendingUpdates"] = [Roles.Deployer],
|
||||
["MgmtDeployArtifacts"] = [Roles.Deployer],
|
||||
["QueryDeployments"] = [Roles.Deployer],
|
||||
["RetryParkedMessage"] = [Roles.Deployer],
|
||||
|
||||
Reference in New Issue
Block a user