77bc010ba9
Replace the Batch-1-stubbed authored-tag flow with a UnsTagReference list on the equipment page's Tags tab (v3 reference-only equipment): columns are effective name, computed raw path, inherited datatype/access (read-only), display-name override (editable), and remove. The retired equipment-authored-tag editor (TagModal.razor) is deleted; the VirtualTag and ScriptedAlarm flows are untouched. - "+ Add reference" opens a new AddReferenceModal that reuses RawTree in a new opt-in PickerMode (Tag-leaf checkboxes + Device/TagGroup "select all tags below"), scoped structurally to the equipment's cluster via LoadReferencePickerRootAsync (a single cluster root — cross-cluster tags are unreachable, not merely hidden). Default /raw usage is byte-unchanged (PickerMode defaults false). - UnsTreeService (+ IUnsTreeService) gain the reference mutations: LoadReferencesForEquipmentAsync (computes each RawPath via the shared RawPathResolver), AddReferencesAsync (cluster-checked, all-or-nothing), RemoveReferenceAsync, SetReferenceOverrideAsync, plus the picker helpers LoadReferencePickerRootAsync / LoadDescendantTagIdsAsync. - Consume IEffectiveNameGuard (constructor-injected, no-op fallback when unregistered): AddReferences, SetReferenceOverride, and the VirtualTag + ScriptedAlarm create/update mutations now call CheckAsync before persisting and surface a collision as the failure. WP2 supplies the implementation + DI registration. - Drop the retired equipment↔driver binding: remove DriverInstanceId from EquipmentInput, the ImportEquipmentModal CSV column, the EquipmentPage Details driver select, and the dead decision-#122 driver-cluster guard. Tests: new UnsTreeServiceReferenceTests (add/remove/override, cross-cluster + duplicate rejection, guard-consumed rejection via a fake guard, RawPath projection, picker helpers); Equipment/Import test suites rewritten to drop the retired driver-guard cases. AdminUI suite green (639 passed, 3 skipped); full solution builds 0 errors. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
117 lines
4.6 KiB
C#
117 lines
4.6 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
|
|
|
/// <summary>
|
|
/// Verifies the bulk <see cref="UnsTreeService.ImportEquipmentAsync"/> path: valid rows insert, rows
|
|
/// whose MachineCode already exists (in the DB or earlier in the same batch) are skipped, and rows
|
|
/// referencing an unknown UNS line are reported as errors. (v3: equipment no longer binds a driver, so
|
|
/// the old DriverInstanceId column and its decision-#122 cluster guard — and their tests — are gone.)
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class UnsTreeServiceImportTests
|
|
{
|
|
private static (UnsTreeService Service, string DbName) Fresh()
|
|
{
|
|
var dbName = $"uns-import-{Guid.NewGuid():N}";
|
|
return (new UnsTreeService(UnsTreeTestDb.Factory(dbName)), dbName);
|
|
}
|
|
|
|
/// <summary>Seeds an area → line (id <c>LINE-1</c>) under <paramref name="cluster"/>.</summary>
|
|
private static void SeedLine(string dbName, string cluster)
|
|
{
|
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
|
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.SaveChanges();
|
|
}
|
|
|
|
private static EquipmentInput Input(string name, string machineCode, string unsLineId) =>
|
|
new(name, machineCode, unsLineId,
|
|
ZTag: null, SAPID: null, Manufacturer: null, Model: null, SerialNumber: null,
|
|
HardwareRevision: null, SoftwareRevision: null, YearOfConstruction: null,
|
|
AssetLocation: null, ManufacturerUri: null, DeviceManualUri: null, Enabled: true);
|
|
|
|
/// <summary>Valid rows insert with system-generated EQ- ids and are counted in <c>Inserted</c>.</summary>
|
|
[Fact]
|
|
public async Task Import_inserts_valid_rows()
|
|
{
|
|
var (service, dbName) = Fresh();
|
|
SeedLine(dbName, "MAIN");
|
|
|
|
var result = await service.ImportEquipmentAsync(
|
|
[
|
|
Input("machine-1", "mc_1", "LINE-1"),
|
|
Input("machine-2", "mc_2", "LINE-1"),
|
|
]);
|
|
|
|
result.Inserted.ShouldBe(2);
|
|
result.Skipped.ShouldBe(0);
|
|
result.Errors.ShouldBeEmpty();
|
|
|
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
|
db.Equipment.Count().ShouldBe(2);
|
|
var eq = db.Equipment.Single(e => e.MachineCode == "mc_1");
|
|
eq.EquipmentId.ShouldStartWith("EQ-");
|
|
eq.EquipmentId.Length.ShouldBe(15); // "EQ-" + 12 hex chars
|
|
eq.Name.ShouldBe("machine-1");
|
|
eq.UnsLineId.ShouldBe("LINE-1");
|
|
eq.Enabled.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// A row whose MachineCode already exists in the DB is skipped (not an error), and a second row
|
|
/// later in the batch that reuses an earlier row's MachineCode is also skipped.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Import_skips_duplicate_machinecode()
|
|
{
|
|
var (service, dbName) = Fresh();
|
|
SeedLine(dbName, "MAIN");
|
|
// Pre-existing equipment with MachineCode "mc_existing".
|
|
await service.CreateEquipmentAsync(Input("machine-x", "mc_existing", "LINE-1"));
|
|
|
|
var result = await service.ImportEquipmentAsync(
|
|
[
|
|
Input("machine-1", "mc_existing", "LINE-1"), // dup of DB row → skip
|
|
Input("machine-2", "mc_new", "LINE-1"), // inserts
|
|
Input("machine-3", "mc_new", "LINE-1"), // dup of in-batch row → skip
|
|
]);
|
|
|
|
result.Inserted.ShouldBe(1);
|
|
result.Skipped.ShouldBe(2);
|
|
result.Errors.ShouldBeEmpty();
|
|
|
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
|
db.Equipment.Count(e => e.MachineCode == "mc_new").ShouldBe(1);
|
|
db.Equipment.Count(e => e.MachineCode == "mc_existing").ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>A row whose UnsLineId does not exist is reported as an error and not inserted.</summary>
|
|
[Fact]
|
|
public async Task Import_reports_unknown_line()
|
|
{
|
|
var (service, dbName) = Fresh();
|
|
SeedLine(dbName, "MAIN");
|
|
|
|
var result = await service.ImportEquipmentAsync(
|
|
[
|
|
Input("machine-1", "mc_1", "LINE-BOGUS"),
|
|
Input("machine-2", "mc_2", "LINE-1"),
|
|
]);
|
|
|
|
result.Inserted.ShouldBe(1);
|
|
result.Skipped.ShouldBe(0);
|
|
result.Errors.Count.ShouldBe(1);
|
|
result.Errors[0].ShouldContain("mc_1");
|
|
result.Errors[0].ShouldContain("LINE-BOGUS");
|
|
|
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
|
db.Equipment.Any(e => e.MachineCode == "mc_1").ShouldBeFalse();
|
|
db.Equipment.Any(e => e.MachineCode == "mc_2").ShouldBeTrue();
|
|
}
|
|
}
|