From fb372006e993154e2dc6f4f2feba8ecc3abba2bb Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Tue, 20 Jan 2026 11:22:03 -0500 Subject: [PATCH 1/4] Add Git utility methods and refactor command handlers to use HandlerPayload - Introduced GitUtility class with methods to check for Git repositories and get the repository root. - Added HandlerPayload record to encapsulate command handler parameters. - Updated command handlers to utilize HandlerPayload for improved parameter management. - Enhanced ReportDocument to include ProjectName property for better report context. --- Utilities/GitUtility.cs | 51 ++++++++++++++ .../Plugins/CommandRegistration.cs | 2 +- .../Plugins/HandlerPayload.cs | 12 ++++ src/CodeMedic/Commands/RootCommandHandler.cs | 66 ++++++++++++++++--- src/CodeMedic/Models/Report/ReportDocument.cs | 5 ++ .../Plugins/BomAnalysis/BomAnalysisPlugin.cs | 6 +- .../HealthAnalysis/HealthAnalysisPlugin.cs | 7 +- .../VulnerabilityAnalysisPlugin.cs | 7 +- 8 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 Utilities/GitUtility.cs create mode 100644 src/CodeMedic.Abstractions/Plugins/HandlerPayload.cs diff --git a/Utilities/GitUtility.cs b/Utilities/GitUtility.cs new file mode 100644 index 0000000..5fa5fcd --- /dev/null +++ b/Utilities/GitUtility.cs @@ -0,0 +1,51 @@ +namespace CodeMedic.Utilities; + +/// +/// Utility class for Git-related operations. +/// +public static class GitUtility +{ + /// + /// Checks if the specified directory is a Git repository. + /// + /// The path to check. + /// True if the path is within a Git repository; otherwise, false. + public static bool IsGitRepository(string path) + { + var directory = new DirectoryInfo(path); + + while (directory != null) + { + var gitDir = Path.Combine(directory.FullName, ".git"); + if (Directory.Exists(gitDir) || File.Exists(gitDir)) + { + return true; + } + directory = directory.Parent; + } + + return false; + } + + /// + /// Gets the root directory of the Git repository. + /// + /// A path within the repository. + /// The repository root path, or null if not in a Git repository. + public static string? GetRepositoryRoot(string path) + { + var directory = new DirectoryInfo(path); + + while (directory != null) + { + var gitDir = Path.Combine(directory.FullName, ".git"); + if (Directory.Exists(gitDir) || File.Exists(gitDir)) + { + return directory.FullName; + } + directory = directory.Parent; + } + + return null; + } +} diff --git a/src/CodeMedic.Abstractions/Plugins/CommandRegistration.cs b/src/CodeMedic.Abstractions/Plugins/CommandRegistration.cs index e305538..fb50659 100644 --- a/src/CodeMedic.Abstractions/Plugins/CommandRegistration.cs +++ b/src/CodeMedic.Abstractions/Plugins/CommandRegistration.cs @@ -20,7 +20,7 @@ public class CommandRegistration /// /// Gets or sets the command handler that will be executed. /// - public required Func> Handler { get; init; } + public required Func> Handler { get; init; } /// /// Gets or sets example usage strings for help text. diff --git a/src/CodeMedic.Abstractions/Plugins/HandlerPayload.cs b/src/CodeMedic.Abstractions/Plugins/HandlerPayload.cs new file mode 100644 index 0000000..7816cfd --- /dev/null +++ b/src/CodeMedic.Abstractions/Plugins/HandlerPayload.cs @@ -0,0 +1,12 @@ +namespace CodeMedic.Abstractions.Plugins; + +/// +/// Payload for command handler execution. +/// +/// Command-line arguments passed to the handler. +/// Title of the project being operated on +/// Renderer instance for output formatting. +public record struct HandlerPayload( + string[] Args, + string ProjectTitle, + IRenderer Renderer); diff --git a/src/CodeMedic/Commands/RootCommandHandler.cs b/src/CodeMedic/Commands/RootCommandHandler.cs index 8915d45..1eecc06 100644 --- a/src/CodeMedic/Commands/RootCommandHandler.cs +++ b/src/CodeMedic/Commands/RootCommandHandler.cs @@ -1,3 +1,4 @@ +using CodeMedic.Abstractions.Plugins; using CodeMedic.Output; using CodeMedic.Utilities; using ModelContextProtocol.Protocol; @@ -44,13 +45,6 @@ public static async Task ProcessArguments(string[] args) return 0; } - - var (flowControl, value) = await HandleConfigCommand(args, version); - if (!flowControl) - { - return value; - } - // Version requested if (args.Contains("--version") || args.Contains("-v") || args.Contains("version")) { @@ -59,6 +53,13 @@ public static async Task ProcessArguments(string[] args) return 0; } + // Handle configuration file command + var (flowControl, value) = await HandleConfigCommand(args, version); + if (!flowControl) + { + return value; + } + // Check if a plugin registered this command var commandName = args[0]; var commandRegistration = _pluginLoader.GetCommand(commandName); @@ -104,7 +105,12 @@ public static async Task ProcessArguments(string[] args) "markdown" or "md" => new MarkdownRenderer(), _ => new ConsoleRenderer() }; - return await commandRegistration.Handler(commandArgsList.ToArray(), renderer); + var handlerPayload = new HandlerPayload( + Args: commandArgsList.ToArray(), + ProjectTitle: IdentifyProjectTitle(), + Renderer: renderer + ); + return await commandRegistration.Handler(handlerPayload); } // Unknown command @@ -113,6 +119,50 @@ public static async Task ProcessArguments(string[] args) return 1; } + private static string IdentifyProjectTitle() + { + + // work through the priorities for identifying project title + + // 1. Solution file in current directory or child directories, if and only if there is exactly one solution file + var currentDir = Directory.GetCurrentDirectory(); + var solutionFiles = Directory.GetFiles(currentDir, "*.sln", SearchOption.AllDirectories); + if (solutionFiles.Length == 1) + { + return Path.GetFileNameWithoutExtension(solutionFiles[0]); + } + + + // 2. Project name from csproj / vbproj / fsproj file in current directory or child directories, if and only if there is exactly one project file + var projectFiles = Directory.GetFiles(currentDir, "*.*proj", SearchOption.AllDirectories) + .Where(f => f.EndsWith(".csproj") || f.EndsWith(".vbproj") || f.EndsWith(".fsproj")) + .ToArray(); + if (projectFiles.Length == 1) + { + return Path.GetFileNameWithoutExtension(projectFiles[0]); + } + + // 3. Git origin remote repository name, if available + var gitDir = GitUtility.FindGitRepositoryRoot(currentDir); + if (gitDir != null) + { + var originUrl = GitUtility.GetGitRemoteOriginUrl(gitDir); + if (!string.IsNullOrEmpty(originUrl)) + { + var repoName = GitUtility.ExtractRepositoryNameFromGitUrl(originUrl); + if (!string.IsNullOrEmpty(repoName)) + { + return repoName; + } + } + } + + // Attempt to identify project title from current directory name + return new DirectoryInfo(currentDir).Name; + + + } + private static async Task<(bool flowControl, int value)> HandleConfigCommand(string[] args, string version) { diff --git a/src/CodeMedic/Models/Report/ReportDocument.cs b/src/CodeMedic/Models/Report/ReportDocument.cs index a17988c..9881160 100644 --- a/src/CodeMedic/Models/Report/ReportDocument.cs +++ b/src/CodeMedic/Models/Report/ReportDocument.cs @@ -10,6 +10,11 @@ public class ReportDocument /// public string Title { get; set; } = string.Empty; + /// + /// Gets or sets the project name associated with the report. + /// + public string ProjectName { get; set; } = string.Empty; + /// /// Gets or sets the report metadata. /// diff --git a/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs b/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs index a1f8c00..252aed5 100644 --- a/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs +++ b/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs @@ -88,8 +88,12 @@ public async Task AnalyzeAsync(string repositoryPath, CancellationToken ]; } - private async Task ExecuteBomCommandAsync(string[] args, IRenderer renderer) + private async Task ExecuteBomCommandAsync(HandlerPayload payload) { + + var args = payload.Args; + var renderer = payload.Renderer; + try { // Parse arguments (target path only) diff --git a/src/CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPlugin.cs b/src/CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPlugin.cs index c736905..fc48ff1 100644 --- a/src/CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPlugin.cs +++ b/src/CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPlugin.cs @@ -78,8 +78,11 @@ public async Task AnalyzeAsync(string repositoryPath, CancellationToken ]; } - private async Task ExecuteHealthCommandAsync(string[] args, IRenderer renderer) - { + private async Task ExecuteHealthCommandAsync(HandlerPayload payload) + { + var args = payload.Args; + var renderer = payload.Renderer; + try { // Parse arguments (target path only) diff --git a/src/CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPlugin.cs b/src/CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPlugin.cs index ead4e83..ca878e8 100644 --- a/src/CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPlugin.cs +++ b/src/CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPlugin.cs @@ -242,8 +242,11 @@ public async Task AnalyzeAsync(string repositoryPath, CancellationToken ]; } - private async Task ExecuteVulnerabilityCommandAsync(string[] args, IRenderer renderer) - { + private async Task ExecuteVulnerabilityCommandAsync(HandlerPayload payload) + { + var args = payload.Args; + var renderer = payload.Renderer; + try { // Parse arguments (target path only) From ff555c04d5ee58ef4460aa4c4a5ff80473f39d11 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Tue, 20 Jan 2026 11:28:35 -0500 Subject: [PATCH 2/4] Add Git utility methods for repository management and update project title extraction --- Utilities/GitUtility.cs | 129 +++++++++++++ src/CodeMedic/Commands/RootCommandHandler.cs | 2 +- src/CodeMedic/Utilities/GitUtility.cs | 180 +++++++++++++++++++ 3 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 src/CodeMedic/Utilities/GitUtility.cs diff --git a/Utilities/GitUtility.cs b/Utilities/GitUtility.cs index 5fa5fcd..4c4cd2e 100644 --- a/Utilities/GitUtility.cs +++ b/Utilities/GitUtility.cs @@ -48,4 +48,133 @@ public static bool IsGitRepository(string path) return null; } + + /// + /// Finds the root directory of the Git repository. + /// Alias for GetRepositoryRoot for semantic clarity. + /// + /// A path within the repository. + /// The repository root path, or null if not in a Git repository. + public static string? FindGitRepositoryRoot(string path) => GetRepositoryRoot(path); + + /// + /// Gets the remote origin URL from a Git repository. + /// + /// The root directory of the Git repository. + /// The origin URL, or null if not found. + public static string? GetGitRemoteOriginUrl(string repositoryRoot) + { + var gitDir = Path.Combine(repositoryRoot, ".git"); + string configPath; + + // Handle both regular .git directory and worktrees (where .git is a file) + if (File.Exists(gitDir)) + { + // This is a worktree - .git is a file pointing to the actual git directory + var gitDirContent = File.ReadAllText(gitDir).Trim(); + if (gitDirContent.StartsWith("gitdir:", StringComparison.OrdinalIgnoreCase)) + { + var actualGitDir = gitDirContent.Substring("gitdir:".Length).Trim(); + // Navigate from worktree git dir to main repo's config + // Worktree gitdir points to .git/worktrees/, we need parent .git/config + var worktreeDir = new DirectoryInfo(actualGitDir); + if (worktreeDir.Parent?.Name == "worktrees" && worktreeDir.Parent?.Parent != null) + { + configPath = Path.Combine(worktreeDir.Parent.Parent.FullName, "config"); + } + else + { + configPath = Path.Combine(actualGitDir, "config"); + } + } + else + { + return null; + } + } + else if (Directory.Exists(gitDir)) + { + configPath = Path.Combine(gitDir, "config"); + } + else + { + return null; + } + + if (!File.Exists(configPath)) + { + return null; + } + + // Parse the git config file to find the origin URL + var lines = File.ReadAllLines(configPath); + var inOriginSection = false; + + foreach (var line in lines) + { + var trimmedLine = line.Trim(); + + if (trimmedLine.StartsWith('[')) + { + inOriginSection = trimmedLine.Equals("[remote \"origin\"]", StringComparison.OrdinalIgnoreCase); + continue; + } + + if (inOriginSection && trimmedLine.StartsWith("url", StringComparison.OrdinalIgnoreCase)) + { + var equalsIndex = trimmedLine.IndexOf('='); + if (equalsIndex > 0) + { + return trimmedLine.Substring(equalsIndex + 1).Trim(); + } + } + } + + return null; + } + + /// + /// Extracts the repository name from a Git URL. + /// + /// The Git URL (SSH or HTTPS format). + /// The repository name without the .git extension, or null if unable to parse. + public static string? ExtractRepositoryNameFromGitUrl(string gitUrl) + { + if (string.IsNullOrWhiteSpace(gitUrl)) + { + return null; + } + + // Handle SSH format: git@github.com:owner/repo.git + if (gitUrl.Contains(':') && gitUrl.Contains('@')) + { + var colonIndex = gitUrl.LastIndexOf(':'); + var path = gitUrl.Substring(colonIndex + 1); + return ExtractRepoNameFromPath(path); + } + + // Handle HTTPS format: https://github.com/owner/repo.git + if (Uri.TryCreate(gitUrl, UriKind.Absolute, out var uri)) + { + var path = uri.AbsolutePath.TrimStart('/'); + return ExtractRepoNameFromPath(path); + } + + return null; + } + + private static string? ExtractRepoNameFromPath(string path) + { + // Remove .git suffix if present + if (path.EndsWith(".git", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(0, path.Length - 4); + } + + // Get the last segment (repository name) + var segments = path.Split('/', '\\'); + var repoName = segments.LastOrDefault(s => !string.IsNullOrEmpty(s)); + + return repoName; + } } diff --git a/src/CodeMedic/Commands/RootCommandHandler.cs b/src/CodeMedic/Commands/RootCommandHandler.cs index 1eecc06..f407a1f 100644 --- a/src/CodeMedic/Commands/RootCommandHandler.cs +++ b/src/CodeMedic/Commands/RootCommandHandler.cs @@ -157,7 +157,7 @@ private static string IdentifyProjectTitle() } } - // Attempt to identify project title from current directory name + // 4. set project title from current directory name return new DirectoryInfo(currentDir).Name; diff --git a/src/CodeMedic/Utilities/GitUtility.cs b/src/CodeMedic/Utilities/GitUtility.cs new file mode 100644 index 0000000..4c4cd2e --- /dev/null +++ b/src/CodeMedic/Utilities/GitUtility.cs @@ -0,0 +1,180 @@ +namespace CodeMedic.Utilities; + +/// +/// Utility class for Git-related operations. +/// +public static class GitUtility +{ + /// + /// Checks if the specified directory is a Git repository. + /// + /// The path to check. + /// True if the path is within a Git repository; otherwise, false. + public static bool IsGitRepository(string path) + { + var directory = new DirectoryInfo(path); + + while (directory != null) + { + var gitDir = Path.Combine(directory.FullName, ".git"); + if (Directory.Exists(gitDir) || File.Exists(gitDir)) + { + return true; + } + directory = directory.Parent; + } + + return false; + } + + /// + /// Gets the root directory of the Git repository. + /// + /// A path within the repository. + /// The repository root path, or null if not in a Git repository. + public static string? GetRepositoryRoot(string path) + { + var directory = new DirectoryInfo(path); + + while (directory != null) + { + var gitDir = Path.Combine(directory.FullName, ".git"); + if (Directory.Exists(gitDir) || File.Exists(gitDir)) + { + return directory.FullName; + } + directory = directory.Parent; + } + + return null; + } + + /// + /// Finds the root directory of the Git repository. + /// Alias for GetRepositoryRoot for semantic clarity. + /// + /// A path within the repository. + /// The repository root path, or null if not in a Git repository. + public static string? FindGitRepositoryRoot(string path) => GetRepositoryRoot(path); + + /// + /// Gets the remote origin URL from a Git repository. + /// + /// The root directory of the Git repository. + /// The origin URL, or null if not found. + public static string? GetGitRemoteOriginUrl(string repositoryRoot) + { + var gitDir = Path.Combine(repositoryRoot, ".git"); + string configPath; + + // Handle both regular .git directory and worktrees (where .git is a file) + if (File.Exists(gitDir)) + { + // This is a worktree - .git is a file pointing to the actual git directory + var gitDirContent = File.ReadAllText(gitDir).Trim(); + if (gitDirContent.StartsWith("gitdir:", StringComparison.OrdinalIgnoreCase)) + { + var actualGitDir = gitDirContent.Substring("gitdir:".Length).Trim(); + // Navigate from worktree git dir to main repo's config + // Worktree gitdir points to .git/worktrees/, we need parent .git/config + var worktreeDir = new DirectoryInfo(actualGitDir); + if (worktreeDir.Parent?.Name == "worktrees" && worktreeDir.Parent?.Parent != null) + { + configPath = Path.Combine(worktreeDir.Parent.Parent.FullName, "config"); + } + else + { + configPath = Path.Combine(actualGitDir, "config"); + } + } + else + { + return null; + } + } + else if (Directory.Exists(gitDir)) + { + configPath = Path.Combine(gitDir, "config"); + } + else + { + return null; + } + + if (!File.Exists(configPath)) + { + return null; + } + + // Parse the git config file to find the origin URL + var lines = File.ReadAllLines(configPath); + var inOriginSection = false; + + foreach (var line in lines) + { + var trimmedLine = line.Trim(); + + if (trimmedLine.StartsWith('[')) + { + inOriginSection = trimmedLine.Equals("[remote \"origin\"]", StringComparison.OrdinalIgnoreCase); + continue; + } + + if (inOriginSection && trimmedLine.StartsWith("url", StringComparison.OrdinalIgnoreCase)) + { + var equalsIndex = trimmedLine.IndexOf('='); + if (equalsIndex > 0) + { + return trimmedLine.Substring(equalsIndex + 1).Trim(); + } + } + } + + return null; + } + + /// + /// Extracts the repository name from a Git URL. + /// + /// The Git URL (SSH or HTTPS format). + /// The repository name without the .git extension, or null if unable to parse. + public static string? ExtractRepositoryNameFromGitUrl(string gitUrl) + { + if (string.IsNullOrWhiteSpace(gitUrl)) + { + return null; + } + + // Handle SSH format: git@github.com:owner/repo.git + if (gitUrl.Contains(':') && gitUrl.Contains('@')) + { + var colonIndex = gitUrl.LastIndexOf(':'); + var path = gitUrl.Substring(colonIndex + 1); + return ExtractRepoNameFromPath(path); + } + + // Handle HTTPS format: https://github.com/owner/repo.git + if (Uri.TryCreate(gitUrl, UriKind.Absolute, out var uri)) + { + var path = uri.AbsolutePath.TrimStart('/'); + return ExtractRepoNameFromPath(path); + } + + return null; + } + + private static string? ExtractRepoNameFromPath(string path) + { + // Remove .git suffix if present + if (path.EndsWith(".git", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(0, path.Length - 4); + } + + // Get the last segment (repository name) + var segments = path.Split('/', '\\'); + var repoName = segments.LastOrDefault(s => !string.IsNullOrEmpty(s)); + + return repoName; + } +} From 4920e0970c2279f06cc7a4aabba66745dc6ac309 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Tue, 20 Jan 2026 11:43:42 -0500 Subject: [PATCH 3/4] Refactor command handlers to utilize HandlerPayload for improved project title handling in reports --- src/CodeMedic/Commands/ConfigurationCommandHandler.cs | 11 ++++++++++- src/CodeMedic/Commands/RootCommandHandler-MCP.cs | 10 +++++++++- .../Plugins/BomAnalysis/BomAnalysisPlugin.cs | 7 ++++++- .../Plugins/HealthAnalysis/HealthAnalysisPlugin.cs | 7 ++++++- .../VulnerabilityAnalysisPlugin.cs | 7 ++++++- 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/CodeMedic/Commands/ConfigurationCommandHandler.cs b/src/CodeMedic/Commands/ConfigurationCommandHandler.cs index 22a3c6e..6fdcf53 100644 --- a/src/CodeMedic/Commands/ConfigurationCommandHandler.cs +++ b/src/CodeMedic/Commands/ConfigurationCommandHandler.cs @@ -1,6 +1,7 @@ using CodeMedic.Utilities; using CodeMedic.Commands; using CodeMedic.Output; +using CodeMedic.Abstractions.Plugins; namespace CodeMedic.Commands; @@ -98,7 +99,15 @@ private async Task RunCommandsForConfigurationAsync(CodeMedicRunConfiguration co // Build arguments array to pass the repository path to the command var commandArgs = new[] { "--path", repoConfig.Path }; - await commandPlugin.Handler(commandArgs, formatter); + // setup the handler payload and execute + var payload = new HandlerPayload + { + Args = commandArgs, + Renderer = formatter, + ProjectTitle = $"CodeMedic Report for {repoConfig.Name} - Command: {commandName}" + }; + + await commandPlugin.Handler(payload); } } diff --git a/src/CodeMedic/Commands/RootCommandHandler-MCP.cs b/src/CodeMedic/Commands/RootCommandHandler-MCP.cs index bb22c62..e6b0a6e 100644 --- a/src/CodeMedic/Commands/RootCommandHandler-MCP.cs +++ b/src/CodeMedic/Commands/RootCommandHandler-MCP.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Nodes; +using CodeMedic.Abstractions.Plugins; using CodeMedic.Output; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; @@ -99,7 +100,14 @@ private static async ValueTask CallTool(RequestContext ExecuteBomCommandAsync(HandlerPayload payload) } // Render banner and header - renderer.RenderBanner(); + renderer.RenderBanner($"Report for project {payload.ProjectTitle}"); renderer.RenderSectionHeader("Bill of Materials (BOM)"); // Run analysis @@ -121,6 +121,11 @@ await renderer.RenderWaitAsync($"Running {AnalysisDescription}...", async () => reportDocument = await AnalyzeAsync(repositoryPath); + if (reportDocument is ReportDocument rd) + { + rd.ProjectName = payload.ProjectTitle; + } + // Render report renderer.RenderReport(reportDocument); diff --git a/src/CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPlugin.cs b/src/CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPlugin.cs index fc48ff1..1915c09 100644 --- a/src/CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPlugin.cs +++ b/src/CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPlugin.cs @@ -91,7 +91,7 @@ private async Task ExecuteHealthCommandAsync(HandlerPayload payload) _limitPackageLists = renderer is ConsoleRenderer; // Render banner and header - renderer.RenderBanner(); + renderer.RenderBanner($"Report for project {payload.ProjectTitle}"); renderer.RenderSectionHeader("Repository Health Dashboard"); // Run analysis @@ -103,6 +103,11 @@ await renderer.RenderWaitAsync($"Running {AnalysisDescription}...", async () => reportDocument = await AnalyzeAsync(repositoryPath); }); + if (reportDocument is ReportDocument rd) + { + rd.ProjectName = payload.ProjectTitle; + } + // Render report if (reportDocument != null) { diff --git a/src/CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPlugin.cs b/src/CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPlugin.cs index ca878e8..cab3bf4 100644 --- a/src/CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPlugin.cs +++ b/src/CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPlugin.cs @@ -253,7 +253,7 @@ private async Task ExecuteVulnerabilityCommandAsync(HandlerPayload payload) string? targetPath = args.IdentifyTargetPathFromArgs(); // Render banner and header - renderer.RenderBanner(); + renderer.RenderBanner($"Report for project {payload.ProjectTitle}"); renderer.RenderSectionHeader("NuGet Package Vulnerability Report"); // Run analysis @@ -268,6 +268,11 @@ await renderer.RenderWaitAsync($"Running {AnalysisDescription}...", async () => // Render report if (reportDocument != null) { + // set the ProjectName property on the report document for footer rendering + if (reportDocument is ReportDocument rd) + { + rd.ProjectName = payload.ProjectTitle; + } renderer.RenderReport(reportDocument); } From 32db3d93ee8a81d13104624e27ee15ff7653df0f Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Tue, 20 Jan 2026 11:57:23 -0500 Subject: [PATCH 4/4] Refactor tests to utilize HandlerPayload for improved command handling and renderer interactions --- .../BomAnalysis/BomAnalysisPluginPathTests.cs | 44 +++++++------ .../HealthAnalysisPluginPathTests.cs | 61 ++++++++++--------- .../VulnerabilityAnalysisCommandTests.cs | 28 ++++++--- .../VulnerabilityAnalysisPluginPathTests.cs | 53 ++++++++-------- 4 files changed, 104 insertions(+), 82 deletions(-) diff --git a/test/Test.CodeMedic/Plugins/BomAnalysis/BomAnalysisPluginPathTests.cs b/test/Test.CodeMedic/Plugins/BomAnalysis/BomAnalysisPluginPathTests.cs index bcef9a0..72e1a66 100644 --- a/test/Test.CodeMedic/Plugins/BomAnalysis/BomAnalysisPluginPathTests.cs +++ b/test/Test.CodeMedic/Plugins/BomAnalysis/BomAnalysisPluginPathTests.cs @@ -26,14 +26,14 @@ public void RegisterCommands_WhenCalled_ThenReturnsCommandWithPathArgument() // Then Assert.NotNull(commands); Assert.Single(commands); - + var command = commands[0]; Assert.Equal("bom", command.Name); Assert.Equal("Generate bill of materials report", command.Description); - + Assert.NotNull(command.Arguments); Assert.Single(command.Arguments); - + var pathArg = command.Arguments[0]; Assert.Equal("Path to the repository to analyze", pathArg.Description); Assert.Equal("p", pathArg.ShortName); @@ -68,7 +68,7 @@ public async Task ExecuteBomCommandAsync_GivenEmptyArgs_WhenCalled_ThenUsesCurre var args = Array.Empty(); // Setup the renderer to avoid actual rendering - mockRenderer.Setup(r => r.RenderBanner()).Verifiable(); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()) @@ -76,13 +76,14 @@ public async Task ExecuteBomCommandAsync_GivenEmptyArgs_WhenCalled_ThenUsesCurre mockRenderer.Setup(r => r.RenderReport(It.IsAny())).Verifiable(); // When & Then - Should not throw and should call renderer methods - var result = await command.Handler(args, mockRenderer.Object); - + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); + // Verify renderer was called appropriately - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("Bill of Materials (BOM)"), Times.Once); mockRenderer.Verify(r => r.RenderWaitAsync( - It.Is(s => s.Contains("Comprehensive dependency and service inventory (BOM)")), + It.Is(s => s.Contains("Comprehensive dependency and service inventory (BOM)")), It.IsAny>()), Times.Once); } @@ -96,7 +97,7 @@ public async Task ExecuteBomCommandAsync_GivenShortPathArg_WhenCalled_ThenUsesSp var args = new[] { "-p", testPath }; // Setup the renderer - mockRenderer.Setup(r => r.RenderBanner()).Verifiable(); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()) @@ -104,10 +105,11 @@ public async Task ExecuteBomCommandAsync_GivenShortPathArg_WhenCalled_ThenUsesSp mockRenderer.Setup(r => r.RenderReport(It.IsAny())).Verifiable(); // When - var result = await command.Handler(args, mockRenderer.Object); + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); // Then - Should complete without throwing - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("Bill of Materials (BOM)"), Times.Once); } @@ -121,7 +123,7 @@ public async Task ExecuteBomCommandAsync_GivenLongPathArg_WhenCalled_ThenUsesSpe var args = new[] { "--path", testPath }; // Setup the renderer - mockRenderer.Setup(r => r.RenderBanner()).Verifiable(); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()) @@ -129,10 +131,11 @@ public async Task ExecuteBomCommandAsync_GivenLongPathArg_WhenCalled_ThenUsesSpe mockRenderer.Setup(r => r.RenderReport(It.IsAny())).Verifiable(); // When - var result = await command.Handler(args, mockRenderer.Object); + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); // Then - Should complete without throwing - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("Bill of Materials (BOM)"), Times.Once); } @@ -164,7 +167,7 @@ public void AnalysisDescription_WhenAccessed_ThenReturnsCorrectDescription() Assert.Equal("Comprehensive dependency and service inventory (BOM)", description); } - [Fact] + [Fact] public async Task InitializeAsync_WhenCalled_ThenCompletesSuccessfully() { // When & Then - Should complete without throwing @@ -180,17 +183,18 @@ public async Task ExecuteBomCommandAsync_GivenRelativePathArg_WhenCalled_ThenPro var args = new[] { "-p", "." }; // Setup minimal renderer mock - mockRenderer.Setup(r => r.RenderBanner()); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()); mockRenderer.Setup(r => r.RenderReport(It.IsAny())); // When & Then - Should not throw - var result = await command.Handler(args, mockRenderer.Object); - + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); + // Verify basic renderer calls were made - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("Bill of Materials (BOM)"), Times.Once); } -} \ No newline at end of file +} diff --git a/test/Test.CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPluginPathTests.cs b/test/Test.CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPluginPathTests.cs index 6a5c58b..9db1295 100644 --- a/test/Test.CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPluginPathTests.cs +++ b/test/Test.CodeMedic/Plugins/HealthAnalysis/HealthAnalysisPluginPathTests.cs @@ -26,14 +26,14 @@ public void RegisterCommands_WhenCalled_ThenReturnsCommandWithPathArgument() // Then Assert.NotNull(commands); Assert.Single(commands); - + var command = commands[0]; Assert.Equal("health", command.Name); Assert.Equal("Display repository health dashboard", command.Description); - + Assert.NotNull(command.Arguments); Assert.Single(command.Arguments); - + var pathArg = command.Arguments[0]; Assert.Equal("Path to the repository to analyze", pathArg.Description); Assert.Equal("p", pathArg.ShortName); @@ -68,7 +68,7 @@ public async Task ExecuteHealthCommandAsync_GivenEmptyArgs_WhenCalled_ThenUsesCu var args = Array.Empty(); // Setup the renderer to avoid actual rendering - mockRenderer.Setup(r => r.RenderBanner()).Verifiable(); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()) @@ -76,13 +76,14 @@ public async Task ExecuteHealthCommandAsync_GivenEmptyArgs_WhenCalled_ThenUsesCu mockRenderer.Setup(r => r.RenderReport(It.IsAny())).Verifiable(); // When & Then - Should not throw and should call renderer methods - var result = await command.Handler(args, mockRenderer.Object); - + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); + // Verify renderer was called appropriately - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("Repository Health Dashboard"), Times.Once); mockRenderer.Verify(r => r.RenderWaitAsync( - It.Is(s => s.Contains("Repository health and code quality analysis")), + It.Is(s => s.Contains("Repository health and code quality analysis")), It.IsAny>()), Times.Once); } @@ -96,7 +97,7 @@ public async Task ExecuteHealthCommandAsync_GivenShortPathArg_WhenCalled_ThenUse var args = new[] { "-p", testPath }; // Setup the renderer - mockRenderer.Setup(r => r.RenderBanner()).Verifiable(); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()) @@ -104,10 +105,11 @@ public async Task ExecuteHealthCommandAsync_GivenShortPathArg_WhenCalled_ThenUse mockRenderer.Setup(r => r.RenderReport(It.IsAny())).Verifiable(); // When - var result = await command.Handler(args, mockRenderer.Object); + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); // Then - Should complete without throwing - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("Repository Health Dashboard"), Times.Once); } @@ -121,7 +123,7 @@ public async Task ExecuteHealthCommandAsync_GivenLongPathArg_WhenCalled_ThenUses var args = new[] { "--path", testPath }; // Setup the renderer - mockRenderer.Setup(r => r.RenderBanner()).Verifiable(); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()) @@ -129,10 +131,11 @@ public async Task ExecuteHealthCommandAsync_GivenLongPathArg_WhenCalled_ThenUses mockRenderer.Setup(r => r.RenderReport(It.IsAny())).Verifiable(); // When - var result = await command.Handler(args, mockRenderer.Object); + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); // Then - Should complete without throwing - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("Repository Health Dashboard"), Times.Once); } @@ -145,17 +148,18 @@ public async Task ExecuteHealthCommandAsync_GivenCurrentDirectoryPath_WhenCalled var args = new[] { "-p", "." }; // Setup minimal renderer mock - mockRenderer.Setup(r => r.RenderBanner()); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()); mockRenderer.Setup(r => r.RenderReport(It.IsAny())); // When & Then - Should not throw - var result = await command.Handler(args, mockRenderer.Object); - + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); + // Verify basic renderer calls were made - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("Repository Health Dashboard"), Times.Once); } @@ -168,17 +172,18 @@ public async Task ExecuteHealthCommandAsync_GivenParentDirectoryPath_WhenCalled_ var args = new[] { "--path", ".." }; // Setup minimal renderer mock - mockRenderer.Setup(r => r.RenderBanner()); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()); mockRenderer.Setup(r => r.RenderReport(It.IsAny())); // When & Then - Should not throw - var result = await command.Handler(args, mockRenderer.Object); - + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); + // Verify basic renderer calls were made - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("Repository Health Dashboard"), Times.Once); } @@ -210,7 +215,7 @@ public void AnalysisDescription_WhenAccessed_ThenReturnsCorrectDescription() Assert.Equal("Repository health and code quality analysis", description); } - [Fact] + [Fact] public async Task InitializeAsync_WhenCalled_ThenCompletesSuccessfully() { // When & Then - Should complete without throwing @@ -224,21 +229,21 @@ public async Task ChaosMonkey_ShouldEnsureHealthAnalysisCanCountToTen_BecauseBas // Given - We need to ensure our health analysis can handle advanced mathematical concepts var expectedResult = 10; var actualResult = 0; - + // When - We count to ten like we're in kindergarten for (int i = 1; i <= 10; i++) { actualResult = i; // Such complex mathematics! Much wow! } - + // Then - We verify that counting works as expected Assert.Equal(expectedResult, actualResult); Assert.True(actualResult > 0, "🐒 Chaos Monkey says: Numbers should be greater than zero, just like our fundraising goals!"); - + // Extra chaos assertion because why not? Assert.True(true, "🎪 This assertion always passes, just like PadreSperanza's generous donation always makes us smile!"); - + // TODO: 🐒 Replace this placeholder with actual health analysis counting logic when we discover what needs counting // Maybe we'll count health issues? Or happy little code trees? Who knows! 🌲 } -} \ No newline at end of file +} diff --git a/test/Test.CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisCommandTests.cs b/test/Test.CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisCommandTests.cs index 5d02e99..62cff23 100644 --- a/test/Test.CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisCommandTests.cs +++ b/test/Test.CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisCommandTests.cs @@ -1,4 +1,5 @@ using CodeMedic.Abstractions; +using CodeMedic.Abstractions.Plugins; using CodeMedic.Models.Report; using CodeMedic.Output; using CodeMedic.Plugins.VulnerabilityAnalysis; @@ -33,7 +34,8 @@ public async Task ExecuteVulnerabilityCommand_GivenValidRepository_WhenExecuting try { // When - var exitCode = await handler!(new[] { testRepo }, mockRenderer.Object); + var payload = new HandlerPayload(new[] { testRepo }, "Test Project", mockRenderer.Object); + var exitCode = await handler!(payload); // Then Assert.Equal(0, exitCode); @@ -54,7 +56,8 @@ public async Task ExecuteVulnerabilityCommand_GivenNoArguments_WhenExecuting_The var mockRenderer = CreateMockRenderer(); // When - var exitCode = await handler!(Array.Empty(), mockRenderer.Object); + var payload = new HandlerPayload(Array.Empty(), "Test Project", mockRenderer.Object); + var exitCode = await handler!(payload); // Then Assert.Equal(0, exitCode); @@ -73,7 +76,8 @@ public async Task ExecuteVulnerabilityCommand_GivenValidRepository_WhenExecuting try { // When - await handler!(new[] { testRepo }, mockRenderer.Object); + var payload = new HandlerPayload(new[] { testRepo }, "Test Project", mockRenderer.Object); + await handler!(payload); // Then mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); @@ -97,7 +101,8 @@ public async Task ExecuteVulnerabilityCommand_GivenValidRepository_WhenExecuting try { // When - await handler!(new[] { testRepo }, mockRenderer.Object); + var payload = new HandlerPayload(new[] { testRepo }, "Test Project", mockRenderer.Object); + await handler!(payload); // Then mockRenderer.Verify( @@ -123,11 +128,12 @@ public async Task ExecuteVulnerabilityCommand_GivenValidRepository_WhenExecuting try { // When - await handler!(new[] { testRepo }, mockRenderer.Object); + var payload = new HandlerPayload(new[] { testRepo }, "Test Project", mockRenderer.Object); + await handler!(payload); // Then mockRenderer.Verify( - r => r.RenderWaitAsync(It.IsAny(), It.IsAny>()), + r => r.RenderWaitAsync(It.IsAny(), It.IsAny>()), Times.Once); } finally @@ -149,7 +155,8 @@ public async Task ExecuteVulnerabilityCommand_GivenValidRepository_WhenExecuting try { // When - await handler!(new[] { testRepo }, mockRenderer.Object); + var payload = new HandlerPayload(new[] { testRepo }, "Test Project", mockRenderer.Object); + await handler!(payload); // Then mockRenderer.Verify( @@ -173,7 +180,8 @@ public async Task ExecuteVulnerabilityCommand_GivenInvalidRepository_WhenExecuti var invalidPath = Path.Combine(Path.GetTempPath(), $"Invalid_{Guid.NewGuid()}_Path"); // When - var exitCode = await handler!(new[] { invalidPath }, mockRenderer.Object); + var payload = new HandlerPayload(new[] { invalidPath }, "Test Project", mockRenderer.Object); + var exitCode = await handler!(payload); // Then // Note: The current implementation returns 0 even for invalid paths @@ -249,7 +257,7 @@ public async Task AnalyzeAsync_GivenRepository_WhenAnalyzing_ThenReportHasRequir // Then Assert.NotNull(result); Assert.NotEmpty(result.Sections); - + // Should have at least a Summary section or Status section var hasRequiredSection = result.Sections.Any(s => s.Title == "Summary" || s.Title == "Status"); @@ -296,7 +304,7 @@ public async Task AnalyzeAsync_GivenRepositoryWithMultipleProjects_WhenAnalyzing private static Mock CreateMockRenderer() { var mockRenderer = new Mock(); - + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) diff --git a/test/Test.CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPluginPathTests.cs b/test/Test.CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPluginPathTests.cs index c3f8c03..0fb9d8a 100644 --- a/test/Test.CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPluginPathTests.cs +++ b/test/Test.CodeMedic/Plugins/VulnerabilityAnalysis/VulnerabilityAnalysisPluginPathTests.cs @@ -26,14 +26,14 @@ public void RegisterCommands_WhenCalled_ThenReturnsCommandWithPathArgument() // Then Assert.NotNull(commands); Assert.Single(commands); - + var command = commands[0]; Assert.Equal("vulnerabilities", command.Name); Assert.Equal("Scan for known vulnerabilities in NuGet packages", command.Description); - + Assert.NotNull(command.Arguments); Assert.Single(command.Arguments); - + var pathArg = command.Arguments[0]; Assert.Equal("Path to the repository to scan", pathArg.Description); Assert.Equal("p", pathArg.ShortName); @@ -68,7 +68,7 @@ public async Task ExecuteVulnerabilityCommandAsync_GivenEmptyArgs_WhenCalled_The var args = Array.Empty(); // Setup the renderer to avoid actual rendering - mockRenderer.Setup(r => r.RenderBanner()).Verifiable(); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()) @@ -76,13 +76,14 @@ public async Task ExecuteVulnerabilityCommandAsync_GivenEmptyArgs_WhenCalled_The mockRenderer.Setup(r => r.RenderReport(It.IsAny())).Verifiable(); // When & Then - Should not throw and should call renderer methods - var result = await command.Handler(args, mockRenderer.Object); - + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); + // Verify renderer was called appropriately - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("NuGet Package Vulnerability Report"), Times.Once); mockRenderer.Verify(r => r.RenderWaitAsync( - It.Is(s => s.Contains("NuGet package vulnerability scan")), + It.Is(s => s.Contains("NuGet package vulnerability scan")), It.IsAny>()), Times.Once); } @@ -96,7 +97,7 @@ public async Task ExecuteVulnerabilityCommandAsync_GivenShortPathArg_WhenCalled_ var args = new[] { "-p", testPath }; // Setup the renderer - mockRenderer.Setup(r => r.RenderBanner()).Verifiable(); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()) @@ -104,10 +105,11 @@ public async Task ExecuteVulnerabilityCommandAsync_GivenShortPathArg_WhenCalled_ mockRenderer.Setup(r => r.RenderReport(It.IsAny())).Verifiable(); // When - var result = await command.Handler(args, mockRenderer.Object); + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); // Then - Should complete without throwing - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("NuGet Package Vulnerability Report"), Times.Once); } @@ -121,7 +123,7 @@ public async Task ExecuteVulnerabilityCommandAsync_GivenLongPathArg_WhenCalled_T var args = new[] { "--path", testPath }; // Setup the renderer - mockRenderer.Setup(r => r.RenderBanner()).Verifiable(); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())).Verifiable(); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()) @@ -129,10 +131,11 @@ public async Task ExecuteVulnerabilityCommandAsync_GivenLongPathArg_WhenCalled_T mockRenderer.Setup(r => r.RenderReport(It.IsAny())).Verifiable(); // When - var result = await command.Handler(args, mockRenderer.Object); + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); // Then - Should complete without throwing - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("NuGet Package Vulnerability Report"), Times.Once); } @@ -164,7 +167,7 @@ public void AnalysisDescription_WhenAccessed_ThenReturnsCorrectDescription() Assert.Equal("NuGet package vulnerability scan", description); } - [Fact] + [Fact] public async Task InitializeAsync_WhenCalled_ThenCompletesSuccessfully() { // When & Then - Should complete without throwing @@ -180,17 +183,18 @@ public async Task ExecuteVulnerabilityCommandAsync_GivenRelativePathArg_WhenCall var args = new[] { "-p", "." }; // Setup minimal renderer mock - mockRenderer.Setup(r => r.RenderBanner()); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()); mockRenderer.Setup(r => r.RenderReport(It.IsAny())); // When & Then - Should not throw - var result = await command.Handler(args, mockRenderer.Object); - + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); + // Verify basic renderer calls were made - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("NuGet Package Vulnerability Report"), Times.Once); } @@ -203,17 +207,18 @@ public async Task ExecuteVulnerabilityCommandAsync_GivenParentDirectoryPath_When var args = new[] { "--path", ".." }; // Setup minimal renderer mock - mockRenderer.Setup(r => r.RenderBanner()); + mockRenderer.Setup(r => r.RenderBanner(It.IsAny())); mockRenderer.Setup(r => r.RenderSectionHeader(It.IsAny())); mockRenderer.Setup(r => r.RenderWaitAsync(It.IsAny(), It.IsAny>())) .Returns>((_, action) => action()); mockRenderer.Setup(r => r.RenderReport(It.IsAny())); // When & Then - Should not throw - var result = await command.Handler(args, mockRenderer.Object); - + var payload = new HandlerPayload(args, "Test Project", mockRenderer.Object); + var result = await command.Handler(payload); + // Verify basic renderer calls were made - mockRenderer.Verify(r => r.RenderBanner(), Times.Once); + mockRenderer.Verify(r => r.RenderBanner(It.IsAny()), Times.Once); mockRenderer.Verify(r => r.RenderSectionHeader("NuGet Package Vulnerability Report"), Times.Once); } -} \ No newline at end of file +}