Merge b640-601: honest deploy acks — Applied only after the address-space outcome, degraded flag + reason (#601)
v2-ci / build (push) Successful in 4m42s
v2-ci / unit-tests (push) Failing after 20m19s

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe

# Conflicts:
#	src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs
This commit is contained in:
Joseph Doherty
2026-10-08 18:00:23 -04:00
28 changed files with 1259 additions and 55 deletions
@@ -201,6 +201,61 @@ public class MeshTransportDtoMapperTests
back.FailureReason.ShouldBeEmpty();
}
[Fact]
public void ApplyAck_Degraded_RoundTrips_WithItsReason()
{
// #601: an Applied ack whose address-space apply was DEGRADED must reach central as degraded —
// a node that loses the flag on the wire reports a clean apply it did not have.
var domain = new ApplyAck(
new DeploymentId(DeploymentGuid),
new NodeId("site-a-1"),
ApplyAckOutcome.Applied,
null,
new CorrelationId(CorrelationGuid),
Degraded: true,
DegradedReason: "failedNodes=3");
var back = MeshTransportDtoMapper.FromProto(Wire(MeshTransportDtoMapper.ToProto(domain)));
back.ShouldBe(domain);
back.Degraded.ShouldBeTrue();
back.DegradedReason.ShouldBe("failedNodes=3");
}
[Fact]
public void ApplyAck_Clean_EncodesNoDegradedFields()
{
var proto = MeshTransportDtoMapper.ToProto(new ApplyAck(
new DeploymentId(DeploymentGuid), new NodeId("site-a-1"), ApplyAckOutcome.Applied, null,
new CorrelationId(CorrelationGuid)));
proto.Degraded.ShouldBeFalse();
proto.HasDegradedReason.ShouldBeFalse();
}
[Fact]
public void ApplyAck_FromAnOldNode_WithoutTheDegradedFields_DecodesAsTodaysCleanAck()
{
// #601 compatibility: a node built before the degraded fields existed sends exactly fields 1-5.
// Its ack must decode exactly as it always did — not degraded, no reason.
var oldNodeBytes = new ApplyAckMessage
{
DeploymentId = DeploymentGuid.ToString("N"),
NodeId = "site-a-1",
Outcome = ApplyOutcome.Applied,
CorrelationId = CorrelationGuid.ToString("N"),
}.ToByteArray();
var back = MeshTransportDtoMapper.FromProto(ApplyAckMessage.Parser.ParseFrom(oldNodeBytes));
back.Outcome.ShouldBe(ApplyAckOutcome.Applied);
back.Degraded.ShouldBeFalse();
back.DegradedReason.ShouldBeNull();
back.ShouldBe(new ApplyAck(
new DeploymentId(DeploymentGuid), new NodeId("site-a-1"), ApplyAckOutcome.Applied, null,
new CorrelationId(CorrelationGuid)));
}
[Fact]
public void ApplyAck_UnspecifiedOutcome_Throws_RatherThanDecodingToAMeaningfulValue()
{