diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/CalculationDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/CalculationDriverProbe.cs
index 060cf5bd..0a34b959 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/CalculationDriverProbe.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/CalculationDriverProbe.cs
@@ -13,10 +13,16 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Calculation;
///
public sealed class CalculationDriverProbe : IDriverProbe
{
+ // Mirrors CalculationDriverFactoryExtensions.JsonOptions / CalculationDriver.ReinitJsonOptions
+ // (AllowTrailingCommas + ReadCommentHandling included) so a config blob the real spawn/reinit path
+ // tolerates never fails Test-Connect as "invalid JSON" — the probe must never be STRICTER than the
+ // path it is standing in for.
private static readonly JsonSerializerOptions Opts = new()
{
PropertyNameCaseInsensitive = true,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ AllowTrailingCommas = true,
Converters = { new JsonStringEnumConverter() },
};
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation.Tests/CalculationDriverProbeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation.Tests/CalculationDriverProbeTests.cs
new file mode 100644
index 00000000..5d1be8f4
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation.Tests/CalculationDriverProbeTests.cs
@@ -0,0 +1,72 @@
+using Shouldly;
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Calculation.Tests;
+
+///
+/// Unit tests for — the Calculation pseudo-driver's Test-Connect
+/// probe (parse-only, since there is no backend to reach).
+///
+public sealed class CalculationDriverProbeTests
+{
+ private static readonly CalculationDriverProbe Sut = new();
+
+ [Fact]
+ public async Task Empty_config_json_fails()
+ {
+ var result = await Sut.ProbeAsync("", TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
+ result.Ok.ShouldBeFalse();
+ result.Message.ShouldNotBeNullOrWhiteSpace();
+ }
+
+ [Fact]
+ public async Task Malformed_json_fails()
+ {
+ var result = await Sut.ProbeAsync("not json", TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
+ result.Ok.ShouldBeFalse();
+ result.Message.ShouldNotBeNullOrWhiteSpace();
+ }
+
+ [Fact]
+ public async Task A_config_with_no_rawTags_array_is_still_valid()
+ {
+ var result = await Sut.ProbeAsync("{}", TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
+ result.Ok.ShouldBeTrue(result.Message);
+ }
+
+ [Fact]
+ public async Task A_well_formed_rawTags_config_is_valid()
+ {
+ const string json = """{"rawTags":[{"rawPath":"calc/e/x","tagConfig":"{}","writeIdempotent":false}]}""";
+ var result = await Sut.ProbeAsync(json, TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
+ result.Ok.ShouldBeTrue(result.Message);
+ }
+
+ ///
+ /// The probe must not be STRICTER than the real spawn/reinit path: both
+ /// CalculationDriverFactoryExtensions.JsonOptions and CalculationDriver.ReinitJsonOptions
+ /// set AllowTrailingCommas + ReadCommentHandling.Skip. Before this pin the probe's own
+ /// options omitted both, so a config a real deploy would accept could fail Test-Connect as "invalid".
+ ///
+ [Fact]
+ public async Task Tolerates_trailing_commas_like_the_real_spawn_and_reinit_paths()
+ {
+ const string json = """{"rawTags":[{"rawPath":"calc/e/x","tagConfig":"{}","writeIdempotent":false},],}""";
+ var result = await Sut.ProbeAsync(json, TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
+ result.Ok.ShouldBeTrue(result.Message);
+ }
+
+ /// Same parity claim for comments, which the spawn/reinit path also tolerates.
+ [Fact]
+ public async Task Tolerates_comments_like_the_real_spawn_and_reinit_paths()
+ {
+ const string json = """
+ {
+ // a hand-edited config comment
+ "rawTags": []
+ }
+ """;
+ var result = await Sut.ProbeAsync(json, TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
+ result.Ok.ShouldBeTrue(result.Message);
+ }
+}