Forge
csharpdeeb25a2
1using System.Text.Json;
2using CascadeIDE.Features.Shell.Application;
3using CascadeIDE.Models;
4using CascadeIDE.Services.Presentation;
5using Tomlyn;
6using Xunit;
7
8namespace CascadeIDE.Tests;
9
10/// <summary>
11/// Контракт TOML: вложенные секции (<c>[markdown.diagrams]</c>, <c>[display.screens.grammar]</c>) и snake_case.
12/// </summary>
13public sealed class CascadeIdeSettingsTomlDeserializeTests
14{
15 private static readonly TomlSerializerOptions Options = new()
16 {
17 PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
18 };
19
20 private static CascadeIdeSettings Deserialize(string text) =>
21 TomlSerializer.Deserialize<CascadeIdeSettings>(text, Options)
22 ?? throw new InvalidOperationException("Deserialize returned null");
23
24 [Fact]
25 public void Deserialize_FontsIntercomAndEditor_ParsesExpected()
26 {
27 const string text =
28 """
29 [fonts.intercom]
30 prose_pt = 13
31 prose_pt_forward = 12
32 composer_pt = 15
33 chrome_title_pt = 14
34 chrome_subtitle_pt = 11
35 chrome_heading_pt = 16
36 card_title_pt = 15
37 panel_title_pt = 16
38 panel_input_pt = 14
39 prose_family = "Segoe UI"
40 mono_family = "Cascadia Mono,Consolas"
41 chip_id_family = "Consolas"
42
43 [fonts.editor]
44 size_pt = 14
45 family = "Consolas,Cascadia Code,monospace"
46 """;
47
48 var s = Deserialize(text);
49 Assert.Equal(13, s.Fonts.Intercom.ProsePt);
50 Assert.Equal(12, s.Fonts.Intercom.ProsePtForward);
51 Assert.Equal(15, s.Fonts.Intercom.ComposerPt);
52 Assert.Equal(14, s.Fonts.Intercom.ChromeTitlePt);
53 Assert.Equal(15f, s.Fonts.Intercom.ResolveComposerPt(forwardHost: false));
54 Assert.Equal(14f, s.Fonts.Intercom.ResolveChromeTitlePt());
55 Assert.Equal(15f, s.Fonts.Intercom.ResolveCardTitleLineHeight(forwardHost: false));
56 Assert.Equal(16f, s.Fonts.Intercom.ResolvePanelTitlePt());
57 Assert.Equal(14f, s.Fonts.Intercom.ResolvePanelInputPt());
58 Assert.Equal("Segoe UI", s.Fonts.Intercom.ProseFamily);
59 Assert.Equal("Cascadia Mono,Consolas", s.Fonts.Intercom.MonoFamily);
60 Assert.Equal("Consolas", s.Fonts.Intercom.ChipIdFamily);
61 Assert.Equal(14, s.Fonts.Editor.SizePt);
62 Assert.Equal("Consolas,Cascadia Code,monospace", s.Fonts.Editor.Family);
63 Assert.Equal(12f, s.Fonts.Intercom.ResolveProsePt(forwardHost: true));
64 Assert.Equal(13f, s.Fonts.Intercom.ResolveProsePt(forwardHost: false));
65 Assert.Equal(14f, s.Fonts.Editor.ResolveSizePt());
66 Assert.Equal("Segoe UI", s.Fonts.Intercom.ResolveProseFamily());
67 Assert.Equal("Consolas,Cascadia Code,monospace", s.Fonts.Editor.ResolveFamily());
68 }
69
70 [Fact]
71 public void Deserialize_LoggingIntercomSendTrace_ParsesExpected()
72 {
73 const string text =
74 """
75 [logging.intercom]
76 send_trace = true
77 """;
78
79 var s = Deserialize(text);
80 Assert.True(s.Logging.Intercom.SendTrace);
81 }
82
83 [Fact]
84 public void Deserialize_DisplayScreensAndMarkdownNested_ParsesExpected()
85 {
86 const string text =
87 """
88 [display.screens]
89 topology = "(P+F) (M)"
90
91 [display.screens.grammar]
92 pfd = "P"
93 forward = "F"
94 mfd = "M"
95
96 [markdown.diagrams]
97 kroki = true
98 kroki_url = "https://kroki.io"
99 """;
100
101 var s = Deserialize(text);
102 Assert.Equal("(P+F) (M)", s.Display.Screens.Topology);
103 Assert.Equal("P", s.Display.Screens.Grammar.Pfd);
104 Assert.True(s.Markdown.Diagrams.Kroki);
105 Assert.Equal("https://kroki.io", s.Markdown.Diagrams.KrokiUrl);
106 }
107
108 [Fact]
109 public void Serialize_RoundTrip_PreservesDisplayScreensTopology()
110 {
111 var original = new CascadeIdeSettings
112 {
113 Display = new DisplaySettings
114 {
115 Screens = new DisplayScreensSettings
116 {
117 Topology = "(0.5PFD + 0.5Forward) (MFD)",
118 },
119 },
120 };
121 var toml = TomlSerializer.Serialize(original, Options);
122 Assert.Contains("[display.screens]", toml, StringComparison.Ordinal);
123 var roundtrip = Deserialize(toml);
124 Assert.Equal("(0.5PFD + 0.5Forward) (MFD)", roundtrip.GetEffectivePresentationLine());
125 }
126
127 [Fact]
128 public void Deserialize_DisplayScreensTopology_AndGrammar_UsedForEffective()
129 {
130 const string text =
131 """
132 [display.screens]
133 topology = "(P)(F)(M)"
134
135 [display.screens.grammar]
136 pfd = "P"
137 forward = "F"
138 mfd = "M"
139 """;
140
141 var s = Deserialize(text);
142 Assert.Equal("(P)(F)(M)", s.GetEffectivePresentationLine());
143 var g = s.GetEffectivePresentationGrammar();
144 Assert.Equal("P", g.Pfd);
145 Assert.Equal("F", g.Forward);
146 Assert.Equal("M", g.Mfd);
147 }
148
149 [Fact]
150 public void Deserialize_CodeNavigationMapSection_ParsesViewAndDepth()
151 {
152 const string text =
153 """
154 [code_navigation_map]
155 view = "both"
156 depth = "controlFlow"
157 detail_level = "glance"
158 """;
159
160 var s = Deserialize(text);
161 Assert.True(s.CodeNavigationMap.WantsCodeNavigationMapList);
162 Assert.True(s.CodeNavigationMap.WantsCodeNavigationMapGraph);
163 Assert.True(s.CodeNavigationMap.IsControlFlowDepth);
164 Assert.Equal("both", CodeNavigationMapSettings.NormalizeView(s.CodeNavigationMap.View));
165 Assert.Equal(CodeNavigationMapLevelKind.ControlFlow, CodeNavigationMapSettings.NormalizeDepth(s.CodeNavigationMap.Depth));
166 Assert.Equal(CodeNavigationMapDetailLevel.Glance, s.CodeNavigationMap.NormalizedDetailLevel);
167 }
168
169 [Fact]
170 public void Deserialize_CodeNavigationMapSection_ParsesControlFlowMainAxis()
171 {
172 const string text =
173 """
174 [code_navigation_map]
175 control_flow_main_axis = "vertical"
176 """;
177
178 var s = Deserialize(text);
179 Assert.Equal(CodeNavigationMapControlFlowMainAxisKind.Vertical, s.CodeNavigationMap.NormalizedControlFlowMainAxis);
180 }
181
182 [Fact]
183 public void Deserialize_DisplayScreensTopology_WithoutGrammar_ParsesAsTwoScreens()
184 {
185 const string text =
186 """
187 [display.screens]
188 topology = "(P+F) (M)"
189 """;
190
191 var s = Deserialize(text);
192 Assert.Equal("(P+F) (M)", s.GetEffectivePresentationLine());
193 var grammar = PresentationGrammarTokens.FromSettings(
194 s.Display.Screens.Grammar.Brackets,
195 s.Display.Screens.Grammar.BetweenScreens,
196 s.Display.Screens.Grammar.BetweenZones,
197 s.Display.Screens.Grammar.Pfd,
198 s.Display.Screens.Grammar.Forward,
199 s.Display.Screens.Grammar.Mfd);
200 var parse = PresentationParser.Parse(s.GetEffectivePresentationLine(), grammar);
201 Assert.True(parse.IsSuccess);
202 Assert.Equal(2, parse.Screens.Count);
203 Assert.True(PresentationLayoutAnalyzer.IsDedicatedMfdSecondScreenPreset(parse.Screens));
204 }
205
206 [Fact]
207 public void Deserialize_AiNested_Adr0083_ParsesExpected()
208 {
209 const string text =
210 """
211 [ai]
212 mode = "local"
213
214 [ai.local]
215 backend = "ollama"
216
217 [ai.local.ollama]
218 model = "qwen2.5-coder:7b"
219
220 [ai.chat]
221 settings_presentation = "mfd"
222 show_thinking_in_history = true
223 """;
224
225 var s = Deserialize(text);
226 Assert.Equal("local", s.Ai.Mode);
227 Assert.Equal("ollama", s.Ai.Local.Backend);
228 Assert.Equal("qwen2.5-coder:7b", s.Ai.Local.Ollama.Model);
229 Assert.Equal("mfd", s.Ai.Chat.SettingsPresentation);
230 Assert.True(s.Ai.Chat.ShowThinkingInHistory);
231 }
232
233 [Fact]
234 public void Deserialize_LanguagesCSharp_executable_env_ParsesExpected()
235 {
236 const string text =
237 """
238 [languages.csharp]
239 mode = "Custom"
240
241 [languages.csharp.custom]
242 executable = "fallback.exe"
243 executable_env = "CASCADE_IDE_CSHARP_EXECUTABLE"
244 """;
245
246 var s = Deserialize(text);
247 Assert.Equal("fallback.exe", s.Languages.CSharp.Custom.Executable);
248 Assert.Equal("CASCADE_IDE_CSHARP_EXECUTABLE", s.Languages.CSharp.Custom.ExecutableEnv);
249 }
250
251 [Fact]
252 public void Deserialize_LanguagesCSharpNestedMode_ParsesExpected()
253 {
254 const string text =
255 """
256 [languages.csharp]
257 mode = "OmniSharp"
258
259 [languages.csharp.omni_sharp]
260 executable = "C:\\omnisharp-win-x64-net6.0\\OmniSharp.exe"
261 arguments = "--languageserver --hostPID 1234"
262 """;
263
264 var s = Deserialize(text);
265 var runtime = s.Languages.CSharp.ResolveForRuntime();
266 Assert.Equal("OmniSharp", runtime.Mode);
267 Assert.Equal("C:\\omnisharp-win-x64-net6.0\\OmniSharp.exe", runtime.Executable);
268 Assert.Equal("--languageserver --hostPID 1234", runtime.Arguments);
269 }
270
271 [Fact]
272 public void Deserialize_LanguagesMarkdownNestedMode_ParsesExpected()
273 {
274 const string text =
275 """
276 [languages.markdown]
277 mode = "Marksman"
278
279 [languages.markdown.marksman]
280 executable = "C:\\tools\\marksman.exe"
281 arguments = "--stdio"
282 """;
283
284 var s = Deserialize(text);
285 var runtime = s.Languages.Markdown.ResolveForRuntime();
286 Assert.Equal("Marksman", runtime.Mode);
287 Assert.Equal("C:\\tools\\marksman.exe", runtime.Executable);
288 Assert.Equal("--stdio", runtime.Arguments);
289 }
290
291 [Fact]
292 public void Deserialize_AgentNotes_ConfigPath_ParsesExpected()
293 {
294 const string toml = """
295 [agent_notes]
296 config_path = "D:/agent-notes-mcp/agent-notes-mcp.toml"
297 """;
298 var s = CascadeTomlSerializer.Deserialize<CascadeIdeSettings>(toml)!;
299 Assert.Equal("D:/agent-notes-mcp/agent-notes-mcp.toml", s.AgentNotes.ConfigPath);
300 }
301
302 [Fact]
303 public void Deserialize_AgentNotes_KbBaseOverlayPath_ParsesExpected()
304 {
305 const string text =
306 """
307 [agent_notes]
308 kb_base_overlay_path = "D:\\vault\\agent-notes"
309 """;
310
311 var s = Deserialize(text);
312 Assert.Equal("D:\\vault\\agent-notes", s.AgentNotes.KbBaseOverlayPath);
313 }
314
315 [Fact]
316 public void Deserialize_IntercomAttachmentsCodeNavigate_ParsesExpected()
317 {
318 const string text =
319 """
320 [intercom.attachments.code]
321 navigate = "select"
322 """;
323
324 var s = Deserialize(text);
325 Assert.True(s.Intercom.Attachments.Code.DefaultNavigateSelects());
326 Assert.Equal("select", s.Intercom.Attachments.Code.Navigate);
327 }
328
329 [Fact]
330 public void Deserialize_IntercomAttachmentsCodeRevealLoadSolution_ParsesExpected()
331 {
332 const string text =
333 """
334 [intercom.attachments.code]
335 reveal_load_solution = "never"
336 """;
337
338 var s = Deserialize(text);
339 Assert.False(s.Intercom.Attachments.Code.ShouldLoadSolutionBeforeReveal());
340 Assert.Equal("never", s.Intercom.Attachments.Code.RevealLoadSolution);
341 }
342
343 [Fact]
344 public void Deserialize_IntercomFeedMetrics_ParsesCompact()
345 {
346 const string text =
347 """
348 [intercom]
349 feed_metrics = "compact"
350 """;
351
352 var s = Deserialize(text);
353 Assert.Equal(IntercomFeedMetricsModes.Compact, s.Intercom.FeedMetrics);
354 Assert.False(s.Intercom.UseComfortableFeedMetrics());
355 }
356
357 [Fact]
358 public void Deserialize_IntercomAttachmentsCode_DefaultsWhenSectionMissing()
359 {
360 const string text =
361 """
362 [hybrid_index]
363 enabled = false
364 """;
365
366 var s = Deserialize(text);
367 Assert.False(s.Intercom.Attachments.Code.DefaultNavigateSelects());
368 Assert.Equal("reveal", s.Intercom.Attachments.Code.Navigate);
369 Assert.True(s.Intercom.Attachments.Code.ShouldLoadSolutionBeforeReveal());
370 }
371
372 [Fact]
373 public void Deserialize_HybridIndexSection_ParsesExpected()
374 {
375 const string text =
376 """
377 [hybrid_index]
378 enabled = true
379 index_dir = ".hci"
380 debounce_ms = 1234
381 auto_reindex_on_solution_open = false
382 watch_files = false
383 scope_mode = "workspace"
384 pause_when_mcp_stdio_host = true
385 """;
386
387 var s = Deserialize(text);
388 Assert.True(s.HybridIndex.Enabled);
389 Assert.Equal(".hci", s.HybridIndex.IndexDir);
390 Assert.Equal(1234, s.HybridIndex.DebounceMs);
391 Assert.False(s.HybridIndex.AutoReindexOnSolutionOpen);
392 Assert.False(s.HybridIndex.WatchFiles);
393 Assert.Equal("workspace", s.HybridIndex.ScopeMode);
394 Assert.True(s.HybridIndex.PauseWhenMcpStdioHost);
395 }
396
397 [Fact]
398 public void CascadeIdeSettings_Clone_CopiesHybridIndex()
399 {
400 var s = new CascadeIdeSettings
401 {
402 HybridIndex = new HybridIndexSettings
403 {
404 Enabled = false,
405 IndexDir = ".hci2",
406 DebounceMs = 999,
407 AutoReindexOnSolutionOpen = false,
408 WatchFiles = false,
409 ScopeMode = "workspace",
410 PauseWhenMcpStdioHost = true,
411 },
412 };
413 var c = (CascadeIdeSettings)s.Clone();
414 Assert.False(c.HybridIndex.Enabled);
415 Assert.Equal(".hci2", c.HybridIndex.IndexDir);
416 Assert.Equal(999, c.HybridIndex.DebounceMs);
417 Assert.False(c.HybridIndex.AutoReindexOnSolutionOpen);
418 Assert.False(c.HybridIndex.WatchFiles);
419 Assert.Equal("workspace", c.HybridIndex.ScopeMode);
420 Assert.True(c.HybridIndex.PauseWhenMcpStdioHost);
421 }
422
423 [Fact]
424 public void CascadeIdeSettings_Is_ComparesHybridIndex()
425 {
426 var a = new CascadeIdeSettings();
427 var b = new CascadeIdeSettings();
428 Assert.True(a.Is(b));
429
430 b.HybridIndex.DebounceMs = 123;
431 Assert.False(a.Is(b));
432 }
433
434 [Fact]
435 public void Deserialize_CommandPaletteGoToSearch_ParsesBackend()
436 {
437 const string text =
438 """
439 [command_palette.go_to_search]
440 backend = "hci"
441 """;
442
443 var s = Deserialize(text);
444 Assert.Equal("hci", s.CommandPalette.GoToSearch.Backend);
445 Assert.Equal(CommandPaletteGoToSearchBackendKind.Hci, CommandPaletteGoToSearchBackendNormalizer.Parse(s.CommandPalette.GoToSearch.Backend));
446 }
447
448 [Fact]
449 public void NormalizeHybridIndexScopeMode_DefaultsExpected()
450 {
451 Assert.Equal("workspace", ShellSettingsPresentationProjection.NormalizeHybridIndexScopeMode("workspace"));
452 Assert.Equal("workspace+solution", ShellSettingsPresentationProjection.NormalizeHybridIndexScopeMode(""));
453 Assert.Equal("workspace+solution", ShellSettingsPresentationProjection.NormalizeHybridIndexScopeMode("garbage"));
454 }
455
456 [Fact]
457 public void Deserialize_CodeNavigationMapControlFlowGrain_ParsesExpected()
458 {
459 const string text =
460 """
461 [code_navigation_map]
462 control_flow_grain = "detailed"
463 """;
464
465 var s = Deserialize(text);
466 Assert.Equal("detailed", s.CodeNavigationMap.ControlFlowGrain);
467 Assert.Equal(CodeNavigationMapControlFlowGrainKind.Detailed, s.CodeNavigationMap.NormalizedControlFlowGrain);
468 }
469
470 [Fact]
471 public void Deserialize_CodeNavigationMapControlFlowGrain_Unknown_NormalizesIntent()
472 {
473 const string text =
474 """
475 [code_navigation_map]
476 control_flow_grain = "micro_cfg"
477 """;
478
479 var s = Deserialize(text);
480 Assert.Equal("micro_cfg", s.CodeNavigationMap.ControlFlowGrain);
481 Assert.Equal(CodeNavigationMapControlFlowGrainKind.Intent, s.CodeNavigationMap.NormalizedControlFlowGrain);
482 }
483
484}
485
View only · write via MCP/CIDE