Forge
csharpdeeb25a2
1using CascadeIDE.Features.Agent.Environment;
2using CascadeIDE.Models;
3using CascadeIDE.Services;
4using Xunit;
5
6namespace CascadeIDE.Tests;
7
8[Trait("Category", "AgentEnvironment")]
9public sealed class AgentRoslynDiagnoseFilesDiagnosticsTests
10{
11 [Fact]
12 public async Task RunAsync_skipped_when_language_or_resolver_missing()
13 {
14 var noLang = new AgentRoslynDiagnoseFilesDiagnostics(null, () => []);
15 var outcome = await noLang.RunAsync();
16 Assert.Contains("no language service", outcome.Detail, StringComparison.Ordinal);
17
18 var noResolver = new AgentRoslynDiagnoseFilesDiagnostics(new CSharpLanguageService(), null);
19 outcome = await noResolver.RunAsync();
20 Assert.Contains("no language service", outcome.Detail, StringComparison.Ordinal);
21 }
22
23 [Fact]
24 public async Task RunAsync_open_tabs_detects_syntax_error()
25 {
26 var path = Path.Combine(Path.GetTempPath(), "cide-diagnose-open-" + Guid.NewGuid().ToString("N") + ".cs");
27 try
28 {
29 var broken = "class Broken { void M( { }";
30 var diagnose = new AgentRoslynDiagnoseFilesDiagnostics(
31 new CSharpLanguageService(),
32 () => [(path, broken)],
33 new AgentEnvironmentLadderSettings { DiagnoseFilesCsScope = AgentDiagnoseFilesCsScopeParser.OpenTabsOnly });
34
35 var outcome = await diagnose.RunAsync();
36
37 Assert.False(outcome.Green);
38 Assert.True(outcome.ErrorCount > 0);
39 Assert.Contains("1 file", outcome.Detail, StringComparison.Ordinal);
40 }
41 finally
42 {
43 try { File.Delete(path); } catch { /* best-effort */ }
44 }
45 }
46
47 [Fact]
48 public async Task RunAsync_git_dirty_loads_cs_from_disk_when_not_open()
49 {
50 var dir = Directory.CreateTempSubdirectory("cide-diagnose-git-");
51 try
52 {
53 var onlyGit = Path.Combine(dir.FullName, "OnlyGit.cs");
54 await File.WriteAllTextAsync(onlyGit, "namespace T; public class Ok { }");
55
56 var git = new GitNameOnlyRunner
57 {
58 UnstagedOutput = "OnlyGit.cs\nreadme.txt",
59 };
60
61 var diagnose = new AgentRoslynDiagnoseFilesDiagnostics(
62 new CSharpLanguageService(),
63 () => [],
64 new AgentEnvironmentLadderSettings { DiagnoseFilesCsScope = AgentDiagnoseFilesCsScopeParser.OpenTabsAndGitDirtyCs },
65 git,
66 () => dir.FullName);
67
68 var outcome = await diagnose.RunAsync();
69
70 Assert.True(outcome.Green);
71 Assert.Contains("1 file", outcome.Detail, StringComparison.Ordinal);
72 Assert.Equal(2, git.Invocations.Count);
73 }
74 finally
75 {
76 try { dir.Delete(recursive: true); } catch { /* best-effort */ }
77 }
78 }
79
80 [Fact]
81 public async Task RunAsync_open_tab_buffer_overrides_disk_for_same_path()
82 {
83 var dir = Directory.CreateTempSubdirectory("cide-diagnose-priority-");
84 try
85 {
86 var cs = Path.Combine(dir.FullName, "Same.cs");
87 await File.WriteAllTextAsync(cs, "namespace T; public class Ok { }");
88
89 var git = new GitNameOnlyRunner { UnstagedOutput = "Same.cs" };
90 var diagnose = new AgentRoslynDiagnoseFilesDiagnostics(
91 new CSharpLanguageService(),
92 () => [(cs, "class Broken { void M( { }")],
93 new AgentEnvironmentLadderSettings { DiagnoseFilesCsScope = AgentDiagnoseFilesCsScopeParser.OpenTabsAndGitDirtyCs },
94 git,
95 () => dir.FullName);
96
97 var outcome = await diagnose.RunAsync();
98
99 Assert.False(outcome.Green);
100 Assert.True(outcome.ErrorCount > 0);
101 }
102 finally
103 {
104 try { dir.Delete(recursive: true); } catch { /* best-effort */ }
105 }
106 }
107
108 [Fact]
109 public async Task RunAsync_git_dirty_respects_max_files_cap()
110 {
111 var dir = Directory.CreateTempSubdirectory("cide-diagnose-cap-");
112 try
113 {
114 for (var i = 0; i < 5; i++)
115 await File.WriteAllTextAsync(Path.Combine(dir.FullName, $"F{i}.cs"), $"namespace T{i}; public class C{i} {{}}");
116
117 var git = new GitNameOnlyRunner
118 {
119 UnstagedOutput = "F0.cs\nF1.cs\nF2.cs\nF3.cs\nF4.cs",
120 };
121
122 var diagnose = new AgentRoslynDiagnoseFilesDiagnostics(
123 new CSharpLanguageService(),
124 () => [],
125 new AgentEnvironmentLadderSettings
126 {
127 DiagnoseFilesCsScope = AgentDiagnoseFilesCsScopeParser.OpenTabsAndGitDirtyCs,
128 DiagnoseFilesGitDirtyMaxFiles = 2,
129 },
130 git,
131 () => dir.FullName);
132
133 var outcome = await diagnose.RunAsync();
134
135 Assert.True(outcome.Green);
136 Assert.Contains("2 file", outcome.Detail, StringComparison.Ordinal);
137 }
138 finally
139 {
140 try { dir.Delete(recursive: true); } catch { /* best-effort */ }
141 }
142 }
143
144 [Fact]
145 public async Task RunAsync_open_tabs_only_does_not_call_git()
146 {
147 var git = new GitNameOnlyRunner { UnstagedOutput = "Ghost.cs" };
148 var diagnose = new AgentRoslynDiagnoseFilesDiagnostics(
149 new CSharpLanguageService(),
150 () => [],
151 new AgentEnvironmentLadderSettings { DiagnoseFilesCsScope = AgentDiagnoseFilesCsScopeParser.OpenTabsOnly },
152 git,
153 () => @"C:\any");
154
155 var outcome = await diagnose.RunAsync();
156
157 Assert.Empty(git.Invocations);
158 Assert.Contains("no .cs inputs", outcome.Detail, StringComparison.Ordinal);
159 }
160
161 [Fact]
162 public async Task RunAsync_warmup_loads_cs_from_disk_when_not_open()
163 {
164 var dir = Directory.CreateTempSubdirectory("cide-diagnose-warmup-");
165 try
166 {
167 var onlyWarmup = Path.Combine(dir.FullName, "WarmOnly.cs");
168 await File.WriteAllTextAsync(onlyWarmup, "namespace T; public class Warm { }");
169
170 var diagnose = new AgentRoslynDiagnoseFilesDiagnostics(
171 new CSharpLanguageService(),
172 () => [],
173 new AgentEnvironmentLadderSettings { DiagnoseFilesIncludeWarmupCs = true },
174 getWarmupCsFilePaths: () => [onlyWarmup]);
175
176 var outcome = await diagnose.RunAsync();
177
178 Assert.True(outcome.Green);
179 Assert.Contains("1 file", outcome.Detail, StringComparison.Ordinal);
180 }
181 finally
182 {
183 try { dir.Delete(recursive: true); } catch { /* best-effort */ }
184 }
185 }
186
187 [Fact]
188 public async Task RunAsync_warmup_respects_max_files_cap()
189 {
190 var dir = Directory.CreateTempSubdirectory("cide-diagnose-warmup-cap-");
191 try
192 {
193 var paths = new List<string>();
194 for (var i = 0; i < 4; i++)
195 {
196 var p = Path.Combine(dir.FullName, $"W{i}.cs");
197 await File.WriteAllTextAsync(p, $"namespace T{i}; public class C{i} {{}}");
198 paths.Add(p);
199 }
200
201 var diagnose = new AgentRoslynDiagnoseFilesDiagnostics(
202 new CSharpLanguageService(),
203 () => [],
204 new AgentEnvironmentLadderSettings
205 {
206 DiagnoseFilesIncludeWarmupCs = true,
207 DiagnoseFilesWarmupMaxFiles = 2,
208 },
209 getWarmupCsFilePaths: () => paths);
210
211 var outcome = await diagnose.RunAsync();
212
213 Assert.True(outcome.Green);
214 Assert.Contains("2 file", outcome.Detail, StringComparison.Ordinal);
215 }
216 finally
217 {
218 try { dir.Delete(recursive: true); } catch { /* best-effort */ }
219 }
220 }
221
222 private sealed class GitNameOnlyRunner : IGitCommandRunner
223 {
224 public string? UnstagedOutput { get; set; }
225
226 public string? StagedOutput { get; set; }
227
228 public List<IReadOnlyList<string>> Invocations { get; } = [];
229
230 public Task<(bool Success, int ExitCode, string Output)> RunAsync(
231 IReadOnlyList<string> args,
232 string workingDirectory,
233 CancellationToken cancellationToken = default)
234 {
235 Invocations.Add(args);
236 var isCached = args.Any(a => string.Equals(a, "--cached", StringComparison.Ordinal));
237 var isNameOnly = args.Any(a => string.Equals(a, "--name-only", StringComparison.Ordinal));
238 if (isNameOnly && isCached)
239 return Task.FromResult((true, 0, StagedOutput ?? ""));
240 if (isNameOnly)
241 return Task.FromResult((true, 0, UnstagedOutput ?? ""));
242 return Task.FromResult((true, 0, ""));
243 }
244 }
245}
246
View only · write via MCP/CIDE