Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -403,4 +403,141 @@ public async Task ListFiles_EmptyDirectoryPath_DefaultsToRoot()
Assert.That(entries, Is.Not.Null);
Assert.That(entries, Does.Contain("rootfile.txt"));
}

// ── RenameFile tests ──

[Test]
public async Task RenameFile_Success_RenamesFile()
{
var tool = FileTools.CreateRenameFileTool(baseDir);
var oldName = $"rename_src_{Guid.NewGuid()}.txt";
var newName = $"rename_dst_{Guid.NewGuid()}.txt";
File.WriteAllText(Path.Combine(baseDir, oldName), "content");

var result = await tool.InvokeAsync(new AIFunctionArguments
{
["oldFilePath"] = oldName,
["newFilePath"] = newName
});

Assert.That(result?.ToString(), Does.Contain("Successfully renamed"));
Assert.That(File.Exists(Path.Combine(baseDir, newName)), Is.True);
Assert.That(File.Exists(Path.Combine(baseDir, oldName)), Is.False);
Assert.That(File.ReadAllText(Path.Combine(baseDir, newName)), Is.EqualTo("content"));
}

[Test]
public void RenameFile_SourceDoesNotExist_ThrowsArgumentException()
{
var tool = FileTools.CreateRenameFileTool(baseDir);

var ex = Assert.ThrowsAsync<ArgumentException>(async () =>
await tool.InvokeAsync(new AIFunctionArguments
{
["oldFilePath"] = "nonexistent.txt",
["newFilePath"] = "new.txt"
}));
Assert.That(ex!.Message, Does.Contain("does not exist"));
}

[Test]
public void RenameFile_DestinationAlreadyExists_ThrowsArgumentException()
{
var tool = FileTools.CreateRenameFileTool(baseDir);
var oldName = $"rename_exists_src_{Guid.NewGuid()}.txt";
var newName = $"rename_exists_dst_{Guid.NewGuid()}.txt";
File.WriteAllText(Path.Combine(baseDir, oldName), "old");
File.WriteAllText(Path.Combine(baseDir, newName), "new");

var ex = Assert.ThrowsAsync<ArgumentException>(async () =>
await tool.InvokeAsync(new AIFunctionArguments
{
["oldFilePath"] = oldName,
["newFilePath"] = newName
}));
Assert.That(ex!.Message, Does.Contain("already exists"));
}

[Test]
public void RenameFile_EmptyOldPath_ThrowsArgumentException()
{
var tool = FileTools.CreateRenameFileTool(baseDir);

var ex = Assert.ThrowsAsync<ArgumentException>(async () =>
await tool.InvokeAsync(new AIFunctionArguments
{
["oldFilePath"] = "",
["newFilePath"] = "new.txt"
}));
Assert.That(ex!.Message, Does.Contain("cannot be null or empty"));
}

[Test]
public void RenameFile_EmptyNewPath_ThrowsArgumentException()
{
var tool = FileTools.CreateRenameFileTool(baseDir);

var ex = Assert.ThrowsAsync<ArgumentException>(async () =>
await tool.InvokeAsync(new AIFunctionArguments
{
["oldFilePath"] = "rootfile.txt",
["newFilePath"] = ""
}));
Assert.That(ex!.Message, Does.Contain("cannot be null or empty"));
}

[Test]
public void RenameFile_PathOutsideBaseDir_ThrowsArgumentException()
{
var tool = FileTools.CreateRenameFileTool(baseDir);

var ex = Assert.ThrowsAsync<ArgumentException>(async () =>
await tool.InvokeAsync(new AIFunctionArguments
{
["oldFilePath"] = "rootfile.txt",
["newFilePath"] = "../outside.txt"
}));
Assert.That(ex!.Message, Does.Contain("outside the allowed base directory"));
}

[Test]
public async Task RenameFile_CallbackInvoked_OnSuccess()
{
string? capturedOld = null;
string? capturedNew = null;
var tool = FileTools.CreateRenameFileTool(baseDir,
onFileRenamed: (o, n) => { capturedOld = o; capturedNew = n; });

var oldName = $"rename_cb_src_{Guid.NewGuid()}.txt";
var newName = $"rename_cb_dst_{Guid.NewGuid()}.txt";
File.WriteAllText(Path.Combine(baseDir, oldName), "content");

await tool.InvokeAsync(new AIFunctionArguments
{
["oldFilePath"] = oldName,
["newFilePath"] = newName
});

Assert.That(capturedOld, Is.EqualTo(oldName));
Assert.That(capturedNew, Is.EqualTo(newName));
}

[Test]
public async Task RenameFile_CreatesDestinationDirectoryIfNeeded()
{
var tool = FileTools.CreateRenameFileTool(baseDir);
var oldName = $"rename_mkdir_src_{Guid.NewGuid()}.txt";
var newDir = $"newdir_{Guid.NewGuid()}";
var newName = Path.Combine(newDir, "moved.txt");
File.WriteAllText(Path.Combine(baseDir, oldName), "content");

await tool.InvokeAsync(new AIFunctionArguments
{
["oldFilePath"] = oldName,
["newFilePath"] = newName
});

Assert.That(File.Exists(Path.Combine(baseDir, newName)), Is.True);
Assert.That(File.ReadAllText(Path.Combine(baseDir, newName)), Is.EqualTo("content"));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Azure.Sdk.Tools.Cli.CopilotAgents;
using Azure.Sdk.Tools.Cli.Helpers;
using Azure.Sdk.Tools.Cli.Models;
using Azure.Sdk.Tools.Cli.Tests.TestHelpers;
Expand Down Expand Up @@ -62,7 +63,7 @@ public async Task FindSamplesDirectory_WithSampleFiles_ReturnsSamplesDirectory()
CreateTestFile(packagePath, "tests/unit/other.cs", "namespace Test; public class NotASample { }");

var packageInfoHelper = new PackageInfoHelper(NullLogger<PackageInfoHelper>.Instance, gitHelper);
var helper = new DotnetLanguageService(processHelper, powershellHelper, gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());
var helper = new DotnetLanguageService(processHelper, powershellHelper, Mock.Of<ICopilotAgentRunner>(), gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());

// Act
var packageInfo = await helper.GetPackageInfo(packagePath);
Expand All @@ -85,7 +86,7 @@ public async Task FindSamplesDirectory_WithSnippetFiles_ReturnsSamplesDirectory(
CreateTestFile(packagePath, "tests/unit/UnitTest.cs", "namespace Test; public class UnitTest { }");

var packageInfoHelper = new PackageInfoHelper(NullLogger<PackageInfoHelper>.Instance, gitHelper);
var helper = new DotnetLanguageService(processHelper, powershellHelper, gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());
var helper = new DotnetLanguageService(processHelper, powershellHelper, Mock.Of<ICopilotAgentRunner>(), gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());

// Act
var packageInfo = await helper.GetPackageInfo(packagePath);
Expand All @@ -104,7 +105,7 @@ public async Task FindSamplesDirectory_WithNoSampleFiles_ReturnsDefaultPath()
CreateTestFile(packagePath, "tests/unit/other.cs", "namespace Test; public class NotASample { }");

var packageInfoHelper = new PackageInfoHelper(NullLogger<PackageInfoHelper>.Instance, gitHelper);
var helper = new DotnetLanguageService(processHelper, powershellHelper, gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());
var helper = new DotnetLanguageService(processHelper, powershellHelper, Mock.Of<ICopilotAgentRunner>(), gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());

// Act
var packageInfo = await helper.GetPackageInfo(packagePath);
Expand All @@ -120,7 +121,7 @@ public async Task FindSamplesDirectory_WithNoTestsDirectory_ReturnsDefaultPath()
var (packagePath, gitHelper, processHelper, powershellHelper, commonValidationHelpers) = await CreateTestPackageAsync();

var packageInfoHelper = new PackageInfoHelper(NullLogger<PackageInfoHelper>.Instance, gitHelper);
var helper = new DotnetLanguageService(processHelper, powershellHelper, gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());
var helper = new DotnetLanguageService(processHelper, powershellHelper, Mock.Of<ICopilotAgentRunner>(), gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());

// Act
var packageInfo = await helper.GetPackageInfo(packagePath);
Expand All @@ -140,7 +141,7 @@ public async Task FindSamplesDirectory_WithSnippetRegions_ReturnsCorrectDirector
CreateTestFile(packagePath, "tests/examples/ExampleSNIPPET.cs", "#region Snippet:AnotherExample\nnamespace Test; public class ExampleSNIPPET { }\n#endregion");

var packageInfoHelper = new PackageInfoHelper(NullLogger<PackageInfoHelper>.Instance, gitHelper);
var helper = new DotnetLanguageService(processHelper, powershellHelper, gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());
var helper = new DotnetLanguageService(processHelper, powershellHelper, Mock.Of<ICopilotAgentRunner>(), gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());

// Act
var packageInfo = await helper.GetPackageInfo(packagePath);
Expand All @@ -160,7 +161,7 @@ public async Task FindSamplesDirectory_WithMultipleDirectories_ReturnsFirstFound
CreateTestFile(packagePath, "tests/snippets/BasicSnippet.cs", "#region Snippet:AnotherSnippet\nnamespace Test; public class BasicSnippet { }\n#endregion");

var packageInfoHelper = new PackageInfoHelper(NullLogger<PackageInfoHelper>.Instance, gitHelper);
var helper = new DotnetLanguageService(processHelper, powershellHelper, gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());
var helper = new DotnetLanguageService(processHelper, powershellHelper, Mock.Of<ICopilotAgentRunner>(), gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());

// Act
var packageInfo = await helper.GetPackageInfo(packagePath);
Expand Down Expand Up @@ -197,7 +198,7 @@ public async Task GetPackageInfo_ParsesSdkTypeFromMSBuild(string sdkTypeValue, S

var realProcessHelper = new ProcessHelper(new TestLogger<ProcessHelper>(), Mock.Of<IRawOutputHelper>());
var packageInfoHelper = new PackageInfoHelper(NullLogger<PackageInfoHelper>.Instance, gitHelper);
var helper = new DotnetLanguageService(realProcessHelper, powershellHelper, gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());
var helper = new DotnetLanguageService(realProcessHelper, powershellHelper, Mock.Of<ICopilotAgentRunner>(), gitHelper, new TestLogger<DotnetLanguageService>(), commonValidationHelpers, packageInfoHelper, Mock.Of<IFileHelper>(), Mock.Of<ISpecGenSdkConfigHelper>(), Mock.Of<IChangelogHelper>());

// Act
var packageInfo = await helper.GetPackageInfo(packagePath);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Sdk.Tools.Cli.Prompts.Templates;

namespace Azure.Sdk.Tools.Cli.Tests.Prompts.Templates;

[TestFixture]
public class DotnetErrorDrivenPatchTemplateTests
{
private const string SampleBuildContext = """
error CS0117: 'WidgetClient' does not contain a definition for 'GetWidgetAsync'
error CS0246: The type or namespace name 'WidgetOptions' could not be found
""";

private const string SamplePackagePath = "/sdk/widget/Azure.Widget";
private const string SampleCustomizationRoot = "/sdk/widget/Azure.Widget/src";

private static readonly List<string> SampleCustomizationFiles =
[
"src/WidgetClientExtensions.cs",
"src/Customized/WidgetOptionsHelper.cs"
];

private static readonly List<string> SamplePatchFilePaths =
[
"WidgetClientExtensions.cs",
"Customized/WidgetOptionsHelper.cs"
];

private DotnetErrorDrivenPatchTemplate CreateTemplate() =>
new(SampleBuildContext, SamplePackagePath, SampleCustomizationRoot,
SampleCustomizationFiles, SamplePatchFilePaths);

[Test]
public void TemplateId_IsDotnetErrorDrivenPatch()
{
var template = CreateTemplate();
Assert.That(template.TemplateId, Is.EqualTo("dotnet-error-driven-patch"));
}

[Test]
public void Version_Is1_0_0()
{
var template = CreateTemplate();
Assert.That(template.Version, Is.EqualTo("1.0.0"));
}

[Test]
public void Description_IsNotEmpty()
{
var template = CreateTemplate();
Assert.That(template.Description, Is.Not.Null.And.Not.Empty);
}

[Test]
public void BuildPrompt_ContainsBuildContext()
{
var prompt = CreateTemplate().BuildPrompt();
Assert.That(prompt, Does.Contain("CS0117"));
Assert.That(prompt, Does.Contain("GetWidgetAsync"));
}

[Test]
public void BuildPrompt_ContainsCustomizationFiles()
{
var prompt = CreateTemplate().BuildPrompt();
Assert.Multiple(() =>
{
Assert.That(prompt, Does.Contain("WidgetClientExtensions.cs"));
Assert.That(prompt, Does.Contain("WidgetOptionsHelper.cs"));
});
}

[Test]
public void BuildPrompt_ContainsPackagePath()
{
var prompt = CreateTemplate().BuildPrompt();
Assert.That(prompt, Does.Contain(SamplePackagePath));
}

[Test]
public void BuildPrompt_ContainsCustomizationRoot()
{
var prompt = CreateTemplate().BuildPrompt();
Assert.That(prompt, Does.Contain(SampleCustomizationRoot));
}

[Test]
public void BuildPrompt_ContainsDotnetSpecificGuidance()
{
var prompt = CreateTemplate().BuildPrompt();
Assert.Multiple(() =>
{
Assert.That(prompt, Does.Contain("partial class").IgnoreCase);
Assert.That(prompt, Does.Contain("Generated/"));
Assert.That(prompt, Does.Contain(".cs"));
Assert.That(prompt, Does.Contain(".NET"));
});
}

[Test]
public void BuildPrompt_ContainsToolDescriptions()
{
var prompt = CreateTemplate().BuildPrompt();
Assert.Multiple(() =>
{
Assert.That(prompt, Does.Contain("GrepSearch"));
Assert.That(prompt, Does.Contain("ReadFile"));
Assert.That(prompt, Does.Contain("CodePatchTool"));
});
}

[Test]
public void BuildPrompt_ContainsSafetyConstraints()
{
var prompt = CreateTemplate().BuildPrompt();
Assert.Multiple(() =>
{
Assert.That(prompt, Does.Contain("CUSTOMIZATION FILES ONLY"));
Assert.That(prompt, Does.Contain("SAFE PATCHES ONLY"));
Assert.That(prompt, Does.Contain("SURGICAL PATCHING"));
Assert.That(prompt, Does.Contain("NO DUPLICATE PATCHES"));
});
}

[Test]
public void BuildPrompt_ContainsWorkflowSteps()
{
var prompt = CreateTemplate().BuildPrompt();
Assert.Multiple(() =>
{
Assert.That(prompt, Does.Contain("Step 1"));
Assert.That(prompt, Does.Contain("Step 2"));
Assert.That(prompt, Does.Contain("Step 3"));
Assert.That(prompt, Does.Contain("Step 4"));
});
}

[Test]
public void BuildPrompt_ContainsReadFilePaths()
{
var prompt = CreateTemplate().BuildPrompt();
foreach (var file in SampleCustomizationFiles)
{
Assert.That(prompt, Does.Contain(file),
$"Expected prompt to contain ReadFile path: {file}");
}
}

[Test]
public void BuildPrompt_ContainsPatchFilePaths()
{
var prompt = CreateTemplate().BuildPrompt();
foreach (var file in SamplePatchFilePaths)
{
Assert.That(prompt, Does.Contain(file),
$"Expected prompt to contain CodePatchTool path: {file}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Azure.Sdk.Tools.Cli.Tests.TestHelpers;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Azure.Sdk.Tools.Cli.CopilotAgents;

namespace Azure.Sdk.Tools.Cli.Tests.Services.Languages;

Expand Down Expand Up @@ -31,6 +32,7 @@ public void SetUp()
_languageChecks = new DotnetLanguageService(
_processHelperMock.Object,
_powerShellHelperMock.Object,
Mock.Of<ICopilotAgentRunner>(),
_gitHelperMock.Object,
NullLogger<DotnetLanguageService>.Instance,
_commonValidationHelperMock.Object,
Expand Down
Loading