fix(ab): Task 22 review r1 — release-note timing and AbLegacy regressions, probe E2E on both transports

- I1: the upgrade restart itself moves every no-key AbCip/AbLegacy driver to Native (every process
  start constructs drivers, incl. boot-from-cache). Docs + CLAUDE.md now say so and tell operators
  to set "transport": "Libplctag" BEFORE upgrading (older builds ignore the unknown key).
- I2: AbLegacy release note leads with the status-bit write refusal (T4:0.DN, C5:0.CU, R6:0.EN, ...)
  on defaulted SLC/MicroLogix drivers and the fallback. The real client status is
  BadCommunicationError, not BadNotSupported (the driver's write catch-all maps the runtime's
  AbTransportException like any transport failure, as #571 does for PLC-5), with handle eviction
  and Degraded health; pinned by a driver-level test (no 0xAA write on the wire, with a positive
  control). ST string byte order stated as unverified. Also corrects the docs' "T4:0/DN" examples:
  the driver's address grammar is "T4:0.DN" ("/DN" is a malformed address).
- I3: DriverProbeHandshakeE2eTests' two AbCip probe tests are a [Theory] over Native and
  Libplctag. Run against ab_server controllogix: all 4 rows pass with identical exact messages.
- M1: documented (docs + resolver XML doc): the AdminUI merges only the probed device, so a
  mixed-family defaulted AbLegacy driver can probe Native while running Libplctag.
- M2: factory ParseEnum rejects undefined integer strings ("7") via Enum.IsDefined (both drivers).
- M3: AbCip driver-level bad value reads "AB CIP config in '<id>' has unknown Transport ..."; the
  named constant is gone (tagName: null now means driver-level).
- M4: the explicit-Native limited-family Warning is once per instance, not per (re)init.
- Nit: EffectiveTransport docs note that injected factories bypass it.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-07 21:06:40 -04:00
parent ed0881d672
commit 1806af54f0
13 changed files with 211 additions and 34 deletions
@@ -168,8 +168,10 @@ public sealed class DriverProbeHandshakeE2eTests
// ---- AbCip : libplctag CIP session handshake (skips unless the sim fixture is up) ----
[Fact]
public async Task AbCip_Green_AgainstSim()
[Theory]
[InlineData("Native")]
[InlineData("Libplctag")]
public async Task AbCip_Green_AgainstSim(string transport)
{
SkipUnless(DockerHost, AbCipPort);
// Probe an explicit tag that the ab_server ControlLogix sim actually defines
@@ -182,9 +184,9 @@ public sealed class DriverProbeHandshakeE2eTests
// exercising the one path the comment above says it does not exercise — and reporting it as a
// failure of the fixture rather than of the config shape.
var result = await new AbCipDriverProbe().ProbeAsync(
// Pinned to libplctag (Task 22 flipped the default to Native): the exact messages below were
// proven against ab_server on libplctag; the native path's ab_server coverage is TransportMatrix.
$"{{\"Transport\":\"Libplctag\",\"Devices\":[{{\"HostAddress\":\"ab://{DockerHost}:{AbCipPort}/1,0\"}}]," +
// Run on BOTH transports (Task 22 review r1 I3): Native is the production default, and the
// probe's green messages are transport-neutral strings, so each row asserts the same text.
$"{{\"Transport\":\"{transport}\",\"Devices\":[{{\"HostAddress\":\"ab://{DockerHost}:{AbCipPort}/1,0\"}}]," +
$"\"RawTags\":[{{\"RawPath\":\"Plant/AbCip/dev1/TestDINT\"," +
$"\"TagConfig\":\"{{\\\"tagPath\\\":\\\"TestDINT\\\",\\\"dataType\\\":\\\"DInt\\\"}}\"," +
$"\"WriteIdempotent\":false,\"DeviceName\":\"ab://{DockerHost}:{AbCipPort}/1,0\"}}]}}",
@@ -214,13 +216,16 @@ public sealed class DriverProbeHandshakeE2eTests
/// <c>Docker/patches/0002-ab_server-answer-tag-not-found-with-cip-0x04.patch</c> — so this test
/// needs the fixture rebuilt from that patch, not merely running.
/// </remarks>
[Fact]
public async Task AbCip_WithNoAuthoredTags_ReportsTheControllerReachable()
[Theory]
[InlineData("Native")]
[InlineData("Libplctag")]
public async Task AbCip_WithNoAuthoredTags_ReportsTheControllerReachable(string transport)
{
SkipUnless(DockerHost, AbCipPort);
var result = await new AbCipDriverProbe().ProbeAsync(
// Pinned to libplctag (Task 22): the 0x04 → ErrorNotFound reasoning above is libplctag's.
$"{{\"Transport\":\"Libplctag\",\"Devices\":[{{\"HostAddress\":\"ab://{DockerHost}:{AbCipPort}/1,0\"}}],\"RawTags\":[]}}",
// Both transports (Task 22 review r1 I3): native maps CIP 0x04 to AbStatus.NotFound exactly as
// libplctag's ErrorNotFound does, so the reachable classification is the same.
$"{{\"Transport\":\"{transport}\",\"Devices\":[{{\"HostAddress\":\"ab://{DockerHost}:{AbCipPort}/1,0\"}}],\"RawTags\":[]}}",
Timeout, Ct);
result.Ok.ShouldBeTrue($"Probe message: {result.Message}");