diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/AbCipDriverPageFormSerializationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/AbCipDriverPageFormSerializationTests.cs index 060ca94a..5ae96fe8 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/AbCipDriverPageFormSerializationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/AbCipDriverPageFormSerializationTests.cs @@ -43,10 +43,6 @@ public sealed class AbCipDriverPageFormSerializationTests [ new AbCipDeviceOptions("ab://10.0.0.1/1,0", AbCipPlcFamily.ControlLogix, "PLC-1"), ], - Tags = - [ - new AbCipTagDefinition("Speed", "ab://10.0.0.1/1,0", "Motor1.Speed", AbCipDataType.Real, Writable: true), - ], }; var json = JsonSerializer.Serialize(original, _opts); @@ -66,9 +62,6 @@ public sealed class AbCipDriverPageFormSerializationTests back.Devices.Count.ShouldBe(1); back.Devices[0].HostAddress.ShouldBe("ab://10.0.0.1/1,0"); back.Devices[0].PlcFamily.ShouldBe(AbCipPlcFamily.ControlLogix); - back.Tags.Count.ShouldBe(1); - back.Tags[0].Name.ShouldBe("Speed"); - back.Tags[0].DataType.ShouldBe(AbCipDataType.Real); } [Fact] @@ -116,43 +109,11 @@ public sealed class AbCipDriverPageFormSerializationTests back.ConnectionSize.ShouldBe(4002); } - [Fact] - public void TagRow_round_trips_through_definition() - { - var row = new AbCipDriverPage.AbCipTagRow - { - Name = "Speed", DeviceHostAddress = "ab://10.0.0.1/1,0", TagPath = "Motor1.Speed", - DataType = AbCipDataType.Real, Writable = true, - }; - var def = row.ToDefinition(); - var back = AbCipDriverPage.AbCipTagRow.FromDefinition(def); - - back.Name.ShouldBe("Speed"); - back.DeviceHostAddress.ShouldBe("ab://10.0.0.1/1,0"); - back.TagPath.ShouldBe("Motor1.Speed"); - back.DataType.ShouldBe(AbCipDataType.Real); - back.Writable.ShouldBeTrue(); - } - - [Fact] - public void TagRow_preserves_unedited_fields() - { - var original = new AbCipTagDefinition( - "Speed", "ab://10.0.0.1/1,0", "Motor1.Speed", AbCipDataType.Structure, - Writable: true, WriteIdempotent: true, - Members: [new AbCipStructureMember("Sub", AbCipDataType.DInt)], - SafetyTag: true); - var row = AbCipDriverPage.AbCipTagRow.FromDefinition(original); - row.Name = "Renamed"; - - var back = row.ToDefinition(); - back.Name.ShouldBe("Renamed"); - back.WriteIdempotent.ShouldBeTrue(); - back.SafetyTag.ShouldBeTrue(); - back.Members.ShouldNotBeNull(); - back.Members!.Count.ShouldBe(1); - back.Members[0].Name.ShouldBe("Sub"); - } + // v3 Batch-1 migration: the pre-declared Tags editor (AbCipDriverPage.AbCipTagRow + + // AbCipDriverOptions.Tags) was retired — tags moved to the Raw tree (/raw, Batch 2). The former + // TagRow_* / ValidateTagRow_* tests and the tag leg of the list-serialization test were removed. + // The multi-device Devices editor is RETAINED, so its coverage (DeviceRow_* + ValidateDeviceRow_* + // + the device-list serialization below, all carrying the AbCipPlcFamily enum) stays. [Fact] public void ValidateDeviceRow_rejects_duplicate_host() @@ -163,32 +124,19 @@ public sealed class AbCipDriverPageFormSerializationTests } [Fact] - public void ValidateTagRow_rejects_duplicate_name() - { - var rows = new List { new() { Name = "Speed" } }; - AbCipDriverPage.AbCipTagRow.ValidateRow(new() { Name = "Speed" }, rows, null) - .ShouldNotBeNull(); - } - - [Fact] - public void Device_and_tag_lists_survive_options_serialize_round_trip() + public void Device_list_survives_options_serialize_round_trip() { var devices = new List { new("ab://10.0.0.1/1,0", AbCipPlcFamily.ControlLogix, "PLC-1"), new("ab://10.0.0.2/1,0", AbCipPlcFamily.CompactLogix, "PLC-2"), }; - var tags = new List - { - new("Speed", "ab://10.0.0.1/1,0", "Motor1.Speed", AbCipDataType.Real), - new("Run", "ab://10.0.0.2/1,0", "Motor2.Run", AbCipDataType.Bool), - }; - var opts = new AbCipDriverPage.FormModel().ToOptions(devices, tags); + var opts = new AbCipDriverPage.FormModel().ToOptions(devices); var json = JsonSerializer.Serialize(opts, TestJsonOpts); var back = JsonSerializer.Deserialize(json, TestJsonOpts)!; back.Devices.Count.ShouldBe(2); back.Devices[0].HostAddress.ShouldBe("ab://10.0.0.1/1,0"); - back.Tags.Count.ShouldBe(2); - back.Tags[0].Name.ShouldBe("Speed"); + back.Devices[0].PlcFamily.ShouldBe(AbCipPlcFamily.ControlLogix); + back.Devices[1].PlcFamily.ShouldBe(AbCipPlcFamily.CompactLogix); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/AbLegacyDriverPageFormSerializationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/AbLegacyDriverPageFormSerializationTests.cs index 1df73121..973db1d1 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/AbLegacyDriverPageFormSerializationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/AbLegacyDriverPageFormSerializationTests.cs @@ -41,11 +41,6 @@ public sealed class AbLegacyDriverPageFormSerializationTests new AbLegacyDeviceOptions("10.0.0.10", AbLegacyPlcFamily.Slc500, "PLC-A"), new AbLegacyDeviceOptions("10.0.0.11", AbLegacyPlcFamily.MicroLogix), ], - Tags = - [ - new AbLegacyTagDefinition("Level", "10.0.0.10", "N7:5", AbLegacyDataType.Int, Writable: false), - new AbLegacyTagDefinition("Pump", "10.0.0.10", "B3:0/0", AbLegacyDataType.Bit, Writable: true), - ], }; var json = JsonSerializer.Serialize(original, _opts); @@ -63,12 +58,6 @@ public sealed class AbLegacyDriverPageFormSerializationTests back.Devices[0].PlcFamily.ShouldBe(AbLegacyPlcFamily.Slc500); back.Devices[0].DeviceName.ShouldBe("PLC-A"); back.Devices[1].PlcFamily.ShouldBe(AbLegacyPlcFamily.MicroLogix); - back.Tags.Count.ShouldBe(2); - back.Tags[0].Name.ShouldBe("Level"); - back.Tags[0].Address.ShouldBe("N7:5"); - back.Tags[0].DataType.ShouldBe(AbLegacyDataType.Int); - back.Tags[0].Writable.ShouldBeFalse(); - back.Tags[1].DataType.ShouldBe(AbLegacyDataType.Bit); } [Fact] @@ -114,37 +103,11 @@ public sealed class AbLegacyDriverPageFormSerializationTests back.DeviceName.ShouldBe("PLC-A"); } - [Fact] - public void TagRow_round_trips_through_definition() - { - var row = new AbLegacyDriverPage.AbLegacyTagRow - { - Name = "Level", DeviceHostAddress = "10.0.0.10", Address = "N7:5", - DataType = AbLegacyDataType.Int, Writable = true, - }; - var def = row.ToDefinition(); - var back = AbLegacyDriverPage.AbLegacyTagRow.FromDefinition(def); - - back.Name.ShouldBe("Level"); - back.DeviceHostAddress.ShouldBe("10.0.0.10"); - back.Address.ShouldBe("N7:5"); - back.DataType.ShouldBe(AbLegacyDataType.Int); - back.Writable.ShouldBeTrue(); - } - - [Fact] - public void TagRow_preserves_unedited_fields() - { - var original = new AbLegacyTagDefinition( - "Level", "10.0.0.10", "N7:5", AbLegacyDataType.Int, - Writable: true, WriteIdempotent: true); - var row = AbLegacyDriverPage.AbLegacyTagRow.FromDefinition(original); - row.Name = "Renamed"; - - var back = row.ToDefinition(); - back.Name.ShouldBe("Renamed"); - back.WriteIdempotent.ShouldBeTrue(); - } + // v3 Batch-1 migration: the pre-declared Tags editor (AbLegacyDriverPage.AbLegacyTagRow + + // AbLegacyDriverOptions.Tags) was retired — tags moved to the Raw tree (/raw, Batch 2). The former + // TagRow_* / ValidateTagRow_* tests and the tag leg of the list-serialization test were removed. + // The multi-device Devices editor is RETAINED, so its coverage (DeviceRow_* + ValidateDeviceRow_* + // + the device-list serialization below, all carrying the AbLegacyPlcFamily enum) stays. [Fact] public void ValidateDeviceRow_rejects_duplicate_host() @@ -155,32 +118,19 @@ public sealed class AbLegacyDriverPageFormSerializationTests } [Fact] - public void ValidateTagRow_rejects_duplicate_name() - { - var rows = new List { new() { Name = "Level" } }; - AbLegacyDriverPage.AbLegacyTagRow.ValidateRow(new() { Name = "Level" }, rows, null) - .ShouldNotBeNull(); - } - - [Fact] - public void Device_and_tag_lists_survive_options_serialize_round_trip() + public void Device_list_survives_options_serialize_round_trip() { var devices = new List { new("10.0.0.10", AbLegacyPlcFamily.Slc500, "PLC-1"), new("10.0.0.11", AbLegacyPlcFamily.MicroLogix, "PLC-2"), }; - var tags = new List - { - new("Level", "10.0.0.10", "N7:5", AbLegacyDataType.Int), - new("Pump", "10.0.0.11", "B3:0/0", AbLegacyDataType.Bit), - }; - var opts = new AbLegacyDriverPage.FormModel().ToOptions(devices, tags); + var opts = new AbLegacyDriverPage.FormModel().ToOptions(devices); var json = JsonSerializer.Serialize(opts, TestJsonOpts); var back = JsonSerializer.Deserialize(json, TestJsonOpts)!; back.Devices.Count.ShouldBe(2); back.Devices[0].HostAddress.ShouldBe("10.0.0.10"); - back.Tags.Count.ShouldBe(2); - back.Tags[0].Name.ShouldBe("Level"); + back.Devices[0].PlcFamily.ShouldBe(AbLegacyPlcFamily.Slc500); + back.Devices[1].PlcFamily.ShouldBe(AbLegacyPlcFamily.MicroLogix); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/FocasDriverPageFormSerializationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/FocasDriverPageFormSerializationTests.cs index 2f057cc6..607e83a7 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/FocasDriverPageFormSerializationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/FocasDriverPageFormSerializationTests.cs @@ -52,7 +52,6 @@ public sealed class FocasDriverPageFormSerializationTests TimerPollInterval = TimeSpan.FromSeconds(60), }, Devices = [], - Tags = [], }; var json = JsonSerializer.Serialize(original, _opts); @@ -73,7 +72,6 @@ public sealed class FocasDriverPageFormSerializationTests back.FixedTree.ProgramPollInterval.ShouldBe(TimeSpan.FromSeconds(2)); back.FixedTree.TimerPollInterval.ShouldBe(TimeSpan.FromSeconds(60)); back.Devices.ShouldBeEmpty(); - back.Tags.ShouldBeEmpty(); } [Fact] @@ -123,7 +121,7 @@ public sealed class FocasDriverPageFormSerializationTests var form = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .FocasDriverPage.FormModel.FromOptions(opts); - var roundTripped = form.ToOptions([], []); + var roundTripped = form.ToOptions([]); roundTripped.Timeout.ShouldBe(TimeSpan.FromSeconds(4)); roundTripped.Probe.Enabled.ShouldBeTrue(); @@ -168,37 +166,11 @@ public sealed class FocasDriverPageFormSerializationTests back.Series.ShouldBe(FocasCncSeries.Thirty_i); } - [Fact] - public void TagRow_round_trips_through_definition() - { - var row = new FocasDriverPage.FocasTagRow - { - Name = "MacroVar", DeviceHostAddress = "192.168.0.10:8193", Address = "MACRO:500", - DataType = FocasDataType.Float64, Writable = true, - }; - var def = row.ToDefinition(); - var back = FocasDriverPage.FocasTagRow.FromDefinition(def); - - back.Name.ShouldBe("MacroVar"); - back.DeviceHostAddress.ShouldBe("192.168.0.10:8193"); - back.Address.ShouldBe("MACRO:500"); - back.DataType.ShouldBe(FocasDataType.Float64); - back.Writable.ShouldBeTrue(); - } - - [Fact] - public void TagRow_preserves_unedited_fields() - { - var original = new FocasTagDefinition( - "MacroVar", "192.168.0.10:8193", "MACRO:500", FocasDataType.Float64, - Writable: true, WriteIdempotent: true); - var row = FocasDriverPage.FocasTagRow.FromDefinition(original); - row.Name = "Renamed"; - - var back = row.ToDefinition(); - back.Name.ShouldBe("Renamed"); - back.WriteIdempotent.ShouldBeTrue(); - } + // v3 Batch-1 migration: the pre-declared Tags editor (FocasDriverPage.FocasTagRow + + // FocasDriverOptions.Tags) was retired — tags moved to the Raw tree (/raw, Batch 2). The former + // TagRow_* / ValidateTagRow_* tests and the tag leg of the list-serialization test were removed. + // The multi-device Devices editor is RETAINED, so its coverage (DeviceRow_* + ValidateDeviceRow_* + // + the device-list serialization below, all carrying the FocasCncSeries enum) stays. [Fact] public void ValidateDeviceRow_rejects_duplicate_host() @@ -209,34 +181,19 @@ public sealed class FocasDriverPageFormSerializationTests } [Fact] - public void ValidateTagRow_rejects_duplicate_name() - { - var rows = new List { new() { Name = "MacroVar" } }; - FocasDriverPage.FocasTagRow.ValidateRow(new() { Name = "MacroVar" }, rows, null) - .ShouldNotBeNull(); - } - - [Fact] - public void Device_and_tag_lists_survive_options_serialize_round_trip() + public void Device_list_survives_options_serialize_round_trip() { var devices = new List { new("192.168.0.10:8193", "CNC1", FocasCncSeries.Thirty_i), new("192.168.0.20:8193", "CNC2", FocasCncSeries.Zero_i_F), }; - var tags = new List - { - new("MacroVar", "192.168.0.10:8193", "MACRO:500", FocasDataType.Float64), - new("Flag", "192.168.0.20:8193", "X0.0", FocasDataType.Bit), - }; - var opts = new FocasDriverPage.FormModel().ToOptions(devices, tags); + var opts = new FocasDriverPage.FormModel().ToOptions(devices); var json = JsonSerializer.Serialize(opts, TestJsonOpts); var back = JsonSerializer.Deserialize(json, TestJsonOpts)!; back.Devices.Count.ShouldBe(2); back.Devices[0].HostAddress.ShouldBe("192.168.0.10:8193"); back.Devices[0].Series.ShouldBe(FocasCncSeries.Thirty_i); - back.Tags.Count.ShouldBe(2); - back.Tags[0].Name.ShouldBe("MacroVar"); - back.Tags[0].Address.ShouldBe("MACRO:500"); + back.Devices[1].Series.ShouldBe(FocasCncSeries.Zero_i_F); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ModbusDriverPageFormSerializationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ModbusDriverPageFormSerializationTests.cs index b0777da0..1b31a7a7 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ModbusDriverPageFormSerializationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ModbusDriverPageFormSerializationTests.cs @@ -15,12 +15,6 @@ public sealed class ModbusDriverPageFormSerializationTests WriteIndented = false, }; - private static readonly JsonSerializerOptions TestJsonOpts = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, - }; - [Fact] public void RoundTrip_PreservesKnownFields() { @@ -112,62 +106,9 @@ public sealed class ModbusDriverPageFormSerializationTests back.ProbeTimeoutSeconds.ShouldBe(10); } - [Fact] - public void TagRow_round_trips_through_definition() - { - var row = new ModbusDriverPage.ModbusTagRow - { - Name = "Pump1_Speed", Region = ModbusRegion.HoldingRegisters, Address = 40001, - DataType = ModbusDataType.Int16, Writable = true, - }; - var def = row.ToDefinition(); - var back = ModbusDriverPage.ModbusTagRow.FromDefinition(def); - - back.Name.ShouldBe("Pump1_Speed"); - back.Address.ShouldBe(40001); - back.DataType.ShouldBe(ModbusDataType.Int16); - back.Writable.ShouldBeTrue(); - } - - [Fact] - public void Tag_list_survives_options_serialize_round_trip() - { - var tags = new List - { - new("A", ModbusRegion.HoldingRegisters, 1, ModbusDataType.Int16), - new("B", ModbusRegion.Coils, 2, ModbusDataType.Bool), - }; - var opts = new ModbusDriverPage.FormModel().ToOptions(tags); - var json = JsonSerializer.Serialize(opts, TestJsonOpts); - var back = JsonSerializer.Deserialize(json, TestJsonOpts)!; - back.Tags.Count.ShouldBe(2); - back.Tags[0].Name.ShouldBe("A"); - } - - [Fact] - public void ValidateRow_rejects_duplicate_name() - { - var rows = new List { new() { Name = "A" } }; - ModbusDriverPage.ModbusTagRow.ValidateRow(new() { Name = "A" }, rows, null) - .ShouldNotBeNull(); - } - - [Fact] - public void ToDefinition_preserves_unedited_fields() - { - var original = new ModbusTagDefinition( - "T", ModbusRegion.HoldingRegisters, 5, ModbusDataType.Int16, - StringByteOrder: ModbusStringByteOrder.LowByteFirst, - ArrayCount: 10, Deadband: 0.5, UnitId: 3, CoalesceProhibited: true); - var row = ModbusDriverPage.ModbusTagRow.FromDefinition(original); - row.Name = "Renamed"; - - var back = row.ToDefinition(); - back.Name.ShouldBe("Renamed"); - back.UnitId.ShouldBe((byte)3); - back.ArrayCount.ShouldBe(10); - back.Deadband.ShouldBe(0.5); - back.StringByteOrder.ShouldBe(ModbusStringByteOrder.LowByteFirst); - back.CoalesceProhibited.ShouldBeTrue(); - } + // v3 Batch-1 migration: the pre-declared Tags editor (ModbusDriverPage.ModbusTagRow + + // ModbusDriverOptions.Tags) was retired — tags moved to the Raw tree (/raw, Batch 2). The former + // TagRow_* / Tag_list_survives_* / ValidateRow_* / ToDefinition_preserves_* tests were removed. + // The connection/protocol-field + enum-serialization coverage (ModbusFamily, MelsecFamily) is + // retained by RoundTrip_PreservesKnownFields above unchanged. } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/S7DriverPageFormSerializationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/S7DriverPageFormSerializationTests.cs index 2e50a9d9..69f11487 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/S7DriverPageFormSerializationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/S7DriverPageFormSerializationTests.cs @@ -7,6 +7,12 @@ using ZB.MOM.WW.OtOpcUa.Driver.S7; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests; +// v3 Batch-1 migration: the S7 driver page dropped its pre-declared Tags editor — tags now live in +// the Raw tree (/raw, Batch 2), and S7DriverOptions.Tags (the typed S7TagDefinition list) + +// S7DriverPage.S7TagRow were retired (the driver now consumes RawTags). The former tag-editor and +// tag-serialization tests (S7TagRow_*, TagList_SerializeRoundTrip_PreservesTags, and the tag legs of +// the round-trip tests) were removed. The connection/protocol-field + enum-serialization round-trip +// coverage (CpuType, S7DataType) is retained below unchanged. public sealed class S7DriverPageFormSerializationTests { private static readonly JsonSerializerOptions _opts = new() @@ -33,7 +39,6 @@ public sealed class S7DriverPageFormSerializationTests Timeout = TimeSpan.FromSeconds(3), }, ProbeTimeoutSeconds = 30, - Tags = [], }; var json = JsonSerializer.Serialize(original, _opts); @@ -51,7 +56,6 @@ public sealed class S7DriverPageFormSerializationTests back.Probe.Interval.ShouldBe(TimeSpan.FromSeconds(15)); back.Probe.Timeout.ShouldBe(TimeSpan.FromSeconds(3)); back.ProbeTimeoutSeconds.ShouldBe(30); - back.Tags.ShouldBeEmpty(); } [Fact] @@ -70,12 +74,6 @@ public sealed class S7DriverPageFormSerializationTests [Fact] public void FormModel_RoundTrip_PreservesEditableFields() { - var tags = new[] - { - new S7TagDefinition("Speed", "DB1.DBD0", S7DataType.Float32, Writable: true), - new S7TagDefinition("Status", "DB1.DBW4", S7DataType.Int16, Writable: false), - }; - var opts = new S7DriverOptions { Host = "192.168.1.50", @@ -91,15 +89,10 @@ public sealed class S7DriverPageFormSerializationTests Timeout = TimeSpan.FromSeconds(4), }, ProbeTimeoutSeconds = 20, - Tags = tags, }; - var form = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers - .S7DriverPage.FormModel.FromOptions(opts); - var tagRows = opts.Tags - .Select(ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers.S7DriverPage.S7TagRow.FromDefinition) - .ToList(); - var roundTripped = form.ToOptions(tagRows.Select(r => r.ToDefinition()).ToList()); + var form = S7DriverPage.FormModel.FromOptions(opts); + var roundTripped = form.ToOptions(); roundTripped.Host.ShouldBe("192.168.1.50"); roundTripped.Port.ShouldBe(102); @@ -111,104 +104,5 @@ public sealed class S7DriverPageFormSerializationTests roundTripped.Probe.Interval.ShouldBe(TimeSpan.FromSeconds(8)); roundTripped.Probe.Timeout.ShouldBe(TimeSpan.FromSeconds(4)); roundTripped.ProbeTimeoutSeconds.ShouldBe(20); - - // Tags must survive the FormModel round-trip unchanged (regression guard for the - // Tags = [] data-loss bug fixed in this PR). - roundTripped.Tags.Count.ShouldBe(2); - roundTripped.Tags[0].Name.ShouldBe("Speed"); - roundTripped.Tags[0].Address.ShouldBe("DB1.DBD0"); - roundTripped.Tags[0].DataType.ShouldBe(S7DataType.Float32); - roundTripped.Tags[1].Name.ShouldBe("Status"); - roundTripped.Tags[1].Writable.ShouldBeFalse(); - } - - [Fact] - public void S7TagRow_RoundTrip_PreservesEditableFields() - { - var def = new S7TagDefinition("Speed", "DB1.DBD0", S7DataType.Float32, Writable: true, StringLength: 80); - - var row = S7DriverPage.S7TagRow.FromDefinition(def); - var back = row.ToDefinition(); - - back.Name.ShouldBe("Speed"); - back.Address.ShouldBe("DB1.DBD0"); - back.DataType.ShouldBe(S7DataType.Float32); - back.Writable.ShouldBeTrue(); - back.StringLength.ShouldBe(80); - } - - [Fact] - public void S7TagRow_CarriesThroughUneditedFields() - { - // WriteIdempotent is not exposed by the editor; it must survive FromDefinition→edit→ToDefinition. - var def = new S7TagDefinition("Setpoint", "DB10.DBD0", S7DataType.Float32, Writable: true, WriteIdempotent: true); - - var row = S7DriverPage.S7TagRow.FromDefinition(def); - row.Name = "SetpointRenamed"; - row.Writable = false; - var back = row.ToDefinition(); - - back.Name.ShouldBe("SetpointRenamed"); - back.Writable.ShouldBeFalse(); - // Un-edited field carried through via _source. - back.WriteIdempotent.ShouldBeTrue(); - } - - [Fact] - public void S7TagRow_ValidateRow_RejectsDuplicateNames() - { - var all = new List - { - S7DriverPage.S7TagRow.FromDefinition(new S7TagDefinition("Speed", "DB1.DBD0", S7DataType.Float32)), - S7DriverPage.S7TagRow.FromDefinition(new S7TagDefinition("Status", "DB1.DBW4", S7DataType.Int16)), - }; - - // Editing index 1 to a name that case-insensitively collides with index 0. - var edited = all[1].Clone(); - edited.Name = "speed"; - S7DriverPage.S7TagRow.ValidateRow(edited, all, editIndex: 1) - .ShouldBe("Duplicate tag name 'speed'."); - - // Required-name guard. - var blank = new S7DriverPage.S7TagRow(); - S7DriverPage.S7TagRow.ValidateRow(blank, all, editIndex: null) - .ShouldBe("Name is required."); - - // Unique name passes. - var ok = all[1].Clone(); - ok.Name = "Torque"; - S7DriverPage.S7TagRow.ValidateRow(ok, all, editIndex: 1).ShouldBeNull(); - } - - [Fact] - public void TagList_SerializeRoundTrip_PreservesTags() - { - var opts = new S7DriverOptions - { - Host = "10.1.1.1", - Tags = - [ - new S7TagDefinition("Speed", "DB1.DBD0", S7DataType.Float32, Writable: true), - new S7TagDefinition("Name", "DB2.DBB0", S7DataType.String, Writable: false, StringLength: 32), - ], - }; - - var optsSkip = new JsonSerializerOptions(_opts) - { - UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, - }; - var json = JsonSerializer.Serialize(opts, optsSkip); - var back = JsonSerializer.Deserialize(json, optsSkip); - - back.ShouldNotBeNull(); - back.Tags.Count.ShouldBe(2); - back.Tags[0].Name.ShouldBe("Speed"); - back.Tags[0].Address.ShouldBe("DB1.DBD0"); - back.Tags[0].DataType.ShouldBe(S7DataType.Float32); - back.Tags[0].Writable.ShouldBeTrue(); - back.Tags[1].Name.ShouldBe("Name"); - back.Tags[1].DataType.ShouldBe(S7DataType.String); - back.Tags[1].StringLength.ShouldBe(32); - back.Tags[1].Writable.ShouldBeFalse(); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ScriptAnalysis/ScriptTagCatalogTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ScriptAnalysis/ScriptTagCatalogTests.cs index 10259d21..3d2db5fe 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ScriptAnalysis/ScriptTagCatalogTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/ScriptAnalysis/ScriptTagCatalogTests.cs @@ -15,9 +15,11 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.ScriptAnalysis; /// /// Fidelity finding (see doc comment): the runtime Akka path resolves /// a ctx.GetTag literal against the driver FullName (the wire reference in -/// Tag.TagConfig.FullName), NOT the UNS browse path — the UNS-path engine is dormant. The -/// catalog therefore emits the FullName for equipment tags, the MXAccess dot-ref for FolderPath-scoped -/// tags (EquipmentId null), and the leaf Name for virtual tags, and deliberately omits UNS browse paths. These +/// Tag.TagConfig.FullName), NOT the UNS browse path — the UNS-path engine is dormant. v3: Tag is +/// Raw-only (no EquipmentId/FolderPath/DriverInstanceId), so the catalog projects +/// the surviving Name/DataType/TagConfig columns: the resolvable path derives from +/// the TagConfig.FullName field when present, else the tag Name; the projected +/// DriverInstanceId is always . Virtual tags emit their leaf Name. These /// assertions check the resolvable key is present AND that the UNS browse paths are absent. /// [Trait("Category", "Unit")] @@ -59,26 +61,23 @@ public sealed class ScriptTagCatalogTests MachineCode = "machine_001", }); - // Equipment driver tag — the GetTag key is the driver FullName from TagConfig. + // Raw tag — the GetTag key is the driver FullName from TagConfig. db.Tags.Add(new Tag { TagId = "TAG-EQ", - DriverInstanceId = "DRV-1", - EquipmentId = "EQ-1", + DeviceId = "DEV-1", Name = "Speed", DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{\"FullName\":\"Motor.Speed\"}", }); - // FolderPath-scoped tag — EquipmentId null, FolderPath set; subscribes by "FolderPath.Name". + // Raw tag whose FullName is an MXAccess dot-ref; resolves by that FullName. db.Tags.Add(new Tag { TagId = "TAG-SP", - DriverInstanceId = "DRV-GALAXY", - EquipmentId = null, + DeviceId = "DEV-1", Name = "DownloadPath", - FolderPath = "DelmiaReceiver_001", DataType = "String", AccessLevel = TagAccessLevel.Read, TagConfig = "{\"FullName\":\"DelmiaReceiver_001.DownloadPath\"}", @@ -174,8 +173,7 @@ public sealed class ScriptTagCatalogTests db.Tags.Add(new Tag { TagId = "TAG-BAD", - DriverInstanceId = "DRV-1", - EquipmentId = "EQ-1", + DeviceId = "DEV-1", Name = "Broken", DataType = "Float", AccessLevel = TagAccessLevel.Read, @@ -205,10 +203,8 @@ public sealed class ScriptTagCatalogTests db.Tags.Add(new Tag { TagId = "TAG-SP-ROOT", - DriverInstanceId = "DRV-GALAXY", - EquipmentId = null, + DeviceId = "DEV-1", Name = "RootScalar", - FolderPath = null, DataType = "String", AccessLevel = TagAccessLevel.Read, TagConfig = "{\"FullName\":\"RootScalar\"}", @@ -233,8 +229,8 @@ public sealed class ScriptTagCatalogTests paths.ShouldBeEmpty(); } - /// A FolderPath-scoped tag resolves by its "FolderPath.Name" dot-ref to a Tag-kind info - /// with the configured DataType + DriverInstanceId. + /// A raw tag whose FullName is a dot-ref resolves by that FullName to a Tag-kind info with + /// the configured DataType. v3: Tag no longer binds a driver, so the projected DriverInstanceId is null. [Fact] public async Task GetTagInfo_unbound_tag_returns_tag_info() { @@ -247,7 +243,7 @@ public sealed class ScriptTagCatalogTests info!.Path.ShouldBe("DelmiaReceiver_001.DownloadPath"); info.Kind.ShouldBe("Tag"); info.DataType.ShouldBe("String"); - info.DriverInstanceId.ShouldBe("DRV-GALAXY"); + info.DriverInstanceId.ShouldBeNull(); } /// A virtual tag resolves by its leaf Name to a "Virtual tag"-kind info with no driver. @@ -353,8 +349,7 @@ public sealed class ScriptTagCatalogTests db.Tags.Add(new Tag { TagId = "TAG-EQ2", - DriverInstanceId = "DRV-1", - EquipmentId = "EQ-1", + DeviceId = "DEV-1", Name = "Speed2", DataType = "Float", AccessLevel = TagAccessLevel.Read, diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/TwinCATDriverPageFormSerializationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/TwinCATDriverPageFormSerializationTests.cs index 67f41ca3..2a3ad37d 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/TwinCATDriverPageFormSerializationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/TwinCATDriverPageFormSerializationTests.cs @@ -31,7 +31,6 @@ public sealed class TwinCATDriverPageFormSerializationTests }, ProbeTimeoutSeconds = 20, Devices = [], - Tags = [], }; var json = JsonSerializer.Serialize(original, _opts); @@ -48,7 +47,6 @@ public sealed class TwinCATDriverPageFormSerializationTests back.Probe.Timeout.ShouldBe(TimeSpan.FromSeconds(3)); back.ProbeTimeoutSeconds.ShouldBe(20); back.Devices.ShouldBeEmpty(); - back.Tags.ShouldBeEmpty(); } [Fact] @@ -84,7 +82,7 @@ public sealed class TwinCATDriverPageFormSerializationTests var form = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.FormModel.FromOptions(opts); - var roundTripped = form.ToOptions([], []); + var roundTripped = form.ToOptions([]); roundTripped.Timeout.ShouldBe(TimeSpan.FromSeconds(3)); roundTripped.UseNativeNotifications.ShouldBeTrue(); @@ -140,69 +138,20 @@ public sealed class TwinCATDriverPageFormSerializationTests error.ShouldContain("Duplicate"); } - [Fact] - public void TagRow_RoundTrip_PreservesEditableFields() - { - var def = new TwinCATTagDefinition("Speed", "192.168.0.1.1.1:851", "MAIN.rSpeed", TwinCATDataType.Real, Writable: false); - - var row = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers - .TwinCATDriverPage.TwinCATTagRow.FromDefinition(def); - var back = row.ToDefinition(); - - back.Name.ShouldBe("Speed"); - back.DeviceHostAddress.ShouldBe("192.168.0.1.1.1:851"); - back.SymbolPath.ShouldBe("MAIN.rSpeed"); - back.DataType.ShouldBe(TwinCATDataType.Real); - back.Writable.ShouldBeFalse(); - } + // v3 Batch-1 migration: the pre-declared Tags editor (TwinCATDriverPage.TwinCATTagRow + + // TwinCATDriverOptions.Tags) was retired — tags moved to the Raw tree (/raw, Batch 2). The former + // TagRow_* tests and the tag leg of FormModel_ToOptions_* were removed. The multi-device Devices + // editor is RETAINED, so its coverage (DeviceRow_* + the device-list serialization below) stays. [Fact] - public void TagRow_CarriesThroughUneditedWriteIdempotent() - { - // WriteIdempotent is not exposed by the editor; it must survive a load→edit→save via _source. - var def = new TwinCATTagDefinition("Cmd", "192.168.0.1.1.1:851", "GVL.Start", TwinCATDataType.Bool, - Writable: true, WriteIdempotent: true); - - var row = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers - .TwinCATDriverPage.TwinCATTagRow.FromDefinition(def); - row.Name = "CmdRenamed"; // touch an edited field - var back = row.ToDefinition(); - - back.Name.ShouldBe("CmdRenamed"); - back.WriteIdempotent.ShouldBeTrue(); - } - - [Fact] - public void TagRow_ValidateRow_RejectsDuplicateName() - { - var existing = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers - .TwinCATDriverPage.TwinCATTagRow.FromDefinition( - new TwinCATTagDefinition("Speed", "192.168.0.1.1.1:851", "MAIN.rSpeed", TwinCATDataType.Real)); - var dup = new ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers - .TwinCATDriverPage.TwinCATTagRow { Name = "SPEED" }; // case-insensitive collision - - var all = new[] { existing, dup }; - var error = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers - .TwinCATDriverPage.TwinCATTagRow.ValidateRow(dup, all, editIndex: 1); - - error.ShouldNotBeNull(); - error.ShouldContain("Duplicate"); - } - - [Fact] - public void FormModel_ToOptions_SerializesDeviceAndTagLists() + public void FormModel_ToOptions_SerializesDeviceList() { var form = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers .TwinCATDriverPage.FormModel.FromOptions(new TwinCATDriverOptions()); var devices = new[] { new TwinCATDeviceOptions("192.168.0.1.1.1:851", "PLC1") }; - var tags = new[] - { - new TwinCATTagDefinition("Speed", "192.168.0.1.1.1:851", "MAIN.rSpeed", TwinCATDataType.Real, - Writable: true, WriteIdempotent: true), - }; - var opts = form.ToOptions(devices, tags); + var opts = form.ToOptions(devices); var json = JsonSerializer.Serialize(opts, _opts); var back = JsonSerializer.Deserialize(json, _opts); @@ -210,12 +159,5 @@ public sealed class TwinCATDriverPageFormSerializationTests back.Devices.Count.ShouldBe(1); back.Devices[0].HostAddress.ShouldBe("192.168.0.1.1.1:851"); back.Devices[0].DeviceName.ShouldBe("PLC1"); - back.Tags.Count.ShouldBe(1); - back.Tags[0].Name.ShouldBe("Speed"); - back.Tags[0].DeviceHostAddress.ShouldBe("192.168.0.1.1.1:851"); - back.Tags[0].SymbolPath.ShouldBe("MAIN.rSpeed"); - back.Tags[0].DataType.ShouldBe(TwinCATDataType.Real); - back.Tags[0].Writable.ShouldBeTrue(); - back.Tags[0].WriteIdempotent.ShouldBeTrue(); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/OpcUaClientTagConfigModelTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/OpcUaClientTagConfigModelTests.cs index 05ffcf02..376344cf 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/OpcUaClientTagConfigModelTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/OpcUaClientTagConfigModelTests.cs @@ -4,6 +4,11 @@ using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; +// v3 Batch-1 migration: OpcUaClientTagConfigModel's address key was renamed FullName → NodeId and +// its serialized JSON key moved from PascalCase "FullName" to camelCase "nodeId" (TagConfig no longer +// carries identity; the upstream node reference is an ordinary driver-address field). The tests below +// track that rename; the preserve-unknown-keys round-trip coverage (unrecognised keys + the TagModal +// isHistorized/historianTagname merge keys) is retained unchanged. public sealed class OpcUaClientTagConfigModelTests { [Theory] @@ -15,52 +20,52 @@ public sealed class OpcUaClientTagConfigModelTests { var m = OpcUaClientTagConfigModel.FromJson(json); - m.FullName.ShouldBe(""); + m.NodeId.ShouldBe(""); } [Fact] - public void FromJson_reads_FullName() + public void FromJson_reads_NodeId() { var m = OpcUaClientTagConfigModel.FromJson( - """{"FullName":"nsu=urn:srv;s=Line3.Temp"}"""); + """{"nodeId":"nsu=urn:srv;s=Line3.Temp"}"""); - m.FullName.ShouldBe("nsu=urn:srv;s=Line3.Temp"); + m.NodeId.ShouldBe("nsu=urn:srv;s=Line3.Temp"); } [Fact] - public void Round_trip_preserves_FullName() + public void Round_trip_preserves_NodeId() { - var m = new OpcUaClientTagConfigModel { FullName = "ns=2;s=Line3.Temp" }; + var m = new OpcUaClientTagConfigModel { NodeId = "ns=2;s=Line3.Temp" }; var json = m.ToJson(); var m2 = OpcUaClientTagConfigModel.FromJson(json); - m2.FullName.ShouldBe("ns=2;s=Line3.Temp"); + m2.NodeId.ShouldBe("ns=2;s=Line3.Temp"); } [Fact] - public void ToJson_emits_PascalCase_FullName() + public void ToJson_emits_camelCase_NodeId() { - var m = new OpcUaClientTagConfigModel { FullName = "ns=2;s=Line3.Temp" }; + var m = new OpcUaClientTagConfigModel { NodeId = "ns=2;s=Line3.Temp" }; var json = m.ToJson(); - // FullName is the composer/walker contract key — PascalCase, case-sensitive. - json.ShouldContain("\"FullName\":\"ns=2;s=Line3.Temp\""); - json.ShouldNotContain("\"fullName\"", Case.Sensitive); + // nodeId is the v3 address key — camelCase, replacing the legacy PascalCase FullName identity key. + json.ShouldContain("\"nodeId\":\"ns=2;s=Line3.Temp\""); + json.ShouldNotContain("\"FullName\"", Case.Sensitive); } [Fact] public void FromJson_then_ToJson_preserves_unknown_keys() { var json = OpcUaClientTagConfigModel - .FromJson("""{"FullName":"ns=2;s=X","samplingIntervalMs":250}""") + .FromJson("""{"nodeId":"ns=2;s=X","samplingIntervalMs":250}""") .ToJson(); json.ShouldContain("samplingIntervalMs"); json.ShouldContain("250"); // and the exposed field still round-trips - json.ShouldContain("\"FullName\":\"ns=2;s=X\""); + json.ShouldContain("\"nodeId\":\"ns=2;s=X\""); } [Fact] @@ -69,32 +74,32 @@ public sealed class OpcUaClientTagConfigModelTests // The TagModal-merge seam writes isHistorized/historianTagname at the TagConfig root; this model // does NOT model them, so they must survive a load→save untouched as preserved unknown keys. var json = OpcUaClientTagConfigModel - .FromJson("""{"FullName":"ns=2;s=X","isHistorized":true,"historianTagname":"Line3_Temp"}""") + .FromJson("""{"nodeId":"ns=2;s=X","isHistorized":true,"historianTagname":"Line3_Temp"}""") .ToJson(); json.ShouldContain("\"isHistorized\":true"); json.ShouldContain("\"historianTagname\":\"Line3_Temp\""); - json.ShouldContain("\"FullName\":\"ns=2;s=X\""); + json.ShouldContain("\"nodeId\":\"ns=2;s=X\""); } [Fact] - public void ToJson_trims_FullName() + public void ToJson_trims_NodeId() { - var json = new OpcUaClientTagConfigModel { FullName = " ns=2;s=X " }.ToJson(); + var json = new OpcUaClientTagConfigModel { NodeId = " ns=2;s=X " }.ToJson(); - json.ShouldContain("\"FullName\":\"ns=2;s=X\""); + json.ShouldContain("\"nodeId\":\"ns=2;s=X\""); } [Fact] - public void Validate_returns_error_when_FullName_blank() + public void Validate_returns_error_when_NodeId_blank() { new OpcUaClientTagConfigModel().Validate().ShouldNotBeNullOrEmpty(); - new OpcUaClientTagConfigModel { FullName = " " }.Validate().ShouldNotBeNullOrEmpty(); + new OpcUaClientTagConfigModel { NodeId = " " }.Validate().ShouldNotBeNullOrEmpty(); } [Fact] - public void Validate_returns_null_when_FullName_present() + public void Validate_returns_null_when_NodeId_present() { - new OpcUaClientTagConfigModel { FullName = "ns=2;s=X" }.Validate().ShouldBeNull(); + new OpcUaClientTagConfigModel { NodeId = "ns=2;s=X" }.Validate().ShouldBeNull(); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagArrayConfigTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagArrayConfigTests.cs index a10ef54c..c27614a3 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagArrayConfigTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagArrayConfigTests.cs @@ -147,19 +147,19 @@ public sealed class TagArrayConfigTests /// Full seam: the array helper and a driver-typed editor compose over the same canonical TagConfig /// blob without clobbering each other. Setting the array keys, round-tripping through the OpcUaClient /// typed editor (FromJson → ToJson), then reading the array keys back recovers them intact — and the - /// editor's own FullName field survives the array merge. + /// editor's own NodeId field survives the array merge. /// [Fact] public void Array_keys_survive_a_typed_editor_round_trip() { - var withArray = TagArrayConfig.Set("""{"FullName":"ns=2;s=X"}""", isArray: true, arrayLength: 12); + var withArray = TagArrayConfig.Set("""{"nodeId":"ns=2;s=X"}""", isArray: true, arrayLength: 12); var afterEditor = OpcUaClientTagConfigModel.FromJson(withArray).ToJson(); var a = TagArrayConfig.Read(afterEditor); a.IsArray.ShouldBeTrue(); a.ArrayLength.ShouldBe(12u); - OpcUaClientTagConfigModel.FromJson(afterEditor).FullName.ShouldBe("ns=2;s=X"); + OpcUaClientTagConfigModel.FromJson(afterEditor).NodeId.ShouldBe("ns=2;s=X"); } /// The array helper and the historize helper compose over the same blob (both preserve diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigValidatorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigValidatorTests.cs index b5f21a45..385b9538 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigValidatorTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigValidatorTests.cs @@ -37,9 +37,10 @@ public sealed class TagConfigValidatorTests TagConfigValidator.Validate(driverType, null).ShouldNotBeNullOrEmpty(); } + // v3 Batch-1: the OpcUaClient editor's address key was renamed FullName → nodeId (camelCase). [Fact] - public void OpcUaClient_with_full_name_is_valid() - => TagConfigValidator.Validate("OpcUaClient", """{"FullName":"ns=2;s=Line3.Temp"}""").ShouldBeNull(); + public void OpcUaClient_with_node_id_is_valid() + => TagConfigValidator.Validate("OpcUaClient", """{"nodeId":"ns=2;s=Line3.Temp"}""").ShouldBeNull(); [Fact] public void S7_with_address_is_valid() diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagHistorizeConfigTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagHistorizeConfigTests.cs index 98e8f964..159691d9 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagHistorizeConfigTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagHistorizeConfigTests.cs @@ -128,19 +128,19 @@ public sealed class TagHistorizeConfigTests /// Full seam: the history helper and a driver-typed editor compose over the same canonical TagConfig /// blob without clobbering each other. Setting the history keys, round-tripping through the /// OpcUaClient typed editor (FromJson → ToJson), then reading the history keys back recovers them - /// intact — and the editor's own FullName field survives the history merge. + /// intact — and the editor's own NodeId field survives the history merge. /// [Fact] public void History_keys_survive_a_typed_editor_round_trip() { var withHistory = TagHistorizeConfig.Set( - """{"FullName":"ns=2;s=X"}""", isHistorized: true, historianTagname: "TN"); + """{"nodeId":"ns=2;s=X"}""", isHistorized: true, historianTagname: "TN"); var afterEditor = OpcUaClientTagConfigModel.FromJson(withHistory).ToJson(); var h = TagHistorizeConfig.Read(afterEditor); h.IsHistorized.ShouldBeTrue(); h.HistorianTagname.ShouldBe("TN"); - OpcUaClientTagConfigModel.FromJson(afterEditor).FullName.ShouldBe("ns=2;s=X"); + OpcUaClientTagConfigModel.FromJson(afterEditor).NodeId.ShouldBe("ns=2;s=X"); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceAreaLineTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceAreaLineTests.cs index efefed76..7abd4cd9 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceAreaLineTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceAreaLineTests.cs @@ -87,10 +87,15 @@ public sealed class UnsTreeServiceAreaLineTests area.ClusterId.ShouldBe("MAIN"); } - /// The #122 guard blocks moving an area to a new cluster when driver-bound - /// equipment would be orphaned from its driver's cluster. + /// + /// v3 Batch-1: the decision-#122 driver-orphan guard was RETIRED — equipment no longer binds a + /// driver (Equipment.DriverInstanceId is gone), so a cluster move can never orphan + /// driver-bound equipment. The former blocked/allowed guard tests are replaced by this one, which + /// proves a cross-cluster area reassignment now succeeds unconditionally (equipment under the area + /// rides along). The equipment↔driver cluster invariant is re-covered by the Batch-2 Raw-tree tests. + /// [Fact] - public async Task UpdateArea_reassign_cluster_blocked_when_driver_bound_equipment_would_orphan() + public async Task UpdateArea_reassign_cluster_now_allowed_guard_retired() { var (service, dbName) = Fresh(); @@ -106,16 +111,6 @@ public sealed class UnsTreeServiceAreaLineTests UnsLineId = "LINE-X", Name = "m", MachineCode = "machine_x", - DriverInstanceId = "DRV-MAIN", - }); - db.DriverInstances.Add(new DriverInstance - { - DriverInstanceId = "DRV-MAIN", - ClusterId = "MAIN", - NamespaceId = "NS-1", - Name = "drv", - DriverType = "Modbus", - DriverConfig = "{}", }); db.SaveChanges(); rv = db.UnsAreas.Single(a => a.UnsAreaId == "AREA-X").RowVersion; @@ -123,97 +118,12 @@ public sealed class UnsTreeServiceAreaLineTests var result = await service.UpdateAreaAsync("AREA-X", "a", null, "SITE-A", rv); - result.Ok.ShouldBeFalse(); - result.Error.ShouldNotBeNull(); - result.Error.ShouldContain("decision #122"); - result.Error.ShouldContain("EQ-BOUND"); - result.Error.ShouldContain("SITE-A"); - result.Error.ShouldContain("MAIN"); - - // The area must not have been moved. - using var verify = UnsTreeTestDb.CreateNamed(dbName); - verify.UnsAreas.Single(a => a.UnsAreaId == "AREA-X").ClusterId.ShouldBe("MAIN"); - } - - /// The #122 guard allows the move when the area's driver-bound equipment's driver - /// is already in the target cluster (driverCluster == newClusterId → no orphan). - [Fact] - public async Task UpdateArea_reassign_cluster_allowed_when_driver_is_in_target_cluster() - { - // Seed: AREA-Z in cluster MAIN, a line, equipment bound to DRV-SITE-A whose cluster is - // SITE-A. Reassigning the area to SITE-A must be allowed because the driver is already - // there — the #122 guard's `driverCluster != newClusterId` condition is false. - var (service, dbName) = Fresh(); - - byte[] rv; - using (var db = UnsTreeTestDb.CreateNamed(dbName)) - { - db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-Z", ClusterId = "MAIN", Name = "a" }); - db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-Z", UnsAreaId = "AREA-Z", Name = "l" }); - db.DriverInstances.Add(new DriverInstance - { - DriverInstanceId = "DRV-SITE-A", - ClusterId = "SITE-A", - NamespaceId = "NS-1", - Name = "drv", - DriverType = "Modbus", - DriverConfig = "{}", - }); - db.Equipment.Add(new Equipment - { - EquipmentId = "EQ-BOUND-Z", - EquipmentUuid = Guid.NewGuid(), - UnsLineId = "LINE-Z", - Name = "m", - MachineCode = "machine_z", - DriverInstanceId = "DRV-SITE-A", - }); - db.SaveChanges(); - rv = db.UnsAreas.Single(a => a.UnsAreaId == "AREA-Z").RowVersion; - } - - var result = await service.UpdateAreaAsync("AREA-Z", "a", null, "SITE-A", rv); - result.Ok.ShouldBeTrue(); result.Error.ShouldBeNull(); - // Verify the area actually moved to SITE-A via a fresh context. + // The area moved to SITE-A. using var verify = UnsTreeTestDb.CreateNamed(dbName); - verify.UnsAreas.Single(a => a.UnsAreaId == "AREA-Z").ClusterId.ShouldBe("SITE-A"); - } - - /// The #122 guard allows the move when the equipment under the area is driver-less - /// (DriverInstanceId == null). - [Fact] - public async Task UpdateArea_reassign_cluster_allowed_when_equipment_driverless() - { - var (service, dbName) = Fresh(); - - byte[] rv; - using (var db = UnsTreeTestDb.CreateNamed(dbName)) - { - db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-Y", ClusterId = "MAIN", Name = "a" }); - db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-Y", UnsAreaId = "AREA-Y", Name = "l" }); - db.Equipment.Add(new Equipment - { - EquipmentId = "EQ-FREE", - EquipmentUuid = Guid.NewGuid(), - UnsLineId = "LINE-Y", - Name = "m", - MachineCode = "machine_y", - DriverInstanceId = null, - }); - db.SaveChanges(); - rv = db.UnsAreas.Single(a => a.UnsAreaId == "AREA-Y").RowVersion; - } - - var result = await service.UpdateAreaAsync("AREA-Y", "a", null, "SITE-A", rv); - - result.Ok.ShouldBeTrue(); - result.Error.ShouldBeNull(); - - using var verify = UnsTreeTestDb.CreateNamed(dbName); - verify.UnsAreas.Single(a => a.UnsAreaId == "AREA-Y").ClusterId.ShouldBe("SITE-A"); + verify.UnsAreas.Single(a => a.UnsAreaId == "AREA-X").ClusterId.ShouldBe("SITE-A"); } /// Updating an area that no longer exists returns the row-gone error. @@ -337,30 +247,22 @@ public sealed class UnsTreeServiceAreaLineTests result.Error.ShouldBe("Row no longer exists."); } - /// The #122 guard blocks reparenting a line to an area in a different cluster when - /// the line's equipment is driver-bound (the driver lives in the original cluster). + /// + /// v3 Batch-1: the decision-#122 driver-orphan guard was RETIRED (equipment no longer binds a + /// driver). The former blocked/allowed line-reparent guard tests are replaced by this one, which + /// proves reparenting a line to an area in a different cluster now succeeds unconditionally. + /// [Fact] - public async Task UpdateLine_reparent_to_other_cluster_blocked_when_driver_bound() + public async Task UpdateLine_reparent_to_other_cluster_now_allowed_guard_retired() { var (service, dbName) = Fresh(); byte[] rv; using (var db = UnsTreeTestDb.CreateNamed(dbName)) { - // Source area A1 in cluster MAIN. db.UnsAreas.Add(new UnsArea { UnsAreaId = "A1-MAIN", ClusterId = "MAIN", Name = "area-main" }); - // Target area A2 in cluster SITE-A. db.UnsAreas.Add(new UnsArea { UnsAreaId = "A2-SITE-A", ClusterId = "SITE-A", Name = "area-site-a" }); db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-BOUND", UnsAreaId = "A1-MAIN", Name = "line" }); - db.DriverInstances.Add(new DriverInstance - { - DriverInstanceId = "DRV-MAIN-122", - ClusterId = "MAIN", - NamespaceId = "NS-1", - Name = "drv", - DriverType = "Modbus", - DriverConfig = "{}", - }); db.Equipment.Add(new Equipment { EquipmentId = "EQ-LINE-BOUND", @@ -368,7 +270,6 @@ public sealed class UnsTreeServiceAreaLineTests UnsLineId = "LINE-BOUND", Name = "eq", MachineCode = "mc_line_bound", - DriverInstanceId = "DRV-MAIN-122", }); db.SaveChanges(); rv = db.UnsLines.Single(l => l.UnsLineId == "LINE-BOUND").RowVersion; @@ -376,52 +277,12 @@ public sealed class UnsTreeServiceAreaLineTests var result = await service.UpdateLineAsync("LINE-BOUND", "line", null, "A2-SITE-A", rv); - result.Ok.ShouldBeFalse(); - result.Error.ShouldNotBeNull(); - result.Error.ShouldContain("decision #122"); - result.Error.ShouldContain("EQ-LINE-BOUND"); - result.Error.ShouldContain("A2-SITE-A"); - result.Error.ShouldContain("MAIN"); - - // The line must not have moved. - using var verify = UnsTreeTestDb.CreateNamed(dbName); - verify.UnsLines.Single(l => l.UnsLineId == "LINE-BOUND").UnsAreaId.ShouldBe("A1-MAIN"); - } - - /// The #122 guard allows reparenting a line to an area in a different cluster when - /// the line's equipment is driver-less (DriverInstanceId == null). - [Fact] - public async Task UpdateLine_reparent_to_other_cluster_allowed_when_equipment_driverless() - { - var (service, dbName) = Fresh(); - - byte[] rv; - using (var db = UnsTreeTestDb.CreateNamed(dbName)) - { - db.UnsAreas.Add(new UnsArea { UnsAreaId = "A1-FREE", ClusterId = "MAIN", Name = "area-main" }); - db.UnsAreas.Add(new UnsArea { UnsAreaId = "A2-FREE", ClusterId = "SITE-A", Name = "area-site-a" }); - db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-FREE", UnsAreaId = "A1-FREE", Name = "line" }); - db.Equipment.Add(new Equipment - { - EquipmentId = "EQ-LINE-FREE", - EquipmentUuid = Guid.NewGuid(), - UnsLineId = "LINE-FREE", - Name = "eq", - MachineCode = "mc_line_free", - DriverInstanceId = null, - }); - db.SaveChanges(); - rv = db.UnsLines.Single(l => l.UnsLineId == "LINE-FREE").RowVersion; - } - - var result = await service.UpdateLineAsync("LINE-FREE", "line", null, "A2-FREE", rv); - result.Ok.ShouldBeTrue(); result.Error.ShouldBeNull(); - // The line's UnsAreaId must have changed to A2-FREE. + // The line moved to the target area. using var verify = UnsTreeTestDb.CreateNamed(dbName); - verify.UnsLines.Single(l => l.UnsLineId == "LINE-FREE").UnsAreaId.ShouldBe("A2-FREE"); + verify.UnsLines.Single(l => l.UnsLineId == "LINE-BOUND").UnsAreaId.ShouldBe("A2-SITE-A"); } // ----- DeleteLine ----- diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceDeleteClusterTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceDeleteClusterTests.cs index 4ad0aa42..d11c303e 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceDeleteClusterTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceDeleteClusterTests.cs @@ -105,7 +105,6 @@ public sealed class UnsTreeServiceDeleteClusterTests { DriverInstanceId = "DRV-CL", ClusterId = "CL-DRV", - NamespaceId = "NS-1", Name = "drv", DriverType = "Modbus", DriverConfig = "{}", @@ -151,33 +150,10 @@ public sealed class UnsTreeServiceDeleteClusterTests verify.ServerClusters.Any(c => c.ClusterId == "CL-NODE").ShouldBeTrue(); } - /// A cluster with a namespace refuses deletion with a friendly message and stays put. - [Fact] - public async Task DeleteCluster_with_namespace_refuses_and_keeps_row() - { - var (service, dbName) = Fresh(); - using (var db = UnsTreeTestDb.CreateNamed(dbName)) - { - db.ServerClusters.Add(NewCluster("CL-NS", "zb")); - db.Namespaces.Add(new Namespace - { - NamespaceId = "NS-CL", - ClusterId = "CL-NS", - Kind = NamespaceKind.Equipment, - NamespaceUri = "urn:zb:cl-ns:equipment", - }); - db.SaveChanges(); - } - - var result = await service.DeleteClusterAsync("CL-NS"); - - result.Ok.ShouldBeFalse(); - result.Error.ShouldNotBeNull(); - result.Error.ShouldContain("CL-NS"); - - using var verify = UnsTreeTestDb.CreateNamed(dbName); - verify.ServerClusters.Any(c => c.ClusterId == "CL-NS").ShouldBeTrue(); - } + // v3 Batch-1: the Namespace entity was deleted (Raw/UNS split), so DescribeClusterChildrenAsync + // no longer carries a namespace child-check. The former DeleteCluster_with_namespace_refuses test + // was removed. The cluster-nodes / UNS-areas / driver-instances refuse-if-children checks (above) + // remain the enforced set. // ----- DeleteEnterprise ----- diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentChildRowsTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentChildRowsTests.cs index 8a4b4016..8d5f53a6 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentChildRowsTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentChildRowsTests.cs @@ -14,16 +14,15 @@ public sealed class UnsTreeServiceEquipmentChildRowsTests return new UnsTreeService(UnsTreeTestDb.Factory(dbName)); } + // v3 Batch-1: the equipment↔Tag binding was retired (Tag is Raw-only; authoring moved to the Raw + // tree in Batch 2). LoadTagsForEquipmentAsync is a stub that always returns empty — no equipment + // has bound driver tags to list. The former in-name-order / EquipmentId-scoping assertions moved + // with the authoring surface to the Batch-2 Raw-tree tests. [Fact] - public async Task LoadTagsForEquipment_returns_tags_in_name_order_scoped() + public async Task LoadTagsForEquipment_returns_empty_stub() { var rows = await SeededService().LoadTagsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId); - rows.Count.ShouldBe(2); // the EquipmentId=null orphan tag is excluded - rows[0].TagId.ShouldBe("TAG-2"); // "running" < "speed" - rows[0].Name.ShouldBe("running"); - rows[0].DataType.ShouldBe("Boolean"); - rows[1].TagId.ShouldBe("TAG-1"); - rows[1].DataType.ShouldBe("Float"); + rows.ShouldBeEmpty(); } [Fact] diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentTests.cs index 2a3c54e4..35059ec0 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentTests.cs @@ -41,7 +41,6 @@ public sealed class UnsTreeServiceEquipmentTests { DriverInstanceId = "DRV-1", ClusterId = driverCluster, - NamespaceId = "NS-1", Name = "drv", DriverType = "Modbus", DriverConfig = "{}", @@ -125,7 +124,11 @@ public sealed class UnsTreeServiceEquipmentTests db.Equipment.Any(e => e.MachineCode == "machine_001").ShouldBeFalse(); } - /// Binding equipment to a driver in the same cluster as the line is allowed. + /// + /// Passing a driver in the same cluster as the line passes the #122 guard and the equipment + /// persists. v3: the equipment↔driver binding column was retired, so the driver is validated by + /// the guard but not stored — the assertion drops to equipment existence. + /// [Fact] public async Task CreateEquipment_driver_in_same_cluster_allowed() { @@ -138,7 +141,7 @@ public sealed class UnsTreeServiceEquipmentTests result.Error.ShouldBeNull(); using var db = UnsTreeTestDb.CreateNamed(dbName); - db.Equipment.Single(e => e.MachineCode == "machine_001").DriverInstanceId.ShouldBe("DRV-1"); + db.Equipment.Single(e => e.MachineCode == "machine_001").UnsLineId.ShouldBe("LINE-1"); } /// Driver-less equipment is allowed regardless of cluster (no #122 guard applies). @@ -154,7 +157,7 @@ public sealed class UnsTreeServiceEquipmentTests result.Error.ShouldBeNull(); using var db = UnsTreeTestDb.CreateNamed(dbName); - db.Equipment.Single(e => e.MachineCode == "machine_001").DriverInstanceId.ShouldBeNull(); + db.Equipment.Single(e => e.MachineCode == "machine_001").UnsLineId.ShouldBe("LINE-1"); } /// @@ -172,7 +175,6 @@ public sealed class UnsTreeServiceEquipmentTests { DriverInstanceId = "DRV-1", ClusterId = "MAIN", - NamespaceId = "NS-1", Name = "drv", DriverType = "Modbus", DriverConfig = "{}", @@ -286,8 +288,9 @@ public sealed class UnsTreeServiceEquipmentTests result.Error.ShouldNotBeNull(); result.Error.ShouldContain("decision #122"); + // The equipment row is unchanged (v3 has no persisted driver-binding column to check). using var verify = UnsTreeTestDb.CreateNamed(dbName); - verify.Equipment.Single(e => e.EquipmentId == equipmentId).DriverInstanceId.ShouldBeNull(); + verify.Equipment.Single(e => e.EquipmentId == equipmentId).MachineCode.ShouldBe("machine_001"); } /// Updating equipment with a MachineCode that already belongs to another row is blocked. @@ -344,8 +347,9 @@ public sealed class UnsTreeServiceEquipmentTests result.Ok.ShouldBeTrue(); result.Error.ShouldBeNull(); + // v3: the driver passes the #122 guard but is not persisted (binding column retired). using var verify = UnsTreeTestDb.CreateNamed(dbName); - verify.Equipment.Single(e => e.EquipmentId == equipmentId).DriverInstanceId.ShouldBe("DRV-1"); + verify.Equipment.Single(e => e.EquipmentId == equipmentId).UnsLineId.ShouldBe("LINE-1"); } /// CreateEquipmentAsync returns the generated EQ- id in CreatedId so callers can navigate to the new page. diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceImportTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceImportTests.cs index 9ae706ed..6544cbb6 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceImportTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceImportTests.cs @@ -36,7 +36,6 @@ public sealed class UnsTreeServiceImportTests { DriverInstanceId = "DRV-1", ClusterId = driverCluster, - NamespaceId = "NS-1", Name = "drv", DriverType = "Modbus", DriverConfig = "{}", @@ -194,7 +193,8 @@ public sealed class UnsTreeServiceImportTests result.Skipped.ShouldBe(0); result.Errors.ShouldBeEmpty(); + // v3: the driver passes the #122 guard but is not persisted (binding column retired). using var db = UnsTreeTestDb.CreateNamed(dbName); - db.Equipment.Single(e => e.MachineCode == "mc_1").DriverInstanceId.ShouldBe("DRV-1"); + db.Equipment.Single(e => e.MachineCode == "mc_1").UnsLineId.ShouldBe("LINE-1"); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceLoadEditTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceLoadEditTests.cs index 6fb14c66..d546617f 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceLoadEditTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceLoadEditTests.cs @@ -108,7 +108,6 @@ public sealed class UnsTreeServiceLoadEditTests { DriverInstanceId = "DRV-B", ClusterId = UnsTreeTestDb.PopulatedClusterId, - NamespaceId = "NS-1", Name = "modbus-b", DriverType = "Modbus", DriverConfig = "{}", @@ -117,7 +116,6 @@ public sealed class UnsTreeServiceLoadEditTests { DriverInstanceId = "DRV-A", ClusterId = UnsTreeTestDb.PopulatedClusterId, - NamespaceId = "NS-1", Name = "galaxy-a", DriverType = "Galaxy", DriverConfig = "{}", @@ -127,7 +125,6 @@ public sealed class UnsTreeServiceLoadEditTests { DriverInstanceId = "DRV-OTHER", ClusterId = UnsTreeTestDb.EmptyClusterId, - NamespaceId = "NS-2", Name = "other", DriverType = "S7", DriverConfig = "{}", @@ -144,7 +141,11 @@ public sealed class UnsTreeServiceLoadEditTests drivers[1].Display.ShouldBe("DRV-B — modbus-b (Modbus)"); } - /// Loading a seeded tag maps its fields, owning equipment, and a non-empty RowVersion. + /// + /// Loading a seeded Raw tag maps its surviving fields (Name/DataType/AccessLevel/TagConfig) and a + /// non-empty RowVersion. v3: Tag is Raw-only — the equipment + driver bindings were retired, so the + /// DTO's EquipmentId / DriverInstanceId project as empty strings. + /// [Fact] public async Task LoadTag_returns_dto() { @@ -154,9 +155,9 @@ public sealed class UnsTreeServiceLoadEditTests dto.ShouldNotBeNull(); dto.TagId.ShouldBe("TAG-1"); - dto.EquipmentId.ShouldBe(UnsTreeTestDb.SeededEquipmentId); + dto.EquipmentId.ShouldBe(string.Empty); dto.Name.ShouldBe("speed"); - dto.DriverInstanceId.ShouldBe("DRV-1"); + dto.DriverInstanceId.ShouldBe(string.Empty); dto.DataType.ShouldBe("Float"); dto.AccessLevel.ShouldBe(TagAccessLevel.Read); dto.TagConfig.ShouldBe("{}"); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceStructureTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceStructureTests.cs index 195b213f..6b941b24 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceStructureTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceStructureTests.cs @@ -70,8 +70,9 @@ public sealed class UnsTreeServiceStructureTests .Children.Single() .Children.Single(); - // Seed: 2 driver tags + 1 virtual tag (the orphan tag has no equipment and is excluded). - equipment.ChildCount.ShouldBe(3); + // v3: Tag is Raw-only — driver tags no longer bind to equipment, so only the 1 virtual tag + // contributes to the per-equipment badge count (the 2 seeded raw tags are not counted). + equipment.ChildCount.ShouldBe(1); equipment.HasLazyChildren.ShouldBeFalse(); } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceTagDriversTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceTagDriversTests.cs index fae92934..1faf05e9 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceTagDriversTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceTagDriversTests.cs @@ -7,20 +7,20 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// -/// Verifies that surfaces each -/// candidate driver's DriverType and DriverConfig alongside its id and display string, -/// so the UNS TagModal can dispatch to a per-driver-type typed tag-config editor (F-uns-1) and feed -/// the selected gateway's config to the Galaxy live-browse address picker. +/// v3 Batch-1 migration: the per-equipment candidate-driver loader +/// () is RETIRED. The equipment↔driver +/// binding and the Namespace/NamespaceKind entities were deleted — equipment no longer +/// scopes a candidate-driver list, and per-driver-typed tag authoring moved to the Raw tree (/raw) in +/// Batch 2. The loader now always returns empty; the former "surfaces DriverType/DriverConfig" test +/// (which fed the TagModal editor dispatch + Galaxy live-browse picker) is replaced by the stub +/// assertion below. That editor-dispatch coverage is re-established under the Batch-2 Raw-tree tests. /// [Trait("Category", "Unit")] public sealed class UnsTreeServiceTagDriversTests { - /// - /// A driver loaded for an equipment carries its DriverType and DriverConfig in the - /// returned tuple. - /// + /// The retired loader returns empty even when a matching cluster driver exists. [Fact] - public async Task LoadTagDriversForEquipment_surfaces_driver_type_and_config() + public async Task LoadTagDriversForEquipment_returns_empty_stub() { var dbName = $"uns-tagdrivers-{Guid.NewGuid():N}"; @@ -45,18 +45,10 @@ public sealed class UnsTreeServiceTagDriversTests Name = "machine-1", MachineCode = "machine_001", }); - db.Namespaces.Add(new Namespace - { - NamespaceId = "NS-EQ", - ClusterId = "MAIN", - Kind = NamespaceKind.Equipment, - NamespaceUri = "urn:zb:eq", - }); db.DriverInstances.Add(new DriverInstance { DriverInstanceId = "DRV-EQ", ClusterId = "MAIN", - NamespaceId = "NS-EQ", Name = "equipment driver", DriverType = "Modbus", DriverConfig = """{"endpoint":"10.0.0.1:502"}""", @@ -68,10 +60,6 @@ public sealed class UnsTreeServiceTagDriversTests var drivers = await service.LoadTagDriversForEquipmentAsync("EQ-1"); - drivers.Count.ShouldBe(1); - drivers[0].DriverInstanceId.ShouldBe("DRV-EQ"); - drivers[0].DriverType.ShouldBe("Modbus"); - // The picker (Galaxy live-browse) opens its session against the selected driver's config. - drivers[0].DriverConfig.ShouldBe("""{"endpoint":"10.0.0.1:502"}"""); + drivers.ShouldBeEmpty(); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceTagTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceTagTests.cs index 4f6ae96e..e2caffe7 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceTagTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceTagTests.cs @@ -1,4 +1,3 @@ -using Microsoft.EntityFrameworkCore; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns; @@ -8,39 +7,46 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// -/// Verifies the equipment-bound Tag CRUD mutations on , including -/// TagConfig JSON validity, the namespace-kind guard (tree tags must bind to an Equipment-kind -/// namespace), the decision-#122 driver-cluster guard, duplicate-id / duplicate-name guards, and -/// the driver-candidate loader scoped to the equipment's cluster. +/// v3 Batch-1 migration of the former equipment-bound Tag CRUD tests. Per-equipment tag authoring +/// was RETIRED: is now Raw-only (no EquipmentId/DriverInstanceId/ +/// FolderPath), the Namespace/NamespaceKind entities were deleted, and tag +/// authoring moved to the Raw tree (/raw) in Batch 2. So 's +/// CreateTagAsync / UpdateTagAsync now return a uniform Batch-1 refusal, and +/// LoadTagsForEquipmentAsync / LoadTagDriversForEquipmentAsync return empty. The tests +/// below assert that stub contract (preserving the intent of the old CRUD-guard / driver-loader +/// tests) plus the still-live DeleteTagAsync path (which continues to remove a raw Tag row). /// /// -/// The EF InMemory provider does not enforce RowVersion concurrency, so the -/// DbUpdateConcurrencyException branches are not exercised here by design. +/// The former TagConfig-JSON-validity, decision-#122 driver-cluster guard, Equipment-kind-namespace +/// guard, and duplicate-id/name guards moved with the authoring surface to the Raw tree — re-covered +/// under the Batch-2 Raw-tree service tests. /// [Trait("Category", "Unit")] public sealed class UnsTreeServiceTagTests { + /// The Batch-1 refusal message the retired per-equipment tag authoring now returns. + private const string TagAuthoringMovedError = "Tag authoring moved to the Raw tree (/raw) in v3 Batch 2."; + private static (UnsTreeService Service, string DbName) Fresh() { var dbName = $"uns-tag-{Guid.NewGuid():N}"; return (new UnsTreeService(UnsTreeTestDb.Factory(dbName)), dbName); } - /// - /// Seeds an area→line→equipment path in . The equipment id is - /// always EQ-1. Optionally seeds an Equipment-kind driver (DRV-EQ) in the equipment's - /// cluster, a non-Equipment-kind (Simulated) driver (DRV-SP) in the equipment's cluster, and - /// an Equipment-kind driver (DRV-OTHER) in . - /// - private static void SeedHierarchyAndDrivers( - string dbName, - string equipmentCluster, - bool seedEquipmentDriver = false, - bool seedNonEquipmentDriver = false, - string? otherCluster = null) + /// Seeds an area→line→equipment path in under equipment EQ-1. + private static void SeedHierarchy(string dbName, string cluster = "MAIN") { using var db = UnsTreeTestDb.CreateNamed(dbName); - db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-1", ClusterId = equipmentCluster, Name = "a" }); + db.ServerClusters.Add(new ServerCluster + { + ClusterId = cluster, + Name = cluster, + Enterprise = "zb", + Site = "s", + RedundancyMode = RedundancyMode.None, + CreatedBy = "test", + }); + db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-1", ClusterId = cluster, Name = "a" }); db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-1", UnsAreaId = "AREA-1", Name = "l" }); db.Equipment.Add(new Equipment { @@ -50,303 +56,77 @@ public sealed class UnsTreeServiceTagTests Name = "machine-1", MachineCode = "machine_001", }); - - // Equipment-kind namespace in the equipment's cluster. - db.Namespaces.Add(new Namespace - { - NamespaceId = "NS-EQ", - ClusterId = equipmentCluster, - Kind = NamespaceKind.Equipment, - NamespaceUri = "urn:zb:eq", - }); - // Non-Equipment-kind (Simulated) namespace in the equipment's cluster. - db.Namespaces.Add(new Namespace - { - NamespaceId = "NS-SP", - ClusterId = equipmentCluster, - Kind = NamespaceKind.Simulated, - NamespaceUri = "urn:zb:sp", - }); - - if (seedEquipmentDriver) - { - db.DriverInstances.Add(new DriverInstance - { - DriverInstanceId = "DRV-EQ", - ClusterId = equipmentCluster, - NamespaceId = "NS-EQ", - Name = "equipment driver", - DriverType = "Modbus", - DriverConfig = "{}", - }); - } - - if (seedNonEquipmentDriver) - { - db.DriverInstances.Add(new DriverInstance - { - DriverInstanceId = "DRV-SP", - ClusterId = equipmentCluster, - NamespaceId = "NS-SP", - Name = "non-equipment driver", - DriverType = "Modbus", - DriverConfig = "{}", - }); - } - - if (otherCluster is not null) - { - // Equipment-kind namespace + driver in a different cluster. - db.Namespaces.Add(new Namespace - { - NamespaceId = "NS-OTHER", - ClusterId = otherCluster, - Kind = NamespaceKind.Equipment, - NamespaceUri = "urn:zb:other", - }); - db.DriverInstances.Add(new DriverInstance - { - DriverInstanceId = "DRV-OTHER", - ClusterId = otherCluster, - NamespaceId = "NS-OTHER", - Name = "other-cluster driver", - DriverType = "Modbus", - DriverConfig = "{}", - }); - } - db.SaveChanges(); } - private static TagInput Input( - string tagId, - string name, - string driverInstanceId, - string tagConfig = "{}") => - new(tagId, name, driverInstanceId, DataType: "Float", + private static TagInput Input(string tagId, string name, string tagConfig = "{}") => + new(tagId, name, "DRV-EQ", DataType: "Float", AccessLevel: TagAccessLevel.Read, WriteIdempotent: false, PollGroupId: null, TagConfig: tagConfig); - // ----- CreateTag ----- + // ----- CreateTag / UpdateTag: retired → Batch-1 refusal ----- - /// A valid equipment-bound tag persists with EquipmentId set and FolderPath null. + /// Per-equipment tag creation is retired: the service returns the Batch-1 "moved to Raw tree" refusal. [Fact] - public async Task CreateTag_equipment_bound_persists() + public async Task CreateTag_is_retired_returns_batch1_stub() { var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true); + SeedHierarchy(dbName); - var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ")); - - result.Ok.ShouldBeTrue(); - result.Error.ShouldBeNull(); - - using var db = UnsTreeTestDb.CreateNamed(dbName); - var tag = db.Tags.Single(t => t.TagId == "TAG-1"); - tag.EquipmentId.ShouldBe("EQ-1"); - tag.FolderPath.ShouldBeNull(); - tag.DriverInstanceId.ShouldBe("DRV-EQ"); - tag.Name.ShouldBe("speed"); - tag.DataType.ShouldBe("Float"); - } - - /// A tag with invalid TagConfig JSON is blocked. - [Fact] - public async Task CreateTag_invalid_json_blocked() - { - var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true); - - var result = await service.CreateTagAsync( - "EQ-1", Input("TAG-1", "speed", "DRV-EQ", tagConfig: "{ not json")); + var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed")); result.Ok.ShouldBeFalse(); - result.Error.ShouldBe("TagConfig is not valid JSON."); + result.Error.ShouldBe(TagAuthoringMovedError); using var db = UnsTreeTestDb.CreateNamed(dbName); db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse(); } - /// A tag with a null TagConfig is blocked with the friendly "not valid JSON" message - /// (rather than an unhandled ArgumentNullException) — AdminUI-002. + /// The refusal is uniform — it does not depend on equipment resolvability or TagConfig validity. [Fact] - public async Task CreateTag_with_null_TagConfig_returns_friendly_error() + public async Task CreateTag_refusal_is_uniform_even_for_unknown_equipment_or_bad_json() { var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true); + SeedHierarchy(dbName); - var result = await service.CreateTagAsync( - "EQ-1", Input("TAG-1", "speed", "DRV-EQ", tagConfig: null!)); + (await service.CreateTagAsync("EQ-NOPE", Input("TAG-1", "speed"))).Error + .ShouldBe(TagAuthoringMovedError); + (await service.CreateTagAsync("EQ-1", Input("TAG-2", "speed", tagConfig: "{ not json"))).Error + .ShouldBe(TagAuthoringMovedError); + } + + /// Per-equipment tag update is retired: the service returns the Batch-1 "moved to Raw tree" refusal. + [Fact] + public async Task UpdateTag_is_retired_returns_batch1_stub() + { + var (service, _) = Fresh(); + + var result = await service.UpdateTagAsync("TAG-1", Input("TAG-1", "renamed"), []); result.Ok.ShouldBeFalse(); - result.Error.ShouldBe("TagConfig is not valid JSON."); - - using var db = UnsTreeTestDb.CreateNamed(dbName); - db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse(); + result.Error.ShouldBe(TagAuthoringMovedError); } - /// A tag with a blank/whitespace TagConfig is blocked with the friendly message — AdminUI-002. - [Fact] - public async Task CreateTag_with_blank_TagConfig_returns_friendly_error() - { - var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true); + // ----- DeleteTag: still live against a raw Tag row ----- - var result = await service.CreateTagAsync( - "EQ-1", Input("TAG-1", "speed", "DRV-EQ", tagConfig: " ")); - - result.Ok.ShouldBeFalse(); - result.Error.ShouldBe("TagConfig is not valid JSON."); - - using var db = UnsTreeTestDb.CreateNamed(dbName); - db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse(); - } - - /// Binding a tag to a driver in a different cluster than the equipment is blocked (#122). - [Fact] - public async Task CreateTag_driver_in_other_cluster_blocked() - { - var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", otherCluster: "SITE-A"); - - var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-OTHER")); - - result.Ok.ShouldBeFalse(); - result.Error.ShouldNotBeNull(); - result.Error.ShouldContain("decision #122"); - result.Error.ShouldContain("DRV-OTHER"); - result.Error.ShouldContain("SITE-A"); - result.Error.ShouldContain("MAIN"); - - using var db = UnsTreeTestDb.CreateNamed(dbName); - db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse(); - } - - /// Binding a tree tag to a driver in a non-Equipment-kind namespace is blocked. - [Fact] - public async Task CreateTag_driver_non_equipment_namespace_blocked() - { - var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedNonEquipmentDriver: true); - - var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-SP")); - - result.Ok.ShouldBeFalse(); - result.Error.ShouldNotBeNull(); - result.Error.ShouldContain("DRV-SP"); - result.Error.ShouldContain("Equipment-kind namespace"); - - using var db = UnsTreeTestDb.CreateNamed(dbName); - db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse(); - } - - /// Creating a tag with a TagId that already exists is blocked. - [Fact] - public async Task CreateTag_duplicate_tagid_blocked() - { - var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true); - await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ")); - - var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "another", "DRV-EQ")); - - result.Ok.ShouldBeFalse(); - result.Error.ShouldBe("Tag 'TAG-1' already exists."); - } - - /// Creating a tag for an equipment id that does not exist returns a not-found error. - [Fact] - public async Task CreateTag_unresolvable_equipment_returns_error() - { - var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true); - - var result = await service.CreateTagAsync("EQ-NOPE", Input("TAG-1", "speed", "DRV-EQ")); - - result.Ok.ShouldBeFalse(); - result.Error.ShouldNotBeNull(); - result.Error.ShouldContain("not found"); - - using var db = UnsTreeTestDb.CreateNamed(dbName); - db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse(); - } - - /// Creating a tag whose Name already exists on the same equipment is blocked. - [Fact] - public async Task CreateTag_duplicate_name_on_equipment_blocked() - { - var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true); - await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ")); - - var result = await service.CreateTagAsync("EQ-1", Input("TAG-2", "speed", "DRV-EQ")); - - result.Ok.ShouldBeFalse(); - result.Error.ShouldBe("A tag named 'speed' already exists on this equipment."); - } - - // ----- UpdateTag ----- - - /// Updating a tag changes its mutable fields and keeps EquipmentId / FolderPath. - [Fact] - public async Task UpdateTag_changes_fields() - { - var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true); - await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ")); - - byte[] rv; - using (var db = UnsTreeTestDb.CreateNamed(dbName)) - { - rv = db.Tags.Single(t => t.TagId == "TAG-1").RowVersion; - } - - var updated = new TagInput("TAG-1", "renamed", "DRV-EQ", DataType: "Int32", - AccessLevel: TagAccessLevel.ReadWrite, WriteIdempotent: true, - PollGroupId: " ", TagConfig: """{ "register": 40001 }"""); - - var result = await service.UpdateTagAsync("TAG-1", updated, rv); - - result.Ok.ShouldBeTrue(); - result.Error.ShouldBeNull(); - - using var verify = UnsTreeTestDb.CreateNamed(dbName); - var after = verify.Tags.Single(t => t.TagId == "TAG-1"); - after.Name.ShouldBe("renamed"); - after.DataType.ShouldBe("Int32"); - after.AccessLevel.ShouldBe(TagAccessLevel.ReadWrite); - after.WriteIdempotent.ShouldBeTrue(); - after.PollGroupId.ShouldBeNull(); // whitespace collapses to null - after.EquipmentId.ShouldBe("EQ-1"); - after.FolderPath.ShouldBeNull(); - } - - /// Updating a tag that no longer exists returns the row-gone error. - [Fact] - public async Task UpdateTag_missing_row_returns_error() - { - var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true); - - var result = await service.UpdateTagAsync("TAG-nope", Input("TAG-nope", "x", "DRV-EQ"), []); - - result.Ok.ShouldBeFalse(); - result.Error.ShouldBe("Row no longer exists."); - } - - // ----- DeleteTag ----- - - /// Deleting a tag removes the row. + /// Deleting a raw tag removes the row (DeleteTagAsync remains functional in Batch 1). [Fact] public async Task DeleteTag_removes_row() { var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true); - await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ")); - byte[] rv; using (var db = UnsTreeTestDb.CreateNamed(dbName)) { + db.Devices.Add(new Device + { + DeviceId = "DEV-1", DriverInstanceId = "DRV-1", Name = "d", DeviceConfig = "{}", + }); + db.Tags.Add(new Tag + { + TagId = "TAG-1", DeviceId = "DEV-1", Name = "speed", DataType = "Float", + AccessLevel = TagAccessLevel.Read, TagConfig = "{}", + }); + db.SaveChanges(); rv = db.Tags.Single(t => t.TagId == "TAG-1").RowVersion; } @@ -371,40 +151,25 @@ public sealed class UnsTreeServiceTagTests result.Error.ShouldBeNull(); } - // ----- LoadTagDriversForEquipmentAsync ----- + // ----- Per-equipment loaders: retired → empty ----- - /// - /// The driver loader returns only Equipment-kind drivers in the equipment's cluster — excluding - /// non-Equipment-kind drivers in the same cluster and Equipment-kind drivers in other clusters. - /// + /// The per-equipment tag list is retired (Tag is Raw-only) → always empty. [Fact] - public async Task LoadTagDriversForEquipment_returns_only_equipment_kind_drivers_in_cluster() + public async Task LoadTagsForEquipment_returns_empty() { var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers( - dbName, - equipmentCluster: "MAIN", - seedEquipmentDriver: true, - seedNonEquipmentDriver: true, - otherCluster: "SITE-A"); + SeedHierarchy(dbName); - var drivers = await service.LoadTagDriversForEquipmentAsync("EQ-1"); - - drivers.Count.ShouldBe(1); - drivers[0].DriverInstanceId.ShouldBe("DRV-EQ"); - drivers[0].Display.ShouldContain("DRV-EQ"); - drivers[0].Display.ShouldContain("equipment driver"); + (await service.LoadTagsForEquipmentAsync("EQ-1")).ShouldBeEmpty(); } - /// An unresolvable equipment yields an empty driver list. + /// The per-equipment candidate-driver loader is retired (equipment↔driver binding gone) → always empty. [Fact] - public async Task LoadTagDriversForEquipment_unresolvable_equipment_returns_empty() + public async Task LoadTagDriversForEquipment_returns_empty() { var (service, dbName) = Fresh(); - SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true); + SeedHierarchy(dbName); - var drivers = await service.LoadTagDriversForEquipmentAsync("EQ-nope"); - - drivers.ShouldBeEmpty(); + (await service.LoadTagDriversForEquipmentAsync("EQ-1")).ShouldBeEmpty(); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeTestDb.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeTestDb.cs index ad4c9d6f..d0f5dd51 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeTestDb.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeTestDb.cs @@ -9,8 +9,10 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// Shared in-memory fixture for UnsTreeService structural tests. Builds an /// over a named InMemory database and seeds a small, /// deterministic UNS hierarchy: enterprise "zb" across two clusters (MAIN populated, -/// SITE-A intentionally empty), plus tags and a virtual tag on one equipment node so -/// the per-equipment count joins can be exercised. +/// SITE-A intentionally empty), a virtual tag on one equipment node (so the per-equipment +/// virtual-tag count join can be exercised), plus a v3 Raw-tree slice +/// (Driver→Device→Tag). Under v3, tags are Raw-only — they no longer bind to equipment — so +/// the per-equipment driver-tag count is always zero; only the virtual-tag count join remains. /// internal static class UnsTreeTestDb { @@ -84,12 +86,22 @@ internal static class UnsTreeTestDb MachineCode = "machine_001", }); - // Two driver tags + one virtual tag on the seeded equipment → ChildCount 3. + // v3 Raw tree: a device → two raw tags. Tags are Raw-only and no longer bind to equipment, + // so they do NOT contribute to any per-equipment count. The owning DriverInstance is left + // unseeded on purpose — no structural test reads it, and seeding one would pollute the + // per-cluster driver-list assertions in UnsTreeServiceLoadEditTests (EF InMemory does not + // enforce the Device→Driver FK). + db.Devices.Add(new Device + { + DeviceId = "DEV-1", + DriverInstanceId = "DRV-1", + Name = "device-1", + DeviceConfig = "{}", + }); db.Tags.Add(new Tag { TagId = "TAG-1", - DriverInstanceId = "DRV-1", - EquipmentId = SeededEquipmentId, + DeviceId = "DEV-1", Name = "speed", DataType = "Float", AccessLevel = TagAccessLevel.Read, @@ -98,24 +110,14 @@ internal static class UnsTreeTestDb db.Tags.Add(new Tag { TagId = "TAG-2", - DriverInstanceId = "DRV-1", - EquipmentId = SeededEquipmentId, + DeviceId = "DEV-1", Name = "running", DataType = "Boolean", AccessLevel = TagAccessLevel.Read, TagConfig = "{}", }); - // A tag with no equipment must be ignored by the count query. - db.Tags.Add(new Tag - { - TagId = "TAG-ORPHAN", - DriverInstanceId = "DRV-1", - EquipmentId = null, - Name = "orphan", - DataType = "Int32", - AccessLevel = TagAccessLevel.Read, - TagConfig = "{}", - }); + + // One virtual tag on the seeded equipment → per-equipment virtual-tag ChildCount 1. db.VirtualTags.Add(new VirtualTag { VirtualTagId = "VTAG-1", diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/VirtualTagEquipTokenValidationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/VirtualTagEquipTokenValidationTests.cs index 58922c56..d8b55a4c 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/VirtualTagEquipTokenValidationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/VirtualTagEquipTokenValidationTests.cs @@ -24,6 +24,14 @@ public sealed class VirtualTagEquipTokenValidationTests private const string EquipBaseScript = "return ctx.GetTag(\"{{equip}}.X\");"; private const string PlainScript = "return ctx.GetTag(\"TestMachine_001.X\");"; + // v3 Batch-1: the equipment↔Tag binding was retired (Tag is Raw-only), so the {{equip}} token's + // equipment-tag-derived base can never resolve — ValidateEquipTokenAsync now derives from an empty + // tag set, so any {{equip}} script is rejected. The token-present/no-base rejection and the + // no-token success cases stay live below; the tests that assert a DERIVABLE base (success) or the + // divergent-prefix derivation logic are dark until per-equipment tag references return in Batch 3. + private const string DarkUntilBatch3 = + "v3 Batch-1: {{equip}} equipment-tag-derived base is dark — per-equipment tag references return in Batch 3."; + private static (UnsTreeService Service, string DbName) Fresh() { var dbName = $"uns-equiptoken-{Guid.NewGuid():N}"; @@ -58,11 +66,11 @@ public sealed class VirtualTagEquipTokenValidationTests }); if (tagFullName is not null) { + // v3: raw tag (no equipment binding). Kept so the dark {{equip}}-base tests still compile. db.Tags.Add(new Tag { TagId = "TAG-1", - DriverInstanceId = "DRV-1", - EquipmentId = "EQ-1", + DeviceId = "DEV-1", Name = "x", DataType = "Float", AccessLevel = TagAccessLevel.Read, @@ -95,7 +103,7 @@ public sealed class VirtualTagEquipTokenValidationTests // ----- Create ----- /// {{equip}} script + a driver tag whose FullName gives a base → create succeeds. - [Fact] + [Fact(Skip = DarkUntilBatch3)] public async Task Create_equip_token_with_derivable_base_succeeds() { var (service, dbName) = Fresh(); @@ -142,7 +150,7 @@ public sealed class VirtualTagEquipTokenValidationTests /// {{equip}} script + TWO driver tags whose FullNames have DIFFERENT object prefixes /// (no single base can be derived) → create rejected, error names equipment + token. /// - [Fact] + [Fact(Skip = DarkUntilBatch3)] public async Task Create_equip_token_with_divergent_prefixes_rejected() { var (service, dbName) = Fresh(); @@ -152,8 +160,7 @@ public sealed class VirtualTagEquipTokenValidationTests db.Tags.Add(new Tag { TagId = "TAG-2", - DriverInstanceId = "DRV-1", - EquipmentId = "EQ-1", + DeviceId = "DEV-1", Name = "y", DataType = "Float", AccessLevel = TagAccessLevel.Read, @@ -176,7 +183,7 @@ public sealed class VirtualTagEquipTokenValidationTests // ----- Update ----- /// {{equip}} script + a derivable base → update succeeds. - [Fact] + [Fact(Skip = DarkUntilBatch3)] public async Task Update_equip_token_with_derivable_base_succeeds() { var (service, dbName) = Fresh();