diff --git a/src/Aspire.Cli/Agents/CopilotCli/CopilotCliRunner.cs b/src/Aspire.Cli/Agents/CopilotCli/CopilotCliRunner.cs index 42cc875c1ec..1f5c1a3110e 100644 --- a/src/Aspire.Cli/Agents/CopilotCli/CopilotCliRunner.cs +++ b/src/Aspire.Cli/Agents/CopilotCli/CopilotCliRunner.cs @@ -52,28 +52,8 @@ internal sealed class CopilotCliRunner(ILogger logger) : ICopi } var output = await outputTask.ConfigureAwait(false); - var versionString = output.Trim(); - if (string.IsNullOrEmpty(versionString)) - { - logger.LogDebug("GitHub Copilot CLI returned empty version output"); - return null; - } - - // Version output may be on the first line if multi-line - var lines = versionString.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); - if (lines.Length > 0) - { - versionString = lines[0].Trim(); - } - - // Try to parse the version string (may have a 'v' prefix like "v1.2.3") - if (versionString.StartsWith('v') || versionString.StartsWith('V')) - { - versionString = versionString[1..]; - } - - if (SemVersion.TryParse(versionString, SemVersionStyles.Any, out var version)) + if (TryParseVersionOutput(output, out var version)) { logger.LogDebug("Found GitHub Copilot CLI version: {Version}", version); return version; @@ -88,4 +68,40 @@ internal sealed class CopilotCliRunner(ILogger logger) : ICopi return null; } } + + internal static bool TryParseVersionOutput(string output, out SemVersion? version) + { + version = null; + var versionString = output.Trim(); + + if (string.IsNullOrEmpty(versionString)) + { + return false; + } + + // Version output may be on the first line if multi-line + var lines = versionString.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); + if (lines.Length > 0) + { + versionString = lines[0].Trim(); + } + + // Try to extract the version from known formats like "GitHub Copilot CLI 0.0.397" + var lastSpaceIndex = versionString.LastIndexOf(' '); + if (lastSpaceIndex >= 0) + { + versionString = versionString[(lastSpaceIndex + 1)..]; + } + + // Trim common trailing punctuation that may follow the version (for example, "0.0.397.") + versionString = versionString.TrimEnd('.', ',', ')'); + + // Try to parse the version string (may have a 'v' prefix like "v1.2.3") + if (versionString.StartsWith('v') || versionString.StartsWith('V')) + { + versionString = versionString[1..]; + } + + return SemVersion.TryParse(versionString, SemVersionStyles.Any, out version); + } } diff --git a/tests/Aspire.Cli.Tests/Agents/CopilotCliRunnerTests.cs b/tests/Aspire.Cli.Tests/Agents/CopilotCliRunnerTests.cs index e4721afe76e..b55a52d79b1 100644 --- a/tests/Aspire.Cli.Tests/Agents/CopilotCliRunnerTests.cs +++ b/tests/Aspire.Cli.Tests/Agents/CopilotCliRunnerTests.cs @@ -24,4 +24,37 @@ public async Task GetVersionAsync_ChecksForCopilot() // we just verify the method completes without throwing // Version can be null (not installed) or a real version } + + [Theory] + [InlineData("GitHub Copilot CLI 0.0.397", 0, 0, 397)] + [InlineData("GitHub Copilot CLI 1.2.3", 1, 2, 3)] + [InlineData("0.0.397", 0, 0, 397)] + [InlineData("1.2.3", 1, 2, 3)] + [InlineData("v1.2.3", 1, 2, 3)] + [InlineData("V1.2.3", 1, 2, 3)] + [InlineData("GitHub Copilot CLI 0.0.397\nsome other output", 0, 0, 397)] + [InlineData(" GitHub Copilot CLI 0.0.397 ", 0, 0, 397)] + [InlineData("GitHub Copilot CLI 0.0.397.", 0, 0, 397)] + public void TryParseVersionOutput_ValidVersionStrings_ReturnsTrue(string input, int major, int minor, int patch) + { + var result = CopilotCliRunner.TryParseVersionOutput(input, out var version); + + Assert.True(result); + Assert.NotNull(version); + Assert.Equal(major, version.Major); + Assert.Equal(minor, version.Minor); + Assert.Equal(patch, version.Patch); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("not a version")] + public void TryParseVersionOutput_InvalidVersionStrings_ReturnsFalse(string input) + { + var result = CopilotCliRunner.TryParseVersionOutput(input, out var version); + + Assert.False(result); + Assert.Null(version); + } }