feat(galaxy): consume the gateway's correlated write statuses; close archreview 06/S-1
The mxaccessgw OnWriteComplete correlation is live (server b948e69, worker 53f69cd — all four unary write kinds), so every write reply's statuses[0] now carries the real COM commit outcome. Live-gating the cutover on windev found and fixed three defects: - StatusCodeMap.FromMxStatus truncated the correlated failure row's MxStatus detail code to a byte (1007 & 0xFF = 239 -> OPC DA Good band), mapping a REFUSED write to Good. New invariant: a row that is not IsSuccess() never maps Good — details > 0xFF (and contradictory Good-band substatuses) route by category (SECURITY_ERROR -> BadUserAccessDenied, named const covered by StatusCodeParityTests; COMMUNICATION/CONFIGURATION keep their codes). - The borrow-from-subscription-registry write optimization could never commit: a subscription handle is plain-advised, and MXAccess refuses both the no-login write through it (SECURITY_ERROR detail 1008, live-captured) and the supervisory advise upgrade (E_INVALIDARG) — one advise per HANDLE, not per attribute. Removed: writer subscribedHandleSource ctor param + borrow branch, GalaxyDriver wiring, SubscriptionRegistry.TryResolveItemHandle + its forward index and test file. The writer always AddItems its own supervisory-advised handle, which commits fine while the same attribute is subscribed (live-proven; pinned by the new write-while-subscribed smoke). - (gateway-side, fixed there) the correlation initially covered WriteSecured only, while the dominant no-login path emits plain Write. Live gate, all legs PASSED against windev: happy write Good off a confirmed status row with galaxy.writes.unconfirmed + advise_failed both FLAT; read-only refusal surfaces Bad-family (the #5 node-revert input Galaxy could never produce before); WriteCompletionWaitMilliseconds=0 control -> provisional Good + metered unconfirmed (override removed after, restoration re-verified). New env-gated smokes: Live_correlated_write_statuses_confirm_commit_and_surface_refusal, Live_write_to_subscribed_tag_commits_via_own_handle (replaces the borrow smoke), GatewayGalaxyWriteMeterLiveTests (run filtered — meters are process-wide).
This commit is contained in:
@@ -108,6 +108,62 @@ public sealed class StatusCodeMapTests
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.Bad);
|
||||
}
|
||||
|
||||
// ===== 06/S-1 write-completion rows: detail is an MxStatus detail code, NOT a quality byte =====
|
||||
// The gateway (≥ 53f69cd) correlates the worker's OnWriteComplete onto the unary write reply as
|
||||
// statuses[0]. Those rows carry MxStatus detail codes (live-captured on windev: 1007 operational
|
||||
// refusal on a read-only attribute, 1008 security refusal on a plain-advised handle). The old
|
||||
// truncation `(byte)(Detail & 0xFF)` pushed 1007 → 239 and 1008 → 240 into the OPC DA Good band,
|
||||
// mapping a REFUSED write to Good — no node revert, silently-lost write reported committed.
|
||||
|
||||
/// <summary>A refused write (operational error, MxStatus detail code) must map to a Bad-family code.</summary>
|
||||
[Fact]
|
||||
public void FromMxStatus_WriteRefusal_OperationalError_Detail1007_IsBad()
|
||||
{
|
||||
// Live shape: writeRaw to read-only TestMachine_002.Tagname.
|
||||
var s = new MxStatusProxy { Success = 0, Category = MxStatusCategory.OperationalError, Detail = 1007, RawDetectedBy = 5 };
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.Bad);
|
||||
}
|
||||
|
||||
/// <summary>A security-refused write must map to BadUserAccessDenied, not Good.</summary>
|
||||
[Fact]
|
||||
public void FromMxStatus_WriteRefusal_SecurityError_Detail1008_IsUserAccessDenied()
|
||||
{
|
||||
// Live shape: writeRaw through a plain-advised (subscription) handle — no supervisory advise.
|
||||
var s = new MxStatusProxy { Success = 0, Category = MxStatusCategory.SecurityError, Detail = 1008, RawDetectedBy = 5 };
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.BadUserAccessDenied);
|
||||
}
|
||||
|
||||
/// <summary>Category-level routing for large details preserves communication/configuration specificity.</summary>
|
||||
[Theory]
|
||||
[InlineData(MxStatusCategory.CommunicationError, 0x80050000u)] // BadCommunicationError
|
||||
[InlineData(MxStatusCategory.ConfigurationError, 0x80890000u)] // BadConfigurationError
|
||||
[InlineData(MxStatusCategory.SoftwareError, 0x80000000u)] // unmapped category → generic Bad
|
||||
public void FromMxStatus_LargeDetail_RoutesByCategory(MxStatusCategory category, uint expected)
|
||||
{
|
||||
var s = new MxStatusProxy { Success = 0, Category = category, Detail = 2001 };
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invariant: a row that is not IsSuccess() must NEVER map to Good — a Good-band quality
|
||||
/// byte (e.g. 216 = GoodLocalOverride) on a failure row is contradictory and falls back to
|
||||
/// the category route. Uncertain-band substatuses remain allowed (uncertain is not Good).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FromMxStatus_FailureRow_GoodBandQualityByte_NeverMapsGood()
|
||||
{
|
||||
var s = new MxStatusProxy { Success = 0, Category = MxStatusCategory.SecurityError, Detail = 216 };
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.BadUserAccessDenied);
|
||||
}
|
||||
|
||||
/// <summary>An Uncertain-band quality substatus on a failure row still maps to its specific Uncertain code.</summary>
|
||||
[Fact]
|
||||
public void FromMxStatus_FailureRow_UncertainBandQualityByte_KeepsUncertainCode()
|
||||
{
|
||||
var s = new MxStatusProxy { Success = 0, Category = MxStatusCategory.OperationalError, Detail = 68 };
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.UncertainLastUsableValue);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that top two bits of status codes follow OPC UA convention.</summary>
|
||||
[Fact]
|
||||
public void TopByteCategoryBits_StayWithinOpcUaConvention()
|
||||
|
||||
Reference in New Issue
Block a user