Forge
csharpdeeb25a2
1using System.Text.Json;
2using CascadeIDE.Services;
3using Xunit;
4using TestContext = Xunit.TestContext;
5
6namespace CascadeIDE.Tests;
7
8public sealed class IdeMcpServerDispatchTests
9{
10 [Fact]
11 public async Task ProxyTool_MapsIdePrefixToCommandId()
12 {
13 var fake = new FakeActions();
14 var args = new Dictionary<string, JsonElement>
15 {
16 ["path"] = JsonSerializer.SerializeToElement(@"C:\tmp\a.txt")
17 };
18
19 var result = await CallToolByConventionAsync(fake, "ide_open_file", args);
20
21 Assert.Equal("OK", result);
22 Assert.Equal(IdeCommands.OpenFile, fake.LastCommandId);
23 Assert.Same(args, fake.LastArgs);
24 }
25
26 [Fact]
27 public async Task DispatcherTool_UsesCommandIdFromArguments()
28 {
29 var fake = new FakeActions();
30 var args = new Dictionary<string, JsonElement>
31 {
32 ["command_id"] = JsonSerializer.SerializeToElement(IdeCommands.ToggleTerminal),
33 ["args"] = JsonSerializer.SerializeToElement(new { visible = true })
34 };
35
36 var result = await CallToolByConventionAsync(fake, "ide_execute_command", args);
37
38 Assert.Equal("OK", result);
39 Assert.Equal(IdeCommands.ToggleTerminal, fake.LastCommandId);
40 Assert.NotNull(fake.LastArgs);
41 Assert.True(fake.LastArgs!.TryGetValue("visible", out var visEl) && visEl.GetBoolean());
42 Assert.False(fake.LastArgs.ContainsKey("args"));
43 }
44
45 [Fact]
46 public async Task DispatcherTool_MergesNestedArgsObjectForExecuteCommand()
47 {
48 var fake = new FakeActions();
49 var args = new Dictionary<string, JsonElement>
50 {
51 ["command_id"] = JsonSerializer.SerializeToElement(IdeCommands.DebugLaunch),
52 ["args"] = JsonSerializer.SerializeToElement(new
53 {
54 workspace_path = @"D:\proj",
55 target_path = @"C:\Program Files\dotnet\dotnet.exe"
56 })
57 };
58
59 var result = await CallToolByConventionAsync(fake, "ide_execute_command", args);
60
61 Assert.Equal("OK", result);
62 Assert.Equal(IdeCommands.DebugLaunch, fake.LastCommandId);
63 Assert.NotNull(fake.LastArgs);
64 Assert.Equal(@"D:\proj", fake.LastArgs!["workspace_path"].GetString());
65 Assert.Equal(@"C:\Program Files\dotnet\dotnet.exe", fake.LastArgs["target_path"].GetString());
66 }
67
68 [Fact]
69 public async Task ProxyTool_DottedCommandId_MapsUnderscoreToolToCanonicalCommandId()
70 {
71 var fake = new FakeActions();
72 var result = await CallToolByConventionAsync(fake, "ide_intercom_reveal_attachment", null);
73
74 Assert.Equal("OK", result);
75 Assert.Equal(IdeCommands.IntercomRevealAttachment, fake.LastCommandId);
76 }
77
78 [Fact]
79 public async Task CompatibilityOverride_IdeBuildMapsToBuildStructured()
80 {
81 var fake = new FakeActions();
82 var args = new Dictionary<string, JsonElement>();
83
84 var result = await CallToolByConventionAsync(fake, "ide_build", args);
85
86 Assert.Equal("OK", result);
87 Assert.Equal(IdeCommands.BuildStructured, fake.LastCommandId);
88 }
89
90 private static async Task<string> CallToolByConventionAsync(
91 IIdeMcpActions actions,
92 string toolName,
93 IReadOnlyDictionary<string, JsonElement>? args)
94 {
95 var mi = typeof(IdeMcpServer).GetMethod(
96 "CallToolByConventionAsync",
97 System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
98 Assert.NotNull(mi);
99
100 var task = (Task<string>)mi!.Invoke(
101 null,
102 new object?[] { actions, toolName, args, TestContext.Current.CancellationToken })!;
103 return await task.ConfigureAwait(false);
104 }
105
106 private sealed class FakeActions : IIdeMcpActions
107 {
108 public string? LastCommandId { get; private set; }
109 public IReadOnlyDictionary<string, JsonElement>? LastArgs { get; private set; }
110
111 public Task<string> ExecuteCommandAsync(string commandId, IReadOnlyDictionary<string, JsonElement>? args, CancellationToken cancellationToken = default)
112 {
113 LastCommandId = commandId;
114 LastArgs = args;
115 return Task.FromResult("OK");
116 }
117
118 public void OpenFile(string path) => throw new NotImplementedException();
119 public void LoadSolution(string path) => throw new NotImplementedException();
120
121 public Task<string> LoadSolutionAndWaitAsync(string path, CancellationToken cancellationToken = default) =>
122 throw new NotImplementedException();
123 public void SelectInEditor(string? filePath, int startLine, int startColumn, int endLine, int endColumn) => throw new NotImplementedException();
124 public Task<string> GetEditorStateAsync(int? maxPreviewChars = null) => throw new NotImplementedException();
125 public Task<string> GetEditorContentRangeAsync(int startLine, int endLine) => throw new NotImplementedException();
126 public Task<string> GetOpenDocumentTextAsync(string? filePath, int? maxChars) => throw new NotImplementedException();
127 public Task<string> ApplyEditAsync(string filePath, int startLine, int startColumn, int endLine, int endColumn, string newText) =>
128 Task.FromResult("OK");
129
130 public Task<string> ReadWorkspaceFileAsync(string filePath, int? offset, int? limit, int? maxChars) =>
131 Task.FromResult("""{"text":""}""");
132
133 public Task<string> SaveDocumentAsync(string? filePath, string? content) =>
134 Task.FromResult("""{"file_path":"x"}""");
135 public void GoToPosition(string? filePath, int line, int column, int? endLine = null, int? endColumn = null) => throw new NotImplementedException();
136 public void RevealEditorRange(string? filePath, int startLine, int endLine, int? durationMs) => throw new NotImplementedException();
137 public string GetSolutionInfo() => throw new NotImplementedException();
138 public Task<string> GetSolutionFilesAsync() => throw new NotImplementedException();
139 public Task<string> SearchWorkspaceTextAsync(string pattern, string? subPath, bool fixedString, string? glob, int maxMatches, string? rgPath) => throw new NotImplementedException();
140 public Task<string> SearchWebPublicQueryAsync(string query, CancellationToken cancellationToken = default) => throw new NotImplementedException();
141 public Task<string> FetchWebPublicUrlAsync(string url, int? maxChars, CancellationToken cancellationToken = default) => throw new NotImplementedException();
142 public Task<string> GetCurrentFileDiagnosticsAsync() => throw new NotImplementedException();
143 public Task<string> GetCodeNavigationContextAsync(string mode, string? filePath, int? line, int? column, int? maxRelated, int? maxNodes, int? maxEdges, string? preset, IReadOnlyList<string>? includeKinds, IReadOnlyList<string>? excludeKinds, string? level) => throw new NotImplementedException();
144 public Task<string> BuildAsync() => throw new NotImplementedException();
145 public Task<string> BuildStructuredAsync() => throw new NotImplementedException();
146 public Task<string> RunTestsAsync() => throw new NotImplementedException();
147 public Task<string> RunAffectedTestsAsync(IReadOnlyList<string>? changedPaths = null) => throw new NotImplementedException();
148 public Task<string> RunCodeCleanupAsync(string? includePath = null) => throw new NotImplementedException();
149 public Task<string> GetCodeMetricsAsync(string? scope = null, string? path = null) => throw new NotImplementedException();
150 public Task<string> GetIdeStateAsync() => throw new NotImplementedException();
151 public Task<string> GetCockpitSurfaceAsync() => throw new NotImplementedException();
152 public Task<string> GetUiModesDiagnosticsAsync() => throw new NotImplementedException();
153 public Task<string> GitStatusAsync() => throw new NotImplementedException();
154 public Task<string> GitDiffAsync(string? path = null, bool staged = false) => throw new NotImplementedException();
155 public Task<string> GitCommitAsync(string message, IReadOnlyList<string>? paths = null) => throw new NotImplementedException();
156 public Task<string> GitPushAsync(string? remote = null, string? branch = null, bool dryRun = false) => throw new NotImplementedException();
157 public Task<string> GitLogAsync(int n = 20) => throw new NotImplementedException();
158 public Task<string> GitFetchAsync(string? remote = null, bool all = false, bool prune = false, bool dryRun = false) => throw new NotImplementedException();
159 public Task<string> GitPullAsync(string? remote = null, string? branch = null, bool ffOnly = true, bool dryRun = false) => throw new NotImplementedException();
160 public Task<string> GitBranchAsync(string? action = null, string? name = null, string? startPoint = null, bool force = false) => throw new NotImplementedException();
161 public Task<string> GitShowAsync(string rev, string? path = null, bool statOnly = false) => throw new NotImplementedException();
162 public Task<string> GitSubmoduleAsync(string? action = null, string? path = null, bool recursive = true) => throw new NotImplementedException();
163 public Task<string> GitPreflightAsync(bool staged = false, bool includeUntracked = true, bool includePatches = true) => throw new NotImplementedException();
164 public Task<string> GitPreflightFixSafeAsync(bool includePatches = true) => throw new NotImplementedException();
165 public string GetBuildOutput() => throw new NotImplementedException();
166 public void SetBreakpoint(string filePath, int line, string? condition = null) => throw new NotImplementedException();
167 public void RemoveBreakpoint(string filePath, int line) => throw new NotImplementedException();
168 public void ShowPreview(string title, string content) => throw new NotImplementedException();
169 public void ShowEditorPreview() => throw new NotImplementedException();
170 public Task<string> RequestConfirmationAsync(string message, CancellationToken cancellationToken = default) => throw new NotImplementedException();
171 public void FocusEditor() => throw new NotImplementedException();
172 public string GetUiTheme() => throw new NotImplementedException();
173 public Task<string> SetUiThemeAsync(string themeJson) => throw new NotImplementedException();
174 public Task<string> GetUiLayoutAsync() => throw new NotImplementedException();
175 public Task<string> GetColorsUnderCursorAsync() => throw new NotImplementedException();
176 public Task<string> GetControlAppearanceAsync(string? name) => throw new NotImplementedException();
177 public Task<string> SetControlLayoutAsync(string controlName, string layoutJson) => throw new NotImplementedException();
178 public Task<string> AddControlAsync(string parentName, string controlType, string? content, string? name) => throw new NotImplementedException();
179 public Task<string> SetControlTextAsync(string controlName, string text) => throw new NotImplementedException();
180 public Task<string> ClickControlAsync(string? controlName) => throw new NotImplementedException();
181 public Task<string> SendKeysAsync(string? controlName, string keys) => throw new NotImplementedException();
182 public Task<string> SetFocusAsync(string? controlName) => throw new NotImplementedException();
183 public Task<string> HighlightControlAsync(string? controlName) => throw new NotImplementedException();
184 public Task<string> SetPanelSizeAsync(string panel, double? width, double? height) => throw new NotImplementedException();
185 public string GetSupportedEditorLanguages() => throw new NotImplementedException();
186 public Task<string> GetDebugSnapshotAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
187 public Task<string> WriteAgentNotesAsync(string content, CancellationToken cancellationToken = default) => throw new NotImplementedException();
188 public Task<string> ReadAgentNotesAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
189 public Task<string> AppendAgentNotesAsync(string content, CancellationToken cancellationToken = default) => throw new NotImplementedException();
190 public Task<string> ListAgentNotesRevisionsAsync(int? limit = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
191 public Task<string> RollbackAgentNotesAsync(string? revisionFile = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
192 public Task<string> ReadHotContextAsync(string? activeScope = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
193 public Task<string> RouteContextAsync(string query, string? activeScope = null, int? maxSections = null, int? maxChars = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
194 public Task<string> MemoryHealthAsync(string? activeScope = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
195 public Task<string> CompactHotContextAsync(bool apply = false, CancellationToken cancellationToken = default) => throw new NotImplementedException();
196 public Task<string> ExtractFromArchiveAsync(string query, string? revisionFile = null, int? headLimit = null, int? contextLines = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
197 public Task<string> UpsertAgentNotesSectionAsync(string sectionId, string content, CancellationToken cancellationToken = default) => throw new NotImplementedException();
198 public Task<string> SearchAgentNotesAsync(string query, int? headLimit = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
199 public Task<string> ReadKnowledgeFileAsync(string filePath, int? offset = null, int? limit = null, string? knowledgeRootId = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
200 public Task<string> ListKnowledgeFilesAsync(string? subdir = null, string? knowledgeRootId = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
201 public Task<string> WriteKnowledgeFileAsync(string filePath, string content, string? knowledgePath = null, bool saveRevision = true, string? knowledgeRootId = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
202 public Task<string> AppendKnowledgeFileAsync(string filePath, string content, string? knowledgePath = null, bool saveRevision = true, string? knowledgeRootId = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
203 public Task<string> UpsertKnowledgeSectionAsync(string filePath, string sectionId, string content, string? knowledgePath = null, bool saveRevision = true, string? knowledgeRootId = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
204 public Task<string> DeleteKnowledgeFileAsync(string filePath, string? knowledgePath = null, string? knowledgeRootId = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
205 public Task<string> DeleteKnowledgeSectionAsync(string filePath, string sectionId, string? knowledgePath = null, string? knowledgeRootId = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
206 public Task<string> CodebaseIndexStatusAsync(string? workspacePath, string? solutionPath, CancellationToken cancellationToken = default) => throw new NotImplementedException();
207 public Task<string> CodebaseIndexSearchAsync(string? workspacePath, string? solutionPath, string query, int topN, string? pathPrefix, IReadOnlyList<string>? excludePathPrefixes, IReadOnlyList<string>? extensions, bool semantic, double alpha, double beta, int vecTopK, CancellationToken cancellationToken = default) => throw new NotImplementedException();
208 public Task<string> CodebaseIndexExplainAsync(string? workspacePath, string? solutionPath, long hitId, CancellationToken cancellationToken = default) => throw new NotImplementedException();
209 public Task<string> CodebaseIndexReindexAsync(string? workspacePath, string? solutionPath, bool fullRebuild, CancellationToken cancellationToken = default) => throw new NotImplementedException();
210 public Task<string> SelectChatMessageAsync(int index) => throw new NotImplementedException();
211 public Task<string> SelectChatMessageByOrdinalAsync(int ordinal, int endOrdinal) => throw new NotImplementedException();
212 public Task<string> GetSelectedChatMessageAsync() => throw new NotImplementedException();
213 public Task<string> FindIntercomMessagesForCodeAsync(IReadOnlyDictionary<string, JsonElement>? args) =>
214 throw new NotImplementedException();
215 public Task<string> RelateIntercomMessageRangeToCodeAsync(IReadOnlyDictionary<string, JsonElement>? args) =>
216 throw new NotImplementedException();
217 public Task<string> EditChatAssistantMessageAsync(string messageId, string newContent, string? reason = null) => throw new NotImplementedException();
218 public Task<string> ExportChatReadableAsync(bool writeFile = false, string? fileName = null) => throw new NotImplementedException();
219 public Task<string> IdeAgentVerifyAsync(string? policy, string? sandboxProfile, string? solutionPath, CancellationToken cancellationToken = default) => throw new NotImplementedException();
220 public Task<string> IdeAgentVerifyBatchAsync(string? policy, string? sandboxProfile, string? solutionPath, bool useWorktree, CancellationToken cancellationToken = default) => throw new NotImplementedException();
221 public Task<string> IdeAgentCancelAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
222 public Task<string> IdeAgentStatusAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
223 public Task<string> IdeAgentLastAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
224 public Task<string> IdeAgentSandboxPrepareAsync(string? profile, string? workspaceRoot, CancellationToken cancellationToken = default) => throw new NotImplementedException();
225 }
226}
227
228
View only · write via MCP/CIDE