Forge
csharpdeeb25a2
1using CascadeIDE.Cockpit.Cds;
2using CascadeIDE.Cockpit.Composition.Shell;
3using CascadeIDE.Models;
4using CascadeIDE.Services.Presentation;
5using Xunit;
6
7using CascadeIDE.Features.Agent.Environment;
8
9namespace CascadeIDE.Tests;
10
11/// <summary>Канонические строки <c>presentation</c> и ожидаемое поведение (ADR 0017, 0168).</summary>
12public static class PresentationTopologyCatalog
13{
14 public static PresentationGrammarTokens DefaultGrammar { get; } =
15 PresentationGrammarTokens.FromSettings("()", " ", "+", "P", "F", "M");
16
17 private static readonly string[] TripleLines =
18 [
19 "(P) (F) (M)",
20 "(P) (M) (F)",
21 "(F) (P) (M)",
22 "(F) (M) (P)",
23 "(M) (P) (F)",
24 "(M) (F) (P)",
25 ];
26
27 public static IReadOnlyList<TopologyCase> All { get; } = BuildAll();
28
29 private static IReadOnlyList<TopologyCase> BuildAll()
30 {
31 var list = new List<TopologyCase>
32 {
33 // --- один экран ---
34 new(
35 "(P+F+M)",
36 MainScreen: 0,
37 MfdHost: false,
38 MfdHostScreen: null,
39 PfdHost: false,
40 PfdHostScreen: null,
41 PmSplitHost: false,
42 PmSplitScreen: null,
43 MaximizeAtStartup: true,
44 MainGridAtStartup: PresentationMainGridLayoutFrameBuilder.DefaultColumnDefinitions,
45 PfdInMainAtStartup: true,
46 MfdInMainAtStartup: true),
47
48 new(
49 "(0.2P+0.3F+0.5M)",
50 MainScreen: 0,
51 MfdHost: false,
52 MfdHostScreen: null,
53 PfdHost: false,
54 PfdHostScreen: null,
55 PmSplitHost: false,
56 PmSplitScreen: null,
57 MaximizeAtStartup: true,
58 MainGridAtStartup: "0.2*,4,0.3*,4,0.5*",
59 PfdInMainAtStartup: true,
60 MfdInMainAtStartup: true),
61
62 new(
63 "(F)",
64 MainScreen: 0,
65 MfdHost: false,
66 MfdHostScreen: null,
67 PfdHost: false,
68 PfdHostScreen: null,
69 PmSplitHost: false,
70 PmSplitScreen: null,
71 MaximizeAtStartup: false,
72 MainGridAtStartup: "0,4,*,4,0",
73 PfdInMainAtStartup: false,
74 MfdInMainAtStartup: false),
75
76 // --- два экрана: F + M ---
77 new(
78 "(F) (M)",
79 MainScreen: 0,
80 MfdHost: true,
81 MfdHostScreen: 1,
82 PfdHost: false,
83 PfdHostScreen: null,
84 PmSplitHost: false,
85 PmSplitScreen: null,
86 MaximizeAtStartup: true,
87 MainGridAtStartup: "0,4,*,4,0",
88 PfdInMainAtStartup: false,
89 MfdInMainAtStartup: false),
90
91 new(
92 "(M) (F)",
93 MainScreen: 1,
94 MfdHost: true,
95 MfdHostScreen: 0,
96 PfdHost: false,
97 PfdHostScreen: null,
98 PmSplitHost: false,
99 PmSplitScreen: null,
100 MaximizeAtStartup: true,
101 MainGridAtStartup: "0,4,*,4,0",
102 PfdInMainAtStartup: false,
103 MfdInMainAtStartup: false),
104
105 // --- два экрана: P+F + M ---
106 new(
107 "(P+F) (M)",
108 MainScreen: 0,
109 MfdHost: true,
110 MfdHostScreen: 1,
111 PfdHost: false,
112 PfdHostScreen: null,
113 PmSplitHost: false,
114 PmSplitScreen: null,
115 MaximizeAtStartup: true,
116 MainGridAtStartup: "220,4,*,4,0",
117 PfdInMainAtStartup: true,
118 MfdInMainAtStartup: false),
119
120 new(
121 "(0.25P + 0.75F) (M)",
122 MainScreen: 0,
123 MfdHost: true,
124 MfdHostScreen: 1,
125 PfdHost: false,
126 PfdHostScreen: null,
127 PmSplitHost: false,
128 PmSplitScreen: null,
129 MaximizeAtStartup: true,
130 MainGridAtStartup: "0.25*,4,0.75*,4,0",
131 PfdInMainAtStartup: true,
132 MfdInMainAtStartup: false),
133
134 // --- два экрана: P+M + F ---
135 new(
136 "(P+M)(F)",
137 MainScreen: 1,
138 MfdHost: false,
139 MfdHostScreen: null,
140 PfdHost: false,
141 PfdHostScreen: null,
142 PmSplitHost: true,
143 PmSplitScreen: 0,
144 MaximizeAtStartup: true,
145 MainGridAtStartup: "0,4,*,4,0",
146 PfdInMainAtStartup: false,
147 MfdInMainAtStartup: false),
148
149 new(
150 "(F)(P+M)",
151 MainScreen: 0,
152 MfdHost: false,
153 MfdHostScreen: null,
154 PfdHost: false,
155 PfdHostScreen: null,
156 PmSplitHost: true,
157 PmSplitScreen: 1,
158 MaximizeAtStartup: true,
159 MainGridAtStartup: "0,4,*,4,0",
160 PfdInMainAtStartup: false,
161 MfdInMainAtStartup: false),
162
163 new(
164 "(0.25P + 0.75M)(F)",
165 MainScreen: 1,
166 MfdHost: false,
167 MfdHostScreen: null,
168 PfdHost: false,
169 PfdHostScreen: null,
170 PmSplitHost: true,
171 PmSplitScreen: 0,
172 MaximizeAtStartup: true,
173 MainGridAtStartup: "0,4,*,4,0",
174 PfdInMainAtStartup: false,
175 MfdInMainAtStartup: false),
176 };
177
178 foreach (var line in TripleLines)
179 list.Add(BuildTripleCase(line));
180
181 return list;
182 }
183
184 private static TopologyCase BuildTripleCase(string line)
185 {
186 var parse = PresentationParser.Parse(line, DefaultGrammar);
187 if (!parse.IsSuccess)
188 throw new InvalidOperationException($"Catalog triple line failed to parse: {line}");
189
190 static int IndexOf(PresentationParseResult parse, PresentationAnchorKind kind)
191 {
192 for (var i = 0; i < parse.Screens.Count; i++)
193 {
194 var screen = parse.Screens[i];
195 if (screen.Count == 1 && screen[0].Kind == kind)
196 return i;
197 }
198
199 throw new InvalidOperationException($"Anchor {kind} not found in triple line.");
200 }
201
202 return new TopologyCase(
203 line,
204 MainScreen: IndexOf(parse, PresentationAnchorKind.Forward),
205 MfdHost: true,
206 MfdHostScreen: IndexOf(parse, PresentationAnchorKind.Mfd),
207 PfdHost: true,
208 PfdHostScreen: IndexOf(parse, PresentationAnchorKind.Pfd),
209 PmSplitHost: false,
210 PmSplitScreen: null,
211 MaximizeAtStartup: true,
212 MainGridAtStartup: "0,4,*,4,0",
213 PfdInMainAtStartup: false,
214 MfdInMainAtStartup: false);
215 }
216
217 public sealed record TopologyCase(
218 string Line,
219 int MainScreen,
220 bool MfdHost,
221 int? MfdHostScreen,
222 bool PfdHost,
223 int? PfdHostScreen,
224 bool PmSplitHost,
225 int? PmSplitScreen,
226 bool MaximizeAtStartup,
227 string MainGridAtStartup,
228 bool PfdInMainAtStartup,
229 bool MfdInMainAtStartup);
230}
231
232public sealed class PresentationTopologyMatrixTests
233{
234 public static IEnumerable<object[]> CatalogCases() =>
235 PresentationTopologyCatalog.All.Select(c => new object[] { c });
236
237 [Theory]
238 [MemberData(nameof(CatalogCases))]
239 public void Parse_Succeeds(PresentationTopologyCatalog.TopologyCase expected)
240 {
241 var parse = PresentationParser.Parse(expected.Line, PresentationTopologyCatalog.DefaultGrammar);
242 Assert.True(parse.IsSuccess, $"Failed to parse: {expected.Line}");
243 Assert.True(parse.Screens.Count > 0);
244 }
245
246 [Theory]
247 [MemberData(nameof(CatalogCases))]
248 public void TopologyFlags_MatchExpectation(PresentationTopologyCatalog.TopologyCase expected)
249 {
250 var parse = PresentationParser.Parse(expected.Line, PresentationTopologyCatalog.DefaultGrammar);
251 var flags = PresentationTopologyResolver.ResolveFlags(parse);
252
253 Assert.Equal(expected.MfdHost, flags.MfdHostTopology);
254 Assert.Equal(expected.PfdHost, flags.PfdHostTopology);
255 Assert.Equal(expected.PmSplitHost, flags.PmHostTopology);
256 }
257
258 [Theory]
259 [MemberData(nameof(CatalogCases))]
260 public void MainWindowScreenIndex_Matches(PresentationTopologyCatalog.TopologyCase expected)
261 {
262 var parse = PresentationParser.Parse(expected.Line, PresentationTopologyCatalog.DefaultGrammar);
263 Assert.Equal(
264 expected.MainScreen,
265 PresentationLayoutAnalyzer.GetMainWindowPresentationScreenIndexOrDefault(parse));
266 }
267
268 [Theory]
269 [MemberData(nameof(CatalogCases))]
270 public void HostScreenIndices_Match(PresentationTopologyCatalog.TopologyCase expected)
271 {
272 var parse = PresentationParser.Parse(expected.Line, PresentationTopologyCatalog.DefaultGrammar);
273
274 if (expected.MfdHostScreen is int mfdIdx)
275 {
276 Assert.True(
277 PresentationLayoutAnalyzer.TryGetMfdHostPresentationScreenIndex(parse.Screens, out var actual),
278 expected.Line);
279 Assert.Equal(mfdIdx, actual);
280 }
281 else
282 {
283 Assert.False(PresentationLayoutAnalyzer.TryGetMfdHostPresentationScreenIndex(parse.Screens, out _));
284 }
285
286 if (expected.PfdHostScreen is int pfdIdx)
287 {
288 Assert.True(
289 PresentationLayoutAnalyzer.TryGetPfdHostPresentationScreenIndex(parse.Screens, out var actual),
290 expected.Line);
291 Assert.Equal(pfdIdx, actual);
292 }
293 else
294 {
295 Assert.False(PresentationLayoutAnalyzer.TryGetPfdHostPresentationScreenIndex(parse.Screens, out _));
296 }
297
298 if (expected.PmSplitScreen is int pmIdx)
299 {
300 Assert.True(
301 PresentationLayoutAnalyzer.TryGetPmSplitHostPresentationScreenIndex(parse.Screens, out var actual),
302 expected.Line);
303 Assert.Equal(pmIdx, actual);
304 }
305 else
306 {
307 Assert.False(PresentationLayoutAnalyzer.TryGetPmSplitHostPresentationScreenIndex(parse.Screens, out _));
308 }
309 }
310
311 [Theory]
312 [MemberData(nameof(CatalogCases))]
313 public void ShouldMaximizeMainWindowAtStartup_Matches(PresentationTopologyCatalog.TopologyCase expected)
314 {
315 var parse = PresentationParser.Parse(expected.Line, PresentationTopologyCatalog.DefaultGrammar);
316 Assert.Equal(
317 expected.MaximizeAtStartup,
318 PresentationLayoutAnalyzer.ShouldMaximizeMainWindowAtStartup(parse.Screens));
319 }
320
321 [Theory]
322 [MemberData(nameof(CatalogCases))]
323 public void MainGridAtStartup_Matches(PresentationTopologyCatalog.TopologyCase expected)
324 {
325 var parse = PresentationParser.Parse(expected.Line, PresentationTopologyCatalog.DefaultGrammar);
326 var flags = PresentationTopologyResolver.ResolveFlags(parse);
327 var frame = PresentationTopologyResolver.BuildMainWindowGridAtStartup(parse, flags);
328
329 Assert.Equal(expected.MainGridAtStartup, frame.ColumnDefinitions);
330 }
331
332 [Theory]
333 [MemberData(nameof(CatalogCases))]
334 public void ShellVisibilityAtStartup_Matches(PresentationTopologyCatalog.TopologyCase expected)
335 {
336 var parse = PresentationParser.Parse(expected.Line, PresentationTopologyCatalog.DefaultGrammar);
337 var shell = MainWindowShellSurfaceCompositor.Compose(
338 new MainWindowShellSurfaceCompositionInput(
339 parse,
340 IntentSolutionExplorerVisible: true,
341 IntentChatPanelExpanded: true,
342 SuppressPfdColumnForPfdHostWindow: expected.PfdHost,
343 SuppressMfdColumnForMfdHostWindow: expected.MfdHost,
344 ExpandedMfdWidthPixels: 340,
345 CollapsedMfdWidthPixels: 8,
346 DisplaySettings: new DisplaySettings(),
347 SafetyLevel: AgentSafetyLevel.Confirm));
348
349 Assert.Equal(expected.PfdInMainAtStartup, shell.PfdSurfaceVisible);
350 Assert.Equal(expected.MfdInMainAtStartup, shell.MfdColumnVisibleInMainGrid);
351 }
352
353 [Theory]
354 [MemberData(nameof(CatalogCases))]
355 public void CockpitPolicy_RequiresZonesOnMainScreen(PresentationTopologyCatalog.TopologyCase expected)
356 {
357 var parse = PresentationParser.Parse(expected.Line, PresentationTopologyCatalog.DefaultGrammar);
358
359 Assert.Equal(
360 expected.PfdInMainAtStartup,
361 CockpitPresentationLayoutPolicy.RequiresPfdRegionInMainWindow(parse));
362 Assert.Equal(
363 expected.MfdInMainAtStartup,
364 CockpitPresentationLayoutPolicy.RequiresMfdRegionInMainWindow(parse));
365 }
366
367 [Fact]
368 public void DedicatedPfM_Unweighted_MainGridHidesMfdTailWhenHostTopology()
369 {
370 var parse = PresentationParser.Parse("(P+F) (M)", PresentationTopologyCatalog.DefaultGrammar);
371 var flags = PresentationTopologyResolver.ResolveFlags(parse);
372 var frame = PresentationTopologyResolver.BuildMainWindowGridAtStartup(parse, flags);
373 Assert.Equal("220,4,*,4,0", frame.ColumnDefinitions);
374 }
375
376 [Fact]
377 public void ForwardOnlyMainScreen_NeverUsesDefaultThreeColumnGrid()
378 {
379 foreach (var c in PresentationTopologyCatalog.All.Where(x => x.MainGridAtStartup == "0,4,*,4,0"))
380 {
381 var parse = PresentationParser.Parse(c.Line, PresentationTopologyCatalog.DefaultGrammar);
382 var flags = PresentationTopologyResolver.ResolveFlags(parse);
383 var frame = PresentationTopologyResolver.BuildMainWindowGridAtStartup(parse, flags);
384
385 Assert.NotEqual(PresentationMainGridLayoutFrameBuilder.DefaultColumnDefinitions, frame.ColumnDefinitions);
386 Assert.DoesNotContain("220", frame.ColumnDefinitions);
387 Assert.DoesNotContain("340", frame.ColumnDefinitions);
388 }
389 }
390}
391
View only · write via MCP/CIDE