feat(ab): native managed transport is the default for AbCip and AbLegacy; libplctag remains as fallback

Task 22 (spec section 8 P5), with the controller rulings that exclude PLC-5 and LogixPccc.

- Transport is nullable on both option records; null (key absent) means "use the default" and is
  never serialized. Factory DTO parse: absent -> null; a bad value still fails via ParseEnum.
- One resolver per driver (AbCipTransportResolver / AbLegacyTransportResolver), used by the driver
  constructor, the Test Connect probe and (through the driver) the CLI.
  AbCip: null -> Native. AbLegacy: null -> Native unless any device is Plc5 or LogixPccc, then the
  whole driver resolves to Libplctag. Explicit values always win; explicit Native with a
  Plc5/LogixPccc device logs one Warning at init.
- Init log shows configured vs effective, e.g. transport=Native (default).
- AdminUI forms: "Default (...)" / "Native (managed)" / "libplctag (fallback)". Default omits the
  key on save (reverses Task 19's explicit re-emit, which would have pinned every edited driver).
- CLIs: --transport default|native|libplctag, default "default" via the resolver, so
  --plc-type Plc5 with no --transport runs libplctag.
- Tests pinned to Libplctag where a default-path runtime was created: found by instrumenting the
  native factories with the current xUnit test name (one unit test, AbLegacyReinitConfigAdoption;
  plus the two fixture-gated AbCip probe E2E tests whose exact messages were proven on libplctag).
  TransportMatrix and the differential harness already pass explicit transports.
- Switch/resolver/CLI/form tests are falsified by mutating the resolver (default -> Libplctag,
  exclusion removed, AbLegacy default -> Libplctag): each mutation turns them red.
- Replaces the inline role-name literal in AbCipDriverFactoryExtensions' ParseEnum call with a named
  constant; RoleAuthorityGuardTests was already red on the base commit for that line.
- Docs: AbCip.md / AbLegacy.md default + release note; CLAUDE.md AB section.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-07 20:49:33 -04:00
parent 17e1170e61
commit ed0881d672
29 changed files with 653 additions and 131 deletions
@@ -182,7 +182,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(
$"{{\"Devices\":[{{\"HostAddress\":\"ab://{DockerHost}:{AbCipPort}/1,0\"}}]," +
// 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\"}}]," +
$"\"RawTags\":[{{\"RawPath\":\"Plant/AbCip/dev1/TestDINT\"," +
$"\"TagConfig\":\"{{\\\"tagPath\\\":\\\"TestDINT\\\",\\\"dataType\\\":\\\"DInt\\\"}}\"," +
$"\"WriteIdempotent\":false,\"DeviceName\":\"ab://{DockerHost}:{AbCipPort}/1,0\"}}]}}",
@@ -217,7 +219,8 @@ public sealed class DriverProbeHandshakeE2eTests
{
SkipUnless(DockerHost, AbCipPort);
var result = await new AbCipDriverProbe().ProbeAsync(
$"{{\"Devices\":[{{\"HostAddress\":\"ab://{DockerHost}:{AbCipPort}/1,0\"}}],\"RawTags\":[]}}",
// Pinned to libplctag (Task 22): the 0x04 → ErrorNotFound reasoning above is libplctag's.
$"{{\"Transport\":\"Libplctag\",\"Devices\":[{{\"HostAddress\":\"ab://{DockerHost}:{AbCipPort}/1,0\"}}],\"RawTags\":[]}}",
Timeout, Ct);
result.Ok.ShouldBeTrue($"Probe message: {result.Message}");