fix(tagconfig): close the four #551 residuals — accept-gate-first Inspect, three authoring rules, CSV/service gate, doc sweep
Gitea #551. 1. CSV import ran no driver-typed config gate. Its typed columns were always strictly parsed, but the free-form TagConfigJson column reached the database unread, so a CSV could author a tag the driver rejects — one that deploys green and reads BadNodeIdUnknown forever. RawTagCsvMapper.Parse now names the offending row in the staged review grid, and RawTreeService enforces the same gate on CreateTag/UpdateTag/ImportTags: a form cannot be the guarantee (the API and any future caller reach none of them, and this repo has no bUnit). A rejected import row is an ERROR — the batch inserts nothing and the row is named — never a skip. Note the brief's premise that manual entry rides ImportTagsAsync is wrong: RawManualTagEntryModal commits row-by-row via CreateTagAsync, which is why the gate had to land on all three write paths. 2/3. The seven pre-#549 Inspect methods ran a hand-list of DescribeInvalidEnum checks, so every rejection cause not on the list was invisible. They are now ACCEPT-GATE-FIRST (the MTConnect/Sql/OpcUaClient/Calculation shape): ask the driver's own reader for the verdict, then explain the refusal, with a catch-all so "the parse rejects but Inspect said nothing" is unreachable. That closes S7's absent dataType, AbCip members[], Modbus stringByteOrder and the type-dependent guards, plus causes the issue did not name (absent region/address/tagPath/symbolPath/topic, TwinCAT Structure, MQTT's Sparkplug tuple). FOCAS and MQTT keep advisories about ACCEPTED blobs, so they diagnose rather than return empty. Authoring half: ModbusTagConfigModel gains stringByteOrder; AbCipTagConfigModel asks the new AbCipTagDefinitionFactory.DescribeInvalidMember; S7TagConfigModel captures whether the SOURCE blob declared dataType and asks S7TagDefinitionFactory.DataTypeKey/DeclaresDataType. One authority per rule. 4. Doc sweep: the six factory Inspect summaries, EquipmentTagConfigInspector's class remarks and AdminOperationsActor's comment all claimed invalid enums are "silently defaulted by the lenient runtime" — false since R2-11 Phase C made the runtime reads strict. They describe rejections now.
This commit is contained in:
@@ -34,6 +34,19 @@ public sealed class RawTreeServiceWaveBTests
|
||||
private static RawTagInput Tag(string name, string dataType = "Float", string tagConfig = "{}") =>
|
||||
new(name, dataType, TagAccessLevel.ReadWrite, WriteIdempotent: false, PollGroupId: null, tagConfig);
|
||||
|
||||
/// <summary>
|
||||
/// A tag input whose <c>TagConfig</c> the <b>S7</b> driver would actually read — <c>RootDeviceId</c>
|
||||
/// hangs off the seed's S7 driver, and since Gitea #551 the service runs the driver-typed
|
||||
/// <c>TagConfigValidator</c> on every tag it writes. S7 requires BOTH an <c>address</c> and a
|
||||
/// present <c>dataType</c> key, so the bare <c>{}</c> the sibling helper defaults to is a config the
|
||||
/// driver rejects and the service now refuses. The Modbus-device tests keep <c>{}</c>: Modbus reads
|
||||
/// only enums out of the blob and accepts an empty one.
|
||||
/// </summary>
|
||||
/// <param name="name">The tag name.</param>
|
||||
/// <returns>The import-ready tag input.</returns>
|
||||
private static RawTagInput S7Tag(string name) =>
|
||||
Tag(name, tagConfig: """{"address":"DB1.DBW0","dataType":"Int16"}""");
|
||||
|
||||
// -------------------------------------------------------------------------------- CreateTag (WP4)
|
||||
|
||||
[Fact]
|
||||
@@ -181,9 +194,9 @@ public sealed class RawTreeServiceWaveBTests
|
||||
|
||||
var rows = new[]
|
||||
{
|
||||
new RawTagImportRow("Line/A", Tag("t1")),
|
||||
new RawTagImportRow("Line/A", Tag("t2")), // reuses Line + Line/A
|
||||
new RawTagImportRow("Line/B", Tag("t3")), // reuses Line, new B
|
||||
new RawTagImportRow("Line/A", S7Tag("t1")),
|
||||
new RawTagImportRow("Line/A", S7Tag("t2")), // reuses Line + Line/A
|
||||
new RawTagImportRow("Line/B", S7Tag("t3")), // reuses Line, new B
|
||||
};
|
||||
|
||||
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows);
|
||||
@@ -207,7 +220,7 @@ public sealed class RawTreeServiceWaveBTests
|
||||
var (service, dbName) = Seeded();
|
||||
|
||||
var result = await service.ImportTagsAsync(
|
||||
RawTreeTestDb.RootDeviceId, new[] { new RawTagImportRow(null, Tag("rootTag")) });
|
||||
RawTreeTestDb.RootDeviceId, new[] { new RawTagImportRow(null, S7Tag("rootTag")) });
|
||||
|
||||
result.Inserted.ShouldBe(1);
|
||||
using var db = RawTreeTestDb.CreateNamed(dbName);
|
||||
@@ -221,9 +234,9 @@ public sealed class RawTreeServiceWaveBTests
|
||||
|
||||
var rows = new[]
|
||||
{
|
||||
new RawTagImportRow("Grp", Tag("good1")),
|
||||
new RawTagImportRow("Grp", S7Tag("good1")),
|
||||
new RawTagImportRow("Grp", Tag("bad", tagConfig: "{oops")), // malformed JSON ⇒ error
|
||||
new RawTagImportRow("Grp", Tag("good2")),
|
||||
new RawTagImportRow("Grp", S7Tag("good2")),
|
||||
};
|
||||
|
||||
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows);
|
||||
@@ -245,8 +258,8 @@ public sealed class RawTreeServiceWaveBTests
|
||||
|
||||
var rows = new[]
|
||||
{
|
||||
new RawTagImportRow("Grp", Tag("dup")),
|
||||
new RawTagImportRow("Grp", Tag("dup")), // same path + name within the batch
|
||||
new RawTagImportRow("Grp", S7Tag("dup")),
|
||||
new RawTagImportRow("Grp", S7Tag("dup")), // same path + name within the batch
|
||||
};
|
||||
|
||||
var result = await service.ImportTagsAsync(RawTreeTestDb.RootDeviceId, rows);
|
||||
@@ -262,7 +275,7 @@ public sealed class RawTreeServiceWaveBTests
|
||||
|
||||
// "temperature" already sits at the device root of RootDevice.
|
||||
var result = await service.ImportTagsAsync(
|
||||
RawTreeTestDb.RootDeviceId, new[] { new RawTagImportRow(null, Tag("temperature")) });
|
||||
RawTreeTestDb.RootDeviceId, new[] { new RawTagImportRow(null, S7Tag("temperature")) });
|
||||
|
||||
result.Inserted.ShouldBe(0);
|
||||
result.Errors.ShouldContain(e => e.Contains("already exists"));
|
||||
|
||||
Reference in New Issue
Block a user