diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectTagDefinitionFactory.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectTagDefinitionFactory.cs
index 02b843f3..04f5104e 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectTagDefinitionFactory.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectTagDefinitionFactory.cs
@@ -74,7 +74,9 @@ public static class MTConnectTagDefinitionFactory
dataItemId = id;
// "driverDataType" (the driver's own tags[] field) or "dataType" (what the AdminUI's typed
- // MTConnectTagConfigModel writes) — see the DTO remarks.
+ // MTConnectTagConfigModel writes) — see the DTO remarks. THIS PRECEDENCE IS MIRRORED by that
+ // model's own load (Gitea #550): a non-blank driverDataType out-ranks dataType there too, so the
+ // editor displays the type the driver will actually read rather than a key it is overriding.
var authoredType = FirstNonBlank(blob.DriverDataType, blob.DataType);
if (authoredType is null) return true;
@@ -230,6 +232,15 @@ public static class MTConnectTagDefinitionFactory
/// and the driver disagree about the type of every tag.
///
///
+ /// The alias out-ranks the canonical key, and the editor mirrors that (Gitea #550).
+ /// MTConnectTagConfigModel used to read dataType alone, so a blob carrying a
+ /// valid driverDataType was displayed and saved under a type the driver then
+ /// overrode — the editor showed a lie — while an INVALID one sailed through authoring and
+ /// was rejected here. It now reads both spellings in this precedence and normalises onto
+ /// dataType on save, dropping the alias, so a tag edited through the editor keeps
+ /// the type this parse read for it.
+ ///
+ ///
/// Kept separate from the driver's strict tags[] DTO rather than reusing it: there a
/// missing fullName is a config error worth failing the deployment over. A raw tag's
/// blob is authored elsewhere and its failure mode is per-tag, not per-driver.
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/MTConnectTagConfigModel.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/MTConnectTagConfigModel.cs
index c2679e46..2e83e343 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/MTConnectTagConfigModel.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/MTConnectTagConfigModel.cs
@@ -35,7 +35,9 @@ public sealed class MTConnectTagConfigModel
/// Logical data type override for the DataItem's value. Defaults to
/// — MTConnect is weakly typed on the wire, so an unset override should never coerce to a numeric type
- /// that can fail to parse (mirrors MTConnectDataTypeInference's own String-leaning default).
+ /// that can fail to parse (mirrors MTConnectDataTypeInference's own String-leaning default).
+ /// Loaded from driverDataType or dataType in the driver's own precedence (see
+ /// ); always saved as dataType, with the alias dropped.
public DriverDataType DataType { get; set; } = DriverDataType.String;
/// The DataItem's probe-sourced category (SAMPLE/EVENT/CONDITION). Read-only display metadata.
@@ -58,19 +60,25 @@ public sealed class MTConnectTagConfigModel
/// consumed by the driver or the runtime.
public string? MtComponent { get; set; }
- // The as-loaded raw "dataType" JSON token, kept so Validate() below can put it to the DRIVER's own
- // accept/reject rule (MTConnectTagDefinitionFactory.IsAcceptedDataTypeName) rather than to the
- // lenient load. TagConfigJson.GetEnum falls back to String on ANY unrecognised value, so without this
- // a misspelled name ("Strng") or a quoted number ("8" -> Float64, the ParseEnum numeric-text trap)
- // loads as a *valid-looking* enum value with no signal that it is not what the operator wrote — while
- // the strict runtime rejects the tag outright (Gitea #549). Captured as the raw TOKEN, not via
- // GetString: the runtime deserialises this key into a `string?` DTO field, so a NON-string token
- // (`"dataType": 8`) throws JsonException there and rejects the tag too, where GetString would report
- // it as simply absent. ToJson() always re-writes dataType as a name, so this can only ever be
- // poisoned on the FIRST load of a blob authored elsewhere (hand-edited, CSV import, API), never after
- // a save through this model.
+ // The as-loaded raw coercion-type JSON token — read under the DRIVER's precedence (see DataTypeKeys)
+ // — kept so Validate() below can put it to the DRIVER's own accept/reject rule
+ // (MTConnectTagDefinitionFactory.IsAcceptedDataTypeName) rather than to the lenient load. A plain
+ // Enum.TryParse falls back to String on ANY unrecognised value, so without this a misspelled name
+ // ("Strng") or a quoted number ("8" -> Float64, the ParseEnum numeric-text trap) loads as a
+ // *valid-looking* enum value with no signal that it is not what the operator wrote — while the strict
+ // runtime rejects the tag outright (Gitea #549). Captured as the raw TOKEN, not via GetString: the
+ // runtime deserialises both spellings into `string?` DTO fields, so a NON-string token
+ // (`"dataType": 8`, `"driverDataType": 8`) throws JsonException there and rejects the tag too, where
+ // GetString would report it as simply absent. ToJson() always re-writes dataType as a name and drops
+ // the alias, so this can only ever be poisoned on the FIRST load of a blob authored elsewhere
+ // (hand-edited, CSV import, API), never after a save through this model.
private string? _rawDataType;
+ // Which of DataTypeKeys supplied _rawDataType, so a rejection names the spelling the operator actually
+ // wrote. Naming "dataType" for a blob that carries "driverDataType" sends them to a key that is not
+ // there — the runtime's own Inspect() names the offending field for the same reason.
+ private string _rawDataTypeKey = CanonicalDataTypeKey;
+
private JsonObject _bag = new();
///
@@ -85,6 +93,34 @@ public sealed class MTConnectTagConfigModel
///
private static readonly string[] IdKeys = ["fullName", "dataItemId", "address"];
+ ///
+ /// The accepted spellings of the coercion type, in the runtime's precedence order, mirroring
+ /// MTConnectTagDefinitionFactory.TryFromTagConfig's
+ /// FirstNonBlank(blob.DriverDataType, blob.DataType): driverDataType (the driver's own
+ /// tags[] field name, and the spelling a hand-authored or driver-shaped blob carries) wins
+ /// over dataType (this model's canonical key).
+ ///
+ /// Reading only dataType was Gitea #550, and it broke in both directions at once: a
+ /// blob carrying "driverDataType": "Strng" sailed through authoring while the runtime
+ /// rejected the tag (the residual slice of #549), and — worse — a VALID driverDataType
+ /// silently overrode whatever type this editor displayed and saved, so the editor showed a lie.
+ ///
+ ///
+ /// therefore normalises onto dataType and DROPS the alias, exactly as
+ /// it does for . Both halves are load-bearing together: the precedence read
+ /// alone would leave the alias in the bag to out-rank the key the editor writes on the next load,
+ /// and the drop alone would retype every existing driverDataType tag to whatever the
+ /// lenient dataType read defaulted to on its next save.
+ ///
+ ///
+ private static readonly string[] DataTypeKeys = ["driverDataType", CanonicalDataTypeKey];
+
+ /// The spelling of the coercion type this model writes — the LAST of ,
+ /// i.e. the one the driver reads only when no alias out-ranks it. drops every other
+ /// entry of that array rather than naming the alias a second time, so adding a third spelling to the
+ /// read cannot leave the normalisation behind.
+ private const string CanonicalDataTypeKey = "dataType";
+
/// Loads a model from a TagConfig JSON string, defaulting any absent field and retaining
/// every original key (so fields this editor doesn't expose survive a load→save).
/// The raw TagConfig JSON string, or null for a new/empty config.
@@ -92,17 +128,19 @@ public sealed class MTConnectTagConfigModel
public static MTConnectTagConfigModel FromJson(string? json)
{
var o = TagConfigJson.ParseOrNew(json);
+ var (rawDataTypeKey, rawDataType) = ReadRawDataType(o);
return new MTConnectTagConfigModel
{
FullName = ReadId(o),
- DataType = TagConfigJson.GetEnum(o, "dataType", DriverDataType.String),
+ DataType = ParseDataType(rawDataType),
MtCategory = TagConfigJson.GetString(o, "mtCategory"),
MtType = TagConfigJson.GetString(o, "mtType"),
MtSubType = TagConfigJson.GetString(o, "mtSubType"),
Units = TagConfigJson.GetString(o, "units"),
MtDevice = TagConfigJson.GetString(o, "mtDevice"),
MtComponent = TagConfigJson.GetString(o, "mtComponent"),
- _rawDataType = ReadRawDataType(o),
+ _rawDataType = rawDataType,
+ _rawDataTypeKey = rawDataTypeKey,
_bag = o,
};
}
@@ -115,11 +153,19 @@ public sealed class MTConnectTagConfigModel
public string ToJson()
{
// Normalise onto the canonical key: a blob loaded via an alias must not keep the alias, or the
- // two spellings drift on the next edit and the driver silently reads the stale one.
+ // two spellings drift on the next edit and the driver silently reads the stale one. This holds
+ // for the coercion type (driverDataType, Gitea #550) exactly as it does for the id spellings —
+ // and because driverDataType OUT-RANKS dataType at the driver, leaving it behind would mean the
+ // type the operator just picked never reached the runtime at all.
TagConfigJson.Set(_bag, "dataItemId", null);
TagConfigJson.Set(_bag, "address", null);
TagConfigJson.Set(_bag, "fullName", FullName.Trim());
- TagConfigJson.Set(_bag, "dataType", DataType);
+ foreach (var alias in DataTypeKeys)
+ {
+ if (alias != CanonicalDataTypeKey) { TagConfigJson.Set(_bag, alias, null); }
+ }
+
+ TagConfigJson.Set(_bag, CanonicalDataTypeKey, DataType);
TagConfigJson.Set(_bag, "mtCategory", Blank(MtCategory));
TagConfigJson.Set(_bag, "mtType", Blank(MtType));
TagConfigJson.Set(_bag, "mtSubType", Blank(MtSubType));
@@ -151,22 +197,47 @@ public sealed class MTConnectTagConfigModel
// DataType above, because the CSV-import and API paths save the blob verbatim.
if (!MTConnectTagDefinitionFactory.IsAcceptedDataTypeName(_rawDataType))
{
- return $"dataType \"{_rawDataType}\" is not a valid DriverDataType name — re-select a data type. "
- + $"Valid: {string.Join(", ", Enum.GetNames())}.";
+ return $"{_rawDataTypeKey} \"{_rawDataType}\" is not a valid DriverDataType name — re-select a "
+ + $"data type. Valid: {string.Join(", ", Enum.GetNames())}.";
}
return null;
}
- // The as-loaded "dataType" TOKEN as text (see the _rawDataType remarks): a JSON string yields its
- // value, any other present token yields its JSON text so Validate() judges what was actually written,
- // and absent / JSON null yields null — the one shape the runtime treats as "no type declared".
- private static string? ReadRawDataType(JsonObject o)
+ // The as-loaded coercion-type TOKEN as text (see the _rawDataType and DataTypeKeys remarks), read in
+ // the RUNTIME's precedence so this model judges — and displays — the token the driver would actually
+ // read. Per key, mirroring FirstNonBlank(blob.DriverDataType, blob.DataType) over a DTO whose two
+ // fields are `string?`:
+ // · absent / JSON null → the field deserialises to null, which FirstNonBlank skips: try the next.
+ // · blank or whitespace → likewise blank to FirstNonBlank: try the next. A present-but-blank
+ // driverDataType must NOT shadow a populated dataType.
+ // · any non-string token → STOP. The runtime deserialise throws JsonException on it and rejects
+ // the whole tag, so it can never fall through to the other spelling;
+ // yield its JSON text so Validate() refuses exactly what the driver does.
+ // · non-blank string → STOP, this is the effective token.
+ // Nothing matched ⇒ null, the one shape the runtime treats as "no type declared" (it then falls back
+ // to the Agent's /probe declaration rather than rejecting the tag).
+ private static (string Key, string? Value) ReadRawDataType(JsonObject o)
{
- if (!o.TryGetPropertyValue("dataType", out var node) || node is null) { return null; }
- return node is JsonValue v && v.TryGetValue(out var s) ? s : node.ToJsonString();
+ foreach (var key in DataTypeKeys)
+ {
+ if (!o.TryGetPropertyValue(key, out var node) || node is null) { continue; }
+ if (node is not JsonValue v || !v.TryGetValue(out var s)) { return (key, node.ToJsonString()); }
+ if (string.IsNullOrWhiteSpace(s)) { continue; }
+ return (key, s);
+ }
+
+ return (CanonicalDataTypeKey, null);
}
+ // The lenient load of the effective token (see ReadRawDataType) for DISPLAY: an unrecognised value
+ // falls back to String so a poisoned blob still opens and can be fixed. Validate() — not this — is
+ // what refuses it, against the driver's own predicate.
+ private static DriverDataType ParseDataType(string? raw)
+ => raw is not null && Enum.TryParse(raw.Trim(), ignoreCase: true, out var v)
+ ? v
+ : DriverDataType.String;
+
// Normalises a blank/whitespace-only string to null so TagConfigJson.Set omits the key rather than
// persisting an empty string.
private static string? Blank(string? value) => string.IsNullOrWhiteSpace(value) ? null : value;
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/MTConnectTagConfigModelTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/MTConnectTagConfigModelTests.cs
index 9105f15e..8374ebe5 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/MTConnectTagConfigModelTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/MTConnectTagConfigModelTests.cs
@@ -218,4 +218,81 @@ public sealed class MTConnectTagConfigModelTests
public void Validate_rejects_a_dataType_written_as_an_unquoted_number()
=> MTConnectTagConfigModel.FromJson("""{"fullName":"dev1_pos","dataType":8}""")
.Validate().ShouldNotBeNull().ShouldContain("8");
+
+ // ---------------------------------------------------------------------------------------------
+ // Gitea #550 — driverDataType is a SECOND spelling of the coercion type, and the runtime prefers it.
+ // ---------------------------------------------------------------------------------------------
+
+ ///
+ /// The retype half of #550. driverDataType out-ranks dataType at the driver
+ /// (FirstNonBlank(blob.DriverDataType, blob.DataType)), so this blob reads as
+ /// Int32 at the runtime while the editor used to show — and save — String. Opening
+ /// and saving the tag therefore RETYPED it, silently, without the operator touching the control.
+ /// The load must show what the driver reads, and the save must keep it.
+ ///
+ [Fact]
+ public void A_driverDataType_out_ranks_dataType_on_load_and_survives_the_save()
+ {
+ var m = MTConnectTagConfigModel.FromJson(
+ """{"driverDataType":"Int32","dataType":"String","fullName":"x"}""");
+
+ m.DataType.ShouldBe(DriverDataType.Int32);
+
+ var saved = m.ToJson();
+ saved.ShouldContain("\"dataType\":\"Int32\"");
+ saved.ShouldNotContain("driverDataType");
+ }
+
+ ///
+ /// The normalisation half. Leaving the alias in the bag would let it out-rank the key this editor
+ /// writes on the very next load, so every later type change would be silently ignored by the
+ /// driver — the alias must be DROPPED, exactly as dataItemId/address are.
+ ///
+ [Fact]
+ public void ToJson_never_emits_the_driverDataType_alias()
+ {
+ var m = MTConnectTagConfigModel.FromJson("""{"fullName":"x","driverDataType":"Float64"}""");
+ m.DataType = DriverDataType.Int16;
+
+ var saved = m.ToJson();
+
+ saved.ShouldNotContain("driverDataType");
+ saved.ShouldContain("\"dataType\":\"Int16\"");
+ // …and a reload of the saved blob agrees, which is what "the two spellings cannot drift" means.
+ MTConnectTagConfigModel.FromJson(saved).DataType.ShouldBe(DriverDataType.Int16);
+ }
+
+ /// A present-but-blank alias declares nothing — the runtime's FirstNonBlank falls
+ /// through to dataType, so this load must too rather than reading the blank as "String".
+ [Theory]
+ [InlineData("""{"fullName":"x","driverDataType":"","dataType":"Int32"}""")]
+ [InlineData("""{"fullName":"x","driverDataType":" ","dataType":"Int32"}""")]
+ [InlineData("""{"fullName":"x","driverDataType":null,"dataType":"Int32"}""")]
+ public void A_blank_driverDataType_falls_through_to_dataType(string json)
+ {
+ var m = MTConnectTagConfigModel.FromJson(json);
+
+ m.DataType.ShouldBe(DriverDataType.Int32);
+ m.Validate().ShouldBeNull();
+ }
+
+ ///
+ /// The residual #549 slice: a misspelled alias was accepted at authoring because nothing read the
+ /// alias, while the driver rejected the tag outright. It is non-blank, so it WINS — a valid
+ /// dataType alongside it does not rescue the blob at the runtime and must not here either.
+ ///
+ [Theory]
+ [InlineData("""{"driverDataType":"Strng","fullName":"x"}""")]
+ [InlineData("""{"driverDataType":"Strng","dataType":"Int32","fullName":"x"}""")]
+ [InlineData("""{"driverDataType":"8","fullName":"x"}""")]
+ public void Validate_rejects_a_misspelled_driverDataType_even_when_dataType_is_valid(string json)
+ => MTConnectTagConfigModel.FromJson(json).Validate().ShouldNotBeNull().ShouldContain("driverDataType");
+
+ /// A NON-string alias token throws JsonException in the runtime's deserialise and takes
+ /// the whole tag down, so it can never fall through to a valid dataType.
+ [Theory]
+ [InlineData("""{"fullName":"x","driverDataType":8}""")]
+ [InlineData("""{"fullName":"x","driverDataType":8,"dataType":"Int32"}""")]
+ public void Validate_rejects_a_driverDataType_written_as_an_unquoted_number(string json)
+ => MTConnectTagConfigModel.FromJson(json).Validate().ShouldNotBeNull().ShouldContain("8");
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigAuthoringDeployParityTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigAuthoringDeployParityTests.cs
index 276c5992..095234bb 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigAuthoringDeployParityTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigAuthoringDeployParityTests.cs
@@ -3,6 +3,7 @@ using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Driver.MTConnect;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
@@ -25,6 +26,14 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
/// lives in AdminUI.Tests because it is the only suite that can see both sides — the models are
/// internal to the AdminUI project, which references ControlPlane.
///
+///
+/// One narrow slice of the reverse direction IS asserted, and the caveat above does not cover
+/// it: where the RUNTIME rejects a TagConfig outright — a present-but-invalid enum spelling — the
+/// modal must refuse it too, or config authored by CSV import or API reaches a driver that skips the
+/// tag with BadNodeIdUnknown (Gitea #549, and its MTConnect driverDataType residual
+/// #550). That is the opposite of the editor being merely opinionated: it is the editor being WEAKER
+/// than the parser, which is never legitimate.
+///
///
public sealed class TagConfigAuthoringDeployParityTests
{
@@ -194,6 +203,80 @@ public sealed class TagConfigAuthoringDeployParityTests
+ "import or API is unchecked — the exact G-7 asymmetry.");
}
+ // -------------------------------------------------------------------------------------------------
+ // Gitea #550 — MTConnect's second spelling of the coercion type. The assertions below drive the REAL
+ // runtime reader (MTConnectTagDefinitionFactory.TryFromTagConfig) rather than the deploy gate alone,
+ // because the defect is about which VALUE the runtime resolves, not merely whether it accepts.
+ // -------------------------------------------------------------------------------------------------
+
+ ///
+ /// Authoring accepts ⇔ the runtime accepts, across every mixed shape of the two spellings — and the
+ /// blob the editor SAVES resolves to the same type the runtime read from the original (no retype).
+ ///
+ [Theory]
+ // driverDataType alone — the driver's own tags[] spelling, and what a hand-authored blob carries.
+ [InlineData("""{"fullName":"x","driverDataType":"Int32"}""", DriverDataType.Int32)]
+ // dataType alone — what this editor writes.
+ [InlineData("""{"fullName":"x","dataType":"Int32"}""", DriverDataType.Int32)]
+ // Both, agreeing — the shape a #550-era save produced when the operator happened to pick the same type.
+ [InlineData("""{"fullName":"x","driverDataType":"Int32","dataType":"Int32"}""", DriverDataType.Int32)]
+ // Both, DISAGREEING — the alias wins at the driver, so it must win here.
+ [InlineData("""{"driverDataType":"Int32","dataType":"String","fullName":"x"}""", DriverDataType.Int32)]
+ // A present-but-blank alias declares nothing and falls through (FirstNonBlank).
+ [InlineData("""{"fullName":"x","driverDataType":"","dataType":"Float64"}""", DriverDataType.Float64)]
+ [InlineData("""{"fullName":"x","driverDataType":" ","dataType":"Float64"}""", DriverDataType.Float64)]
+ [InlineData("""{"fullName":"x","driverDataType":null,"dataType":"Float64"}""", DriverDataType.Float64)]
+ // Casing and surrounding whitespace are the runtime's business, and it accepts both.
+ [InlineData("""{"fullName":"x","driverDataType":" float64 "}""", DriverDataType.Float64)]
+ public void MTConnect_dataType_spellings_the_runtime_accepts_are_authorable_and_never_retyped(
+ string tagConfig, DriverDataType expected)
+ {
+ // The runtime's verdict on the ORIGINAL blob, which is the thing that must be preserved.
+ MTConnectTagDefinitionFactory.TryFromTagConfig(tagConfig, out _, out var authored).ShouldBeTrue(
+ "the runtime must accept this blob, or the authoring parity below proves nothing.");
+ authored.ShouldNotBeNull().DriverDataType.ShouldBe(expected);
+
+ // Authoring agrees: it neither blocks the save nor draws a deploy warning.
+ var model = MTConnectTagConfigModel.FromJson(tagConfig);
+ model.Validate().ShouldBeNull("the editor refused a blob the runtime serves.");
+ TagConfigValidator.Validate(DriverTypeNames.MTConnect, tagConfig).ShouldBeNull();
+ EquipmentTagConfigInspector.Inspect(DriverTypeNames.MTConnect, tagConfig).ShouldBeEmpty();
+
+ // …and it DISPLAYS the type the runtime read, not the key the runtime is overriding.
+ model.DataType.ShouldBe(expected);
+
+ // The no-retype invariant: one load→save leaves a blob the runtime resolves identically.
+ var saved = model.ToJson();
+ saved.Contains("driverDataType", StringComparison.OrdinalIgnoreCase).ShouldBeFalse(
+ "the alias must be dropped on save, or it out-ranks the key the editor writes on the next load.");
+ MTConnectTagDefinitionFactory.TryFromTagConfig(saved, out _, out var afterSave).ShouldBeTrue();
+ afterSave.ShouldNotBeNull().DriverDataType.ShouldBe(expected);
+ }
+
+ ///
+ /// The falsification direction, and the residual slice of #549 that #550 names: every blob here is
+ /// REJECTED by the runtime, so authoring must refuse it too. A non-blank but invalid alias wins over
+ /// a perfectly valid dataType — it is not rescued — and a non-string alias token throws in the
+ /// runtime's deserialise and takes the whole tag down.
+ ///
+ [Theory]
+ [InlineData("""{"driverDataType":"Strng","fullName":"x"}""")]
+ [InlineData("""{"driverDataType":"Strng","dataType":"Int32","fullName":"x"}""")]
+ [InlineData("""{"fullName":"x","driverDataType":"8"}""")]
+ [InlineData("""{"fullName":"x","driverDataType":8}""")]
+ [InlineData("""{"fullName":"x","driverDataType":8,"dataType":"Int32"}""")]
+ // A blank alias falls through — onto a dataType that is itself unreadable.
+ [InlineData("""{"fullName":"x","driverDataType":"","dataType":"Strng"}""")]
+ public void MTConnect_dataType_spellings_the_runtime_rejects_are_refused_at_authoring(string tagConfig)
+ {
+ MTConnectTagDefinitionFactory.TryFromTagConfig(tagConfig, out _, out _).ShouldBeFalse(
+ "the runtime must reject this blob, or this control is testing the wrong thing.");
+
+ TagConfigValidator.Validate(DriverTypeNames.MTConnect, tagConfig).ShouldNotBeNullOrEmpty(
+ "the modal would let an operator save a blob the driver skips with BadNodeIdUnknown.");
+ EquipmentTagConfigInspector.Inspect(DriverTypeNames.MTConnect, tagConfig).ShouldNotBeEmpty();
+ }
+
/// Asserts the editor accepted the config (so the parity claim is not vacuous) and that the
/// deploy gate then found nothing to say about it.
/// The canonical driver-type string both gates dispatch on.