diff --git a/sidecar/src/Kopaya.AgentHost/Kopaya.AgentHost.csproj b/sidecar/src/Kopaya.AgentHost/Kopaya.AgentHost.csproj index b95853d..d12b7ef 100644 --- a/sidecar/src/Kopaya.AgentHost/Kopaya.AgentHost.csproj +++ b/sidecar/src/Kopaya.AgentHost/Kopaya.AgentHost.csproj @@ -8,6 +8,7 @@ + diff --git a/sidecar/src/Kopaya.AgentHost/Services/CopilotCliPathResolver.cs b/sidecar/src/Kopaya.AgentHost/Services/CopilotCliPathResolver.cs index f1dac8b..9e1f921 100644 --- a/sidecar/src/Kopaya.AgentHost/Services/CopilotCliPathResolver.cs +++ b/sidecar/src/Kopaya.AgentHost/Services/CopilotCliPathResolver.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using GitHub.Copilot.SDK; namespace Kopaya.AgentHost.Services; @@ -8,62 +7,34 @@ internal static class CopilotCliPathResolver private const string CopilotCommandName = "copilot"; private const string DefaultWindowsPathExtensions = ".COM;.EXE;.BAT;.CMD"; - public static CopilotClientOptions? CreateClientOptions() + public static CopilotClientOptions CreateClientOptions() { - CopilotCliResolution resolution = Resolve( - Environment.ProcessPath, + string? cliPath = Resolve( Environment.GetEnvironmentVariable("PATH"), Environment.GetEnvironmentVariable("PATHEXT"), - RuntimeInformation.IsOSPlatform(OSPlatform.Windows), + OperatingSystem.IsWindows(), File.Exists); - if (!resolution.ShouldOverrideCliPath) - { - return null; - } - - if (string.IsNullOrWhiteSpace(resolution.CliPath)) + if (string.IsNullOrWhiteSpace(cliPath)) { throw new InvalidOperationException( - "Development sidecar could not find the system-installed 'copilot' command on PATH. Install the GitHub Copilot CLI or provide an explicit CliPath."); + "Kopaya requires the system-installed 'copilot' command on PATH. Install the GitHub Copilot CLI and ensure it is available in the current environment."); } return new CopilotClientOptions { - CliPath = resolution.CliPath, + CliPath = cliPath, }; } - internal static CopilotCliResolution Resolve( - string? processPath, + internal static string? Resolve( string? pathValue, string? pathExtValue, bool isWindows, Func fileExists) { ArgumentNullException.ThrowIfNull(fileExists); - - if (!IsDevelopmentHost(processPath)) - { - return default; - } - - return new CopilotCliResolution( - ShouldOverrideCliPath: true, - CliPath: ResolveCliPath(pathValue, pathExtValue, isWindows, fileExists)); - } - - private static bool IsDevelopmentHost(string? processPath) - { - if (string.IsNullOrWhiteSpace(processPath)) - { - return false; - } - - return string.Equals( - Path.GetFileNameWithoutExtension(processPath), - "dotnet", - StringComparison.OrdinalIgnoreCase); + return ResolveCliPath(pathValue, pathExtValue, isWindows, fileExists); } private static string? ResolveCliPath( @@ -128,5 +99,3 @@ internal static class CopilotCliPathResolver } } } - -internal readonly record struct CopilotCliResolution(bool ShouldOverrideCliPath, string? CliPath); diff --git a/sidecar/src/Kopaya.AgentHost/Services/CopilotWorkflowRunner.cs b/sidecar/src/Kopaya.AgentHost/Services/CopilotWorkflowRunner.cs index 9574acf..fc8ddb1 100644 --- a/sidecar/src/Kopaya.AgentHost/Services/CopilotWorkflowRunner.cs +++ b/sidecar/src/Kopaya.AgentHost/Services/CopilotWorkflowRunner.cs @@ -170,11 +170,11 @@ public sealed class CopilotWorkflowRunner { List disposables = []; List agents = []; - CopilotClientOptions? clientOptions = CopilotCliPathResolver.CreateClientOptions(); + CopilotClientOptions clientOptions = CopilotCliPathResolver.CreateClientOptions(); foreach (PatternAgentDefinitionDto definition in pattern.Agents) { - CopilotClient client = clientOptions is null ? new() : new(clientOptions); + CopilotClient client = new(clientOptions); await client.StartAsync(cancellationToken).ConfigureAwait(false); SessionConfig sessionConfig = new() @@ -263,7 +263,7 @@ public sealed class CopilotWorkflowRunner { return Task.FromResult(new PermissionRequestResult { - Kind = "approved", + Kind = PermissionRequestResultKind.Approved, }); } } diff --git a/sidecar/tests/Kopaya.AgentHost.Tests/CopilotCliPathResolverTests.cs b/sidecar/tests/Kopaya.AgentHost.Tests/CopilotCliPathResolverTests.cs index 4ce75ac..de20d35 100644 --- a/sidecar/tests/Kopaya.AgentHost.Tests/CopilotCliPathResolverTests.cs +++ b/sidecar/tests/Kopaya.AgentHost.Tests/CopilotCliPathResolverTests.cs @@ -5,7 +5,7 @@ namespace Kopaya.AgentHost.Tests; public sealed class CopilotCliPathResolverTests { [Fact] - public void Resolve_UsesCopilotFromPathDuringDevelopment() + public void Resolve_UsesCopilotFromPath() { string copilotDirectory = @"C:\tools\copilot"; HashSet existingFiles = new(StringComparer.OrdinalIgnoreCase) @@ -13,42 +13,42 @@ public sealed class CopilotCliPathResolverTests Path.Combine(copilotDirectory, "copilot.exe"), }; - CopilotCliResolution resolution = CopilotCliPathResolver.Resolve( - processPath: @"C:\Program Files\dotnet\dotnet.exe", + string? cliPath = CopilotCliPathResolver.Resolve( pathValue: $"C:\\other;\"{copilotDirectory}\"", pathExtValue: ".COM;.EXE;.BAT;.CMD", isWindows: true, fileExists: existingFiles.Contains); - Assert.True(resolution.ShouldOverrideCliPath); - Assert.Equal(Path.Combine(copilotDirectory, "copilot.exe"), resolution.CliPath, ignoreCase: true); + Assert.Equal(Path.Combine(copilotDirectory, "copilot.exe"), cliPath, ignoreCase: true); } [Fact] - public void Resolve_LeavesPackagedRuntimeOnBundledCli() + public void Resolve_UsesDefaultWindowsExtensionsWhenPathExtMissing() { - CopilotCliResolution resolution = CopilotCliPathResolver.Resolve( - processPath: @"C:\Program Files\Kopaya\Kopaya.AgentHost.exe", - pathValue: @"C:\tools", - pathExtValue: ".COM;.EXE;.BAT;.CMD", + string copilotDirectory = @"C:\tools\copilot"; + HashSet existingFiles = new(StringComparer.OrdinalIgnoreCase) + { + Path.Combine(copilotDirectory, "copilot.cmd"), + }; + + string? cliPath = CopilotCliPathResolver.Resolve( + pathValue: $"C:\\other;\"{copilotDirectory}\"", + pathExtValue: null, isWindows: true, - fileExists: _ => true); + fileExists: existingFiles.Contains); - Assert.False(resolution.ShouldOverrideCliPath); - Assert.Null(resolution.CliPath); + Assert.Equal(Path.Combine(copilotDirectory, "copilot.cmd"), cliPath, ignoreCase: true); } [Fact] - public void Resolve_ReportsMissingCopilotWhenDevelopmentPathDoesNotContainIt() + public void Resolve_ReturnsNullWhenPathDoesNotContainCopilot() { - CopilotCliResolution resolution = CopilotCliPathResolver.Resolve( - processPath: @"C:\Program Files\dotnet\dotnet.exe", + string? cliPath = CopilotCliPathResolver.Resolve( pathValue: @"C:\tools;C:\other", pathExtValue: ".COM;.EXE;.BAT;.CMD", isWindows: true, fileExists: _ => false); - Assert.True(resolution.ShouldOverrideCliPath); - Assert.Null(resolution.CliPath); + Assert.Null(cliPath); } }