feat(server): equipment-tag node writability from Tag.AccessLevel (parity-safe, no migration)
This commit is contained in:
+87
@@ -62,6 +62,80 @@ public sealed class DeploymentArtifactAliasParityTests
|
||||
tag.DataType.ShouldBe("Int32");
|
||||
tag.FolderPath.ShouldBe(string.Empty);
|
||||
tag.FullName.ShouldBe("TestMachine_020.TestChangingInt");
|
||||
// No AccessLevel in the blob → defaults to non-writable (read-only node).
|
||||
tag.Writable.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>The artifact decoder reads <c>AccessLevel</c> into <c>EquipmentTagPlan.Writable</c>:
|
||||
/// ReadWrite → true, Read → false. ConfigComposer emits the enum numerically (no string converter),
|
||||
/// so the numeric form (1 = ReadWrite) is the canonical wire shape, but the decoder also tolerates
|
||||
/// the string form ("ReadWrite") defensively — mirroring how the Kind gate accepts both.</summary>
|
||||
[Theory]
|
||||
[InlineData(1, true)] // numeric ReadWrite
|
||||
[InlineData(0, false)] // numeric Read
|
||||
public void ParseComposition_maps_numeric_AccessLevel_to_Writable(int accessLevel, bool expectedWritable)
|
||||
{
|
||||
var blob = JsonSerializer.SerializeToUtf8Bytes(new
|
||||
{
|
||||
Namespaces = new[] { new { NamespaceId = "ns-eq", Kind = 0 } },
|
||||
DriverInstances = new[]
|
||||
{
|
||||
new { DriverInstanceId = "drv", DriverType = "Modbus", DriverConfig = "{}", NamespaceId = "ns-eq" },
|
||||
},
|
||||
Tags = new object[]
|
||||
{
|
||||
new
|
||||
{
|
||||
TagId = "tag-1",
|
||||
DriverInstanceId = "drv",
|
||||
EquipmentId = "eq-1",
|
||||
Name = "Speed",
|
||||
FolderPath = (string?)null,
|
||||
DataType = "Float",
|
||||
AccessLevel = accessLevel,
|
||||
TagConfig = "{\"FullName\":\"40001\"}",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var c = DeploymentArtifact.ParseComposition(blob);
|
||||
|
||||
c.EquipmentTags.ShouldHaveSingleItem().Writable.ShouldBe(expectedWritable);
|
||||
}
|
||||
|
||||
/// <summary>The decoder also tolerates the string enum form ("ReadWrite"/"Read") in case a future
|
||||
/// serializer registers a string converter — byte-parity safety, mirroring the Kind gate.</summary>
|
||||
[Theory]
|
||||
[InlineData("ReadWrite", true)]
|
||||
[InlineData("Read", false)]
|
||||
public void ParseComposition_maps_string_AccessLevel_to_Writable(string accessLevel, bool expectedWritable)
|
||||
{
|
||||
var blob = JsonSerializer.SerializeToUtf8Bytes(new
|
||||
{
|
||||
Namespaces = new[] { new { NamespaceId = "ns-eq", Kind = 0 } },
|
||||
DriverInstances = new[]
|
||||
{
|
||||
new { DriverInstanceId = "drv", DriverType = "Modbus", DriverConfig = "{}", NamespaceId = "ns-eq" },
|
||||
},
|
||||
Tags = new object[]
|
||||
{
|
||||
new
|
||||
{
|
||||
TagId = "tag-1",
|
||||
DriverInstanceId = "drv",
|
||||
EquipmentId = "eq-1",
|
||||
Name = "Speed",
|
||||
FolderPath = (string?)null,
|
||||
DataType = "Float",
|
||||
AccessLevel = accessLevel,
|
||||
TagConfig = "{\"FullName\":\"40001\"}",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var c = DeploymentArtifact.ParseComposition(blob);
|
||||
|
||||
c.EquipmentTags.ShouldHaveSingleItem().Writable.ShouldBe(expectedWritable);
|
||||
}
|
||||
|
||||
/// <summary>An equipment-scoped tag in a non-Equipment (Simulated) namespace must NOT surface in
|
||||
@@ -239,6 +313,7 @@ public sealed class DeploymentArtifactAliasParityTests
|
||||
d.Name.ShouldBe(x.Name);
|
||||
d.DataType.ShouldBe(x.DataType);
|
||||
d.FullName.ShouldBe(x.FullName);
|
||||
d.Writable.ShouldBe(x.Writable);
|
||||
}
|
||||
|
||||
var galaxyPlan = decoded.EquipmentTags.Single(t => t.TagId == "tag-galaxy");
|
||||
@@ -246,6 +321,15 @@ public sealed class DeploymentArtifactAliasParityTests
|
||||
galaxyPlan.EquipmentId.ShouldBe("eq-galaxy");
|
||||
galaxyPlan.DriverInstanceId.ShouldBe("drv-galaxy");
|
||||
galaxyPlan.FolderPath.ShouldBe(string.Empty); // null FolderPath coalesced identically on both sides
|
||||
|
||||
// Writability flows from Tag.AccessLevel: the Galaxy tag is Read (read-only node), the Modbus
|
||||
// tag is ReadWrite (writable node). Both producers must derive the same Writable flag, and the
|
||||
// SequenceEqual above already proves they agree element-wise.
|
||||
galaxyPlan.Writable.ShouldBeFalse(); // AccessLevel = Read
|
||||
var modbusPlan = decoded.EquipmentTags.Single(t => t.TagId == "tag-modbus");
|
||||
modbusPlan.Writable.ShouldBeTrue(); // AccessLevel = ReadWrite
|
||||
composed.EquipmentTags.Single(t => t.TagId == "tag-galaxy").Writable.ShouldBeFalse();
|
||||
composed.EquipmentTags.Single(t => t.TagId == "tag-modbus").Writable.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>The full Pascal-case snapshot a <see cref="Tag"/> EF entity serialises to in the
|
||||
@@ -258,6 +342,9 @@ public sealed class DeploymentArtifactAliasParityTests
|
||||
t.Name,
|
||||
t.FolderPath,
|
||||
t.DataType,
|
||||
// ConfigComposer serialises with no JsonStringEnumConverter, so the TagAccessLevel enum lands
|
||||
// as its numeric value (Read = 0, ReadWrite = 1) — exactly like Kind = (int)ns.Kind above.
|
||||
AccessLevel = (int)t.AccessLevel,
|
||||
t.TagConfig,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user