Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/MqttUnbindableLeafMessageTests.cs
T
Joseph Doherty 7f29a75edb
v2-ci / unit-tests (push) Has been cancelled
feat(mqtt): read Sparkplug B array metrics as 1-D OPC UA arrays (lmxopcua#754)
R06:M-5 made Sparkplug *Array leaves unbindable because the ingestor refused
them. The ingestor now decodes the packed bytes_value of all 13 array types
(new SparkplugArrayCodec: LE fixed widths, int64-ms DateTime, uint32-count +
MSB-first BooleanArray, NUL-terminated UTF-8 StringArray; golden vectors are
the spec's examples, reproduced byte-for-byte by SparkplugNet 1.3.10) and
publishes a typed 1-D array through the scalar coercion, all-or-nothing.

Shape uses the fleet's one array model: the tag's platform isArray/arrayLength
keys (ValueRank=1, ArrayDimensions=[arrayLength] or [0] = variable length).
The driver reads them with TagConfigIntent's exact semantics and never rejects
on them, so composer, driver, authoring model and deploy inspector agree
(MqttArrayShapeParityTests). Shape mismatch (array metric on a scalar tag, or
the reverse) is BadTypeMismatch - a String tag no longer gets the packed
buffer as base64; a malformed body or a length differing from a declared
arrayLength is BadDecodingError (MTConnect #627 verdict). Template/DataSet
members of an array type take the same path. Arrays stay read-only (encoder
refuses them; MQTT is not in DriverArrayWriteSupport).

Browser: array leaves are bindable again and state isArray in AddressFields;
the commit mapper writes "isArray": true (no arrayLength). Discovery reports
IsArray/ArrayDim. Docs: Mqtt.md "Array metrics", design table, drivers rule.

Tests: codec (golden + malformed), ingest through the real codec + Dispatch
(the project's MiniBroker never forwards a PUBLISH, so there is no in-process
broker harness), browse, discovery, commit mapper, parity corpus. Mutating the
fix back out fails 18 MQTT + 9 AdminUI tests.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 13:58:38 -05:00

60 lines
2.8 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
/// <summary>
/// G2-F2: a Sparkplug array / unsupported-type metric is refused at commit with a "not bindable in this
/// version" message that echoes the browser's descriptor — not the "attribute lookup returned nothing"
/// text, which is reserved for a lookup that really did return nothing.
/// </summary>
[Trait("Category", "Unit")]
public sealed class MqttUnbindableLeafMessageTests
{
[Fact]
public void An_unsupported_metric_says_it_is_not_bindable_and_echoes_the_descriptor()
{
// (Array metrics were the original example; they are bindable since #754 — an unsupported type is not.)
const string descriptor = "Metric · DataSet · alias 7 · unsupported in v1 — not bindable";
var error = RawBrowseCommitMapper.DescribeUncommittableLeaf("Mqtt", "Pressures", addressFields: null, descriptor);
error.ShouldNotBeNull();
error!.ShouldContain("'Pressures' cannot be bound in this version");
error.ShouldContain(descriptor);
error.ShouldNotContain("attribute lookup returned nothing");
}
[Fact]
public void A_lookup_that_returned_nothing_keeps_the_original_message()
{
// Positive control: the new branch is selected by the descriptor, not by MQTT in general.
RawBrowseCommitMapper.DescribeUncommittableLeaf("Mqtt", "Temperature", addressFields: null, descriptor: null)!
.ShouldContain("attribute lookup returned nothing");
RawBrowseCommitMapper.DescribeUncommittableLeaf("Mqtt", "Temperature", null, "Metric · Double")!
.ShouldContain("attribute lookup returned nothing");
}
[Fact]
public void A_bindable_leaf_is_never_refused_whatever_its_descriptor_says()
=> RawBrowseCommitMapper.DescribeUncommittableLeaf(
"Mqtt", "Temperature",
new Dictionary<string, string> { [ZB.MOM.WW.OtOpcUa.Driver.Mqtt.MqttTagConfigKeys.MetricName] = "Temperature" },
"anything · not bindable")
.ShouldBeNull();
[Fact]
public void The_marker_matches_what_the_sparkplug_browser_writes()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "ZB.MOM.WW.OtOpcUa.slnx"))) dir = dir.Parent;
var browser = File.ReadAllText(Path.Combine(dir!.FullName, "src", "Drivers",
"ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser", "MqttBrowseSession.cs"));
browser.ShouldContain("— " + RawBrowseCommitMapper.NotBindableMarker + "\")");
AdminUiSource.Read("Components", "Shared", "Raw", "RawBrowseModal.razor")
.ShouldContain("DescribeUncommittableLeaf(_driverType, s.Name, s.AddressFields, s.Descriptor)");
}
}