test(galaxy): #872 pin the detected_by rule per source
v2-ci / unit-tests (push) Has been cancelled

The rule fix itself landed in 1a08b4f7 (detail-0 failure rows route on
category); its test only exercised raw_detected_by = 3. Add per-source
coverage for every MxStatusSource value mxaccessgw's MapSource emits
(-1, 0..5, and an out-of-range value) across the failure categories, plus
the live-captured shape the old rule got wrong (OperationalError detected
by the responding automation object, mxaccessgw docs/AlarmProbeFindings.md)
and a RequestingLmx CommunicationError positive control. All 10 new cases
fail against the old 'detail 0 && raw_detected_by != 0 => comms' rule.

Comment in FromMxStatus now cites MapSource and says why NMX (2/3) is not
a transport signal. Item 1 (Warning/Pending) stays open pending a capture.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-11-12 07:17:53 -05:00
parent 135099d722
commit 633f43170a
2 changed files with 65 additions and 8 deletions
@@ -133,6 +133,9 @@ internal static class StatusCodeMap
// the belief that a non-zero source meant a pre-MXAccess transport failure — but every detected_by value
// names an MXAccess layer (0 requesting LMX, 1 responding LMX, 2/3 NMX, 4/5 automation object; the
// gateway's own failures arrive as ProtocolStatus, never as a status row), so the category decides (#872).
// The numbering is mxaccessgw's MxStatusProxyConverter.MapSource; NMX (2/3) names the inter-node message
// layer that noticed the failure, not a transport fault — a real comms loss is category CommunicationError
// or detail 2/5, which FromCategory already maps to BadCommunicationError.
return FromCategory(status, logger);
}
@@ -140,18 +140,72 @@ public sealed class StatusCodeMapTests
logger.Entries.Count.ShouldBe(1);
}
// ===== #872: detected_by never decides the code — the category does =====
// raw_detected_by is MXAccess's MxStatusSource, decoded by mxaccessgw's MxStatusProxyConverter.MapSource:
// -1 Unknown, 0 RequestingLmx, 1 RespondingLmx, 2 RequestingNmx, 3 RespondingNmx,
// 4 RequestingAutomationObject, 5 RespondingAutomationObject, anything else Unknown. Every value names the
// MXAccess layer that noticed the failure, not the kind of failure — the live write-refusal capture in
// mxaccessgw docs/AlarmProbeFindings.md is an OperationalError detected by RespondingAutomationObject (5).
// A transport/network failure shows up as category CommunicationError (or detail 2/5), and the gateway's own
// failures arrive as ProtocolStatus, never as a status row. The old rule (detail 0 and raw_detected_by != 0
// => BadCommunicationError) turned every one of those object/LMX-detected failures into a comms error.
/// <summary>
/// #872: detected_by names an MXAccess layer (3 = responding NMX), not a transport failure, so a detail-0
/// failure row routes on its category — a communication-category row is still BadCommunicationError.
/// #872: a detail-0 failure row maps on its category for every detected_by source — including the
/// NMX sources, which name the layer that noticed the failure, not a transport failure.
/// </summary>
/// <param name="rawDetectedBy">The raw MxStatusSource value.</param>
[Theory]
[InlineData(MxStatusCategory.Unspecified, 0x80000000u)]
[InlineData(MxStatusCategory.CommunicationError, 0x80050000u)]
[InlineData(MxStatusCategory.ConfigurationError, 0x80890000u)]
public void FromMxStatus_SuccessZero_DetailZero_RoutesByCategory_NotDetectedBy(MxStatusCategory category, uint expected)
[InlineData(-1)] // Unknown
[InlineData(0)] // RequestingLmx
[InlineData(1)] // RespondingLmx
[InlineData(2)] // RequestingNmx
[InlineData(3)] // RespondingNmx
[InlineData(4)] // RequestingAutomationObject
[InlineData(5)] // RespondingAutomationObject
[InlineData(77)] // outside the documented range -> Unknown
public void FromMxStatus_DetailZero_EverySource_RoutesByCategory(int rawDetectedBy)
{
var s = new MxStatusProxy { Success = 0, Detail = 0, RawDetectedBy = 3, Category = category };
StatusCodeMap.FromMxStatus(s).ShouldBe(expected);
Map(MxStatusCategory.OperationalError).ShouldBe(StatusCodeMap.Bad);
Map(MxStatusCategory.SoftwareError).ShouldBe(StatusCodeMap.Bad);
Map(MxStatusCategory.OtherError).ShouldBe(StatusCodeMap.Bad);
Map(MxStatusCategory.Unspecified).ShouldBe(StatusCodeMap.Bad);
Map(MxStatusCategory.ConfigurationError).ShouldBe(StatusCodeMap.BadConfigurationError);
Map(MxStatusCategory.SecurityError).ShouldBe(StatusCodeMap.BadUserAccessDenied);
Map(MxStatusCategory.CommunicationError).ShouldBe(StatusCodeMap.BadCommunicationError);
uint Map(MxStatusCategory category) => StatusCodeMap.FromMxStatus(
new MxStatusProxy { Success = 0, Detail = 0, RawDetectedBy = rawDetectedBy, Category = category });
}
/// <summary>
/// #872: the shape the old rule got wrong — an operational failure the responding automation object
/// detected (raw 5, the live write-refusal source) is not a communication error.
/// </summary>
[Fact]
public void FromMxStatus_DetailZero_RespondingAutomationObject_OperationalError_IsNotCommunicationError()
{
var s = new MxStatusProxy
{
Success = 0, Detail = 0, Category = MxStatusCategory.OperationalError,
DetectedBy = MxStatusSource.RespondingAutomationObject, RawDetectedBy = 5,
};
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.Bad);
}
/// <summary>
/// #872 positive control: a communication-category row stays BadCommunicationError when detected by
/// the requesting LMX (raw 0) — the source the old rule treated as "never a comms error".
/// </summary>
[Fact]
public void FromMxStatus_DetailZero_RequestingLmx_CommunicationError_IsCommunicationError()
{
var s = new MxStatusProxy
{
Success = 0, Detail = 0, Category = MxStatusCategory.CommunicationError,
DetectedBy = MxStatusSource.RequestingLmx, RawDetectedBy = 0,
};
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.BadCommunicationError);
}
/// <summary>Verifies that all-zero status maps to Bad status code.</summary>