diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
index bac5f1c2..a212173a 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
@@ -1140,6 +1140,18 @@ public class ManagementActor : ReceiveActor
: throw new ManagementCommandException(result.Error);
}
+ ///
+ /// ScadaBridge#105: override names are matched exactly (the flattener is ordinal), so a name
+ /// that differs from a member only by case is rejected, never stored. When exactly one
+ /// member matches case-insensitively, name it in the error so the caller can correct it;
+ /// an ambiguous match gets no hint.
+ ///
+ internal static string CaseMismatchHint(string name, IEnumerable candidates)
+ {
+ var matches = candidates.Where(c => string.Equals(c, name, StringComparison.OrdinalIgnoreCase)).Distinct(StringComparer.Ordinal).Take(2).ToList();
+ return matches.Count == 1 ? $" (names are case-sensitive; did you mean '{matches[0]}'?)" : string.Empty;
+ }
+
private static async Task HandleSetInstanceOverrides(IServiceProvider sp, SetInstanceOverridesCommand cmd, AuthenticatedUser user)
{
await EnforceSiteScopeForInstance(sp, user, cmd.InstanceId);
@@ -1163,8 +1175,8 @@ public class ManagementActor : ReceiveActor
{
if (!attrsByName.TryGetValue(attrName, out var templateAttr))
throw new ManagementCommandException(
- $"Attribute '{attrName}' does not exist in template {instance.TemplateId}. " +
- "No overrides were applied.");
+ $"Attribute '{attrName}' does not exist in template {instance.TemplateId}" +
+ CaseMismatchHint(attrName, attrsByName.Keys) + ". No overrides were applied.");
if (templateAttr.IsLocked)
throw new ManagementCommandException(
$"Attribute '{attrName}' is locked and cannot be overridden. No overrides were applied.");
@@ -1288,7 +1300,9 @@ public class ManagementActor : ReceiveActor
.FirstOrDefault(s => string.Equals(s.CanonicalName, cmd.SourceCanonicalName, StringComparison.Ordinal));
if (resolvedSource == null)
throw new ManagementCommandException(
- $"Native alarm source '{cmd.SourceCanonicalName}' does not resolve for this instance and cannot be overridden.");
+ $"Native alarm source '{cmd.SourceCanonicalName}' does not resolve for this instance and cannot be overridden" +
+ CaseMismatchHint(cmd.SourceCanonicalName,
+ flattenResult.Value.Configuration.NativeAlarmSources.Select(x => x.CanonicalName)) + ".");
if (resolvedSource.IsLocked)
throw new ManagementCommandException(
$"Native alarm source '{cmd.SourceCanonicalName}' is locked at the template level and cannot be overridden.");
@@ -1349,7 +1363,8 @@ public class ManagementActor : ReceiveActor
if (!resolvedByName.TryGetValue(entry.SourceCanonicalName, out var resolvedSource))
throw new ManagementCommandException(
$"Native alarm source '{entry.SourceCanonicalName}' does not resolve for this instance " +
- "and cannot be overridden. No overrides were applied.");
+ "and cannot be overridden" + CaseMismatchHint(entry.SourceCanonicalName, resolvedByName.Keys) +
+ ". No overrides were applied.");
if (resolvedSource.IsLocked)
throw new ManagementCommandException(
$"Native alarm source '{entry.SourceCanonicalName}' is locked at the template level and " +
diff --git a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs
index 17b20579..51409e70 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs
@@ -2811,6 +2811,58 @@ public class ManagementActorTests : TestKit, IDisposable
// command up front WITHOUT persisting any of the overrides.
// ========================================================================
+ /// ScadaBridge#105: a bulk attribute override differing from the member only by case is rejected with a hint, nothing stored.
+ [Fact]
+ public void SetInstanceOverrides_CaseVariantName_IsRejectedWithHint_PersistsNothing()
+ {
+ var instance = new Instance("Pump5") { Id = 5, SiteId = 1, TemplateId = 1 };
+ _templateRepo.GetInstanceByIdAsync(5, Arg.Any()).Returns(instance);
+ _templateRepo.GetAttributesByTemplateIdAsync(1, Arg.Any())
+ .Returns(new List { new("Setpoint") { Id = 1, TemplateId = 1 } });
+ _templateRepo.GetOverridesByInstanceIdAsync(5, Arg.Any())
+ .Returns(new List());
+
+ var actor = CreateActor();
+ actor.Tell(Envelope(new SetInstanceOverridesCommand(5, new Dictionary { ["setpoint"] = "1" }), "Deployer"));
+
+ var response = ExpectMsg(TimeSpan.FromSeconds(5));
+ Assert.Equal("COMMAND_FAILED", response.ErrorCode);
+ Assert.Contains("did you mean 'Setpoint'", response.Error);
+ _templateRepo.DidNotReceiveWithAnyArgs().AddInstanceAttributeOverrideAsync(default!, default);
+ }
+
+ /// ScadaBridge#105: a bulk native-source override differing only by case is rejected, nothing stored.
+ [Fact]
+ public void SetInstanceNativeAlarmSourceOverrides_CaseVariantName_IsRejectedWithHint()
+ {
+ StubFlattenedNativeSources("Pressure");
+ var actor = CreateActor();
+ actor.Tell(Envelope(
+ new SetInstanceNativeAlarmSourceOverridesCommand(1, new List
+ {
+ new("pressure", "Opc2", null, null)
+ }),
+ "Deployer"));
+
+ var response = ExpectMsg(TimeSpan.FromSeconds(5));
+ Assert.Contains("did you mean 'Pressure'", response.Error);
+ _templateRepo.DidNotReceiveWithAnyArgs().AddInstanceNativeAlarmSourceOverrideAsync(default!, default);
+ }
+
+ /// ScadaBridge#105: a single native-source override differing only by case is rejected, nothing stored.
+ [Fact]
+ public void SetInstanceNativeAlarmSourceOverride_CaseVariantName_IsRejectedWithHint()
+ {
+ StubFlattenedNativeSources("Pressure");
+ var actor = CreateActor();
+ actor.Tell(Envelope(
+ new SetInstanceNativeAlarmSourceOverrideCommand(1, "PRESSURE", "Opc2", null, null), "Deployer"));
+
+ var response = ExpectMsg(TimeSpan.FromSeconds(5));
+ Assert.Contains("did you mean 'Pressure'", response.Error);
+ _templateRepo.DidNotReceiveWithAnyArgs().AddInstanceNativeAlarmSourceOverrideAsync(default!, default);
+ }
+
/// Verifies that set instance overrides, when with one invalid attribute, persists no overrides.
[Fact]
public void SetInstanceOverrides_WithOneInvalidAttribute_PersistsNoOverrides()