Skip to content

Commit 940f31e

Browse files
authored
Fix VS Code reconnect credential option (#73)
## Summary - Accept the hidden `-vscode-credential` / `--connect-vscode-credential` option on the interactive `connect` command. - Route the option to the existing `VisualStudioCodeCredential` reconnect path. - Keep hidden options out of public help/completion/MCP surfaces while still recognizing them for shell highlighting and LSP diagnostics. Fixes AzureCosmosDB/cosmosdb-shell-preview#24 ## Validation - `dotnet test .\CosmosDBShell.Tests\CosmosDBShell.Tests.csproj --filter "FullyQualifiedName~ConnectCommandTests|FullyQualifiedName~CosmosShellCompletionHandlerTests" -p:BaseOutputPath=.\obj\agent-hidden-test\`
2 parents a466b6d + 0b6561f commit 940f31e

6 files changed

Lines changed: 79 additions & 6 deletions

File tree

CosmosDBShell.Tests/CommandTests/ConnectCommandTests.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
namespace CosmosShell.Tests.CommandTests;
66

7+
using Azure.Data.Cosmos.Shell.Commands;
78
using Azure.Data.Cosmos.Shell.Core;
9+
using Azure.Data.Cosmos.Shell.Lsp.Semantics;
10+
using Azure.Data.Cosmos.Shell.Parser;
811
using Microsoft.Azure.Cosmos;
912

1013
public class ConnectCommandTests
@@ -21,4 +24,58 @@ await Assert.ThrowsAnyAsync<OperationCanceledException>(() => shell.ConnectAsync
2124
mode: ConnectionMode.Gateway,
2225
token: cancellationTokenSource.Token));
2326
}
27+
28+
[Fact]
29+
public async Task ConnectCommand_VSCodeCredentialOption_BindsHiddenInteractiveFlag()
30+
{
31+
var command = await BindConnectCommandAsync("connect https://example.documents.azure.com:443/ -vscode-credential");
32+
33+
Assert.Equal("https://example.documents.azure.com:443/", command.ConnectionString);
34+
Assert.True(command.UseVSCodeCredential);
35+
}
36+
37+
[Fact]
38+
public async Task ConnectCommand_StartupVSCodeCredentialOptionAlias_BindsHiddenInteractiveFlag()
39+
{
40+
var command = await BindConnectCommandAsync("connect https://example.documents.azure.com:443/ --connect-vscode-credential");
41+
42+
Assert.Equal("https://example.documents.azure.com:443/", command.ConnectionString);
43+
Assert.True(command.UseVSCodeCredential);
44+
}
45+
46+
[Fact]
47+
public void ConnectCommand_VSCodeCredentialOption_IsHiddenButKnownToCommandMetadata()
48+
{
49+
Assert.True(CommandFactory.TryCreateFactory(typeof(ConnectCommand), out var factory));
50+
51+
Assert.DoesNotContain(factory.Options, option => option.MatchesArgument("vscode-credential"));
52+
Assert.Contains(factory.AllOptions, option => option.MatchesArgument("vscode-credential"));
53+
Assert.True(factory.HasOption("vscode-credential"));
54+
55+
using var shell = ShellInterpreter.CreateInstance();
56+
Assert.True(shell.App.IsOptionPrefix("connect", "vscode-credential"));
57+
}
58+
59+
[Fact]
60+
public void ConnectCommand_VSCodeCredentialOption_DoesNotProduceUnknownOptionDiagnostic()
61+
{
62+
const string CommandText = "connect https://example.documents.azure.com:443/ -vscode-credential";
63+
var parser = new StatementParser(CommandText);
64+
var statements = parser.ParseStatements();
65+
66+
var model = new SemanticAnalyzer().Analyze(statements, CommandText);
67+
68+
Assert.DoesNotContain(model.Diagnostics, diagnostic => diagnostic.Code == "SEM002");
69+
}
70+
71+
private static async Task<ConnectCommand> BindConnectCommandAsync(string commandText)
72+
{
73+
var parser = new StatementParser(commandText);
74+
var statement = Assert.IsType<CommandStatement>(Assert.Single(parser.ParseStatements()));
75+
76+
Assert.True(CommandFactory.TryCreateFactory(typeof(ConnectCommand), out var factory));
77+
using var shell = ShellInterpreter.CreateInstance();
78+
var command = await statement.CreateCommandAsync(factory, shell, new CommandState(), CancellationToken.None);
79+
return Assert.IsType<ConnectCommand>(command);
80+
}
2481
}

CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/CommandFactory.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ public McpAnnotationAttribute? McpAnnotation
5454
/// </summary>
5555
public List<Option> Options { get; } = [];
5656

57+
/// <summary>
58+
/// Gets all options for the command, including options hidden from public surfaces.
59+
/// </summary>
60+
internal List<Option> AllOptions { get; } = [];
61+
5762
/// <summary>
5863
/// Gets a value indicating whether the command is external.
5964
/// </summary>
@@ -112,7 +117,13 @@ public static bool TryCreateFactory(Type type, out CommandFactory factory)
112117
var optattr = p.GetCustomAttribute<CosmosOptionAttribute>();
113118
if (optattr != null)
114119
{
115-
factory.Options.Add(new Option(p, optattr));
120+
var option = new Option(p, optattr);
121+
factory.AllOptions.Add(option);
122+
123+
if (!optattr.Hidden)
124+
{
125+
factory.Options.Add(option);
126+
}
116127
}
117128
}
118129

@@ -141,7 +152,7 @@ public CosmosCommand CreateCommand()
141152
/// <returns>True if the option exists; otherwise, false.</returns>
142153
internal bool HasOption(string optionName)
143154
{
144-
foreach (var opt in this.Options)
155+
foreach (var opt in this.AllOptions)
145156
{
146157
if (opt.MatchesArgument(optionName))
147158
{

CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ internal partial class ConnectCommand : CosmosCommand
4343
[CosmosOption("managed-identity")]
4444
public string? ManagedIdentityClientId { get; set; }
4545

46+
[CosmosOption("vscode-credential", "connect-vscode-credential", Hidden = true)]
47+
public bool UseVSCodeCredential { get; init; }
48+
4649
public async override Task<CommandState> ExecuteAsync(ShellInterpreter shell, CommandState commandState, string commandText, CancellationToken token)
4750
{
4851
// If no connection string provided, show current connection info
@@ -82,7 +85,7 @@ public async override Task<CommandState> ExecuteAsync(ShellInterpreter shell, Co
8285

8386
try
8487
{
85-
await shell.ConnectAsync(this.ConnectionString, this.LoginHint, connectionMode, tenantId: this.TenantId, authorityHost: this.AuthorityHost, managedIdentityClientId: this.ManagedIdentityClientId, token: token);
88+
await shell.ConnectAsync(this.ConnectionString, this.LoginHint, connectionMode, tenantId: this.TenantId, authorityHost: this.AuthorityHost, managedIdentityClientId: this.ManagedIdentityClientId, useVSCodeCredential: this.UseVSCodeCredential, token: token);
8689
var returnState = new CommandState
8790
{
8891
IsPrinted = true,

CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CommandRunner.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ internal bool IsOptionPrefix(string? currentCommand, string name)
5656
return true;
5757
}
5858

59-
return factory.Options.Any(o => o.Name.Any(optionName => optionName.StartsWith(name, StringComparison.OrdinalIgnoreCase)));
59+
return factory.AllOptions.Any(o => o.Name.Any(optionName => optionName.StartsWith(name, StringComparison.OrdinalIgnoreCase)));
6060
}
6161

6262
return false;

CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CosmosOptionAttribute.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ public CosmosOptionAttribute(params string[] name)
1818
public string[] Names { get; }
1919

2020
public object? DefaultValue { get; set; }
21+
22+
public bool Hidden { get; set; }
2123
}

CosmosDBShell/Azure.Data.Cosmos.Shell.Lsp.Semantics/SemanticAnalyzer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ private void ValidateCommandOptions(CommandStatement cmd, CommandFactory factory
134134
{
135135
// Collect valid option names (case-insensitive) including all aliases
136136
var valid = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
137-
foreach (var opt in factory.Options)
137+
foreach (var opt in factory.AllOptions)
138138
{
139139
foreach (var n in opt.Name)
140140
{
@@ -186,7 +186,7 @@ private void ValidateCommandOptions(CommandStatement cmd, CommandFactory factory
186186
continue;
187187
}
188188

189-
var optDef = factory.Options.FirstOrDefault(o => o.MatchesArgument(optName));
189+
var optDef = factory.AllOptions.FirstOrDefault(o => o.MatchesArgument(optName));
190190
if (optDef != null)
191191
{
192192
if (optDef.IsBool && optExpr.Value != null)

0 commit comments

Comments
 (0)