test(overrides): case-variant override names verified end to end through the flattener; UI single path gets the hint (ScadaBridge#105)
Re-verified #105 item 2 on the real write paths: ManagementActor bulk attribute overrides (CLI set-overrides / management API) and InstanceService.SetAttributeOverrideAsync (Central UI single editor) both match the template member ordinally and refuse a case-variant name, so nothing is stored for the flattener to drop. Native-source overrides already validate against the flattener's own output. - New theory tests drive each path, feed whatever it stored into the real FlatteningService, and assert every stored override is applied (exact-case row is the positive control). Mutation: accepting case variants (OrdinalIgnoreCase) fails them with "stored ... but the flattener dropped it as unresolved". - The did-you-mean hint moves to TemplateEngine.OverrideNameMatching and is now also on the UI single-attribute path; ambiguous matches get no hint. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -1170,10 +1170,7 @@ public class ManagementActor : ReceiveActor
|
||||
/// an ambiguous match gets no hint.
|
||||
/// </summary>
|
||||
internal static string CaseMismatchHint(string name, IEnumerable<string> 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;
|
||||
}
|
||||
=> OverrideNameMatching.CaseMismatchHint(name, candidates);
|
||||
|
||||
private static async Task<object?> HandleSetInstanceOverrides(IServiceProvider sp, SetInstanceOverridesCommand cmd, AuthenticatedUser user)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine;
|
||||
|
||||
/// <summary>
|
||||
/// ScadaBridge#105: instance override names address a template member by its exact
|
||||
/// (ordinal) name, because the flattener matches overrides ordinally and silently skips
|
||||
/// one it cannot resolve. Every override write path (ManagementActor bulk attribute and
|
||||
/// native-source commands, used by the CLI and the management API; and
|
||||
/// <see cref="Services.InstanceService.SetAttributeOverrideAsync"/>, used by the Central UI)
|
||||
/// therefore rejects a name that differs from a member only by case instead of storing a
|
||||
/// dead override. This helper builds the shared "did you mean" hint for that rejection.
|
||||
/// </summary>
|
||||
public static class OverrideNameMatching
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a hint naming the single candidate that equals <paramref name="name"/>
|
||||
/// case-insensitively, or an empty string when there is no such candidate or the
|
||||
/// case-insensitive match is ambiguous.
|
||||
/// </summary>
|
||||
/// <param name="name">The override name that did not match any member exactly.</param>
|
||||
/// <param name="candidates">The member names the override could address.</param>
|
||||
/// <returns>A hint suffix (leading space, no trailing period), or an empty string.</returns>
|
||||
public static string CaseMismatchHint(string name, IEnumerable<string> candidates)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(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;
|
||||
}
|
||||
}
|
||||
@@ -168,7 +168,9 @@ public class InstanceService
|
||||
var templateAttr = templateAttrs.FirstOrDefault(a => a.Name == attributeName);
|
||||
if (templateAttr == null)
|
||||
return Result<InstanceAttributeOverride>.Failure(
|
||||
$"Attribute '{attributeName}' does not exist in template {instance.TemplateId}. Cannot add new attributes via overrides.");
|
||||
$"Attribute '{attributeName}' does not exist in template {instance.TemplateId}" +
|
||||
OverrideNameMatching.CaseMismatchHint(attributeName, templateAttrs.Select(a => a.Name)) +
|
||||
". Cannot add new attributes via overrides.");
|
||||
|
||||
if (templateAttr.IsLocked)
|
||||
return Result<InstanceAttributeOverride>.Failure(
|
||||
|
||||
Reference in New Issue
Block a user