| 1 | using System.Collections.ObjectModel; |
| 2 | using System.Globalization; |
| 3 | using System.Text.Json; |
| 4 | using CascadeIDE.Contracts; |
| 5 | using CascadeIDE.Models; |
| 6 | using CascadeIDE.Features.Workspace.Application; |
| 7 | |
| 8 | namespace 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] |
| 15 | public 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 | |