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:
+30
-12
@@ -108,7 +108,8 @@ public sealed class AbCipDriverPageFormSerializationTests
|
||||
[Theory]
|
||||
[InlineData(AbCipTransport.Libplctag)]
|
||||
[InlineData(AbCipTransport.Native)]
|
||||
public void Transport_survives_FromOptions_ToOptions(AbCipTransport transport)
|
||||
[InlineData(null)]
|
||||
public void Transport_survives_FromOptions_ToOptions(AbCipTransport? transport)
|
||||
{
|
||||
var original = new AbCipDriverOptions { Transport = transport };
|
||||
var back = AbCipDriverForm.FormModel.FromOptions(original).ToOptions([]);
|
||||
@@ -123,35 +124,52 @@ public sealed class AbCipDriverPageFormSerializationTests
|
||||
json.ShouldContain("\"transport\":\"Native\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Editing_an_unrelated_field_preserves_a_saved_Native_transport()
|
||||
[Theory]
|
||||
[InlineData("Native", AbCipTransport.Native)]
|
||||
[InlineData("Libplctag", AbCipTransport.Libplctag)]
|
||||
public void Editing_an_unrelated_field_preserves_an_explicit_transport(string savedValue, AbCipTransport expected)
|
||||
{
|
||||
// The regression the review found: FromOptions/ToOptions dropped Transport, so an operator who set
|
||||
// Native was silently reverted to libplctag the next time they saved ANY other edit.
|
||||
const string saved = """{"transport":"Native","timeout":"00:00:02","devices":[]}""";
|
||||
// a transport was silently reverted the next time they saved ANY other edit.
|
||||
var saved = $$"""{"transport":"{{savedValue}}","timeout":"00:00:02","devices":[]}""";
|
||||
var loaded = AbCipDriverForm.TryDeserialize(saved);
|
||||
loaded.ShouldNotBeNull();
|
||||
loaded.Transport.ShouldBe(AbCipTransport.Native);
|
||||
loaded.Transport.ShouldBe(expected);
|
||||
|
||||
var form = AbCipDriverForm.FormModel.FromOptions(loaded);
|
||||
form.TimeoutSeconds = 7;
|
||||
var json = AbCipDriverForm.Serialize(form.ToOptions(loaded.Devices));
|
||||
|
||||
json.ShouldContain("\"transport\":\"Native\"");
|
||||
json.ShouldContain($"\"transport\":\"{savedValue}\"");
|
||||
json.ShouldContain("\"timeout\":\"00:00:07\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_config_without_transport_loads_as_Libplctag_and_re_emits_it_explicitly()
|
||||
public void A_config_without_transport_loads_as_Default_and_an_unrelated_edit_keeps_the_key_absent()
|
||||
{
|
||||
// Chosen behaviour: an absent key round-trips as an explicit "Libplctag" (the form always writes the
|
||||
// field). The dropdown therefore shows "libplctag (default)".
|
||||
// Task 22 reverses Task 19's "re-emit an explicit Libplctag": that would pin every edited driver to
|
||||
// a concrete transport and bypass the driver-side default (and AbLegacy's PLC-5 / LogixPccc
|
||||
// exclusion). Absent loads as Default (null) and a save that edits something else still omits it.
|
||||
var loaded = AbCipDriverForm.TryDeserialize("""{"probeTimeoutSeconds":5}""");
|
||||
loaded.ShouldNotBeNull();
|
||||
var form = AbCipDriverForm.FormModel.FromOptions(loaded);
|
||||
form.Transport.ShouldBeNull();
|
||||
|
||||
form.TimeoutSeconds = 7;
|
||||
var json = AbCipDriverForm.Serialize(form.ToOptions(loaded.Devices));
|
||||
json.ShouldNotContain("transport", Case.Insensitive);
|
||||
json.ShouldContain("\"timeout\":\"00:00:07\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Choosing_Default_over_an_explicit_transport_drops_the_key()
|
||||
{
|
||||
var loaded = AbCipDriverForm.TryDeserialize("""{"transport":"Libplctag"}""");
|
||||
loaded.ShouldNotBeNull();
|
||||
var form = AbCipDriverForm.FormModel.FromOptions(loaded);
|
||||
form.Transport.ShouldBe(AbCipTransport.Libplctag);
|
||||
|
||||
var json = AbCipDriverForm.Serialize(form.ToOptions(loaded.Devices));
|
||||
json.ShouldContain("\"transport\":\"Libplctag\"");
|
||||
form.Transport = null; // the "Default (...)" dropdown option
|
||||
AbCipDriverForm.Serialize(form.ToOptions(loaded.Devices)).ShouldNotContain("transport", Case.Insensitive);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-12
@@ -104,7 +104,8 @@ public sealed class AbLegacyDriverPageFormSerializationTests
|
||||
[Theory]
|
||||
[InlineData(AbLegacyTransport.Libplctag)]
|
||||
[InlineData(AbLegacyTransport.Native)]
|
||||
public void Transport_survives_FromOptions_ToOptions(AbLegacyTransport transport)
|
||||
[InlineData(null)]
|
||||
public void Transport_survives_FromOptions_ToOptions(AbLegacyTransport? transport)
|
||||
{
|
||||
var original = new AbLegacyDriverOptions { Transport = transport };
|
||||
var back = AbLegacyDriverForm.FormModel.FromOptions(original).ToOptions([]);
|
||||
@@ -119,35 +120,52 @@ public sealed class AbLegacyDriverPageFormSerializationTests
|
||||
json.ShouldContain("\"transport\":\"Native\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Editing_an_unrelated_field_preserves_a_saved_Native_transport()
|
||||
[Theory]
|
||||
[InlineData("Native", AbLegacyTransport.Native)]
|
||||
[InlineData("Libplctag", AbLegacyTransport.Libplctag)]
|
||||
public void Editing_an_unrelated_field_preserves_an_explicit_transport(string savedValue, AbLegacyTransport expected)
|
||||
{
|
||||
// The regression the review found: FromOptions/ToOptions dropped Transport, so an operator who set
|
||||
// Native was silently reverted to libplctag the next time they saved ANY other edit.
|
||||
const string saved = """{"transport":"Native","timeout":"00:00:02","devices":[]}""";
|
||||
// a transport was silently reverted the next time they saved ANY other edit.
|
||||
var saved = $$"""{"transport":"{{savedValue}}","timeout":"00:00:02","devices":[]}""";
|
||||
var loaded = AbLegacyDriverForm.TryDeserialize(saved);
|
||||
loaded.ShouldNotBeNull();
|
||||
loaded.Transport.ShouldBe(AbLegacyTransport.Native);
|
||||
loaded.Transport.ShouldBe(expected);
|
||||
|
||||
var form = AbLegacyDriverForm.FormModel.FromOptions(loaded);
|
||||
form.TimeoutSeconds = 7;
|
||||
var json = AbLegacyDriverForm.Serialize(form.ToOptions(loaded.Devices));
|
||||
|
||||
json.ShouldContain("\"transport\":\"Native\"");
|
||||
json.ShouldContain($"\"transport\":\"{savedValue}\"");
|
||||
json.ShouldContain("\"timeout\":\"00:00:07\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_config_without_transport_loads_as_Libplctag_and_re_emits_it_explicitly()
|
||||
public void A_config_without_transport_loads_as_Default_and_an_unrelated_edit_keeps_the_key_absent()
|
||||
{
|
||||
// Chosen behaviour: an absent key round-trips as an explicit "Libplctag" (the form always writes the
|
||||
// field). The dropdown therefore shows "libplctag (default)".
|
||||
// Task 22 reverses Task 19's "re-emit an explicit Libplctag": that would pin every edited driver to
|
||||
// a concrete transport and bypass the driver-side default (and AbLegacy's PLC-5 / LogixPccc
|
||||
// exclusion). Absent loads as Default (null) and a save that edits something else still omits it.
|
||||
var loaded = AbLegacyDriverForm.TryDeserialize("""{"probeTimeoutSeconds":5}""");
|
||||
loaded.ShouldNotBeNull();
|
||||
var form = AbLegacyDriverForm.FormModel.FromOptions(loaded);
|
||||
form.Transport.ShouldBeNull();
|
||||
|
||||
form.TimeoutSeconds = 7;
|
||||
var json = AbLegacyDriverForm.Serialize(form.ToOptions(loaded.Devices));
|
||||
json.ShouldNotContain("transport", Case.Insensitive);
|
||||
json.ShouldContain("\"timeout\":\"00:00:07\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Choosing_Default_over_an_explicit_transport_drops_the_key()
|
||||
{
|
||||
var loaded = AbLegacyDriverForm.TryDeserialize("""{"transport":"Libplctag"}""");
|
||||
loaded.ShouldNotBeNull();
|
||||
var form = AbLegacyDriverForm.FormModel.FromOptions(loaded);
|
||||
form.Transport.ShouldBe(AbLegacyTransport.Libplctag);
|
||||
|
||||
var json = AbLegacyDriverForm.Serialize(form.ToOptions(loaded.Devices));
|
||||
json.ShouldContain("\"transport\":\"Libplctag\"");
|
||||
form.Transport = null; // the "Default (...)" dropdown option
|
||||
AbLegacyDriverForm.Serialize(form.ToOptions(loaded.Devices)).ShouldNotContain("transport", Case.Insensitive);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -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}");
|
||||
|
||||
Reference in New Issue
Block a user