Compare commits
4 Commits
895f7a9a9c
...
b271b32bf3
| Author | SHA1 | Date | |
|---|---|---|---|
| b271b32bf3 | |||
| a553770fc3 | |||
| 87636453eb | |||
| 8eeb6389df |
22
DevelopmentAgent/DevelopmentAgent.csproj
Normal file
22
DevelopmentAgent/DevelopmentAgent.csproj
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>DeploymentAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.2.25163.2" />
|
||||
<PackageReference Include="Services" Version="2.0.0-alpha.0.149" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Prompts\TestProjectGenerator.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
36
DevelopmentAgent/Program.cs
Normal file
36
DevelopmentAgent/Program.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using DeploymentAgent.Shell;
|
||||
using DeploymentAgent.TestsGenerator;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Services.Gitea;
|
||||
using Services.OpenAI;
|
||||
using Services.Vault;
|
||||
|
||||
IHost builder = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureServices((_, services) =>
|
||||
{
|
||||
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
||||
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
||||
var vaultService = new VaultService(vaultUrl, vaultToken);
|
||||
|
||||
services.AddSingleton<IVaultService>(vaultService);
|
||||
|
||||
services.AddScoped<IGiteaService, GiteaService>();
|
||||
services.AddScoped<IOpenAiService, OpenAiService>();
|
||||
services.AddScoped<IShellRunner, ShellRunner>();
|
||||
services.AddScoped<ITestsGeneratorAgent, TestsGeneratorAgent>();
|
||||
}).Build();
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
Console.WriteLine("Usage: DeploymentAgent <command> [options]");
|
||||
Console.WriteLine("Commands: tests-generator");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return args[0] switch
|
||||
{
|
||||
"tests-generator" => await builder.Services.GetRequiredService<ITestsGeneratorAgent>()
|
||||
.GenerateTestsAsync(args.ElementAtOrDefault(1) ?? string.Empty),
|
||||
_ => 1
|
||||
};
|
||||
147
DevelopmentAgent/Prompts/TestProjectGenerator.txt
Normal file
147
DevelopmentAgent/Prompts/TestProjectGenerator.txt
Normal file
@@ -0,0 +1,147 @@
|
||||
Jesteś C# Developerem specjalizującym się w tworzeniu projektów testowych dla aplikacji .NET.
|
||||
|
||||
Wejście:
|
||||
Dostajesz pojedynczy obiekt JSON w formacie:
|
||||
{
|
||||
"projectName": string, // ścieżka katalogu projektu, dla którego mają zostać wygenerowane testy (np. "BroseCumulativeReport" lub "src/BroseCumulativeReport")
|
||||
"solutionPath": string, // pełna ścieżka do pliku .sln, do którego ma zostać dodany projekt testowy (np. "/Users/user/RiderProjects/FA/FA.sln")
|
||||
"files": [
|
||||
{
|
||||
"path": string, // ścieżka pliku C# względem katalogu solution (np. "BroseCumulativeReport/Services/FooService.cs")
|
||||
"content": string // pełna zawartość pliku C#
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Twoje zadanie:
|
||||
|
||||
1. Przeanalizuj wszystkie pliki z pola "files":
|
||||
- wykryj publiczne klasy, metody i właściwości,
|
||||
- wygeneruj testy jednostkowe pokrywające każdą publiczną metodę i właściwość,
|
||||
- uwzględnij różne scenariusze, w tym przypadki brzegowe i obsługę wyjątków,
|
||||
- użyj xUnit jako frameworka testowego,
|
||||
- tam, gdzie klasy mają zależności (np. przez konstruktor lub wstrzykiwanie interfejsów), sugeruj ich mockowanie za pomocą Moq (np. new Mock<IMyService>()).
|
||||
|
||||
2. Na podstawie wartości "projectName":
|
||||
- przyjmij, że "projectName" wskazuje katalog projektu głównego względem katalogu solution (np. "BroseCumulativeReport" albo "src/BroseCumulativeReport"),
|
||||
- pobierz ostatni segment tej ścieżki jako nazwę projektu głównego, np.:
|
||||
- projectName: "BroseCumulativeReport" → nazwa projektu głównego: "BroseCumulativeReport",
|
||||
- projectName: "src/BroseCumulativeReport" → nazwa projektu głównego: "BroseCumulativeReport",
|
||||
- nazwij projekt testowy "{NazwaProjektuGłównego}.Tests", np. "BroseCumulativeReport.Tests".
|
||||
|
||||
3. Przygotuj plik projektu testowego "{TestProjectName}.csproj" o zawartości:
|
||||
|
||||
- <Project Sdk="Microsoft.NET.Sdk">
|
||||
- <PropertyGroup> z:
|
||||
- <TargetFramework> dopasowany do projektu głównego; jeśli nie znasz dokładnej wartości, użyj "net8.0",
|
||||
- <IsPackable>false</IsPackable>.
|
||||
- <ItemGroup> z referencjami do paczek:
|
||||
- Microsoft.NET.Test.Sdk
|
||||
- xunit
|
||||
- xunit.runner.visualstudio
|
||||
- coverlet.collector
|
||||
- Moq
|
||||
- <ItemGroup> z:
|
||||
- <ProjectReference Include="ŚCIEŻKA_DO_PROJEKTU_GŁÓWNEGO.csproj" />
|
||||
|
||||
WAŻNE (ProjectReference i ścieżki):
|
||||
|
||||
- Przyjmij standardowy układ katalogów:
|
||||
- katalog z solutionPath (ROOT), np. "/Users/user/RiderProjects/FA",
|
||||
- w nim katalog projektu głównego, np. "BroseCumulativeReport" (ostatni segment projectName),
|
||||
- w nim plik .csproj projektu głównego: "{NazwaProjektuGłównego}.csproj" (np. "BroseCumulativeReport/BroseCumulativeReport.csproj"),
|
||||
- projekt testowy w katalogu "{TestProjectName}" obok projektu głównego: "{TestProjectName}" pod ROOT (np. "BroseCumulativeReport.Tests").
|
||||
- Katalog projektu testowego będzie więc:
|
||||
- ROOT/{TestProjectName}, np. "/Users/user/RiderProjects/FA/BroseCumulativeReport.Tests".
|
||||
- Ścieżka w `<ProjectReference Include="...">` musi być RELATYWNA względem katalogu projektu testowego (katalogu, w którym leży plik .csproj testów), czyli:
|
||||
- z "BroseCumulativeReport.Tests" przechodzisz poziom wyżej do ROOT, a następnie do katalogu projektu głównego.
|
||||
- poprawny wzór:
|
||||
"../{NazwaProjektuGłównego}/{NazwaProjektuGłównego}.csproj"
|
||||
- PRZYKŁAD:
|
||||
- solutionPath: "/Users/piotrkus/RiderProjects/FA/FA.sln"
|
||||
- projectName: "BroseCumulativeReport"
|
||||
- nazwa projektu głównego: "BroseCumulativeReport"
|
||||
- katalog projektu testowego: "/Users/piotrkus/RiderProjects/FA/BroseCumulativeReport.Tests"
|
||||
- katalog projektu głównego: "/Users/piotrkus/RiderProjects/FA/BroseCumulativeReport"
|
||||
- wtedy w pliku BroseCumulativeReport.Tests.csproj wstaw:
|
||||
<ProjectReference Include="../BroseCumulativeReport/BroseCumulativeReport.csproj" />
|
||||
- Wygeneruj `<ProjectReference Include="...">` jako LITERALNY tekst w XML, bez używania zmiennych powłoki bash (nie używaj $MAIN_PROJECT_REL_PATH). Wstaw pełną ścieżkę względną względem katalogu projektu testowego.
|
||||
|
||||
4. Zaprojektuj strukturę katalogów testowych:
|
||||
|
||||
- katalog główny projektu testowego: "{TestProjectName}" (np. "BroseCumulativeReport.Tests"),
|
||||
- wewnątrz niego ewentualne podkatalogi odzwierciedlające strukturę namespace/folderów projektu głównego (np. "Model", "Configuration"),
|
||||
- dla każdej publicznej klasy z projektu głównego wygeneruj odpowiadający plik testowy:
|
||||
- np. BroseCumulativeReport.App → AppTests.cs,
|
||||
- BroseCumulativeReport.ExcelGenerator → ExcelGeneratorTests.cs,
|
||||
- BroseCumulativeReport.Model.MainRow → Model/MainRowTests.cs, itd.
|
||||
- w każdej klasie testowej:
|
||||
- wygeneruj metody testowe dla wszystkich publicznych metod i właściwości,
|
||||
- używaj nazewnictwa w stylu: MethodName_ShouldDoSomething_WhenCondition,
|
||||
- korzystaj z [Fact] dla prostych przypadków oraz [Theory] z [InlineData(...)] tam, gdzie można sensownie zaproponować różne dane wejściowe,
|
||||
- stosuj strukturę Arrange / Act / Assert z TODO-komentarzami tam, gdzie potrzebne są decyzje domenowe,
|
||||
- dla zależności korzystaj z Moq (np. var dependency = new Mock<IMyService>(); i dependency.Object wstrzykiwane do konstruktora).
|
||||
|
||||
5. Na podstawie powyższych informacji wygeneruj skrypt powłoki /bin/sh, który:
|
||||
|
||||
- jest samowystarczalny: NIE czyta JSON-a ani żadnych danych wejściowych,
|
||||
- nie przyjmuje żadnych argumentów,
|
||||
- zakłada, że:
|
||||
- plik solution znajduje się dokładnie pod ścieżką podaną w "solutionPath" w wejściowym JSON-ie,
|
||||
- projekt testowy ma być utworzony w tym samym katalogu co plik solution (czyli obok pliku .sln, nie w katalogu projektu głównego).
|
||||
|
||||
Skrypt powinien wykonywać następujące kroki:
|
||||
|
||||
1) Ustawić zmienną SOLUTION_PATH na wartość z pola "solutionPath" (osadzoną na stałe w skrypcie, np. SOLUTION_PATH="/Users/piotrkus/RiderProjects/FA/FA.sln").
|
||||
2) Wyznaczyć SOLUTION_DIR jako katalog nadrzędny pliku solution:
|
||||
SOLUTION_DIR=$(dirname "$SOLUTION_PATH")
|
||||
3) Utworzyć katalog projektu testowego:
|
||||
TEST_PROJECT_NAME="{TestProjectName}" (np. "BroseCumulativeReport.Tests")
|
||||
TEST_PROJECT_DIR="$SOLUTION_DIR/$TEST_PROJECT_NAME"
|
||||
oraz podkatalogi, które zaprojektowałeś (np. "$TEST_PROJECT_DIR/Model", "$TEST_PROJECT_DIR/Configuration").
|
||||
4) W katalogu TEST_PROJECT_DIR zapisać plik "{TestProjectName}.csproj" z wygenerowaną zawartością, używając heredoca.
|
||||
- W pliku .csproj wstaw literalną ścieżkę ProjectReference zgodnie z regułą:
|
||||
"../{NazwaProjektuGłównego}/{NazwaProjektuGłównego}.csproj"
|
||||
- Możesz użyć heredoca z 'EOF', ponieważ nie potrzebujesz interpolacji zmiennych bash wewnątrz XML.
|
||||
5) W katalogu TEST_PROJECT_DIR utworzyć strukturę podkatalogów testowych i zapisać wszystkie wygenerowane pliki testowe (po jednym heredocu na plik).
|
||||
6) Wykonać polecenie:
|
||||
dotnet sln "$SOLUTION_PATH" add "$TEST_PROJECT_DIR/{TestProjectName}.csproj"
|
||||
aby dodać projekt testowy do rozwiązania.
|
||||
7) Wypisać na stderr lub stdout krótki komunikat o sukcesie (np. "Test project {TestProjectName} created and added to solution").
|
||||
|
||||
Skrypt może zawierać podstawowe sprawdzenia:
|
||||
- czy polecenie "dotnet" jest dostępne,
|
||||
- czy plik solution istnieje pod ścieżką SOLUTION_PATH,
|
||||
- w razie krytycznych błędów powinien zakończyć się niezerowym kodem wyjścia.
|
||||
|
||||
WAŻNE:
|
||||
|
||||
- Cała logika analizy kodu C# i generowania treści plików testowych musi być wykonana przez Ciebie PRZED wygenerowaniem skryptu (na podstawie "files").
|
||||
- Skrypt ma tylko:
|
||||
- utworzyć katalog projektu testowego,
|
||||
- zapisać gotowy plik .csproj i pliki testowe (dokładnie takie, jakie wygenerujesz),
|
||||
- dodać projekt do solution.
|
||||
- Skrypt NIE może parsować JSON-a, NIE może czytać plików źródłowych projektu głównego ani próbować ich analizować – wszystko, czego potrzebuje, musi być już wpisane w jego treść.
|
||||
- W szczególności w pliku .csproj:
|
||||
- NIE używaj zmiennych powłoki takich jak $MAIN_PROJECT_REL_PATH wewnątrz `<ProjectReference Include="...">`.
|
||||
- Wstaw pełną ścieżkę względną względem katalogu projektu testowego, np.:
|
||||
<ProjectReference Include="../BroseCumulativeReport/BroseCumulativeReport.csproj" />
|
||||
|
||||
FORMAT ODPOWIEDZI:
|
||||
|
||||
Zwróć dokładnie jeden obiekt JSON o strukturze:
|
||||
|
||||
{
|
||||
"script": "KOMPLETNY SKRYPT /bin/sh",
|
||||
"comments": "Dodatkowe uwagi lub instrukcje dotyczące użycia skryptu"
|
||||
}
|
||||
|
||||
Zasady formatowania:
|
||||
- W polu "script" umieść kompletny skrypt /bin/sh gotowy do zapisania do pliku i uruchomienia bez modyfikacji.
|
||||
- Skrypt w "script" nie może zawierać żadnych znaczników Markdown ani bloków ``` – ma to być czysty tekst skryptu.
|
||||
- W polu "comments" podaj zwięzłe informacje:
|
||||
- jak uruchomić skrypt,
|
||||
- jakie są wymagania (np. zainstalowany .NET SDK),
|
||||
- ewentualne założenia dotyczące ścieżek.
|
||||
- Nie dodawaj żadnego tekstu poza tym jednym obiektem JSON.
|
||||
- Upewnij się, że wygenerowany JSON jest poprawny składniowo (prawidłowe cudzysłowy, przecinki, brak zbędnych znaków).
|
||||
6
DevelopmentAgent/Shell/IShellRunner.cs
Normal file
6
DevelopmentAgent/Shell/IShellRunner.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace DeploymentAgent.Shell;
|
||||
|
||||
public interface IShellRunner
|
||||
{
|
||||
(int ExitCode, string StdOut, string StdErr) RunScript(string script);
|
||||
}
|
||||
35
DevelopmentAgent/Shell/ShellRunner.cs
Normal file
35
DevelopmentAgent/Shell/ShellRunner.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace DeploymentAgent.Shell;
|
||||
|
||||
public class ShellRunner : IShellRunner
|
||||
{
|
||||
public (int ExitCode, string StdOut, string StdErr) RunScript(string script)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/bin/sh",
|
||||
Arguments = "-s",
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = new Process();
|
||||
process.StartInfo = psi;
|
||||
|
||||
process.Start();
|
||||
|
||||
process.StandardInput.WriteLine(script);
|
||||
process.StandardInput.Close();
|
||||
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
var error = process.StandardError.ReadToEnd();
|
||||
|
||||
process.WaitForExit();
|
||||
|
||||
return (process.ExitCode, output, error);
|
||||
}
|
||||
}
|
||||
6
DevelopmentAgent/TestsGenerator/ITestsGeneratorAgent.cs
Normal file
6
DevelopmentAgent/TestsGenerator/ITestsGeneratorAgent.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace DeploymentAgent.TestsGenerator;
|
||||
|
||||
public interface ITestsGeneratorAgent
|
||||
{
|
||||
Task<int> GenerateTestsAsync(string codePath);
|
||||
}
|
||||
7
DevelopmentAgent/TestsGenerator/Models/FileModel.cs
Normal file
7
DevelopmentAgent/TestsGenerator/Models/FileModel.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace DeploymentAgent.TestsGenerator.Models;
|
||||
|
||||
public class FileModel
|
||||
{
|
||||
public string Path { get; set; } = null!;
|
||||
public string Content { get; set; } = null!;
|
||||
}
|
||||
8
DevelopmentAgent/TestsGenerator/Models/RequestModel.cs
Normal file
8
DevelopmentAgent/TestsGenerator/Models/RequestModel.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace DeploymentAgent.TestsGenerator.Models;
|
||||
|
||||
public class RequestModel
|
||||
{
|
||||
public string ProjectName { get; set; } = null!;
|
||||
public string SolutionPath { get; set; } = null!;
|
||||
public List<FileModel> Files { get; set; } = new();
|
||||
}
|
||||
80
DevelopmentAgent/TestsGenerator/TestsGeneratorAgent.cs
Normal file
80
DevelopmentAgent/TestsGenerator/TestsGeneratorAgent.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using DeploymentAgent.Shell;
|
||||
using DeploymentAgent.TestsGenerator.Models;
|
||||
using Services.OpenAI;
|
||||
|
||||
namespace DeploymentAgent.TestsGenerator;
|
||||
|
||||
public class TestsGeneratorAgent(IOpenAiService openAiService, IShellRunner shellRunner) : ITestsGeneratorAgent
|
||||
{
|
||||
public async Task<int> GenerateTestsAsync(string codePath)
|
||||
{
|
||||
var prompt = await File.ReadAllTextAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Prompts",
|
||||
"TestProjectGenerator.txt"));
|
||||
|
||||
var requestModel = GetFormattedJsonRequest(codePath);
|
||||
var decisionDoc = await openAiService.GetResponseFromChat(requestModel, prompt);
|
||||
|
||||
if (decisionDoc == null)
|
||||
{
|
||||
Console.WriteLine("AI‑Gate: no response from LLM");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var generatedTests = decisionDoc.RootElement.GetProperty("script").GetString();
|
||||
var comments = decisionDoc.RootElement.GetProperty("comments").GetString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(generatedTests)) return 1;
|
||||
|
||||
var (exitCode, stdout, stderr) = shellRunner.RunScript(generatedTests);
|
||||
Console.WriteLine("AI‑Gate: PowerShell script executed.");
|
||||
Console.WriteLine("Comments from LLM:");
|
||||
Console.WriteLine(comments);
|
||||
Console.WriteLine("Shell output:");
|
||||
Console.WriteLine(exitCode);
|
||||
Console.WriteLine(stdout);
|
||||
Console.WriteLine(stderr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private RequestModel GetFormattedJsonRequest(string rootPath)
|
||||
{
|
||||
var files = new List<FileModel>();
|
||||
|
||||
foreach (var filePath in Directory.EnumerateFiles(rootPath, "*.cs", SearchOption.AllDirectories))
|
||||
{
|
||||
var content = File.ReadAllText(filePath);
|
||||
files.Add(new FileModel
|
||||
{
|
||||
Path = filePath,
|
||||
Content = content
|
||||
});
|
||||
}
|
||||
|
||||
return new RequestModel
|
||||
{
|
||||
ProjectName = rootPath,
|
||||
SolutionPath = GetSolutionName(rootPath) ?? string.Empty,
|
||||
Files = files
|
||||
};
|
||||
}
|
||||
|
||||
private string? GetSolutionName(string csprojPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(csprojPath))
|
||||
throw new ArgumentException("projectDirectory is null or empty", nameof(csprojPath));
|
||||
|
||||
var dir = new DirectoryInfo(csprojPath);
|
||||
|
||||
while (dir != null)
|
||||
{
|
||||
var sln = dir.GetFiles("*.sln", SearchOption.TopDirectoryOnly)
|
||||
.FirstOrDefault();
|
||||
if (sln != null)
|
||||
return sln.FullName;
|
||||
|
||||
dir = dir.Parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -71,16 +71,15 @@ public class GiteaService : IGiteaService
|
||||
}
|
||||
|
||||
public async Task<ContentsResponse> GetFileContentAsync(RepositoryApi repositoryApi,
|
||||
GiteaConfiguration configuration,
|
||||
string filename)
|
||||
GiteaConfiguration configuration, string filename, string branch = "master")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(repositoryApi);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
throw new ArgumentException("Filename cannot be null or empty.", nameof(filename));
|
||||
|
||||
var repoGetContentsAsync =
|
||||
await repositoryApi.RepoGetContentsAsync(configuration.Owner, configuration.Repository, filename);
|
||||
var repoGetContentsAsync = await repositoryApi.RepoGetContentsAsync(configuration.Owner,
|
||||
configuration.Repository, filename, varRef: branch);
|
||||
return repoGetContentsAsync;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ public interface IGiteaService
|
||||
Task<List<Commit>> GetLastCommitsAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration, int limit);
|
||||
|
||||
Task<ContentsResponse> GetFileContentAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration,
|
||||
string filename);
|
||||
string filename, string branch = "master");
|
||||
|
||||
Task<Commit?> GetCommitByIdAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration, string commitId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user