feat(controlplane): deploy-gate TagConfig strictness — Warn default, Error opt-in at the draft gate (R2-11, 05/CONV-2)

This commit is contained in:
Joseph Doherty
2026-07-13 11:15:34 -04:00
parent 8401055c9d
commit 4260b6ecaa
5 changed files with 199 additions and 6 deletions
@@ -0,0 +1,131 @@
using Akka.Actor;
using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests;
/// <summary>
/// R2-11 (05/CONV-2) — the deploy gate's TagConfig strictness inspection, exercised THROUGH the actor
/// (the F10b/PR#423 lesson: an inspector built but never called is inert). Warn mode (default) accepts
/// the deploy with the warning surfaced in the result Message; Error mode rejects the same draft; a
/// clean config and an unmapped (Galaxy) tag produce no TagConfig noise.
/// </summary>
public sealed class AdminOperationsActorTagConfigGateTests : ControlPlaneActorTestBase
{
private static void Seed(IDbContextFactory<OtOpcUaConfigDbContext> dbFactory, string driverType, string tagConfig,
string tagName = "flow")
{
var uuid = Guid.NewGuid();
var equipmentId = DraftValidator.DeriveEquipmentId(uuid);
using var db = dbFactory.CreateDbContext();
db.Namespaces.Add(new Namespace
{
NamespaceId = "ns-1", ClusterId = "", Kind = NamespaceKind.Equipment, NamespaceUri = "urn:eq",
});
db.DriverInstances.Add(new DriverInstance
{
DriverInstanceId = "d-1", ClusterId = "", NamespaceId = "ns-1",
Name = "drv", DriverType = driverType, DriverConfig = "{}",
});
db.Equipment.Add(new Equipment
{
EquipmentUuid = uuid, EquipmentId = equipmentId, Name = "pump-01",
DriverInstanceId = "d-1", UnsLineId = "line-a", MachineCode = "PUMP01",
});
db.Tags.Add(new Tag
{
TagId = "tag-1", DriverInstanceId = "d-1", EquipmentId = equipmentId,
Name = tagName, DataType = "Int16", AccessLevel = TagAccessLevel.Read, TagConfig = tagConfig,
});
db.SaveChanges();
}
private const string TypoModbus = "{\"region\":\"HoldingRegisters\",\"address\":1,\"dataType\":\"Intt16\"}";
private const string CleanModbus = "{\"region\":\"HoldingRegisters\",\"address\":1,\"dataType\":\"Int16\"}";
[Fact]
public void Warn_mode_accepts_and_surfaces_the_warning_in_the_message()
{
var dbFactory = NewInMemoryDbFactory();
Seed(dbFactory, "Modbus", TypoModbus);
var coordinator = CreateTestProbe("coord");
var actor = Sys.ActorOf(AdminOperationsActor.Props(
dbFactory, coordinator.Ref, Enumerable.Empty<IDriverProbe>(), TagConfigValidationMode.Warn));
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3));
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
reply.Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
reply.Message.ShouldNotBeNull();
reply.Message.ShouldContain("Intt16");
reply.Message.ShouldContain("tag-1");
}
[Fact]
public void Error_mode_rejects_the_same_draft()
{
var dbFactory = NewInMemoryDbFactory();
Seed(dbFactory, "Modbus", TypoModbus);
var coordinator = CreateTestProbe("coord");
var actor = Sys.ActorOf(AdminOperationsActor.Props(
dbFactory, coordinator.Ref, Enumerable.Empty<IDriverProbe>(), TagConfigValidationMode.Error));
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
reply.Outcome.ShouldBe(StartDeploymentOutcome.Rejected);
reply.Message.ShouldNotBeNull();
reply.Message.ShouldContain("TagConfigInvalid");
reply.Message.ShouldContain("Intt16");
using var verify = dbFactory.CreateDbContext();
verify.Deployments.Count().ShouldBe(0);
}
[Fact]
public void Clean_config_accepts_with_no_tagconfig_noise()
{
var dbFactory = NewInMemoryDbFactory();
Seed(dbFactory, "Modbus", CleanModbus);
var coordinator = CreateTestProbe("coord");
var actor = Sys.ActorOf(AdminOperationsActor.Props(
dbFactory, coordinator.Ref, Enumerable.Empty<IDriverProbe>(), TagConfigValidationMode.Warn));
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3));
var reply = ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3));
reply.Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
(reply.Message is null || !reply.Message.Contains("Intt16")).ShouldBeTrue();
}
[Fact]
public void Unmapped_galaxy_tag_is_skipped_even_in_error_mode()
{
var dbFactory = NewInMemoryDbFactory();
// Galaxy tag carrying a FullName (passes the DraftValidator Galaxy rule); the inspector has no
// Galaxy mapping, so it is skipped regardless of mode.
Seed(dbFactory, "GalaxyMxGateway", "{\"FullName\":\"Obj.Attr\",\"dataType\":\"nonsense\"}");
var coordinator = CreateTestProbe("coord");
var actor = Sys.ActorOf(AdminOperationsActor.Props(
dbFactory, coordinator.Ref, Enumerable.Empty<IDriverProbe>(), TagConfigValidationMode.Error));
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectMsg<DispatchDeployment>(TimeSpan.FromSeconds(3));
ExpectMsg<StartDeploymentResult>(TimeSpan.FromSeconds(3)).Outcome.ShouldBe(StartDeploymentOutcome.Accepted);
}
}