fix(uns): reject driver-bind on unresolvable line + enforce MachineCode uniqueness on update (review)

This commit is contained in:
Joseph Doherty
2026-06-08 12:55:36 -04:00
parent 2836a0704b
commit ab0ff8aedf
2 changed files with 114 additions and 4 deletions
@@ -403,6 +403,11 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
if (await db.Equipment.AnyAsync(e => e.MachineCode == input.MachineCode && e.EquipmentId != equipmentId, ct))
{
return new UnsMutationResult(false, $"MachineCode '{input.MachineCode}' already exists in this fleet.");
}
var guard = await CheckDriverClusterGuardAsync(db, input, ct);
if (guard is not null)
{
@@ -471,9 +476,14 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
/// <summary>
/// Decision #122: an equipment may only bind to a driver in the same cluster as its line.
/// Only runs when a driver is requested. Resolves the line's cluster (line → area → cluster)
/// and the driver's cluster; when both are known and differ, returns the guard failure.
/// Returns <c>null</c> when the bind is allowed (driver-less, unresolvable cluster, or match).
/// Policy:
/// <list type="bullet">
/// <item>Driver-less (<c>DriverInstanceId</c> empty) → always allowed (returns <c>null</c>).</item>
/// <item>Driver bound but the line/area does not resolve to a cluster → rejected (unresolvable
/// line cannot guarantee cluster alignment, so the bind is unsafe).</item>
/// <item>Driver bound, line resolves, clusters differ → rejected.</item>
/// <item>Driver bound, line resolves, clusters match → allowed (returns <c>null</c>).</item>
/// </list>
/// </summary>
private static async Task<UnsMutationResult?> CheckDriverClusterGuardAsync(
OtOpcUaConfigDbContext db,
@@ -489,12 +499,19 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
var area = line is null ? null : await db.UnsAreas.FirstOrDefaultAsync(a => a.UnsAreaId == line.UnsAreaId, ct);
var lineCluster = area?.ClusterId;
if (lineCluster is null)
{
return new UnsMutationResult(
false,
$"Cannot bind driver '{input.DriverInstanceId}': UNS line '{input.UnsLineId}' does not resolve to a cluster (decision #122).");
}
var driverCluster = await db.DriverInstances
.Where(d => d.DriverInstanceId == input.DriverInstanceId)
.Select(d => d.ClusterId)
.FirstOrDefaultAsync(ct);
if (driverCluster is not null && lineCluster is not null && driverCluster != lineCluster)
if (driverCluster is not null && driverCluster != lineCluster)
{
return new UnsMutationResult(
false,