-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(agent): Support creating unit tests (#28)
- Loading branch information
Showing
19 changed files
with
896 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
src/FlowPair/Agent/Operations/CreateUnitTest/CreateUnitTestChatDefinition.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
using Ciandt.FlowTools.FlowPair.Agent.Infrastructure; | ||
using Ciandt.FlowTools.FlowPair.Agent.Operations.CreateUnitTest.v1; | ||
using Ciandt.FlowTools.FlowPair.Chats.Contracts.v1; | ||
using Ciandt.FlowTools.FlowPair.Chats.Models; | ||
using Ciandt.FlowTools.FlowPair.Chats.Services; | ||
|
||
namespace Ciandt.FlowTools.FlowPair.Agent.Operations.CreateUnitTest; | ||
|
||
public interface ICreateUnitTestChatDefinition : IChatDefinition<CreateUnitTestResponse>; | ||
|
||
public sealed class CreateUnitTestChatDefinition( | ||
AgentJsonContext jsonContext) | ||
: ICreateUnitTestChatDefinition | ||
{ | ||
private const string CodeResponseKey = "Markdown"; | ||
private const string JsonResponseKey = "JSON"; | ||
|
||
public ChatScript ChatScript { get; } = new( | ||
"Create unit tests chat script", | ||
[ | ||
/* Python */".py", ".pyw", ".pyx", ".pxd", ".pxi", | ||
/* JavaScript */".js", ".jsx", ".mjs", ".cjs", | ||
/* Java */".java", | ||
/* C# */".cs", ".csx", | ||
/* C++ */".cpp", ".cxx", ".cc", ".c++", ".hpp", ".hxx", ".h", ".hh", ".h++", | ||
/* PHP */".php", ".phtml", ".phps", | ||
/* Ruby */".rb", ".rbw", ".rake", | ||
/* Swift */".swift", | ||
/* R */".r", | ||
/* SQL */".sql", | ||
/* Kotlin */".kt", ".kts", | ||
/* TypeScript */".ts", ".tsx", | ||
/* Go (Golang) */".go", | ||
/* Rust */".rs", | ||
/* Scala */".scala", ".sc", | ||
/* Dart */".dart", | ||
/* Perl */".pl", ".pm", ".t", ".pod", | ||
/* MATLAB */".m", | ||
], | ||
""" | ||
You are an expert developer, your task is to create unit tests following the best practices. | ||
You are given a set of project files, containing the filenames and their contents. | ||
""", | ||
[ | ||
Instruction.StepInstruction.Of( | ||
"Create unit tests for the specified code"), | ||
Instruction.StepInstruction.Of( | ||
"Ensure the unit test cover every path"), | ||
Instruction.StepInstruction.Of( | ||
"Ensure the unit test does not create any mutants on mutation analysis"), | ||
Instruction.CodeExtractInstruction.Of( | ||
CodeResponseKey, | ||
"Return only the unit tests code inside a code block (```)"), | ||
Instruction.StepInstruction.Of( | ||
"Where the new file for the unit tests should be located " + | ||
"according to language and project standards?"), | ||
Instruction.JsonConvertInstruction.Of( | ||
JsonResponseKey, | ||
""" | ||
Reply the file path in a valid JSON format. | ||
The schema of the JSON object must be: | ||
""", | ||
FilePathResponse.Schema), | ||
]); | ||
|
||
public Result<object, string> Parse(string key, string input) => key switch | ||
{ | ||
CodeResponseKey => MarkdownCodeExtractor | ||
.TryExtractSingle(input) | ||
.Select(static object (x) => x), | ||
JsonResponseKey => JsonContentDeserializer | ||
.TryDeserialize(input, jsonContext.FilePathResponse) | ||
.Select(static object (x) => x), | ||
_ => $"Unknown output key '{key}'" | ||
}; | ||
|
||
public Option<CreateUnitTestResponse> ConvertResult(ChatWorkspace chatWorkspace) => | ||
from filePath in OutputProcessor.GetFirst<FilePathResponse>(chatWorkspace, JsonResponseKey) | ||
from codeSnippet in OutputProcessor.GetFirst<CodeSnippet>(chatWorkspace, CodeResponseKey) | ||
select new CreateUnitTestResponse(filePath.FilePath, codeSnippet.Content); | ||
} |
121 changes: 121 additions & 0 deletions
121
src/FlowPair/Agent/Operations/CreateUnitTest/CreateUnitTestCommand.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
using System.IO.Abstractions; | ||
using System.Text; | ||
using Ciandt.FlowTools.FlowPair.Agent.Operations.CreateUnitTest.v1; | ||
using Ciandt.FlowTools.FlowPair.Agent.Operations.Login; | ||
using Ciandt.FlowTools.FlowPair.Agent.Services; | ||
using Ciandt.FlowTools.FlowPair.Chats.Models; | ||
using Ciandt.FlowTools.FlowPair.Chats.Services; | ||
using Ciandt.FlowTools.FlowPair.Common; | ||
using Ciandt.FlowTools.FlowPair.LocalFileSystem.Services; | ||
using ConsoleAppFramework; | ||
using Spectre.Console; | ||
|
||
namespace Ciandt.FlowTools.FlowPair.Agent.Operations.CreateUnitTest; | ||
|
||
public sealed class CreateUnitTestCommand( | ||
IAnsiConsole console, | ||
IFileSystem fileSystem, | ||
ICreateUnitTestChatDefinition chatDefinition, | ||
IWorkingDirectoryWalker workingDirectoryWalker, | ||
IProjectFilesMessageFactory projectFilesMessageFactory, | ||
IDirectoryStructureMessageFactory directoryStructureMessageFactory, | ||
ILoginUseCase loginUseCase, | ||
IChatService chatService) | ||
{ | ||
/// <summary> | ||
/// Create unit test for the code on the file. | ||
/// </summary> | ||
/// <param name="filePath">-f, The file path of the code to test.</param> | ||
/// <param name="exampleFilePath">-e, The example unit test file path.</param> | ||
[Command("create-unittest")] | ||
public int Execute( | ||
string filePath, | ||
string? exampleFilePath = null) | ||
{ | ||
var fileInfo = fileSystem.FileInfo.New(PathAnalyzer.Normalize(filePath)); | ||
if (!fileInfo.Exists) | ||
{ | ||
console.MarkupLine("[red]Error:[/] The specified file does not exist."); | ||
return 1; | ||
} | ||
|
||
var exampleFileInfo = exampleFilePath is not null | ||
? fileSystem.FileInfo.New(PathAnalyzer.Normalize(exampleFilePath)) | ||
: null; | ||
if (exampleFileInfo is not null && !exampleFileInfo.Exists) | ||
{ | ||
console.MarkupLine("[red]Error:[/] The specified example file does not exist."); | ||
return 2; | ||
} | ||
|
||
return (from rootPath in workingDirectoryWalker.TryFindRepositoryRoot(fileInfo.Directory?.FullName) | ||
.OkOrElse(HandleFindRepositoryRootError) | ||
from session in loginUseCase.Execute(isBackground: true) | ||
.UnwrapErrOr(0) | ||
.Ensure(n => n == 0, 4) | ||
let initialMessages = BuildInitialMessages(fileInfo, exampleFileInfo, rootPath).ToList() | ||
from response in chatService | ||
.Run(console.Progress(), LlmModelType.Claude35Sonnet, chatDefinition, initialMessages) | ||
.MapErr(HandleChatServiceError) | ||
let testFile = CreateUnitTestFile(rootPath, response) | ||
select 0) | ||
.UnwrapEither(); | ||
} | ||
|
||
private int HandleFindRepositoryRootError() | ||
{ | ||
console.MarkupLine("[red]Error:[/] Could not locate Git repository."); | ||
return 3; | ||
} | ||
|
||
private int HandleChatServiceError(string errorMessage) | ||
{ | ||
console.MarkupLineInterpolated($"[red]Error:[/] {errorMessage}"); | ||
return 5; | ||
} | ||
|
||
private IEnumerable<Message> BuildInitialMessages( | ||
IFileInfo fileInfo, | ||
IFileInfo? exampleFileInfo, | ||
IDirectoryInfo rootPath) | ||
{ | ||
yield return projectFilesMessageFactory.CreateWithProjectFilesContent(rootPath); | ||
yield return directoryStructureMessageFactory.CreateWithRepositoryStructure(rootPath); | ||
|
||
if (exampleFileInfo is not null) | ||
{ | ||
yield return new Message( | ||
SenderRole.User, | ||
$""" | ||
Unit tests example: | ||
``` | ||
{exampleFileInfo.ReadAllText()} | ||
``` | ||
"""); | ||
} | ||
|
||
yield return new Message( | ||
SenderRole.User, | ||
$""" | ||
The source file to be tested is located at '{rootPath.GetRelativePath(fileInfo.FullName)}' and its content is: | ||
``` | ||
{fileInfo.ReadAllText()} | ||
``` | ||
"""); | ||
} | ||
|
||
private Unit CreateUnitTestFile(IDirectoryInfo rootPath, CreateUnitTestResponse response) | ||
{ | ||
var normalizedFilePath = PathAnalyzer.Normalize(response.FilePath); | ||
|
||
var testPath = fileSystem.Path.Combine(rootPath.FullName, normalizedFilePath); | ||
|
||
var dirPath = fileSystem.Path.GetDirectoryName(testPath); | ||
if (dirPath != null && !fileSystem.Directory.Exists(dirPath)) | ||
fileSystem.Directory.CreateDirectory(dirPath); | ||
|
||
fileSystem.File.WriteAllText(testPath, response.Content, Encoding.UTF8); | ||
console.MarkupLineInterpolated($"[green]File created:[/] {response.FilePath}"); | ||
return Unit(); | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
src/FlowPair/Agent/Operations/CreateUnitTest/v1/CreateUnitTestResponse.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
namespace Ciandt.FlowTools.FlowPair.Agent.Operations.CreateUnitTest.v1; | ||
|
||
public sealed record CreateUnitTestResponse( | ||
string FilePath, | ||
string Content); |
23 changes: 23 additions & 0 deletions
23
src/FlowPair/Agent/Operations/CreateUnitTest/v1/FilePathResponse.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
namespace Ciandt.FlowTools.FlowPair.Agent.Operations.CreateUnitTest.v1; | ||
|
||
public sealed record FilePathResponse( | ||
string FilePath) | ||
{ | ||
public const string Schema = | ||
""" | ||
{ | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"type": "object", | ||
"description": "Represents a response for creating a unit test file", | ||
"properties": { | ||
"filePath": { | ||
"type": "string", | ||
"description": "The file path of the created unit test (relative to the repository root directory)", | ||
"minLength": 1 | ||
} | ||
}, | ||
"required": ["filePath"], | ||
"additionalProperties": false | ||
} | ||
"""; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.