feat(deploy): reject Tag/VirtualTag NodeId collisions at deploy (surgical DraftValidator gate)

This commit is contained in:
Joseph Doherty
2026-06-07 10:42:13 -04:00
parent fce66d104a
commit 1023209d52
4 changed files with 223 additions and 0 deletions
@@ -43,6 +43,60 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
db.ConfigEdits.Single().EntityType.ShouldBe("Deployment");
}
/// <summary>Verifies the surgical DraftValidator gate: a Tag↔VirtualTag NodeId collision in
/// the live config rejects the deploy (422-mapped <see cref="StartDeploymentOutcome.Rejected"/>)
/// before any coordinator dispatch — and inserts no Deployment row.</summary>
[Fact]
public void StartDeployment_rejects_on_Tag_VirtualTag_NodeId_collision()
{
var dbFactory = NewInMemoryDbFactory();
using (var db = dbFactory.CreateDbContext())
{
db.Equipment.Add(new Configuration.Entities.Equipment
{
EquipmentUuid = Guid.NewGuid(),
EquipmentId = "eq-1",
Name = "eq",
DriverInstanceId = "d",
UnsLineId = "line-a",
MachineCode = "m",
});
db.Tags.Add(new Configuration.Entities.Tag
{
TagId = "tag-speed",
DriverInstanceId = "d",
EquipmentId = "eq-1",
Name = "speed",
DataType = "Float",
AccessLevel = TagAccessLevel.Read,
TagConfig = "{}",
});
db.VirtualTags.Add(new Configuration.Entities.VirtualTag
{
VirtualTagId = "vtag-speed",
EquipmentId = "eq-1",
Name = "speed",
DataType = "Float",
ScriptId = "s-1",
});
db.SaveChanges();
}
var coordinator = CreateTestProbe("coord");
var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref, Enumerable.Empty<IDriverProbe>()));
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("collide"); // the rule's message text
using var verify = dbFactory.CreateDbContext();
verify.Deployments.Count().ShouldBe(0);
}
/// <summary>Verifies that starting a deployment is refused when another is in flight.</summary>
[Fact]
public void StartDeployment_refuses_when_another_is_in_flight()