fix(mtconnect): serve an empty string-typed value as Good "" (#824)

Owner decision 2026-11-11 (Java parity): a genuinely empty observation on a
String/Reference or unauthored item is Good "" instead of BadNoCommunication.
Non-string items (numeric, bool, DateTime, arrays) keep BadNoCommunication -
the Agent supplied no value; BadTypeMismatch would wrongly blame the authored
type. The UNAVAILABLE sentinel is unchanged, and a structured observation with
no text stays BadNotSupported (structured check now precedes the empty check).
Parity matrix, quality-mapping section and drivers rule updated.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
Joseph Doherty
2026-11-11 11:56:30 -05:00
parent e857135e8b
commit bdbc47f5fc
4 changed files with 136 additions and 14 deletions
@@ -320,21 +320,107 @@ public sealed class MTConnectObservationIndexTests
.ShouldBe(new DateTime(2026, 7, 24, 12, 0, 0, 500, DateTimeKind.Utc));
}
/// <summary>An observation whose text is empty is "the Agent supplied no value" — never a Good empty string.</summary>
/// <summary>
/// lmxopcua#824 (Java parity): an empty element on a string-typed item is a Good empty string, not an
/// absence. Whitespace-only text is the same (the parser trims it).
/// </summary>
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Empty_observation_value_maps_to_BadNoCommunication(string value)
[InlineData("", DriverDataType.String)]
[InlineData(" ", DriverDataType.String)]
[InlineData("", DriverDataType.Reference)]
public void Empty_observation_value_on_a_string_item_is_a_Good_empty_string(string value, DriverDataType type)
{
var idx = new MTConnectObservationIndex([Tag("x", DriverDataType.String)]);
var idx = new MTConnectObservationIndex([Tag("x", type)]);
idx.Apply(Synthetic("x", value));
var snap = idx.Get("x");
snap.StatusCode.ShouldBe(Good);
snap.Value.ShouldBe(string.Empty);
}
/// <summary>An unauthored item coerces as String, so its empty value is a Good empty string too (#824).</summary>
[Fact]
public void Empty_observation_value_on_an_unauthored_item_is_a_Good_empty_string()
{
var idx = new MTConnectObservationIndex();
idx.Apply(Synthetic("x", ""));
var snap = idx.Get("x");
snap.StatusCode.ShouldBe(Good);
snap.Value.ShouldBe(string.Empty);
}
/// <summary>
/// #824: on a non-string item "" cannot be a value, so it stays BadNoCommunication (the Agent supplied
/// no value) — not BadTypeMismatch, which would blame the tag's authored type.
/// </summary>
[Theory]
[InlineData(DriverDataType.Float64)]
[InlineData(DriverDataType.Float32)]
[InlineData(DriverDataType.Int32)]
[InlineData(DriverDataType.Int64)]
[InlineData(DriverDataType.Boolean)]
[InlineData(DriverDataType.DateTime)]
public void Empty_observation_value_on_a_non_string_item_stays_BadNoCommunication(DriverDataType type)
{
var idx = new MTConnectObservationIndex([Tag("x", type)]);
idx.Apply(Synthetic("x", " "));
var snap = idx.Get("x");
snap.StatusCode.ShouldBe(BadNoCommunication);
snap.Value.ShouldBeNull();
}
/// <summary>
/// #824 control: the UNAVAILABLE sentinel is unaffected — still BadNoCommunication on a String item, where an
/// empty value is now Good.
/// </summary>
[Fact]
public void Unavailable_on_a_string_item_is_still_BadNoCommunication()
{
var idx = new MTConnectObservationIndex([Tag("x", DriverDataType.String)]);
idx.Apply(Synthetic("x", "UNAVAILABLE"));
var snap = idx.Get("x");
snap.StatusCode.ShouldBe(BadNoCommunication);
snap.Value.ShouldBeNull();
}
/// <summary>#824 end to end through the parser: an empty, a self-closing and a blank element are all Good "".</summary>
[Theory]
[InlineData("""<Program dataItemId="p" timestamp="2026-07-24T12:00:00Z" sequence="1"></Program>""")]
[InlineData("""<Program dataItemId="p" timestamp="2026-07-24T12:00:00Z" sequence="1"/>""")]
[InlineData("""<Program dataItemId="p" timestamp="2026-07-24T12:00:00Z" sequence="1"> </Program>""")]
public void A_parsed_empty_event_on_a_string_item_is_a_Good_empty_string(string element)
{
var idx = new MTConnectObservationIndex([Tag("p", DriverDataType.String)]);
idx.Apply(MTConnectStreamsParser.Parse($"""
<MTConnectStreams>
<Header instanceId="1" nextSequence="2" firstSequence="1"/>
<Streams><DeviceStream><ComponentStream><Events>{element}</Events></ComponentStream></DeviceStream></Streams>
</MTConnectStreams>
"""));
var snap = idx.Get("p");
snap.StatusCode.ShouldBe(Good);
snap.Value.ShouldBe(string.Empty);
}
/// <summary>A structured observation with no text stays BadNotSupported — it never becomes a Good "" (#824).</summary>
[Fact]
public void A_structured_observation_with_no_text_stays_BadNotSupported()
{
var idx = new MTConnectObservationIndex([Tag("x", DriverDataType.String)]);
idx.Apply(Synthetic("x", "", isStructured: true));
idx.Get("x").StatusCode.ShouldBe(BadNotSupported);
}
// -------------------------------------------------------------------------- unknown lookups
/// <summary>A dataItemId that is neither authored nor ever observed is unknown to this driver.</summary>