Forge
csharpa2ee7322
1using System.Collections.ObjectModel;
2using System.Globalization;
3using System.Text.Json;
4using CascadeIDE.Contracts;
5using CascadeIDE.Models;
6using CascadeIDE.Features.Workspace.Application;
7
8namespace CascadeIDE.Features.IdeMcp.Application;
9
10/// <summary>
11/// Application-level orchestrator helpers for IDE MCP build/test actions.
12/// Keeps payload/filter/log shaping out of MainWindowViewModel.
13/// </summary>
14[ApplicationOrchestrator]
15public static class IdeMcpBuildTestOrchestrator
16{
17 public static string MissingSolutionMessage() =>
18 "No solution loaded or file not found.";
19
20 /// <summary>Текст MCP и содержимое панели при отсутствии решения (MCP-сборка с показом панели).</summary>
21 public readonly record struct IdeMcpBuildMissingSolutionPanel(string McpReplyText, string BuildOutputPanelFullText);
22
23 public static IdeMcpBuildMissingSolutionPanel BuildMissingSolutionPanelSurface()
24 {
25 var msg = MissingSolutionMessage();
26 return new IdeMcpBuildMissingSolutionPanel(msg, BuildPanelLine(msg));
27 }
28
29 /// <summary>Текст MCP и содержимое панели при исключении в цепочке MCP-сборки.</summary>
30 public readonly record struct IdeMcpBuildFailurePanel(string McpReplyText, string BuildOutputPanelFullText);
31
32 public static IdeMcpBuildFailurePanel FailedBuildPanelSurface(string exceptionMessage)
33 {
34 var msg = BuildErrorMessage(exceptionMessage);
35 return new IdeMcpBuildFailurePanel(msg, BuildPanelLine(msg));
36 }
37
38 public static string BuildOperationHeader(string operation, string path) =>
39 $"{operation}: {path}\r\n";
40
41 public static string BuildErrorMessage(string exceptionMessage) =>
42 "Error: " + exceptionMessage;
43
44 public static string BuildPanelLine(string text) =>
45 text + "\r\n";
46
47 public static string BuildTestSummary(int passed, int total, int failed) =>
48 $"{passed}/{total} passed, {failed} failed";
49
50 public static string SerializeSolutionFilesPayload<TEntry, TNode>(IReadOnlyList<TEntry> fileEntries, IReadOnlyList<TNode> solutionTree) =>
51 JsonSerializer.Serialize(new { file_entries = fileEntries, solution_tree = solutionTree });
52
53 /// <summary>Плоские записи файлов и дерево решения для MCP <c>get_solution_files</c>; обход через <see cref="McpSolutionTree"/>.</summary>
54 public static string BuildSolutionFilesJson(string? solutionPath, ObservableCollection<SolutionItem> solutionRoots)
55 {
56 var entries = McpSolutionTree.CollectFileEntries(solutionRoots).Select(e => new
57 {
58 path = e.FullPath,
59 title = e.Title,
60 relative_path = McpSolutionTree.GetRelativePath(solutionPath, e.FullPath)
61 }).ToList();
62 var tree = solutionRoots.Select(r => McpSolutionTree.BuildSolutionTreeNode(r, solutionPath)).ToList();
63 return SerializeSolutionFilesPayload(entries, tree);
64 }
65
66 public static (string? filterExpression, string mode, IReadOnlyList<string>? tokens) BuildAffectedTestsRequest(
67 IReadOnlyList<string>? changedPaths)
68 {
69 var tokens = Services.McpDotnetBuildTestService.BuildAffectedTestTokens(changedPaths);
70 if (tokens.Count == 0)
71 return (null, "fallback_all", null);
72
73 var filter = string.Join('|', tokens.Select(t => $"FullyQualifiedName~{t}"));
74 return (filter, "affected", tokens);
75 }
76
77 public static string SerializeMissingSolutionError(string mode) =>
78 JsonSerializer.Serialize(new { success = false, error = "No solution loaded or file not found.", mode });
79
80 public static string SerializeCodeCleanupFailure(string error) =>
81 JsonSerializer.Serialize(new { success = false, error });
82
83 public static string BuildTruncatedRawOutput(string output, int maxChars) =>
84 output.Length > maxChars ? output[..maxChars] + "\n... (output truncated)" : output;
85
86 public static string SerializeCodeCleanupResult(bool success, int exitCode, string rawOutput) =>
87 JsonSerializer.Serialize(new
88 {
89 success,
90 exit_code = exitCode,
91 raw_output = rawOutput
92 });
93
94 public static string BuildTestResultLogBlock(string summary, string consoleOutput)
95 {
96 var stamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
97 return $"=== {stamp} ===\n{summary}\n\n{consoleOutput}\n\n";
98 }
99
100 public static string BuildTestErrorLogBlock(string errorMessage)
101 {
102 var stamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
103 return $"=== {stamp} (ошибка) ===\n{errorMessage}\n\n";
104 }
105
106 public static string AppendLogWithLimit(string existing, string block, int maxChars)
107 {
108 var combined = existing + block;
109 return combined.Length > maxChars ? combined[^maxChars..] : combined;
110 }
111
112 public static string BuildUpdatedTestResultsOutput(string existingOutput, string summary, string consoleOutput) =>
113 AppendLogWithLimit(existingOutput, BuildTestResultLogBlock(summary, consoleOutput), 120_000);
114
115 public static string BuildUpdatedTestErrorOutput(string existingOutput, string errorMessage) =>
116 AppendLogWithLimit(existingOutput, BuildTestErrorLogBlock(errorMessage), 120_000);
117
118 public static bool ShouldOpenTestsPage(bool instrumentationTabsEnabled) =>
119 instrumentationTabsEnabled;
120
121 public static (string summary, int impactedTestsBadge, string updatedOutput, bool shouldOpenTestsPage)
122 BuildTestUiOutcome(
123 int passed,
124 int total,
125 int failed,
126 string existingOutput,
127 string consoleOutput,
128 bool instrumentationTabsEnabled)
129 {
130 var summary = BuildTestSummary(passed, total, failed);
131 var updatedOutput = BuildUpdatedTestResultsOutput(existingOutput, summary, consoleOutput);
132 return (summary, failed, updatedOutput, ShouldOpenTestsPage(instrumentationTabsEnabled));
133 }
134
135 public static (string summary, int impactedTestsBadge, string updatedOutput)
136 BuildTestErrorUiOutcome(string existingOutput, string errorMessage) =>
137 ("", 0, BuildUpdatedTestErrorOutput(existingOutput, errorMessage));
138
139 /// <summary>Дифф для UI «Инструментация · тесты» и DataBus после MCP-тестового прогона (без ссылок на VM).</summary>
140 public readonly record struct IdeMcpTestRunInstrumentationMutation(
141 string Summary,
142 int ImpactedTestsBadge,
143 string UpdatedTestResultsOutput,
144 bool ShouldOpenTestsPage)
145 {
146 public static IdeMcpTestRunInstrumentationMutation FromSuccessfulParse(
147 int passed,
148 int total,
149 int failed,
150 string existingInstrumentationOutput,
151 string consoleOutput,
152 bool instrumentationTabsEnabled)
153 {
154 var u = BuildTestUiOutcome(
155 passed,
156 total,
157 failed,
158 existingInstrumentationOutput,
159 consoleOutput,
160 instrumentationTabsEnabled);
161 return new IdeMcpTestRunInstrumentationMutation(
162 u.summary,
163 u.impactedTestsBadge,
164 u.updatedOutput,
165 u.shouldOpenTestsPage);
166 }
167
168 /// <remarks>Не обновляет «последний summary» успешного прогона — только журнал и сброс бейджа на шину (совместимо с прежней обработкой в VM).</remarks>
169 public static IdeMcpTestRunInstrumentationMutation FromThrownException(
170 string existingInstrumentationOutput,
171 string exceptionMessage)
172 {
173 var u = BuildTestErrorUiOutcome(existingInstrumentationOutput, exceptionMessage);
174 return new IdeMcpTestRunInstrumentationMutation(
175 u.summary,
176 u.impactedTestsBadge,
177 u.updatedOutput,
178 ShouldOpenTestsPage: false);
179 }
180 }
181}
182
View only · write via MCP/CIDE