Skip to content

Commit edd3e19

Browse files
committed
feat: Add project integration tests and enhance project resolution logic
1 parent c04710d commit edd3e19

4 files changed

Lines changed: 364 additions & 23 deletions

File tree

BlazorExpress.ChartJS.MCP.Tests/ChartExampleGeneratorTests.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,19 @@ public void Build()
193193
process.StartInfo.UseShellExecute = false;
194194
process.Start();
195195

196-
var output = process.StandardOutput.ReadToEnd();
197-
var error = process.StandardError.ReadToEnd();
198-
process.WaitForExit(120_000);
196+
var outputTask = process.StandardOutput.ReadToEndAsync();
197+
var errorTask = process.StandardError.ReadToEndAsync();
198+
var exited = process.WaitForExit(120_000);
199+
if (!exited)
200+
{
201+
process.Kill(entireProcessTree: true);
202+
process.WaitForExit(30_000);
203+
}
204+
205+
var output = outputTask.GetAwaiter().GetResult();
206+
var error = errorTask.GetAwaiter().GetResult();
199207

200-
Assert.True(process.ExitCode == 0, output + Environment.NewLine + error);
208+
Assert.True(exited && process.ExitCode == 0, output + Environment.NewLine + error);
201209
}
202210

203211
public void Dispose()

BlazorExpress.ChartJS.MCP.Tests/ChartJsMcpToolsTests.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,45 @@ public void PreviewProjectIntegration_Returns_Structured_Error_For_Invalid_Targe
4444
AssertToolError(json, "targetProjectPath");
4545
}
4646

47+
[Fact]
48+
public void PreviewProjectIntegration_Returns_Structured_Error_For_Ambiguous_Folder()
49+
{
50+
using var workspace = new TemporaryWorkspace();
51+
var firstProject = workspace.CreateWebAssemblyProject(Path.Combine("src", "FirstApp"), "FirstApp");
52+
var secondProject = workspace.CreateWebAssemblyProject(Path.Combine("src", "SecondApp"), "SecondApp");
53+
54+
var json = ChartJsMcpTools.PreviewProjectIntegration(workspace.Root, "Bar");
55+
56+
AssertToolError(json, "targetProjectPath");
57+
Assert.Contains(EscapeJsonPath(Path.Combine(firstProject, "FirstApp.csproj")), json);
58+
Assert.Contains(EscapeJsonPath(Path.Combine(secondProject, "SecondApp.csproj")), json);
59+
}
60+
61+
[Fact]
62+
public void PreviewProjectIntegration_Resolves_Nested_Single_Blazor_Project()
63+
{
64+
using var workspace = new TemporaryWorkspace();
65+
var project = workspace.CreateWebAssemblyProject(Path.Combine("src", "App"), "App");
66+
67+
var json = ChartJsMcpTools.PreviewProjectIntegration(workspace.Root, "Bar");
68+
69+
Assert.DoesNotContain("\"success\": false", json);
70+
Assert.Contains(EscapeJsonPath(Path.Combine(project, "App.csproj")), json);
71+
}
72+
73+
[Fact]
74+
public void PreviewDashboardIntegration_Uses_Same_Project_Resolver()
75+
{
76+
using var workspace = new TemporaryWorkspace();
77+
var project = workspace.CreateWebAssemblyProject(Path.Combine("src", "App"), "App");
78+
79+
var json = ChartJsMcpTools.PreviewDashboardIntegration(workspace.Root, title: "Charts");
80+
81+
Assert.DoesNotContain("\"success\": false", json);
82+
Assert.Contains("ChartDashboardPage.razor", json);
83+
Assert.Contains(EscapeJsonPath(Path.Combine(project, "App.csproj")), json);
84+
}
85+
4786
[Fact]
4887
public void GenerateChartExample_Accepts_Color_String()
4988
{
@@ -108,4 +147,52 @@ private static void AssertToolError(string json, string? expectedField = null)
108147
if (expectedField is not null)
109148
Assert.Equal(expectedField, document.RootElement.GetProperty("error").GetProperty("field").GetString());
110149
}
150+
151+
private static string EscapeJsonPath(string path) =>
152+
path.Replace("\\", "\\\\", StringComparison.Ordinal);
153+
154+
private sealed class TemporaryWorkspace : IDisposable
155+
{
156+
private readonly string root = Path.Combine(Path.GetTempPath(), $"bex-chartjs-mcp-tool-tests-{Guid.NewGuid():N}");
157+
158+
public string Root => root;
159+
160+
public string CreateWebAssemblyProject(string relativePath, string projectName)
161+
{
162+
var projectRoot = Path.Combine(root, relativePath);
163+
Directory.CreateDirectory(projectRoot);
164+
Directory.CreateDirectory(Path.Combine(projectRoot, "wwwroot"));
165+
Directory.CreateDirectory(Path.Combine(projectRoot, "Pages"));
166+
Directory.CreateDirectory(Path.Combine(projectRoot, "Shared"));
167+
168+
File.WriteAllText(Path.Combine(projectRoot, $"{projectName}.csproj"), """
169+
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
170+
<PropertyGroup>
171+
<TargetFramework>net10.0</TargetFramework>
172+
</PropertyGroup>
173+
</Project>
174+
""");
175+
File.WriteAllText(Path.Combine(projectRoot, "_Imports.razor"), "@using Microsoft.AspNetCore.Components" + Environment.NewLine);
176+
File.WriteAllText(Path.Combine(projectRoot, "wwwroot", "index.html"), """
177+
<html>
178+
<body>
179+
<div id="app"></div>
180+
</body>
181+
</html>
182+
""");
183+
File.WriteAllText(Path.Combine(projectRoot, "Shared", "NavMenu.razor"), """
184+
<nav>
185+
<NavLink class="nav-link" href="">Home</NavLink>
186+
</nav>
187+
""");
188+
189+
return projectRoot;
190+
}
191+
192+
public void Dispose()
193+
{
194+
if (Directory.Exists(root))
195+
Directory.Delete(root, recursive: true);
196+
}
197+
}
111198
}

BlazorExpress.ChartJS.MCP.Tests/ProjectIntegrationServiceTests.cs

Lines changed: 160 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,113 @@ public void Preview_For_WebAssembly_Project_Creates_Plan_Without_Writing_Files()
2525
Assert.False(File.Exists(Path.Combine(projectDirectory, "Pages", "LineChartPage.razor")));
2626
}
2727

28+
[Fact]
29+
public void Preview_Accepts_Exact_Project_File_Path()
30+
{
31+
using var workspace = new TemporaryWorkspace();
32+
var projectDirectory = workspace.CreateWebAssemblyProject();
33+
var projectFilePath = Path.Combine(projectDirectory, "Sample.csproj");
34+
var service = new ProjectIntegrationService(new ChartExampleGenerator());
35+
36+
var plan = service.Preview(new PreviewIntegrationRequest
37+
{
38+
TargetProjectPath = projectFilePath,
39+
Chart = new ChartGenerationRequest { ChartType = "Line" },
40+
});
41+
42+
Assert.Equal(projectFilePath, plan.ProjectFilePath);
43+
}
44+
45+
[Fact]
46+
public void Preview_Resolves_Repo_Folder_With_One_Nested_Blazor_App()
47+
{
48+
using var workspace = new TemporaryWorkspace();
49+
var projectDirectory = workspace.CreateWebAssemblyProject(Path.Combine("src", "App"), "App");
50+
var service = new ProjectIntegrationService(new ChartExampleGenerator());
51+
52+
var plan = service.Preview(new PreviewIntegrationRequest
53+
{
54+
TargetProjectPath = workspace.Root,
55+
Chart = new ChartGenerationRequest { ChartType = "Line" },
56+
});
57+
58+
Assert.Equal(Path.Combine(projectDirectory, "App.csproj"), plan.ProjectFilePath);
59+
}
60+
61+
[Fact]
62+
public void Preview_Rejects_Repo_Folder_With_Multiple_Blazor_App_Candidates()
63+
{
64+
using var workspace = new TemporaryWorkspace();
65+
var firstProjectDirectory = workspace.CreateWebAssemblyProject(Path.Combine("src", "FirstApp"), "FirstApp");
66+
var secondProjectDirectory = workspace.CreateWebAssemblyProject(Path.Combine("src", "SecondApp"), "SecondApp");
67+
var service = new ProjectIntegrationService(new ChartExampleGenerator());
68+
69+
var exception = Assert.Throws<ToolInputException>(() => service.Preview(new PreviewIntegrationRequest
70+
{
71+
TargetProjectPath = workspace.Root,
72+
Chart = new ChartGenerationRequest { ChartType = "Line" },
73+
}));
74+
75+
Assert.Equal("targetProjectPath", exception.Field);
76+
Assert.Contains(Path.Combine(firstProjectDirectory, "FirstApp.csproj"), exception.Details);
77+
Assert.Contains(Path.Combine(secondProjectDirectory, "SecondApp.csproj"), exception.Details);
78+
}
79+
80+
[Fact]
81+
public void Preview_Rejects_Folder_With_Only_NonBlazor_Projects()
82+
{
83+
using var workspace = new TemporaryWorkspace();
84+
var projectDirectory = workspace.CreateLibraryProject(Path.Combine("src", "Library"), "Library");
85+
var service = new ProjectIntegrationService(new ChartExampleGenerator());
86+
87+
var exception = Assert.Throws<ToolInputException>(() => service.Preview(new PreviewIntegrationRequest
88+
{
89+
TargetProjectPath = workspace.Root,
90+
Chart = new ChartGenerationRequest { ChartType = "Line" },
91+
}));
92+
93+
Assert.Equal("targetProjectPath", exception.Field);
94+
Assert.Contains(Path.Combine(projectDirectory, "Library.csproj"), exception.Details);
95+
}
96+
97+
[Fact]
98+
public void Preview_Resolves_Solution_With_One_Blazor_App()
99+
{
100+
using var workspace = new TemporaryWorkspace();
101+
var projectDirectory = workspace.CreateWebAssemblyProject(Path.Combine("src", "App"), "App");
102+
var libraryDirectory = workspace.CreateLibraryProject(Path.Combine("src", "Library"), "Library");
103+
var solutionPath = workspace.CreateSolution("Sample.sln", Path.Combine(projectDirectory, "App.csproj"), Path.Combine(libraryDirectory, "Library.csproj"));
104+
var service = new ProjectIntegrationService(new ChartExampleGenerator());
105+
106+
var plan = service.Preview(new PreviewIntegrationRequest
107+
{
108+
TargetProjectPath = solutionPath,
109+
Chart = new ChartGenerationRequest { ChartType = "Line" },
110+
});
111+
112+
Assert.Equal(Path.Combine(projectDirectory, "App.csproj"), plan.ProjectFilePath);
113+
}
114+
115+
[Fact]
116+
public void Preview_Rejects_Solution_With_Multiple_Blazor_App_Candidates()
117+
{
118+
using var workspace = new TemporaryWorkspace();
119+
var firstProjectDirectory = workspace.CreateWebAssemblyProject(Path.Combine("src", "FirstApp"), "FirstApp");
120+
var secondProjectDirectory = workspace.CreateWebAssemblyProject(Path.Combine("src", "SecondApp"), "SecondApp");
121+
var solutionPath = workspace.CreateSolution("Sample.sln", Path.Combine(firstProjectDirectory, "FirstApp.csproj"), Path.Combine(secondProjectDirectory, "SecondApp.csproj"));
122+
var service = new ProjectIntegrationService(new ChartExampleGenerator());
123+
124+
var exception = Assert.Throws<ToolInputException>(() => service.Preview(new PreviewIntegrationRequest
125+
{
126+
TargetProjectPath = solutionPath,
127+
Chart = new ChartGenerationRequest { ChartType = "Line" },
128+
}));
129+
130+
Assert.Equal("targetProjectPath", exception.Field);
131+
Assert.Contains(Path.Combine(firstProjectDirectory, "FirstApp.csproj"), exception.Details);
132+
Assert.Contains(Path.Combine(secondProjectDirectory, "SecondApp.csproj"), exception.Details);
133+
}
134+
28135
[Fact]
29136
public void Apply_Writes_Files_For_Matching_Preview()
30137
{
@@ -102,35 +209,78 @@ private sealed class TemporaryWorkspace : IDisposable
102209
{
103210
private readonly string root = Path.Combine(Path.GetTempPath(), $"bex-chartjs-mcp-tests-{Guid.NewGuid():N}");
104211

105-
public string CreateWebAssemblyProject()
212+
public string Root => root;
213+
214+
public string CreateWebAssemblyProject(string relativePath = "", string projectName = "Sample")
106215
{
107-
Directory.CreateDirectory(root);
108-
Directory.CreateDirectory(Path.Combine(root, "wwwroot"));
109-
Directory.CreateDirectory(Path.Combine(root, "Pages"));
110-
Directory.CreateDirectory(Path.Combine(root, "Shared"));
216+
var projectRoot = string.IsNullOrWhiteSpace(relativePath) ? root : Path.Combine(root, relativePath);
217+
Directory.CreateDirectory(projectRoot);
218+
Directory.CreateDirectory(Path.Combine(projectRoot, "wwwroot"));
219+
Directory.CreateDirectory(Path.Combine(projectRoot, "Pages"));
220+
Directory.CreateDirectory(Path.Combine(projectRoot, "Shared"));
111221

112-
File.WriteAllText(Path.Combine(root, "Sample.csproj"), """
222+
File.WriteAllText(Path.Combine(projectRoot, $"{projectName}.csproj"), """
113223
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
114224
<PropertyGroup>
115225
<TargetFramework>net10.0</TargetFramework>
116226
</PropertyGroup>
117227
</Project>
118228
""");
119-
File.WriteAllText(Path.Combine(root, "_Imports.razor"), "@using Microsoft.AspNetCore.Components" + Environment.NewLine);
120-
File.WriteAllText(Path.Combine(root, "wwwroot", "index.html"), """
229+
File.WriteAllText(Path.Combine(projectRoot, "_Imports.razor"), "@using Microsoft.AspNetCore.Components" + Environment.NewLine);
230+
File.WriteAllText(Path.Combine(projectRoot, "wwwroot", "index.html"), """
121231
<html>
122232
<body>
123233
<div id="app"></div>
124234
</body>
125235
</html>
126236
""");
127-
File.WriteAllText(Path.Combine(root, "Shared", "NavMenu.razor"), """
237+
File.WriteAllText(Path.Combine(projectRoot, "Shared", "NavMenu.razor"), """
128238
<nav>
129239
<NavLink class="nav-link" href="">Home</NavLink>
130240
</nav>
131241
""");
132242

133-
return root;
243+
return projectRoot;
244+
}
245+
246+
public string CreateLibraryProject(string relativePath, string projectName)
247+
{
248+
var projectRoot = Path.Combine(root, relativePath);
249+
Directory.CreateDirectory(projectRoot);
250+
File.WriteAllText(Path.Combine(projectRoot, $"{projectName}.csproj"), """
251+
<Project Sdk="Microsoft.NET.Sdk">
252+
<PropertyGroup>
253+
<TargetFramework>net10.0</TargetFramework>
254+
</PropertyGroup>
255+
</Project>
256+
""");
257+
258+
return projectRoot;
259+
}
260+
261+
public string CreateSolution(string fileName, params string[] projectPaths)
262+
{
263+
Directory.CreateDirectory(root);
264+
var solutionPath = Path.Combine(root, fileName);
265+
var lines = new List<string>
266+
{
267+
"Microsoft Visual Studio Solution File, Format Version 12.00",
268+
"# Visual Studio Version 17",
269+
};
270+
271+
foreach (var projectPath in projectPaths)
272+
{
273+
var relativePath = Path.GetRelativePath(root, projectPath);
274+
var projectName = Path.GetFileNameWithoutExtension(projectPath);
275+
lines.Add($"Project(\"{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}\") = \"{projectName}\", \"{relativePath}\", \"{{{Guid.NewGuid():D}}}\"");
276+
lines.Add("EndProject");
277+
}
278+
279+
lines.Add("Global");
280+
lines.Add("EndGlobal");
281+
File.WriteAllText(solutionPath, string.Join(Environment.NewLine, lines));
282+
283+
return solutionPath;
134284
}
135285

136286
public void Dispose()

0 commit comments

Comments
 (0)