Forge
csharp4405de34
1using CascadeIDE.Cockpit.Cds;
2using CascadeIDE.Cockpit.Channels.TraceFlow;
3using CascadeIDE.Cockpit.Composition.TraceFlow;
4using CascadeIDE.Cockpit.Graph;
5using CascadeIDE.Features.WorkspaceNavigation.Application;
6using CascadeIDE.Models;
7using Xunit;
8
9namespace CascadeIDE.Tests;
10
11public sealed class WorkspaceNavigationMapRefreshComposerTests
12{
13 private static WorkspaceNavigationMapRefreshComposer.Dependencies TestDeps() =>
14 new(
15 new CodeNavigationMapCompositor(),
16 new TraceFlowChannelCoordinator(
17 [
18 new CodeFlowTraceChannel(),
19 new UnitTestTraceChannel()
20 ]),
21 new TraceFlowCdsRouter(),
22 new TraceFlowSurfaceCompositor());
23
24 [Fact]
25 public void Malformed_json_returns_friendly_status_without_invoking_cds()
26 {
27 var r = WorkspaceNavigationMapRefreshComposer.Compose(
28 TestDeps(),
29 "{bad",
30 useSubgraphMode: false,
31 wantList: false,
32 currentPath: null,
33 solutionPath: null,
34 CodeNavigationMapLevelKind.File,
35 graphWidth: 100,
36 graphHeight: 100,
37 CodeNavigationMapDetailLevel.Normal,
38 CodeNavigationMapRelatedGraphLayoutKind.Radial,
39 CodeNavigationMapControlFlowMainAxisKind.Auto,
40 new CodeNavigationMapSettings(),
41 new WorkspaceNavigationMapRefreshComposer.TraceSignals(0, null),
42 cockpitSurfaceCapturedOnUi: null);
43
44 Assert.Contains("разобрать", r.Status);
45 Assert.Null(r.Scene);
46 Assert.Empty(r.ListRows);
47 }
48
49 [Fact]
50 public void Error_document_skips_scene_and_lists_and_uses_message()
51 {
52 var json = """{"error":"other","message":"short reason"}""";
53 var r = WorkspaceNavigationMapRefreshComposer.Compose(
54 TestDeps(),
55 json,
56 useSubgraphMode: false,
57 wantList: false,
58 currentPath: null,
59 solutionPath: null,
60 CodeNavigationMapLevelKind.File,
61 graphWidth: 100,
62 graphHeight: 100,
63 CodeNavigationMapDetailLevel.Normal,
64 CodeNavigationMapRelatedGraphLayoutKind.Radial,
65 CodeNavigationMapControlFlowMainAxisKind.Auto,
66 new CodeNavigationMapSettings(),
67 new WorkspaceNavigationMapRefreshComposer.TraceSignals(0, null),
68 cockpitSurfaceCapturedOnUi: null);
69
70 Assert.Equal("short reason", r.Status);
71 Assert.Null(r.Scene);
72 }
73
74 [Fact]
75 public void File_level_subgraph_from_builder_composes_graph_scene()
76 {
77 var tmp = Path.Combine(Path.GetTempPath(), "cide-navmap-" + Guid.NewGuid().ToString("N"));
78 Directory.CreateDirectory(tmp);
79 try
80 {
81 var anchor = Path.Combine(tmp, "A.cs");
82 var peer = Path.Combine(tmp, "B.cs");
83 File.WriteAllText(anchor, "class A {}");
84 File.WriteAllText(peer, "class B {}");
85 var paths = new[] { anchor, peer };
86 var json = WorkspaceNavigationMapContextJsonBuilder.Build(
87 CodeNavigationMapLevelKind.File,
88 wantGraph: true,
89 currentPath: anchor,
90 editorText: null,
91 cursorLine: null,
92 cursorColumn: null,
93 paths,
94 solutionPath: null,
95 navSettings: null);
96
97 Assert.True(GraphDocumentJson.TryParse(json, out _, out var parseErr), parseErr);
98
99 var r = WorkspaceNavigationMapRefreshComposer.Compose(
100 TestDeps(),
101 json,
102 useSubgraphMode: true,
103 wantList: false,
104 currentPath: anchor,
105 solutionPath: null,
106 CodeNavigationMapLevelKind.File,
107 graphWidth: 280,
108 graphHeight: 120,
109 CodeNavigationMapDetailLevel.Normal,
110 CodeNavigationMapRelatedGraphLayoutKind.Radial,
111 CodeNavigationMapControlFlowMainAxisKind.Auto,
112 new CodeNavigationMapSettings(),
113 new WorkspaceNavigationMapRefreshComposer.TraceSignals(0, null),
114 cockpitSurfaceCapturedOnUi: null);
115
116 Assert.DoesNotContain("разобрать", r.Status);
117 Assert.NotNull(r.Scene);
118 Assert.True(r.Scene!.Nodes.Count >= 2);
119 }
120 finally
121 {
122 try
123 {
124 Directory.Delete(tmp, recursive: true);
125 }
126 catch
127 {
128 // best-effort cleanup
129 }
130 }
131 }
132
133 [Fact]
134 public void Control_flow_subgraph_with_file_level_does_not_throw_generic_parse_error()
135 {
136 const string json = """
137 {
138 "mode":"subgraph",
139 "graph_kind":"code_intent_code_navigation_map",
140 "anchor_path":"D:/w/Program.cs",
141 "nodes":[
142 {"id":"n0","path":"D:/w/Program.cs","kind":"anchor","label":"Program.cs"},
143 {"id":"n1","path":"D:/w/Program.cs","kind":"call_step","label":"S1","line_start":1,"line_end":2}
144 ],
145 "edges":[{"from_id":"n0","to_id":"n1","kind":"Call"}]
146 }
147 """;
148
149 var r = WorkspaceNavigationMapRefreshComposer.Compose(
150 TestDeps(),
151 json,
152 useSubgraphMode: true,
153 wantList: false,
154 currentPath: "D:/w/Program.cs",
155 solutionPath: null,
156 CodeNavigationMapLevelKind.File,
157 graphWidth: 280,
158 graphHeight: 120,
159 CodeNavigationMapDetailLevel.Normal,
160 CodeNavigationMapRelatedGraphLayoutKind.TopDown,
161 CodeNavigationMapControlFlowMainAxisKind.Auto,
162 new CodeNavigationMapSettings(),
163 new WorkspaceNavigationMapRefreshComposer.TraceSignals(0, null),
164 cockpitSurfaceCapturedOnUi: null);
165
166 Assert.DoesNotContain("разобрать", r.Status);
167 Assert.NotNull(r.Scene);
168 }
169
170 [Fact]
171 public void File_level_zero_viewport_does_not_throw_parse_error()
172 {
173 const string json = """
174 {
175 "mode":"subgraph",
176 "graph_kind":"related_files",
177 "anchor_path":"D:/w/A.cs",
178 "nodes":[
179 {"id":"n0","path":"D:/w/A.cs","kind":"anchor","label":"A.cs"},
180 {"id":"n1","path":"D:/w/B.cs","kind":"project_peer","label":"B.cs"}
181 ],
182 "edges":[{"from_id":"n0","to_id":"n1","kind":"related_to"}]
183 }
184 """;
185
186 var r = WorkspaceNavigationMapRefreshComposer.Compose(
187 TestDeps(),
188 json,
189 useSubgraphMode: true,
190 wantList: false,
191 currentPath: "D:/w/A.cs",
192 solutionPath: null,
193 CodeNavigationMapLevelKind.File,
194 graphWidth: 0,
195 graphHeight: 0,
196 CodeNavigationMapDetailLevel.Normal,
197 CodeNavigationMapRelatedGraphLayoutKind.Radial,
198 CodeNavigationMapControlFlowMainAxisKind.Auto,
199 new CodeNavigationMapSettings(),
200 new WorkspaceNavigationMapRefreshComposer.TraceSignals(0, null),
201 cockpitSurfaceCapturedOnUi: null);
202
203 Assert.DoesNotContain("разобрать", r.Status);
204 }
205
206 [Fact]
207 public void File_level_nan_viewport_height_does_not_throw_parse_error()
208 {
209 const string json = """
210 {
211 "mode":"subgraph",
212 "graph_kind":"related_files",
213 "anchor_path":"D:/w/A.cs",
214 "nodes":[{"id":"n0","path":"D:/w/A.cs","kind":"anchor","label":"A.cs"}],
215 "edges":[]
216 }
217 """;
218
219 var r = WorkspaceNavigationMapRefreshComposer.Compose(
220 TestDeps(),
221 json,
222 useSubgraphMode: true,
223 wantList: false,
224 currentPath: "D:/w/A.cs",
225 solutionPath: null,
226 CodeNavigationMapLevelKind.File,
227 graphWidth: 280,
228 graphHeight: double.NaN,
229 CodeNavigationMapDetailLevel.Normal,
230 CodeNavigationMapRelatedGraphLayoutKind.Radial,
231 CodeNavigationMapControlFlowMainAxisKind.Auto,
232 new CodeNavigationMapSettings(),
233 new WorkspaceNavigationMapRefreshComposer.TraceSignals(0, null),
234 cockpitSurfaceCapturedOnUi: null);
235
236 Assert.DoesNotContain("разобрать", r.Status);
237 }
238
239 [Fact]
240 public void File_level_related_wire_with_subgraph_mode_still_composes_graph()
241 {
242 const string json = """
243 {
244 "mode":"related",
245 "anchor_path":"D:/w/A.cs",
246 "items":[
247 {"path":"D:/w/B.cs","kind":"project_peer","rationale":"peer"}
248 ]
249 }
250 """;
251
252 var r = WorkspaceNavigationMapRefreshComposer.Compose(
253 TestDeps(),
254 json,
255 useSubgraphMode: true,
256 wantList: false,
257 currentPath: "D:/w/A.cs",
258 solutionPath: null,
259 CodeNavigationMapLevelKind.File,
260 graphWidth: 280,
261 graphHeight: 120,
262 CodeNavigationMapDetailLevel.Normal,
263 CodeNavigationMapRelatedGraphLayoutKind.Radial,
264 CodeNavigationMapControlFlowMainAxisKind.Auto,
265 new CodeNavigationMapSettings(),
266 new WorkspaceNavigationMapRefreshComposer.TraceSignals(0, null),
267 cockpitSurfaceCapturedOnUi: null);
268
269 Assert.DoesNotContain("разобрать", r.Status);
270 Assert.NotNull(r.Scene);
271 Assert.Equal(2, r.Scene!.Nodes.Count);
272 }
273
274 /// <summary>
275 /// Сквозной путь UI refresh: related wire + top_down + много связей (регрессия Math.Clamp в hierarchy layout).
276 /// </summary>
277 [Fact]
278 public void File_level_related_wire_top_down_many_items_composes_without_parse_error()
279 {
280 var items = new System.Text.StringBuilder();
281 for (var i = 1; i <= 37; i++)
282 {
283 if (i > 1)
284 items.Append(',');
285 items.Append($$"""{"path":"D:/w/B{{i}}.cs","kind":"project_peer","rationale":"peer"}""");
286 }
287
288 var json = $$"""
289 {
290 "mode":"related",
291 "anchor_path":"D:/w/A.cs",
292 "items":[{{items}}]
293 }
294 """;
295
296 var r = WorkspaceNavigationMapRefreshComposer.Compose(
297 TestDeps(),
298 json,
299 useSubgraphMode: true,
300 wantList: false,
301 currentPath: "D:/w/A.cs",
302 solutionPath: null,
303 CodeNavigationMapLevelKind.File,
304 graphWidth: 280,
305 graphHeight: 120,
306 CodeNavigationMapDetailLevel.Normal,
307 CodeNavigationMapRelatedGraphLayoutKind.TopDown,
308 CodeNavigationMapControlFlowMainAxisKind.Auto,
309 new CodeNavigationMapSettings(),
310 new WorkspaceNavigationMapRefreshComposer.TraceSignals(0, null),
311 cockpitSurfaceCapturedOnUi: null);
312
313 Assert.DoesNotContain("разобрать", r.Status);
314 Assert.DoesNotContain("cannot be greater than", r.Status, StringComparison.OrdinalIgnoreCase);
315 Assert.NotNull(r.Scene);
316 Assert.Equal(38, r.Scene!.Nodes.Count);
317 }
318}
319
View only · write via MCP/CIDE