fix(adminui): authoring must not accept a TagConfig enum the runtime rejects (#549)

MTConnectTagConfigModel read `dataType` with TagConfigJson.GetEnum, which falls
back to String on ANY unrecognised value. A misspelled name ("Strng") from a
hand-authored blob, a CSV import or the API was therefore accepted at authoring
and silently rewritten, while the driver's strict parse rejected it and the tag
died with BadNodeIdUnknown. Found by the G-7 parity tests' falsifiability
control, which had to use the digits-only shape to have a case that failed.

The rule is not restated in the AdminUI. MTConnectTagDefinitionFactory exposes
its parse as `IsAcceptedDataTypeName`, documented as the one authority the
driver, the deploy gate and the authoring model all consult; the model's
Validate() asks it about the raw value the blob carries. The old digits-only
message is NOT retained as a special case: the runtime draws no distinction
between a bare number and a typo — one predicate, one Inspect warning — so a
second message here would be a second statement of a rule this model does not
own. The raw value is captured as the JSON TOKEN, because the runtime
deserialises the key into a `string?` DTO field and an unquoted number throws
there too.

The survey found the same asymmetry at six more editors, contrary to the
assumption that those runtimes silently default: R2-11 Phase C moved every
driver's TagConfig enum read to TagConfigJson.TryReadEnumStrict, which REJECTS
the tag on a present-but-invalid string, while the editors kept loading them
with a silent fallback. Modbus (region/dataType/byteOrder), S7, AbCip, AbLegacy,
TwinCAT and FOCAS now check their enums in Validate() through the runtime's own
TagConfigJson.DescribeInvalidEnumText — added alongside a public
ClassifyEnumText that TryReadEnum now delegates to, so the JSON-token door and
the string door reach one implementation. Mqtt and Sql already carried
equivalent strict checks and are untouched.

Care taken NOT to become stricter than the runtime: a present-but-non-string
token stays accepted for those six (TryReadEnum calls it absent), as does an
absent key, each pinned by a test.

Also adds a direct ProjectReference to Core.Abstractions from the AdminUI — the
save gate now leans on a type it was consuming only transitively.
This commit is contained in:
Joseph Doherty
2026-08-09 22:08:42 -04:00
parent fefcd2c872
commit a3c0ec037f
20 changed files with 466 additions and 25 deletions
@@ -120,6 +120,18 @@ public sealed class EquipmentTagConfigInspectorTests
=> EquipmentTagConfigInspector.Inspect("MTConnect", """{"fullName":"avail","dataType":"8"}""")
.ShouldContain(w => w.Contains("dataType"));
/// <summary>
/// Gitea #549 companion to <c>MTConnectTagConfigModelTests</c>: an UNQUOTED number is rejected by
/// the runtime too, and by a different mechanism than the quoted one — the blob DTO's type field is
/// a <c>string?</c>, so the deserialize itself throws and the whole tag goes. The authoring model
/// therefore has to judge the raw JSON token rather than "the string value, or null", or it would
/// read this as an absent type and accept a blob the driver drops.
/// </summary>
[Fact]
public void MTConnect_unquoted_numeric_dataType_is_reported()
=> EquipmentTagConfigInspector.Inspect("MTConnect", """{"fullName":"avail","dataType":8}""")
.ShouldNotBeEmpty();
[Fact]
public void MTConnect_config_naming_no_dataitem_is_reported()
=> EquipmentTagConfigInspector.Inspect("MTConnect", """{"dataType":"String"}""")