parity: triage 3 false-positives from first-rig run (2026-04-30)
After running the matrix end-to-end against the live rig for the first time, three of the nine failures were false positives — bugs in the harness and test invariants, not real backend deltas: 1. ParityHarness configured the legacy backend with OTOPCUA_GALAXY_BACKEND=db, which is Discover-only. Reads, writes, and reinits all returned "MXAccess code lift pending — DB-backed backend covers Discover only". Switched to mxaccess backend; the ZB connection string still drives the discovery path. 2. HistoryReadParityTests asserted "neither backend implements IHistoryProvider" — but the legacy GalaxyProxyDriver still does (it's an accepted back-compat delta retired in PR 7.2). The architectural pin we *want* is "the new path doesn't regress to per-driver history", so the test now asserts only the mxgw side. 3. AlarmTransitionParityTests strict-pinned the five sub-attribute refs (InAlarmRef, etc.) on the legacy condition. PR 2.1 added those refs specifically so the new mxgw driver could populate them via AlarmRefBuilder; legacy pre-dates PR 2.1 and leaves them null — that's correct, not a regression. Test now asserts a one-way invariant: when legacy populated a ref, mxgw must match. When legacy is null, mxgw is free to populate (the mxgw → server-side AlarmConditionService direction). The six remaining failures are real: - 2 from the gw-side `[]` array suffix (filed in mxaccessgw/requirements-array-suffix-fix.md) - 2 write-StatusCode mapping deltas (0x80050000 vs 0x80020000) — Bad-status both ways but mapped to different OPC UA codes - 1 event-rate ratio of 5x (mxgw dispatches 5x legacy in the same 3s window) - (Plus the 2 ScanState scenarios that skip cleanly — single-platform rig as documented) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,10 +56,29 @@ public sealed class AlarmTransitionParityTests
|
|||||||
$"alarm severity parity for '{kvp.Key}'");
|
$"alarm severity parity for '{kvp.Key}'");
|
||||||
mxgw[kvp.Key].SourceName.ShouldBe(kvp.Value.SourceName,
|
mxgw[kvp.Key].SourceName.ShouldBe(kvp.Value.SourceName,
|
||||||
$"alarm SourceName parity for '{kvp.Key}'");
|
$"alarm SourceName parity for '{kvp.Key}'");
|
||||||
mxgw[kvp.Key].InAlarmRef.ShouldBe(kvp.Value.InAlarmRef,
|
|
||||||
$"alarm InAlarmRef parity for '{kvp.Key}'");
|
// PR 2.1 added the five sub-attribute refs (InAlarmRef / PriorityRef /
|
||||||
mxgw[kvp.Key].DescAttrNameRef.ShouldBe(kvp.Value.DescAttrNameRef,
|
// DescAttrNameRef / AckedRef / AckMsgWriteRef) so the new server-side
|
||||||
$"alarm DescAttrNameRef parity for '{kvp.Key}'");
|
// AlarmConditionService can subscribe + ack-write without help from the
|
||||||
|
// driver. The new mxgw GalaxyDriver populates them via AlarmRefBuilder
|
||||||
|
// (PR 4.1). The legacy GalaxyProxyDriver pre-dates PR 2.1 and leaves them
|
||||||
|
// null — that's an accepted delta until the legacy backend retires in
|
||||||
|
// PR 7.2. Asserting "mxgw populated when legacy didn't" is *correct*
|
||||||
|
// behavior, not a regression.
|
||||||
|
//
|
||||||
|
// We pin the weaker invariant: if legacy populated a ref, mxgw must
|
||||||
|
// populate the same value. If legacy is null, mxgw is allowed to be
|
||||||
|
// either null or populated (the population-from-AlarmRefBuilder direction).
|
||||||
|
if (kvp.Value.InAlarmRef is not null)
|
||||||
|
{
|
||||||
|
mxgw[kvp.Key].InAlarmRef.ShouldBe(kvp.Value.InAlarmRef,
|
||||||
|
$"alarm InAlarmRef parity for '{kvp.Key}' (both populated)");
|
||||||
|
}
|
||||||
|
if (kvp.Value.DescAttrNameRef is not null)
|
||||||
|
{
|
||||||
|
mxgw[kvp.Key].DescAttrNameRef.ShouldBe(kvp.Value.DescAttrNameRef,
|
||||||
|
$"alarm DescAttrNameRef parity for '{kvp.Key}' (both populated)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,17 +50,20 @@ public sealed class HistoryReadParityTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Neither_Galaxy_backend_implements_IHistoryProvider_directly()
|
public async Task The_new_Galaxy_backend_does_not_implement_IHistoryProvider_directly()
|
||||||
{
|
{
|
||||||
// Pinning the architectural decision from Phase 1 (PR 1.3): per-driver
|
// Pinning the architectural decision from Phase 1 (PR 1.3): per-driver
|
||||||
// IHistoryProvider was retired in favor of the server-owned HistoryRouter.
|
// IHistoryProvider was retired in favor of the server-owned HistoryRouter
|
||||||
// If a regression brings IHistoryProvider back on either Galaxy driver,
|
// for the *new* in-process GalaxyDriver. The legacy GalaxyProxyDriver
|
||||||
// this test fires.
|
// still surfaces IHistoryProvider for back-compat with the legacy server
|
||||||
|
// bootstrap path (it's an accepted delta — the legacy driver retires in
|
||||||
|
// PR 7.2 alongside the rest of the legacy projects). The architectural
|
||||||
|
// pin we want to enforce is "the *new* path doesn't regress to per-driver
|
||||||
|
// history".
|
||||||
_h.RequireBoth();
|
_h.RequireBoth();
|
||||||
|
|
||||||
(_h.LegacyDriver as IHistoryProvider).ShouldBeNull(
|
|
||||||
"legacy GalaxyProxyDriver must not surface IHistoryProvider — history routes through HistoryRouter");
|
|
||||||
(_h.MxGatewayDriver as IHistoryProvider).ShouldBeNull(
|
(_h.MxGatewayDriver as IHistoryProvider).ShouldBeNull(
|
||||||
"in-process GalaxyDriver must not surface IHistoryProvider — history routes through HistoryRouter");
|
"in-process GalaxyDriver must not surface IHistoryProvider — history routes through HistoryRouter");
|
||||||
|
await Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,7 +163,12 @@ public sealed class ParityHarness : IAsyncLifetime
|
|||||||
["OTOPCUA_GALAXY_PIPE"] = pipe,
|
["OTOPCUA_GALAXY_PIPE"] = pipe,
|
||||||
["OTOPCUA_ALLOWED_SID"] = sid,
|
["OTOPCUA_ALLOWED_SID"] = sid,
|
||||||
["OTOPCUA_GALAXY_SECRET"] = LegacySecret,
|
["OTOPCUA_GALAXY_SECRET"] = LegacySecret,
|
||||||
["OTOPCUA_GALAXY_BACKEND"] = "db",
|
// PR 5.W triage 2026-04-30: db-backend is Discover-only. The parity
|
||||||
|
// matrix needs Read / Write / Subscribe over a real MxAccess session,
|
||||||
|
// so use the mxaccess backend. ZB conn string is still consulted for
|
||||||
|
// the discovery path (the mxaccess backend layers MxAccess on top of
|
||||||
|
// the same DB).
|
||||||
|
["OTOPCUA_GALAXY_BACKEND"] = "mxaccess",
|
||||||
["OTOPCUA_GALAXY_ZB_CONN"] = "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;",
|
["OTOPCUA_GALAXY_ZB_CONN"] = "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user