From a94f4e805e3b3d0ffa1d1d00e7a1e5cbe72220b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 07:26:06 +0000 Subject: [PATCH] Trim the semantic Command constructor's name, description and category [patch] The semantic-type constructor trimmed only the id, so a padded category was stored as given and GetCommandsByCategory, which trims its input, never found it until a save and reload. It now normalizes the same way the string constructor does: name, description and category are trimmed, a blank description or category becomes null, and a blank name is refused. Fixes #146 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CCikcgWw27K5y1T5jxpzcG --- .../SemanticCommandNormalizationTests.cs | 57 +++++++++++++++++++ Keybinding/Models/Command.cs | 18 ++++-- 2 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 Keybinding.Test/SemanticCommandNormalizationTests.cs diff --git a/Keybinding.Test/SemanticCommandNormalizationTests.cs b/Keybinding.Test/SemanticCommandNormalizationTests.cs new file mode 100644 index 0000000..1a7e5ef --- /dev/null +++ b/Keybinding.Test/SemanticCommandNormalizationTests.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Keybinding.Test; + +using ktsu.Keybinding.Core.Models; +using ktsu.Keybinding.Core.Services; + +[TestClass] +public class SemanticCommandNormalizationTests +{ + private static Command CreatePaddedCommand() => + new(CommandId.Create("edit.copy"), CommandName.Create(" Copy "), CommandDescription.Create(" "), CommandCategory.Create("Editing ")); + + [TestMethod] + public void Constructor_PaddedNameAndCategory_AreTrimmed() + { + Command command = CreatePaddedCommand(); + + Assert.AreEqual("Copy", command.Name.ToString()); + Assert.AreEqual("Editing", command.Category?.ToString()); + } + + [TestMethod] + public void Constructor_WhitespaceOnlyDescriptionAndCategory_BecomeNull() + { + Command command = new(CommandId.Create("edit.copy"), CommandName.Create("Copy"), CommandDescription.Create(" "), CommandCategory.Create(" ")); + + Assert.IsNull(command.Description); + Assert.IsNull(command.Category); + } + + [TestMethod] + public void Constructor_WhitespaceOnlyName_Throws() + { + Assert.ThrowsExactly(() => new Command(CommandId.Create("edit.copy"), CommandName.Create(" "))); + } + + [TestMethod] + public void Constructor_PaddedValues_MatchStringConstructedCommand() + { + Command expected = new("edit.copy", " Copy ", " ", "Editing "); + Command actual = CreatePaddedCommand(); + + Assert.AreEqual(expected.Name.ToString(), actual.Name.ToString()); + Assert.AreEqual(expected.Description?.ToString(), actual.Description?.ToString()); + Assert.AreEqual(expected.Category?.ToString(), actual.Category?.ToString()); + } + + [TestMethod] + public void GetCommandsByCategory_PaddedSemanticCategory_FindsCommandWithoutReload() + { + CommandRegistry registry = new(); + Assert.IsTrue(registry.RegisterCommand(CreatePaddedCommand())); + + Assert.AreEqual(1, registry.GetCommandsByCategory("Editing").Count); + } +} diff --git a/Keybinding/Models/Command.cs b/Keybinding/Models/Command.cs index 6d3dc04..8a94203 100644 --- a/Keybinding/Models/Command.cs +++ b/Keybinding/Models/Command.cs @@ -33,9 +33,14 @@ public Command(CommandId id, CommandName name, CommandDescription? description = } Id = trimmedId == rawId ? id : CommandId.Create(trimmedId); - Name = name; - Description = description; - Category = category; + + // Normalize the rest the same way too, so a command reads the same before and after a save and reload. + string trimmedName = TrimToNull(name.ToString()) + ?? throw new ArgumentException("Command name cannot be null or whitespace", nameof(name)); + + Name = trimmedName == name.ToString() ? name : CommandName.Create(trimmedName); + Description = TrimToNull(description?.ToString()) is string trimmedDescription ? CommandDescription.Create(trimmedDescription) : null; + Category = TrimToNull(category?.ToString()) is string trimmedCategory ? CommandCategory.Create(trimmedCategory) : null; } /// @@ -60,10 +65,13 @@ public Command(string id, string name, string? description = null, string? categ Id = CommandId.Create(id.Trim()); Name = CommandName.Create(name.Trim()); - Description = !string.IsNullOrWhiteSpace(description) ? CommandDescription.Create(description.Trim()) : null; - Category = !string.IsNullOrWhiteSpace(category) ? CommandCategory.Create(category.Trim()) : null; + Description = TrimToNull(description) is string trimmedDescription ? CommandDescription.Create(trimmedDescription) : null; + Category = TrimToNull(category) is string trimmedCategory ? CommandCategory.Create(trimmedCategory) : null; } + private static string? TrimToNull(string? value) => + string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + /// /// Gets the unique identifier for the command ///