diff --git a/docs b/docs index 7f3750772..09132cdc0 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 7f3750772ffb06fc40598feb5c7cff890b5668b3 +Subproject commit 09132cdc016d3316795e03e19709f9f6b25a273e diff --git a/src/.editorconfig b/src/.editorconfig index 7932f011a..90040b38c 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -85,22 +85,24 @@ dotnet_diagnostic.RCS1168.severity = warning # Parameter name 'foo' differs from dotnet_diagnostic.RCS1175.severity = warning # Unused 'this' parameter 'operation'. dotnet_diagnostic.RCS1192.severity = warning # Unnecessary usage of verbatim string literal. dotnet_diagnostic.RCS1194.severity = warning # Implement exception constructors. -dotnet_diagnostic.RCS1211.severity = warning # Remove unnecessary else clause. +dotnet_diagnostic.RCS1211.severity = none # Remove unnecessary else clause. dotnet_diagnostic.RCS1214.severity = warning # Unnecessary interpolated string. dotnet_diagnostic.RCS1225.severity = warning # Make class sealed. dotnet_diagnostic.RCS1232.severity = warning # Order elements in documentation comment. # Diagnostics elevated as warnings dotnet_diagnostic.CA1000.severity = warning # Do not declare static members on generic types -dotnet_diagnostic.CA1031.severity = warning # Do not catch general exception types +dotnet_diagnostic.CA1031.severity = none # Do not catch general exception types dotnet_diagnostic.CA1050.severity = warning # Declare types in namespaces dotnet_diagnostic.CA1063.severity = warning # Implement IDisposable correctly dotnet_diagnostic.CA1064.severity = warning # Exceptions should be public dotnet_diagnostic.CA1303.severity = warning # Do not pass literals as localized parameters +dotnet_diagnostic.CA1307.severity = none # StringComparison parameter - using default culture comparison is intentional for query parsing +dotnet_diagnostic.CA1308.severity = none # Case-insensitive string comparisons are explicitly required by design dotnet_diagnostic.CA1416.severity = warning # Validate platform compatibility dotnet_diagnostic.CA1508.severity = warning # Avoid dead conditional code dotnet_diagnostic.CA1852.severity = warning # Sealed classes -dotnet_diagnostic.CA1859.severity = warning # Use concrete types when possible for improved performance +dotnet_diagnostic.CA1859.severity = none # Use concrete types when possible for improved performance dotnet_diagnostic.CA1860.severity = warning # Prefer comparing 'Count' to 0 rather than using 'Any()', both for clarity and for performance dotnet_diagnostic.CA2000.severity = warning # Call System.IDisposable.Dispose on object before all references to it are out of scope dotnet_diagnostic.CA2007.severity = error # Do not directly await a Task @@ -132,7 +134,7 @@ dotnet_diagnostic.IDE0161.severity = warning # Use file-scoped namespace dotnet_diagnostic.RCS1032.severity = warning # Remove redundant parentheses. dotnet_diagnostic.RCS1118.severity = warning # Mark local variable as const. -dotnet_diagnostic.RCS1141.severity = warning # Add 'param' element to documentation comment. +dotnet_diagnostic.RCS1141.severity = none # Add 'param' element to documentation comment. dotnet_diagnostic.RCS1197.severity = warning # Optimize StringBuilder.AppendLine call. dotnet_diagnostic.RCS1205.severity = warning # Order named arguments according to the order of parameters. dotnet_diagnostic.RCS1229.severity = warning # Use async/await when necessary. diff --git a/src/Core/.editorconfig b/src/Core/.editorconfig deleted file mode 100644 index 39796f417..000000000 --- a/src/Core/.editorconfig +++ /dev/null @@ -1,10 +0,0 @@ -# Temporary configuration to allow development progress -# TODO: Fix all RCS1141 violations before final PR - -[*.cs] - -# RCS1141: Missing param documentation - reduce to suggestion during development -dotnet_diagnostic.RCS1141.severity = suggestion - -# RCS1211: Unnecessary else - reduce to suggestion -dotnet_diagnostic.RCS1211.severity = suggestion diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index ccf00caf6..2a3b66727 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -17,4 +17,8 @@ + + + + diff --git a/src/Core/GlobalSuppressions.cs b/src/Core/GlobalSuppressions.cs deleted file mode 100644 index 4a9c1f34d..000000000 --- a/src/Core/GlobalSuppressions.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; - -// CA1308: Case-insensitive string comparisons are explicitly required by design (Q7 in requirements) -// All field names and string values must be case-insensitive per specification -[assembly: SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Case-insensitive comparisons are required by design specification (Q7)", Scope = "namespaceanddescendants", Target = "~N:KernelMemory.Core.Search")] - -// CA1307: StringComparison parameter - using default culture comparison is intentional for query parsing -[assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison for clarity", Justification = "Default culture comparison is correct for field path checks", Scope = "member", Target = "~M:KernelMemory.Core.Search.Query.QueryLinqBuilder.GetFieldExpression(KernelMemory.Core.Search.Query.Ast.FieldNode)~System.Linq.Expressions.Expression")] - -// CA1305: Culture-specific ToString - using invariant culture would be correct, but this is for diagnostic output -[assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Diagnostic output, invariant culture would be better but not critical", Scope = "member", Target = "~M:KernelMemory.Core.Search.Query.Parsers.MongoJsonQueryParser.ParseArrayValue(System.Text.Json.JsonElement)~KernelMemory.Core.Search.Query.Ast.LiteralNode")] - -// CA1031: Catch general exception in query validation - intentional to provide user-friendly error messages -[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Query validation should handle all exceptions gracefully", Scope = "member", Target = "~M:KernelMemory.Core.Search.SearchService.ValidateQueryAsync(System.String,System.Threading.CancellationToken)~System.Threading.Tasks.Task{KernelMemory.Core.Search.Models.QueryValidationResult}")] - -// CA1859: Return type specificity - keeping base type for flexibility in visitor pattern -[assembly: SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance", Justification = "Visitor pattern requires base type returns for flexibility", Scope = "namespaceanddescendants", Target = "~N:KernelMemory.Core.Search.Query")] diff --git a/src/Core/Search/Query/Parsers/MongoJsonQueryParser.cs b/src/Core/Search/Query/Parsers/MongoJsonQueryParser.cs index 60560dd34..5ac7cb731 100644 --- a/src/Core/Search/Query/Parsers/MongoJsonQueryParser.cs +++ b/src/Core/Search/Query/Parsers/MongoJsonQueryParser.cs @@ -1,4 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. + +using System.Globalization; using System.Text.Json; using KernelMemory.Core.Search.Query.Ast; @@ -320,7 +322,7 @@ private LiteralNode ParseArrayValue(JsonElement element) } else if (item.ValueKind == JsonValueKind.Number) { - items.Add(item.GetDouble().ToString()); + items.Add(item.GetDouble().ToString(CultureInfo.CurrentCulture)); } else { diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 71b3fc761..1fcfc1abc 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -26,10 +26,10 @@ - - + + - + diff --git a/src/Main/CLI/CliApplicationBuilder.cs b/src/Main/CLI/CliApplicationBuilder.cs index 16f9f158e..bfa8eded8 100644 --- a/src/Main/CLI/CliApplicationBuilder.cs +++ b/src/Main/CLI/CliApplicationBuilder.cs @@ -48,23 +48,23 @@ public sealed class CliApplicationBuilder public CommandApp Build(string[]? args = null) { // 1. Determine config path from args early (before command execution) - var configPath = this.DetermineConfigPath(args ?? Array.Empty()); + string configPath = this.DetermineConfigPath(args ?? []); // 2. Load config ONCE (happens before any command runs) - var config = ConfigParser.LoadFromFile(configPath); + AppConfig config = ConfigParser.LoadFromFile(configPath); // 3. Create DI container and register AppConfig as singleton - var services = new ServiceCollection(); + ServiceCollection services = new(); services.AddSingleton(config); // Also register the config path so commands can access it services.AddSingleton(new ConfigPathService(configPath)); // 4. Create type registrar for Spectre.Console.Cli DI integration - var registrar = new TypeRegistrar(services); + TypeRegistrar registrar = new(services); // 5. Build CommandApp with DI support - var app = new CommandApp(registrar); + CommandApp app = new(registrar); this.Configure(app); return app; } @@ -162,4 +162,4 @@ public void Configure(CommandApp app) config.ValidateExamples(); }); } -} +} \ No newline at end of file diff --git a/src/Main/CLI/Commands/BaseCommand.cs b/src/Main/CLI/Commands/BaseCommand.cs index 96d76337f..d7bf1aad1 100644 --- a/src/Main/CLI/Commands/BaseCommand.cs +++ b/src/Main/CLI/Commands/BaseCommand.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. + using System.Diagnostics.CodeAnalysis; using KernelMemory.Core.Config; using KernelMemory.Core.Config.ContentIndex; @@ -128,7 +129,7 @@ protected ContentService CreateContentService(NodeConfig node, bool readonlyMode builder.AddConsole(); builder.SetMinimumLevel(LogLevel.Warning); }); - var searchIndexes = Services.SearchIndexFactory.CreateIndexes(node.SearchIndexes, loggerFactory); + var searchIndexes = SearchIndexFactory.CreateIndexes(node.SearchIndexes, loggerFactory); // Create storage service with search indexes var storage = new ContentStorageService(context, cuidGenerator, logger, searchIndexes); diff --git a/src/Main/CLI/Commands/ConfigCommand.cs b/src/Main/CLI/Commands/ConfigCommand.cs index 97e66705a..dddd2366a 100644 --- a/src/Main/CLI/Commands/ConfigCommand.cs +++ b/src/Main/CLI/Commands/ConfigCommand.cs @@ -1,9 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. + using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Serialization; +using KernelMemory.Core.Config; using KernelMemory.Main.CLI.Infrastructure; using KernelMemory.Main.CLI.Models; +using KernelMemory.Main.CLI.OutputFormatters; using Spectre.Console; using Spectre.Console.Cli; @@ -36,7 +40,7 @@ public class ConfigCommand : BaseCommand { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; private readonly ConfigPathService _configPathService; @@ -47,7 +51,7 @@ public class ConfigCommand : BaseCommand /// Application configuration (injected by DI). /// Service providing the config file path (injected by DI). public ConfigCommand( - KernelMemory.Core.Config.AppConfig config, + AppConfig config, ConfigPathService configPathService) : base(config) { this._configPathService = configPathService ?? throw new ArgumentNullException(nameof(configPathService)); @@ -55,7 +59,7 @@ public ConfigCommand( [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Top-level command handler must catch all exceptions to return appropriate exit codes and error messages")] - public override async Task ExecuteAsync( + public async Task ExecuteAsync( CommandContext context, ConfigCommandSettings settings) { @@ -63,7 +67,7 @@ public override async Task ExecuteAsync( { // ConfigCommand doesn't need node selection - it queries the entire configuration // So we skip Initialize() and just use the injected config directly - var formatter = CLI.OutputFormatters.OutputFormatterFactory.Create(settings); + var formatter = OutputFormatterFactory.Create(settings); var configPath = this._configPathService.Path; var configFileExists = File.Exists(configPath); @@ -128,11 +132,16 @@ public override async Task ExecuteAsync( } catch (Exception ex) { - var formatter = CLI.OutputFormatters.OutputFormatterFactory.Create(settings); + var formatter = OutputFormatterFactory.Create(settings); return this.HandleError(ex, formatter); } } + public override Task ExecuteAsync(CommandContext context, ConfigCommandSettings settings, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + /// /// Handles the --create flag to write the configuration to disk. /// @@ -142,7 +151,7 @@ public override async Task ExecuteAsync( /// Exit code. [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Config creation must catch all exceptions to return appropriate exit codes")] - private int HandleCreateConfig(string configPath, bool configFileExists, CLI.OutputFormatters.IOutputFormatter formatter) + private int HandleCreateConfig(string configPath, bool configFileExists, IOutputFormatter formatter) { try { diff --git a/src/Main/CLI/Commands/DeleteCommand.cs b/src/Main/CLI/Commands/DeleteCommand.cs index f48ab0f8d..7e764fc79 100644 --- a/src/Main/CLI/Commands/DeleteCommand.cs +++ b/src/Main/CLI/Commands/DeleteCommand.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. + using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; +using KernelMemory.Core.Config; +using KernelMemory.Main.CLI.OutputFormatters; using Spectre.Console; using Spectre.Console.Cli; @@ -41,15 +43,14 @@ public class DeleteCommand : BaseCommand /// Initializes a new instance of the class. /// /// Application configuration (injected by DI). - public DeleteCommand(KernelMemory.Core.Config.AppConfig config) : base(config) + public DeleteCommand(AppConfig config) : base(config) { } - [SuppressMessage("Design", "CA1031:Do not catch general exception types", - Justification = "Top-level command handler must catch all exceptions to return appropriate exit codes and error messages")] public override async Task ExecuteAsync( CommandContext context, - DeleteCommandSettings settings) + DeleteCommandSettings settings, + CancellationToken cancellationToken) { try { @@ -79,7 +80,7 @@ public override async Task ExecuteAsync( } catch (Exception ex) { - var formatter = CLI.OutputFormatters.OutputFormatterFactory.Create(settings); + var formatter = OutputFormatterFactory.Create(settings); return this.HandleError(ex, formatter); } } diff --git a/src/Main/CLI/Commands/ExamplesCommand.cs b/src/Main/CLI/Commands/ExamplesCommand.cs index 4b20208a9..0554586a7 100644 --- a/src/Main/CLI/Commands/ExamplesCommand.cs +++ b/src/Main/CLI/Commands/ExamplesCommand.cs @@ -22,7 +22,7 @@ public sealed class Settings : CommandSettings } /// - public override int Execute(CommandContext context, Settings settings) + public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken) { if (!string.IsNullOrEmpty(settings.Command)) { diff --git a/src/Main/CLI/Commands/GetCommand.cs b/src/Main/CLI/Commands/GetCommand.cs index 393080e7d..092cd81cd 100644 --- a/src/Main/CLI/Commands/GetCommand.cs +++ b/src/Main/CLI/Commands/GetCommand.cs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. + using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; +using KernelMemory.Core.Config; +using KernelMemory.Core.Storage.Models; using KernelMemory.Main.CLI.Exceptions; +using KernelMemory.Main.CLI.OutputFormatters; using Spectre.Console; using Spectre.Console.Cli; @@ -46,15 +49,14 @@ public class GetCommand : BaseCommand /// Initializes a new instance of the class. /// /// Application configuration (injected by DI). - public GetCommand(KernelMemory.Core.Config.AppConfig config) : base(config) + public GetCommand(AppConfig config) : base(config) { } - [SuppressMessage("Design", "CA1031:Do not catch general exception types", - Justification = "Top-level command handler must catch all exceptions to return appropriate exit codes and error messages")] public override async Task ExecuteAsync( CommandContext context, - GetCommandSettings settings) + GetCommandSettings settings, + CancellationToken cancellationToken) { try { @@ -70,7 +72,7 @@ public override async Task ExecuteAsync( } // Wrap result with node information - var response = Core.Storage.Models.ContentDtoWithNode.FromContentDto(result, node.Id); + var response = ContentDtoWithNode.FromContentDto(result, node.Id); formatter.Format(response); @@ -84,7 +86,7 @@ public override async Task ExecuteAsync( } catch (Exception ex) { - var formatter = CLI.OutputFormatters.OutputFormatterFactory.Create(settings); + var formatter = OutputFormatterFactory.Create(settings); return this.HandleError(ex, formatter); } } @@ -95,7 +97,7 @@ public override async Task ExecuteAsync( /// Command settings for output format. private void ShowFirstRunMessage(GetCommandSettings settings) { - var formatter = CLI.OutputFormatters.OutputFormatterFactory.Create(settings); + var formatter = OutputFormatterFactory.Create(settings); // For JSON/YAML, return null (valid, parseable output) if (!settings.Format.Equals("human", StringComparison.OrdinalIgnoreCase)) diff --git a/src/Main/CLI/Commands/ListCommand.cs b/src/Main/CLI/Commands/ListCommand.cs index 33b3b7694..4f9d57970 100644 --- a/src/Main/CLI/Commands/ListCommand.cs +++ b/src/Main/CLI/Commands/ListCommand.cs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. + using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; +using KernelMemory.Core.Config; +using KernelMemory.Core.Storage.Models; using KernelMemory.Main.CLI.Exceptions; +using KernelMemory.Main.CLI.OutputFormatters; using Spectre.Console; using Spectre.Console.Cli; @@ -53,15 +56,14 @@ public class ListCommand : BaseCommand /// Initializes a new instance of the class. /// /// Application configuration (injected by DI). - public ListCommand(KernelMemory.Core.Config.AppConfig config) : base(config) + public ListCommand(AppConfig config) : base(config) { } - [SuppressMessage("Design", "CA1031:Do not catch general exception types", - Justification = "Top-level command handler must catch all exceptions to return appropriate exit codes and error messages")] public override async Task ExecuteAsync( CommandContext context, - ListCommandSettings settings) + ListCommandSettings settings, + CancellationToken cancellationToken) { try { @@ -76,7 +78,7 @@ public override async Task ExecuteAsync( // Wrap items with node information var itemsWithNode = items.Select(item => - Core.Storage.Models.ContentDtoWithNode.FromContentDto(item, node.Id)); + ContentDtoWithNode.FromContentDto(item, node.Id)); // Format list with pagination info formatter.FormatList(itemsWithNode, totalCount, settings.Skip, settings.Take); @@ -91,7 +93,7 @@ public override async Task ExecuteAsync( } catch (Exception ex) { - var formatter = CLI.OutputFormatters.OutputFormatterFactory.Create(settings); + var formatter = OutputFormatterFactory.Create(settings); return this.HandleError(ex, formatter); } } @@ -102,12 +104,12 @@ public override async Task ExecuteAsync( /// Command settings for output format. private void ShowFirstRunMessage(ListCommandSettings settings) { - var formatter = CLI.OutputFormatters.OutputFormatterFactory.Create(settings); + var formatter = OutputFormatterFactory.Create(settings); // For JSON/YAML, return empty list (valid, parseable output) if (!settings.Format.Equals("human", StringComparison.OrdinalIgnoreCase)) { - formatter.FormatList(Array.Empty(), 0, 0, settings.Take); + formatter.FormatList(Array.Empty(), 0, 0, settings.Take); return; } diff --git a/src/Main/CLI/Commands/NodesCommand.cs b/src/Main/CLI/Commands/NodesCommand.cs index 9a653155d..4d858f45f 100644 --- a/src/Main/CLI/Commands/NodesCommand.cs +++ b/src/Main/CLI/Commands/NodesCommand.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics.CodeAnalysis; + +using KernelMemory.Core.Config; +using KernelMemory.Main.CLI.OutputFormatters; using Spectre.Console.Cli; namespace KernelMemory.Main.CLI.Commands; @@ -20,15 +22,14 @@ public class NodesCommand : BaseCommand /// Initializes a new instance of the class. /// /// Application configuration (injected by DI). - public NodesCommand(KernelMemory.Core.Config.AppConfig config) : base(config) + public NodesCommand(AppConfig config) : base(config) { } - [SuppressMessage("Design", "CA1031:Do not catch general exception types", - Justification = "Top-level command handler must catch all exceptions to return appropriate exit codes and error messages")] public override async Task ExecuteAsync( CommandContext context, - NodesCommandSettings settings) + NodesCommandSettings settings, + CancellationToken cancellationToken) { try { @@ -45,7 +46,7 @@ public override async Task ExecuteAsync( } catch (Exception ex) { - var formatter = CLI.OutputFormatters.OutputFormatterFactory.Create(settings); + var formatter = OutputFormatterFactory.Create(settings); return this.HandleError(ex, formatter); } } diff --git a/src/Main/CLI/Commands/SearchCommand.cs b/src/Main/CLI/Commands/SearchCommand.cs index d08705d7d..748c18d80 100644 --- a/src/Main/CLI/Commands/SearchCommand.cs +++ b/src/Main/CLI/Commands/SearchCommand.cs @@ -1,9 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. + using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using KernelMemory.Core.Config; using KernelMemory.Core.Search; +using KernelMemory.Core.Search.Exceptions; using KernelMemory.Core.Search.Models; using KernelMemory.Main.CLI.Exceptions; +using KernelMemory.Main.CLI.OutputFormatters; using Spectre.Console; using Spectre.Console.Cli; @@ -140,19 +144,18 @@ public class SearchCommand : BaseCommand /// Initializes a new instance of the class. /// /// Application configuration (injected by DI). - public SearchCommand(KernelMemory.Core.Config.AppConfig config) : base(config) + public SearchCommand(AppConfig config) : base(config) { } - [SuppressMessage("Design", "CA1031:Do not catch general exception types", - Justification = "Top-level command handler must catch all exceptions to return appropriate exit codes and error messages")] public override async Task ExecuteAsync( CommandContext context, - SearchCommandSettings settings) + SearchCommandSettings settings, + CancellationToken cancellationToken) { try { - var formatter = CLI.OutputFormatters.OutputFormatterFactory.Create(settings); + var formatter = OutputFormatterFactory.Create(settings); // Create search service var searchService = this.CreateSearchService(); @@ -180,15 +183,15 @@ public override async Task ExecuteAsync( this.ShowFirstRunMessage(settings); return Constants.ExitCodeSuccess; // Not a user error } - catch (Core.Search.Exceptions.SearchException ex) + catch (SearchException ex) { - var formatter = CLI.OutputFormatters.OutputFormatterFactory.Create(settings); + var formatter = OutputFormatterFactory.Create(settings); formatter.FormatError($"Search error: {ex.Message}"); return Constants.ExitCodeUserError; } catch (Exception ex) { - var formatter = CLI.OutputFormatters.OutputFormatterFactory.Create(settings); + var formatter = OutputFormatterFactory.Create(settings); return this.HandleError(ex, formatter); } } @@ -200,12 +203,12 @@ public override async Task ExecuteAsync( /// The command settings. /// The output formatter. /// Exit code (0 for valid, 1 for invalid). - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance", + [SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance", Justification = "Using interface provides flexibility for testing and future implementations")] private async Task ValidateQueryAsync( ISearchService searchService, SearchCommandSettings settings, - CLI.OutputFormatters.IOutputFormatter formatter) + IOutputFormatter formatter) { var result = await searchService.ValidateQueryAsync(settings.Query, CancellationToken.None).ConfigureAwait(false); @@ -357,7 +360,7 @@ private Dictionary ParseNodeWeights(string nodeWeights) private void FormatSearchResults( SearchResponse response, SearchCommandSettings settings, - CLI.OutputFormatters.IOutputFormatter formatter) + IOutputFormatter formatter) { if (settings.Format.Equals("json", StringComparison.OrdinalIgnoreCase)) { @@ -447,7 +450,7 @@ private void FormatSearchResultsHuman(SearchResponse response, SearchCommandSett /// Creates a SearchService instance with all configured nodes. /// /// A configured SearchService. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "ContentService instances must remain alive for the duration of the search operation. CLI commands are short-lived and process exit handles cleanup.")] private SearchService CreateSearchService() { @@ -486,7 +489,7 @@ private SearchService CreateSearchService() /// The command settings. private void ShowFirstRunMessage(SearchCommandSettings settings) { - var formatter = CLI.OutputFormatters.OutputFormatterFactory.Create(settings); + var formatter = OutputFormatterFactory.Create(settings); if (!settings.Format.Equals("human", StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Main/CLI/Commands/UpsertCommand.cs b/src/Main/CLI/Commands/UpsertCommand.cs index 2b82ab7c7..c83a1b2e7 100644 --- a/src/Main/CLI/Commands/UpsertCommand.cs +++ b/src/Main/CLI/Commands/UpsertCommand.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. + using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; +using KernelMemory.Core.Config; using KernelMemory.Core.Storage.Models; +using KernelMemory.Main.CLI.OutputFormatters; using Spectre.Console; using Spectre.Console.Cli; @@ -63,15 +65,14 @@ public class UpsertCommand : BaseCommand /// Initializes a new instance of the class. /// /// Application configuration (injected by DI). - public UpsertCommand(KernelMemory.Core.Config.AppConfig config) : base(config) + public UpsertCommand(AppConfig config) : base(config) { } - [SuppressMessage("Design", "CA1031:Do not catch general exception types", - Justification = "Top-level command handler must catch all exceptions to return appropriate exit codes and error messages")] public override async Task ExecuteAsync( CommandContext context, - UpsertCommandSettings settings) + UpsertCommandSettings settings, + CancellationToken cancellationToken) { try { @@ -119,7 +120,7 @@ public override async Task ExecuteAsync( } catch (Exception ex) { - var formatter = CLI.OutputFormatters.OutputFormatterFactory.Create(settings); + var formatter = OutputFormatterFactory.Create(settings); return this.HandleError(ex, formatter); } } diff --git a/tests/Main.Tests/Integration/CliIntegrationTests.cs b/tests/Main.Tests/Integration/CliIntegrationTests.cs index 194fcc7f7..1a87eaa4b 100644 --- a/tests/Main.Tests/Integration/CliIntegrationTests.cs +++ b/tests/Main.Tests/Integration/CliIntegrationTests.cs @@ -74,7 +74,7 @@ public async Task UpsertCommand_WithMinimalOptions_CreatesContent() var context = CreateTestContext("put"); // Act - var exitCode = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var exitCode = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); @@ -99,7 +99,7 @@ public async Task UpsertCommand_WithCustomId_UsesProvidedId() var context = CreateTestContext("put"); // Act - var exitCode = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var exitCode = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); @@ -113,7 +113,7 @@ public async Task UpsertCommand_WithCustomId_UsesProvidedId() }; var getCommand = new GetCommand(config); - var getExitCode = await getCommand.ExecuteAsync(context, getSettings).ConfigureAwait(false); + var getExitCode = await getCommand.ExecuteAsync(context, getSettings, CancellationToken.None).ConfigureAwait(false); Assert.Equal(Constants.ExitCodeSuccess, getExitCode); } @@ -137,7 +137,7 @@ public async Task UpsertCommand_WithAllMetadata_StoresAllFields() var context = CreateTestContext("put"); // Act - var exitCode = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var exitCode = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); @@ -159,7 +159,7 @@ public async Task GetCommand_ExistingId_ReturnsContent() var upsertCommand = new UpsertCommand(config); var context = CreateTestContext("put"); - await upsertCommand.ExecuteAsync(context, upsertSettings).ConfigureAwait(false); + await upsertCommand.ExecuteAsync(context, upsertSettings, CancellationToken.None).ConfigureAwait(false); // Act - Get the content var getSettings = new GetCommandSettings @@ -170,7 +170,7 @@ public async Task GetCommand_ExistingId_ReturnsContent() }; var getCommand = new GetCommand(config); - var exitCode = await getCommand.ExecuteAsync(context, getSettings).ConfigureAwait(false); + var exitCode = await getCommand.ExecuteAsync(context, getSettings, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); @@ -190,7 +190,7 @@ public async Task GetCommand_NonExistentId_ReturnsUserError() Content = "Some content to create the DB" }; var upsertCommand = new UpsertCommand(config); - await upsertCommand.ExecuteAsync(CreateTestContext("put"), upsertSettings).ConfigureAwait(false); + await upsertCommand.ExecuteAsync(CreateTestContext("put"), upsertSettings, CancellationToken.None).ConfigureAwait(false); // Now try to get non-existent ID from existing DB var settings = new GetCommandSettings @@ -204,7 +204,7 @@ public async Task GetCommand_NonExistentId_ReturnsUserError() var context = CreateTestContext("get"); // Act - var exitCode = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var exitCode = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); // Assert - ID not found in existing DB is user error Assert.Equal(Constants.ExitCodeUserError, exitCode); @@ -227,7 +227,7 @@ public async Task GetCommand_WithFullFlag_ReturnsAllDetails() var upsertCommand = new UpsertCommand(config); var context = CreateTestContext("put"); - await upsertCommand.ExecuteAsync(context, upsertSettings).ConfigureAwait(false); + await upsertCommand.ExecuteAsync(context, upsertSettings, CancellationToken.None).ConfigureAwait(false); // Act - Get with full flag var getSettings = new GetCommandSettings @@ -239,7 +239,7 @@ public async Task GetCommand_WithFullFlag_ReturnsAllDetails() }; var getCommand = new GetCommand(config); - var exitCode = await getCommand.ExecuteAsync(context, getSettings).ConfigureAwait(false); + var exitCode = await getCommand.ExecuteAsync(context, getSettings, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); @@ -259,7 +259,7 @@ public async Task ListCommand_EmptyDatabase_ReturnsEmptyList() }; var upsertCommand = new UpsertCommand(config); var context = CreateTestContext("put"); - await upsertCommand.ExecuteAsync(context, upsertSettings).ConfigureAwait(false); + await upsertCommand.ExecuteAsync(context, upsertSettings, CancellationToken.None).ConfigureAwait(false); // Delete the content to have empty database var deleteSettings = new DeleteCommandSettings @@ -269,7 +269,7 @@ public async Task ListCommand_EmptyDatabase_ReturnsEmptyList() Id = "temp-id" }; var deleteCommand = new DeleteCommand(config); - await deleteCommand.ExecuteAsync(context, deleteSettings).ConfigureAwait(false); + await deleteCommand.ExecuteAsync(context, deleteSettings, CancellationToken.None).ConfigureAwait(false); // Now test list on empty database var settings = new ListCommandSettings @@ -282,7 +282,7 @@ public async Task ListCommand_EmptyDatabase_ReturnsEmptyList() var listContext = CreateTestContext("list"); // Act - var exitCode = await command.ExecuteAsync(listContext, settings).ConfigureAwait(false); + var exitCode = await command.ExecuteAsync(listContext, settings, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); @@ -306,7 +306,7 @@ public async Task Bug3_ListCommand_EmptyDatabase_HumanFormat_ShouldHandleGracefu }; var upsertCommand = new UpsertCommand(config); var context = CreateTestContext("put"); - await upsertCommand.ExecuteAsync(context, upsertSettings).ConfigureAwait(false); + await upsertCommand.ExecuteAsync(context, upsertSettings, CancellationToken.None).ConfigureAwait(false); // Delete the content to have empty database var deleteSettings = new DeleteCommandSettings @@ -316,7 +316,7 @@ public async Task Bug3_ListCommand_EmptyDatabase_HumanFormat_ShouldHandleGracefu Id = "temp-id-human" }; var deleteCommand = new DeleteCommand(config); - await deleteCommand.ExecuteAsync(context, deleteSettings).ConfigureAwait(false); + await deleteCommand.ExecuteAsync(context, deleteSettings, CancellationToken.None).ConfigureAwait(false); // Now test list on empty database with human format var settings = new ListCommandSettings @@ -329,7 +329,7 @@ public async Task Bug3_ListCommand_EmptyDatabase_HumanFormat_ShouldHandleGracefu var listContext = CreateTestContext("list"); // Act - var exitCode = await command.ExecuteAsync(listContext, settings).ConfigureAwait(false); + var exitCode = await command.ExecuteAsync(listContext, settings, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); @@ -351,7 +351,7 @@ public async Task ListCommand_WithContent_ReturnsList() var upsertCommand = new UpsertCommand(config); var context = CreateTestContext("put"); - await upsertCommand.ExecuteAsync(context, upsertSettings).ConfigureAwait(false); + await upsertCommand.ExecuteAsync(context, upsertSettings, CancellationToken.None).ConfigureAwait(false); // Act - List content var listSettings = new ListCommandSettings @@ -361,7 +361,7 @@ public async Task ListCommand_WithContent_ReturnsList() }; var listCommand = new ListCommand(config); - var exitCode = await listCommand.ExecuteAsync(context, listSettings).ConfigureAwait(false); + var exitCode = await listCommand.ExecuteAsync(context, listSettings, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); @@ -383,7 +383,7 @@ public async Task ListCommand_WithPagination_RespectsSkipAndTake() Format = "json", Content = $"Content {i}" }; - await upsertCommand.ExecuteAsync(context, upsertSettings).ConfigureAwait(false); + await upsertCommand.ExecuteAsync(context, upsertSettings, CancellationToken.None).ConfigureAwait(false); } // Act - List with pagination @@ -396,7 +396,7 @@ public async Task ListCommand_WithPagination_RespectsSkipAndTake() }; var listCommand = new ListCommand(config); - var exitCode = await listCommand.ExecuteAsync(context, listSettings).ConfigureAwait(false); + var exitCode = await listCommand.ExecuteAsync(context, listSettings, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); @@ -418,7 +418,7 @@ public async Task DeleteCommand_ExistingId_DeletesSuccessfully() var upsertCommand = new UpsertCommand(config); var context = CreateTestContext("put"); - await upsertCommand.ExecuteAsync(context, upsertSettings).ConfigureAwait(false); + await upsertCommand.ExecuteAsync(context, upsertSettings, CancellationToken.None).ConfigureAwait(false); // Act - Delete the content var deleteSettings = new DeleteCommandSettings @@ -429,7 +429,7 @@ public async Task DeleteCommand_ExistingId_DeletesSuccessfully() }; var deleteCommand = new DeleteCommand(config); - var exitCode = await deleteCommand.ExecuteAsync(context, deleteSettings).ConfigureAwait(false); + var exitCode = await deleteCommand.ExecuteAsync(context, deleteSettings, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); @@ -443,7 +443,7 @@ public async Task DeleteCommand_ExistingId_DeletesSuccessfully() }; var getCommand = new GetCommand(config); - var getExitCode = await getCommand.ExecuteAsync(context, getSettings).ConfigureAwait(false); + var getExitCode = await getCommand.ExecuteAsync(context, getSettings, CancellationToken.None).ConfigureAwait(false); Assert.Equal(Constants.ExitCodeUserError, getExitCode); } @@ -463,7 +463,7 @@ public async Task DeleteCommand_WithQuietVerbosity_SucceedsWithMinimalOutput() var upsertCommand = new UpsertCommand(config); var context = CreateTestContext("put"); - await upsertCommand.ExecuteAsync(context, upsertSettings).ConfigureAwait(false); + await upsertCommand.ExecuteAsync(context, upsertSettings, CancellationToken.None).ConfigureAwait(false); // Act - Delete with quiet verbosity var deleteSettings = new DeleteCommandSettings @@ -475,7 +475,7 @@ public async Task DeleteCommand_WithQuietVerbosity_SucceedsWithMinimalOutput() }; var deleteCommand = new DeleteCommand(config); - var exitCode = await deleteCommand.ExecuteAsync(context, deleteSettings).ConfigureAwait(false); + var exitCode = await deleteCommand.ExecuteAsync(context, deleteSettings, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); @@ -499,7 +499,7 @@ public async Task EndToEndWorkflow_UpsertGetListDelete_AllSucceed() Tags = "e2e,test" }; var upsertCommand = new UpsertCommand(config); - var upsertExitCode = await upsertCommand.ExecuteAsync(context, upsertSettings).ConfigureAwait(false); + var upsertExitCode = await upsertCommand.ExecuteAsync(context, upsertSettings, CancellationToken.None).ConfigureAwait(false); Assert.Equal(Constants.ExitCodeSuccess, upsertExitCode); // 2. Get @@ -510,7 +510,7 @@ public async Task EndToEndWorkflow_UpsertGetListDelete_AllSucceed() Id = testId }; var getCommand = new GetCommand(config); - var getExitCode = await getCommand.ExecuteAsync(context, getSettings).ConfigureAwait(false); + var getExitCode = await getCommand.ExecuteAsync(context, getSettings, CancellationToken.None).ConfigureAwait(false); Assert.Equal(Constants.ExitCodeSuccess, getExitCode); // 3. List @@ -520,7 +520,7 @@ public async Task EndToEndWorkflow_UpsertGetListDelete_AllSucceed() Format = "json" }; var listCommand = new ListCommand(config); - var listExitCode = await listCommand.ExecuteAsync(context, listSettings).ConfigureAwait(false); + var listExitCode = await listCommand.ExecuteAsync(context, listSettings, CancellationToken.None).ConfigureAwait(false); Assert.Equal(Constants.ExitCodeSuccess, listExitCode); // 4. Delete @@ -531,11 +531,11 @@ public async Task EndToEndWorkflow_UpsertGetListDelete_AllSucceed() Id = testId }; var deleteCommand = new DeleteCommand(config); - var deleteExitCode = await deleteCommand.ExecuteAsync(context, deleteSettings).ConfigureAwait(false); + var deleteExitCode = await deleteCommand.ExecuteAsync(context, deleteSettings, CancellationToken.None).ConfigureAwait(false); Assert.Equal(Constants.ExitCodeSuccess, deleteExitCode); // 5. Verify deleted - var verifyExitCode = await getCommand.ExecuteAsync(context, getSettings).ConfigureAwait(false); + var verifyExitCode = await getCommand.ExecuteAsync(context, getSettings, CancellationToken.None).ConfigureAwait(false); Assert.Equal(Constants.ExitCodeUserError, verifyExitCode); } @@ -554,7 +554,7 @@ public async Task NodesCommand_WithJsonFormat_ListsAllNodes() var context = CreateTestContext("nodes"); // Act - var exitCode = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var exitCode = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); @@ -575,7 +575,7 @@ public async Task NodesCommand_WithYamlFormat_ListsAllNodes() var context = CreateTestContext("nodes"); // Act - var exitCode = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var exitCode = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); @@ -712,7 +712,7 @@ public async Task Bug2_ConfigCommand_HumanFormat_ShouldNotLeakTypeNames() /// private sealed class EmptyRemainingArguments : IRemainingArguments { - public IReadOnlyList Raw => Array.Empty(); + public IReadOnlyList Raw => []; public ILookup Parsed => Enumerable.Empty().ToLookup(x => x, x => (string?)null); } } diff --git a/tests/Main.Tests/Integration/CommandExecutionTests.cs b/tests/Main.Tests/Integration/CommandExecutionTests.cs index a3183b1c3..ddf743678 100644 --- a/tests/Main.Tests/Integration/CommandExecutionTests.cs +++ b/tests/Main.Tests/Integration/CommandExecutionTests.cs @@ -67,7 +67,7 @@ public async Task UpsertCommand_WithValidContent_ReturnsSuccess() var command = new UpsertCommand(config); var context = new CommandContext(new[] { "--config", this._configPath }, new EmptyRemainingArguments(), "put", null); - var result = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var result = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); Assert.Equal(0, result); } @@ -85,7 +85,7 @@ public async Task GetCommand_WithNonExistentId_ReturnsError() }; var putCommand = new UpsertCommand(config); var putContext = new CommandContext(new[] { "--config", this._configPath }, new EmptyRemainingArguments(), "put", null); - await putCommand.ExecuteAsync(putContext, putSettings).ConfigureAwait(false); + await putCommand.ExecuteAsync(putContext, putSettings, CancellationToken.None).ConfigureAwait(false); // Now try to get non-existent ID from existing DB var settings = new GetCommandSettings @@ -96,7 +96,7 @@ public async Task GetCommand_WithNonExistentId_ReturnsError() var command = new GetCommand(config); var context = new CommandContext(new[] { "--config", this._configPath }, new EmptyRemainingArguments(), "get", null); - var result = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var result = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); Assert.Equal(1, result); // User error - ID not found in existing DB } @@ -115,7 +115,7 @@ public async Task DeleteCommand_WithNonExistentId_ReturnsSuccess() var command = new DeleteCommand(config); var context = new CommandContext(new[] { "--config", this._configPath }, new EmptyRemainingArguments(), "delete", null); - var result = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var result = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); Assert.Equal(0, result); // Success (idempotent) } @@ -133,7 +133,7 @@ public async Task ListCommand_WithEmptyDatabase_ReturnsSuccess() }; var putCommand = new UpsertCommand(config); var context = new CommandContext(new[] { "--config", this._configPath }, new EmptyRemainingArguments(), "put", null); - await putCommand.ExecuteAsync(context, putSettings).ConfigureAwait(false); + await putCommand.ExecuteAsync(context, putSettings, CancellationToken.None).ConfigureAwait(false); // Delete to make it empty var deleteSettings = new DeleteCommandSettings @@ -142,7 +142,7 @@ public async Task ListCommand_WithEmptyDatabase_ReturnsSuccess() Id = "temp-id" }; var deleteCommand = new DeleteCommand(config); - await deleteCommand.ExecuteAsync(context, deleteSettings).ConfigureAwait(false); + await deleteCommand.ExecuteAsync(context, deleteSettings, CancellationToken.None).ConfigureAwait(false); // Now test list on empty database var settings = new ListCommandSettings @@ -151,7 +151,7 @@ public async Task ListCommand_WithEmptyDatabase_ReturnsSuccess() }; var command = new ListCommand(config); - var result = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var result = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); Assert.Equal(0, result); } @@ -166,9 +166,9 @@ public async Task NodesCommand_WithValidConfig_ReturnsSuccess() ConfigPath = this._configPath }; var command = new NodesCommand(config); - var context = new CommandContext(new[] { "--config", this._configPath }, new EmptyRemainingArguments(), "nodes", null); + var context = new CommandContext(["--config", this._configPath], new EmptyRemainingArguments(), "nodes", null); - var result = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var result = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); Assert.Equal(0, result); } @@ -242,7 +242,7 @@ public async Task GetCommand_WithFullFlag_ReturnsSuccess() }; var putCommand = new UpsertCommand(config); var putContext = new CommandContext(new[] { "--config", this._configPath }, new EmptyRemainingArguments(), "put", null); - await putCommand.ExecuteAsync(putContext, putSettings).ConfigureAwait(false); + await putCommand.ExecuteAsync(putContext, putSettings, CancellationToken.None).ConfigureAwait(false); // Then get with full flag - will fail because we don't know the ID // But this still exercises the code path @@ -255,7 +255,7 @@ public async Task GetCommand_WithFullFlag_ReturnsSuccess() var getCommand = new GetCommand(config); var getContext = new CommandContext(new[] { "--config", this._configPath }, new EmptyRemainingArguments(), "get", null); - var result = await getCommand.ExecuteAsync(getContext, getSettings).ConfigureAwait(false); + var result = await getCommand.ExecuteAsync(getContext, getSettings, CancellationToken.None).ConfigureAwait(false); Assert.True(result >= 0); // Either success or user error } diff --git a/tests/Main.Tests/Integration/ReadonlyCommandTests.cs b/tests/Main.Tests/Integration/ReadonlyCommandTests.cs index b0ed87532..36c99012b 100644 --- a/tests/Main.Tests/Integration/ReadonlyCommandTests.cs +++ b/tests/Main.Tests/Integration/ReadonlyCommandTests.cs @@ -85,7 +85,7 @@ public async Task BugA_ListCommand_NonExistentDatabase_ShouldNotCreateDirectory( var context = CreateTestContext("list"); // Act - var exitCode = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var exitCode = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); // Assert - With friendly first-run UX, missing DB returns success (0) not error // The key is that it should NOT create any files/directories @@ -121,7 +121,7 @@ public async Task BugA_GetCommand_NonExistentDatabase_ShouldNotCreateDirectory() var context = CreateTestContext("get"); // Act - var exitCode = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var exitCode = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); // Assert - With friendly first-run UX, missing DB returns success (0) not error // The key is that it should NOT create any files/directories @@ -156,7 +156,7 @@ public async Task BugA_NodesCommand_NonExistentDatabase_ShouldNotCreateDirectory var context = CreateTestContext("nodes"); // Act - var exitCode = await command.ExecuteAsync(context, settings).ConfigureAwait(false); + var exitCode = await command.ExecuteAsync(context, settings, CancellationToken.None).ConfigureAwait(false); // Assert - This test SHOULD FAIL initially (reproducing the bug) // NodesCommand only reads config, shouldn't touch the database at all diff --git a/tests/Main.Tests/Integration/UserDataProtectionTests.cs b/tests/Main.Tests/Integration/UserDataProtectionTests.cs index 63cf4a540..385768e46 100644 --- a/tests/Main.Tests/Integration/UserDataProtectionTests.cs +++ b/tests/Main.Tests/Integration/UserDataProtectionTests.cs @@ -98,7 +98,7 @@ public async Task CriticalBug_CommandExecutionTests_MustNotTouchUserData() // Act - This WILL write to ~/.km if bug exists try { - await command.ExecuteAsync(context, settingsWithoutConfigPath).ConfigureAwait(false); + await command.ExecuteAsync(context, settingsWithoutConfigPath, CancellationToken.None).ConfigureAwait(false); } catch (InvalidOperationException) { @@ -154,7 +154,7 @@ public async Task Fixed_SettingsWithConfigPath_MustUseTestDirectory() null); // Act - var exitCode = await command.ExecuteAsync(context, settingsWithConfigPath).ConfigureAwait(false); + var exitCode = await command.ExecuteAsync(context, settingsWithConfigPath, CancellationToken.None).ConfigureAwait(false); // Assert Assert.Equal(Constants.ExitCodeSuccess, exitCode); diff --git a/tests/Main.Tests/Unit/Commands/BaseCommandTests.cs b/tests/Main.Tests/Unit/Commands/BaseCommandTests.cs index c7deb37df..9c145044c 100644 --- a/tests/Main.Tests/Unit/Commands/BaseCommandTests.cs +++ b/tests/Main.Tests/Unit/Commands/BaseCommandTests.cs @@ -3,6 +3,7 @@ using KernelMemory.Main.CLI.Commands; using KernelMemory.Main.CLI.OutputFormatters; using Moq; +using Spectre.Console.Cli; namespace KernelMemory.Main.Tests.Unit.Commands; @@ -84,7 +85,7 @@ public TestCommand() : base(CreateTestConfig()) { } - public override Task ExecuteAsync(Spectre.Console.Cli.CommandContext context, GlobalOptions settings) + public override Task ExecuteAsync(CommandContext context, GlobalOptions settings, CancellationToken cancellationToken) { throw new NotImplementedException(); }