Skip to content

Commit ca2f40d

Browse files
committed
Adapt to new command pattern
1 parent c253626 commit ca2f40d

File tree

3 files changed

+122
-64
lines changed

3 files changed

+122
-64
lines changed

tools/Azure.Mcp.Tools.Extension/src/Commands/CliGenerateCommand.cs

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the MIT License.
33

44
using Azure.Mcp.Core.Commands;
5+
using Azure.Mcp.Core.Extensions;
6+
using Azure.Mcp.Core.Models.Option;
57
using Azure.Mcp.Tools.Extension.Models;
68
using Azure.Mcp.Tools.Extension.Options;
79
using Azure.Mcp.Tools.Extension.Services;
@@ -13,8 +15,6 @@ public sealed class CliGenerateCommand(ILogger<CliGenerateCommand> logger) : Glo
1315
{
1416
private const string CommandTitle = "Generate CLI Command";
1517
private readonly ILogger<CliGenerateCommand> _logger = logger;
16-
private readonly Option<string> _intentOption = ExtensionOptionDefinitions.CliGenerate.Intent;
17-
private readonly Option<string> _cliTypeOption = ExtensionOptionDefinitions.CliGenerate.CliType;
1818
private readonly string[] _allowedCliTypeValues = ["az"];
1919

2020
public override string Name => "generate";
@@ -26,62 +26,70 @@ public sealed class CliGenerateCommand(ILogger<CliGenerateCommand> logger) : Glo
2626

2727
public override string Title => CommandTitle;
2828

29-
public override ToolMetadata Metadata => new() { Destructive = false, ReadOnly = true };
29+
public override ToolMetadata Metadata => new()
30+
{
31+
Destructive = false,
32+
OpenWorld = false,
33+
Idempotent = true,
34+
ReadOnly = true,
35+
Secret = false,
36+
LocalRequired = false
37+
};
3038

3139
protected override void RegisterOptions(Command command)
3240
{
3341
base.RegisterOptions(command);
34-
command.Options.Add(_intentOption);
35-
command.Options.Add(_cliTypeOption);
42+
command.Options.Add(ExtensionOptionDefinitions.CliGenerate.Intent.AsRequired());
43+
command.Options.Add(ExtensionOptionDefinitions.CliGenerate.CliType.AsRequired());
3644
}
3745

3846
protected override CliGenerateOptions BindOptions(ParseResult parseResult)
3947
{
4048
var options = base.BindOptions(parseResult);
41-
options.Intent = parseResult.GetValue(_intentOption);
42-
options.CliType = parseResult.GetValue(_cliTypeOption);
49+
options.Intent = parseResult.GetValueOrDefault<string>(ExtensionOptionDefinitions.CliGenerate.Intent.Name);
50+
options.CliType = parseResult.GetValueOrDefault<string>(ExtensionOptionDefinitions.CliGenerate.CliType.Name);
4351
return options;
4452
}
4553

4654
public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult)
4755
{
48-
56+
4957
if (!Validate(parseResult.CommandResult, context.Response).IsValid)
5058
{
5159
return context.Response;
5260
}
61+
5362
var options = BindOptions(parseResult);
5463

5564
try
5665
{
57-
ArgumentNullException.ThrowIfNull(options.Intent);
5866
var intent = options.Intent;
59-
var cliType = options.CliType?.ToLowerInvariant();
67+
ArgumentNullException.ThrowIfNull(intent);
6068

61-
if (_allowedCliTypeValues.Contains(cliType)) {
62-
// Only log the cli type when we know for sure it doesn't have private data.
63-
context.Activity?.AddTag("cliType", cliType);
69+
var cliType = options.CliType?.ToLowerInvariant();
6470

65-
if (cliType == Constants.AzureCliType)
66-
{
67-
ICliGenerateService cliGenerateService = context.GetService<ICliGenerateService>();
71+
if (!_allowedCliTypeValues.Contains(cliType))
72+
{
73+
throw new ArgumentException($"Invalid CLI type: {options.CliType}. Supported values are: {string.Join(", ", _allowedCliTypeValues)}");
74+
}
75+
ICliGenerateService cliGenerateService = context.GetService<ICliGenerateService>();
6876

69-
using HttpResponseMessage responseMessage = await cliGenerateService.GenerateAzureCLICommandAsync(intent);
70-
responseMessage.EnsureSuccessStatusCode();
77+
// Only log the cli type when we know for sure it doesn't have private data.
78+
context.Activity?.AddTag("cliType", cliType);
7179

72-
var responseBody = await responseMessage.Content.ReadAsStringAsync();
73-
CliGenerateResult result = new(responseBody, cliType);
74-
context.Response.Results = ResponseResult.Create(result, ExtensionJsonContext.Default.CliGenerateResult);
75-
}
76-
}
77-
else
80+
if (cliType == Constants.AzureCliType)
7881
{
79-
throw new ArgumentException($"Invalid CLI type: {options.CliType}. Supported values are: {string.Join(", ", _allowedCliTypeValues)}");
82+
using HttpResponseMessage responseMessage = await cliGenerateService.GenerateAzureCLICommandAsync(intent);
83+
responseMessage.EnsureSuccessStatusCode();
84+
85+
var responseBody = await responseMessage.Content.ReadAsStringAsync();
86+
CliGenerateResult result = new(responseBody, cliType);
87+
context.Response.Results = ResponseResult.Create(result, ExtensionJsonContext.Default.CliGenerateResult);
8088
}
8189
}
8290
catch (Exception ex)
8391
{
84-
_logger.LogError(ex, "An exception occurred generating cli command. Cli type: {CliType}.", options.CliType);
92+
_logger.LogError(ex, "Error in {Operation}. Options: {@Options}", Name, options);
8593
HandleException(context, ex);
8694
}
8795

tools/Azure.Mcp.Tools.Extension/src/Services/ICliGenerateService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ namespace Azure.Mcp.Tools.Extension.Services;
77

88
public interface ICliGenerateService
99
{
10-
public Task<HttpResponseMessage> GenerateAzureCLICommandAsync(string intent);
10+
public Task<HttpResponseMessage> GenerateAzureCLICommandAsync(
11+
string intent);
1112
}
Lines changed: 86 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,132 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.CommandLine;
45
using System.Net;
5-
using System.Text;
66
using System.Text.Json;
7-
using Azure.Core;
87
using Azure.Mcp.Core.Models.Command;
98
using Azure.Mcp.Core.Services.Http;
109
using Azure.Mcp.Tools.Extension.Commands;
11-
using Azure.Mcp.Tools.Extension.Options;
1210
using Azure.Mcp.Tools.Extension.Services;
1311
using Microsoft.Extensions.DependencyInjection;
1412
using Microsoft.Extensions.Logging;
1513
using NSubstitute;
14+
using NSubstitute.ExceptionExtensions;
1615
using Xunit;
1716

1817
namespace Azure.Mcp.Tools.Extension.UnitTests;
1918

2019
public sealed class CliGenerateCommandTests
2120
{
2221
private readonly IServiceProvider _serviceProvider;
23-
private readonly ILogger<CliGenerateCommand> _logger;
2422
private readonly IHttpClientService _httpClientService;
2523
private readonly ICliGenerateService _cliGenerateService;
24+
private readonly ILogger<CliGenerateCommand> _logger;
25+
private readonly CliGenerateCommand _command;
26+
private readonly CommandContext _context;
27+
private readonly Command _commandDefinition;
2628

2729
public CliGenerateCommandTests()
2830
{
29-
_logger = Substitute.For<ILogger<CliGenerateCommand>>();
30-
3131
_httpClientService = Substitute.For<IHttpClientService>();
3232
_cliGenerateService = Substitute.For<ICliGenerateService>();
33+
_logger = Substitute.For<ILogger<CliGenerateCommand>>();
3334

3435
var collection = new ServiceCollection();
3536
collection.AddSingleton(_httpClientService);
3637
collection.AddSingleton(_cliGenerateService);
37-
3838
_serviceProvider = collection.BuildServiceProvider();
39+
_command = new(_logger);
40+
_context = new(_serviceProvider);
41+
_commandDefinition = _command.GetCommand();
3942
}
4043

4144
[Fact]
42-
public async Task ExecuteAsync_CanAcquireToken()
45+
public void Constructor_InitializesCommandCorrectly()
4346
{
44-
var command = new CliGenerateCommand(_logger);
47+
var command = _command.GetCommand();
48+
Assert.Equal("generate", command.Name);
49+
Assert.NotNull(command.Description);
50+
Assert.NotEmpty(command.Description);
51+
}
4552

46-
const string mockResponseBody = "mock response body";
47-
_cliGenerateService.GenerateAzureCLICommandAsync(Arg.Any<string>())
48-
.Returns(
49-
new HttpResponseMessage(HttpStatusCode.OK)
53+
[Theory]
54+
[InlineData("", false)]
55+
[InlineData("--intent mock_intent", false)]
56+
[InlineData("--cli-type az", false)]
57+
[InlineData("--cli-type wrong_cli_type", false)]
58+
[InlineData("--intent mock_intent --cli-type az", true)]
59+
public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed)
60+
{
61+
// Arrange
62+
if (shouldSucceed)
63+
{
64+
_cliGenerateService.GenerateAzureCLICommandAsync(Arg.Any<string>())
65+
.Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
5066
{
51-
Content = new StringContent(mockResponseBody)
52-
}
53-
);
67+
Content = new StringContent("Command")
68+
}));
69+
}
5470

55-
var mockIntent = "\"Create a resource group named 'TestRG' in the 'eastus' region\"";
56-
var mockCliType = "\"az\"";
57-
var args = command.GetCommand().Parse($"--{ExtensionOptionDefinitions.CliGenerate.IntentName} {mockIntent} --{ExtensionOptionDefinitions.CliGenerate.CliTypeName} {mockCliType}");
58-
var context = new CommandContext(_serviceProvider);
71+
// Build args from a single string in tests using the test-only splitter
72+
var parseResult = _commandDefinition.Parse(args);
5973

60-
try
61-
{
62-
// Act
63-
var response = await command.ExecuteAsync(context, args);
74+
// Act
75+
var response = await _command.ExecuteAsync(_context, parseResult);
6476

65-
// Assert
66-
Assert.NotNull(response);
67-
Assert.Equal(200, response.Status);
77+
// Assert
78+
Assert.Equal(shouldSucceed ? 200 : 400, response.Status);
79+
if (shouldSucceed)
80+
{
6881
Assert.NotNull(response.Results);
69-
using var stream = new MemoryStream();
70-
using var writer = new Utf8JsonWriter(stream);
71-
response.Results.Write(writer);
72-
writer.Flush();
73-
var resultString = Encoding.UTF8.GetString(stream.ToArray());
74-
Assert.Contains(mockResponseBody, resultString);
75-
Assert.Contains(mockCliType, resultString);
82+
Assert.Equal("Success", response.Message);
7683
}
77-
finally
84+
else
7885
{
79-
// Cleanup
80-
// noop
86+
Assert.Contains("required", response.Message.ToLower());
8187
}
8288
}
89+
90+
[Fact]
91+
public async Task ExecuteAsync_DeserializationValidation()
92+
{
93+
// Arrange
94+
_cliGenerateService.GenerateAzureCLICommandAsync(Arg.Any<string>())
95+
.Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
96+
{
97+
Content = new StringContent("Command")
98+
}));
99+
100+
var parseResult = _commandDefinition.Parse("--intent mock_intent --cli-type az");
101+
102+
// Act
103+
var response = await _command.ExecuteAsync(_context, parseResult);
104+
105+
// Assert
106+
Assert.Equal(200, response.Status);
107+
Assert.NotNull(response.Results);
108+
109+
var json = JsonSerializer.Serialize(response.Results);
110+
var result = JsonSerializer.Deserialize(json, ExtensionJsonContext.Default.CliGenerateResult);
111+
112+
Assert.NotNull(result);
113+
Assert.Equal("az", result.CliType);
114+
Assert.Equal("Command", result.Command);
115+
}
116+
117+
[Fact]
118+
public async Task ExecuteAsync_HandlesServiceErrors()
119+
{
120+
// Arrange
121+
_cliGenerateService.GenerateAzureCLICommandAsync(Arg.Any<string>()).ThrowsAsync(new Exception("Test error"));
122+
123+
var parseResult = _commandDefinition.Parse("--intent mock_intent --cli-type az");
124+
125+
// Act
126+
var response = await _command.ExecuteAsync(_context, parseResult);
127+
128+
// Assert
129+
Assert.Equal(500, response.Status);
130+
Assert.Contains("Test error", response.Message);
131+
}
83132
}

0 commit comments

Comments
 (0)