| 1 | using System.Text.Json; |
| 2 | using CascadeIDE.Features.WebAiPortal.Application; |
| 3 | using CascadeIDE.Services; |
| 4 | using Xunit; |
| 5 | using TestContext = Xunit.TestContext; |
| 6 | |
| 7 | namespace CascadeIDE.Tests; |
| 8 | |
| 9 | public sealed class WebAiPortalCommandBridgeTests |
| 10 | { |
| 11 | [Fact] |
| 12 | public async Task Execute_rejects_when_bridge_not_armed() |
| 13 | { |
| 14 | var fake = new FakeActions(); |
| 15 | var bridge = new WebAiPortalCommandBridge(fake); |
| 16 | bridge.BridgeConsented = true; |
| 17 | bridge.BridgeArmed = false; |
| 18 | var r = await bridge.ExecuteFromWebJsonAsync( |
| 19 | """{"command_id":"codebase_index_search","args":{"query":"x"}}""", |
| 20 | TestContext.Current.CancellationToken); |
| 21 | Assert.Contains("not armed", r, StringComparison.OrdinalIgnoreCase); |
| 22 | } |
| 23 | |
| 24 | [Fact] |
| 25 | public async Task Execute_rejects_unknown_command_id() |
| 26 | { |
| 27 | var fake = new FakeActions(); |
| 28 | var bridge = new WebAiPortalCommandBridge(fake) |
| 29 | { |
| 30 | BridgeConsented = true, |
| 31 | BridgeArmed = true, |
| 32 | }; |
| 33 | var r = await bridge.ExecuteFromWebJsonAsync( |
| 34 | """{"command_id":"open_file"}""", |
| 35 | TestContext.Current.CancellationToken); |
| 36 | Assert.Contains("not allowed", r, StringComparison.OrdinalIgnoreCase); |
| 37 | } |
| 38 | |
| 39 | [Fact] |
| 40 | public async Task Execute_forwards_whitelisted_command() |
| 41 | { |
| 42 | var fake = new FakeActions(); |
| 43 | var bridge = new WebAiPortalCommandBridge(fake) |
| 44 | { |
| 45 | BridgeConsented = true, |
| 46 | BridgeArmed = true, |
| 47 | }; |
| 48 | var r = await bridge.ExecuteFromWebJsonAsync( |
| 49 | """{"command_id":"codebase_index_search","args":{"query":"test"}}""", |
| 50 | TestContext.Current.CancellationToken); |
| 51 | Assert.Equal("{\"hits\":[]}", r); |
| 52 | Assert.Equal(IdeCommands.CodebaseIndexSearch, fake.LastCommandId); |
| 53 | } |
| 54 | |
| 55 | private sealed class FakeActions : IIdeMcpActions |
| 56 | { |
| 57 | public string? LastCommandId { get; private set; } |
| 58 | |
| 59 | public Task<string> ExecuteCommandAsync( |
| 60 | string commandId, |
| 61 | IReadOnlyDictionary<string, JsonElement>? args, |
| 62 | CancellationToken cancellationToken = default) |
| 63 | { |
| 64 | LastCommandId = commandId; |
| 65 | if (commandId == IdeCommands.CodebaseIndexSearch) |
| 66 | return Task.FromResult("{\"hits\":[]}"); |
| 67 | return Task.FromResult("OK"); |
| 68 | } |
| 69 | |
| 70 | public Task<string> RequestConfirmationAsync(string message, CancellationToken cancellationToken = default) => |
| 71 | Task.FromResult(ConfirmationResponses.Ok); |
| 72 | |
| 73 | public void OpenFile(string path) => throw new NotImplementedException(); |
| 74 | public void LoadSolution(string path) => throw new NotImplementedException(); |
| 75 | |
| 76 | public Task<string> LoadSolutionAndWaitAsync(string path, CancellationToken cancellationToken = default) => |
| 77 | throw new NotImplementedException(); |
| 78 | public void SelectInEditor(string? filePath, int startLine, int startColumn, int endLine, int endColumn) => throw new NotImplementedException(); |
| 79 | public Task<string> GetEditorStateAsync(int? maxPreviewChars = null) => throw new NotImplementedException(); |
| 80 | public Task<string> GetEditorContentRangeAsync(int startLine, int endLine) => throw new NotImplementedException(); |
| 81 | public Task<string> GetOpenDocumentTextAsync(string? filePath, int? maxChars) => throw new NotImplementedException(); |
| 82 | public Task<string> ApplyEditAsync(string filePath, int startLine, int startColumn, int endLine, int endColumn, string newText) => |
| 83 | throw new NotImplementedException(); |
| 84 | |
| 85 | public Task<string> ReadWorkspaceFileAsync(string filePath, int? offset, int? limit, int? maxChars) => |
| 86 | throw new NotImplementedException(); |
| 87 | |
| 88 | public Task<string> SaveDocumentAsync(string? filePath, string? content) => |
| 89 | throw new NotImplementedException(); |
| 90 | public void GoToPosition(string? filePath, int line, int column, int? endLine = null, int? endColumn = null) => throw new NotImplementedException(); |
| 91 | public void RevealEditorRange(string? filePath, int startLine, int endLine, int? durationMs) => throw new NotImplementedException(); |
| 92 | public string GetSolutionInfo() => throw new NotImplementedException(); |
| 93 | public Task<string> GetSolutionFilesAsync() => throw new NotImplementedException(); |
| 94 | public Task<string> SearchWorkspaceTextAsync(string pattern, string? subPath, bool fixedString, string? glob, int maxMatches, string? rgPath) => throw new NotImplementedException(); |
| 95 | public Task<string> SearchWebPublicQueryAsync(string query, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 96 | public Task<string> FetchWebPublicUrlAsync(string url, int? maxChars, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 97 | public Task<string> GetCurrentFileDiagnosticsAsync() => throw new NotImplementedException(); |
| 98 | 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(); |
| 99 | public Task<string> BuildAsync() => throw new NotImplementedException(); |
| 100 | public Task<string> BuildStructuredAsync() => throw new NotImplementedException(); |
| 101 | public Task<string> RunTestsAsync() => throw new NotImplementedException(); |
| 102 | public Task<string> RunAffectedTestsAsync(IReadOnlyList<string>? changedPaths = null) => throw new NotImplementedException(); |
| 103 | public Task<string> RunCodeCleanupAsync(string? includePath = null) => throw new NotImplementedException(); |
| 104 | public Task<string> GetCodeMetricsAsync(string? scope = null, string? path = null) => throw new NotImplementedException(); |
| 105 | public Task<string> GetIdeStateAsync() => throw new NotImplementedException(); |
| 106 | public Task<string> GetCockpitSurfaceAsync() => throw new NotImplementedException(); |
| 107 | public Task<string> GetUiModesDiagnosticsAsync() => throw new NotImplementedException(); |
| 108 | public Task<string> GitStatusAsync() => throw new NotImplementedException(); |
| 109 | public Task<string> GitDiffAsync(string? path = null, bool staged = false) => throw new NotImplementedException(); |
| 110 | public Task<string> GitCommitAsync(string message, IReadOnlyList<string>? paths = null) => throw new NotImplementedException(); |
| 111 | public Task<string> GitPushAsync(string? remote = null, string? branch = null, bool dryRun = false) => throw new NotImplementedException(); |
| 112 | public Task<string> GitLogAsync(int n = 20) => throw new NotImplementedException(); |
| 113 | public Task<string> GitFetchAsync(string? remote = null, bool all = false, bool prune = false, bool dryRun = false) => throw new NotImplementedException(); |
| 114 | public Task<string> GitPullAsync(string? remote = null, string? branch = null, bool ffOnly = true, bool dryRun = false) => throw new NotImplementedException(); |
| 115 | public Task<string> GitBranchAsync(string? action = null, string? name = null, string? startPoint = null, bool force = false) => throw new NotImplementedException(); |
| 116 | public Task<string> GitShowAsync(string rev, string? path = null, bool statOnly = false) => throw new NotImplementedException(); |
| 117 | public Task<string> GitSubmoduleAsync(string? action = null, string? path = null, bool recursive = true) => throw new NotImplementedException(); |
| 118 | public Task<string> GitPreflightAsync(bool staged = false, bool includeUntracked = true, bool includePatches = true) => throw new NotImplementedException(); |
| 119 | public Task<string> GitPreflightFixSafeAsync(bool includePatches = true) => throw new NotImplementedException(); |
| 120 | public string GetBuildOutput() => throw new NotImplementedException(); |
| 121 | public void SetBreakpoint(string filePath, int line, string? condition = null) => throw new NotImplementedException(); |
| 122 | public void RemoveBreakpoint(string filePath, int line) => throw new NotImplementedException(); |
| 123 | public void ShowPreview(string title, string content) => throw new NotImplementedException(); |
| 124 | public void ShowEditorPreview() => throw new NotImplementedException(); |
| 125 | public void FocusEditor() => throw new NotImplementedException(); |
| 126 | public string GetUiTheme() => throw new NotImplementedException(); |
| 127 | public Task<string> SetUiThemeAsync(string themeJson) => throw new NotImplementedException(); |
| 128 | public Task<string> GetUiLayoutAsync() => throw new NotImplementedException(); |
| 129 | public Task<string> GetColorsUnderCursorAsync() => throw new NotImplementedException(); |
| 130 | public Task<string> GetControlAppearanceAsync(string? name) => throw new NotImplementedException(); |
| 131 | public Task<string> SetControlLayoutAsync(string controlName, string layoutJson) => throw new NotImplementedException(); |
| 132 | public Task<string> AddControlAsync(string parentName, string controlType, string? content, string? name) => throw new NotImplementedException(); |
| 133 | public Task<string> SetControlTextAsync(string controlName, string text) => throw new NotImplementedException(); |
| 134 | public Task<string> ClickControlAsync(string? controlName) => throw new NotImplementedException(); |
| 135 | public Task<string> SendKeysAsync(string? controlName, string keys) => throw new NotImplementedException(); |
| 136 | public Task<string> SelectChatMessageAsync(int index) => throw new NotImplementedException(); |
| 137 | public Task<string> SelectChatMessageByOrdinalAsync(int ordinal, int endOrdinal) => throw new NotImplementedException(); |
| 138 | public Task<string> GetSelectedChatMessageAsync() => throw new NotImplementedException(); |
| 139 | public Task<string> FindIntercomMessagesForCodeAsync(IReadOnlyDictionary<string, JsonElement>? args) => |
| 140 | throw new NotImplementedException(); |
| 141 | public Task<string> RelateIntercomMessageRangeToCodeAsync(IReadOnlyDictionary<string, JsonElement>? args) => |
| 142 | throw new NotImplementedException(); |
| 143 | public Task<string> EditChatAssistantMessageAsync(string messageId, string newContent, string? reason = null) => throw new NotImplementedException(); |
| 144 | public Task<string> ExportChatReadableAsync(bool writeFile = false, string? fileName = null) => throw new NotImplementedException(); |
| 145 | public Task<string> SetFocusAsync(string? controlName) => throw new NotImplementedException(); |
| 146 | public Task<string> HighlightControlAsync(string? controlName) => throw new NotImplementedException(); |
| 147 | public Task<string> SetPanelSizeAsync(string panel, double? width, double? height) => throw new NotImplementedException(); |
| 148 | public string GetSupportedEditorLanguages() => throw new NotImplementedException(); |
| 149 | public Task<string> GetDebugSnapshotAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 150 | public Task<string> WriteAgentNotesAsync(string content, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 151 | public Task<string> ReadAgentNotesAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 152 | public Task<string> AppendAgentNotesAsync(string content, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 153 | public Task<string> ListAgentNotesRevisionsAsync(int? limit = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 154 | public Task<string> RollbackAgentNotesAsync(string? revisionFile = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 155 | public Task<string> ReadHotContextAsync(string? activeScope = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 156 | public Task<string> RouteContextAsync(string query, string? activeScope = null, int? maxSections = null, int? maxChars = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 157 | public Task<string> MemoryHealthAsync(string? activeScope = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 158 | public Task<string> CompactHotContextAsync(bool apply = false, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 159 | public Task<string> ExtractFromArchiveAsync(string query, string? revisionFile = null, int? headLimit = null, int? contextLines = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 160 | public Task<string> UpsertAgentNotesSectionAsync(string sectionId, string content, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 161 | public Task<string> SearchAgentNotesAsync(string query, int? headLimit = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 162 | public Task<string> ReadKnowledgeFileAsync(string filePath, int? offset = null, int? limit = null, string? knowledgeRootId = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 163 | public Task<string> ListKnowledgeFilesAsync(string? subdir = null, string? knowledgeRootId = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 164 | public Task<string> WriteKnowledgeFileAsync(string filePath, string content, string? knowledgePath = null, bool saveRevision = true, string? knowledgeRootId = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 165 | public Task<string> AppendKnowledgeFileAsync(string filePath, string content, string? knowledgePath = null, bool saveRevision = true, string? knowledgeRootId = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 166 | 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(); |
| 167 | public Task<string> DeleteKnowledgeFileAsync(string filePath, string? knowledgePath = null, string? knowledgeRootId = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 168 | public Task<string> DeleteKnowledgeSectionAsync(string filePath, string sectionId, string? knowledgePath = null, string? knowledgeRootId = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 169 | public Task<string> CodebaseIndexStatusAsync(string? workspacePath, string? solutionPath, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 170 | 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(); |
| 171 | public Task<string> CodebaseIndexExplainAsync(string? workspacePath, string? solutionPath, long hitId, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 172 | public Task<string> CodebaseIndexReindexAsync(string? workspacePath, string? solutionPath, bool fullRebuild, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 173 | public Task<string> IdeAgentVerifyAsync(string? policy, string? sandboxProfile, string? solutionPath, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 174 | public Task<string> IdeAgentVerifyBatchAsync(string? policy, string? sandboxProfile, string? solutionPath, bool useWorktree, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 175 | public Task<string> IdeAgentCancelAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 176 | public Task<string> IdeAgentStatusAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 177 | public Task<string> IdeAgentLastAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 178 | public Task<string> IdeAgentSandboxPrepareAsync(string? profile, string? workspaceRoot, CancellationToken cancellationToken = default) => throw new NotImplementedException(); |
| 179 | } |
| 180 | } |
| 181 | |