Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fb9883e
improve avm module completion readability with suffix labels and pref…
polatengin Feb 6, 2026
dce23e1
fixing failing tests
polatengin Feb 6, 2026
20fa6e7
enhance module completion documentation with detailed display names, …
polatengin Feb 21, 2026
1e250f0
refactor completion item handling for improved readability and mainta…
polatengin Mar 2, 2026
d29a6c0
reverting tests
polatengin Mar 2, 2026
e3d01a0
improve completion item documentation with detailed display names, pa…
polatengin Mar 2, 2026
9c2af8f
add AVM module display name provider and integrate into completion se…
polatengin Mar 2, 2026
4387a1d
refactor completion item documentation to improve readability by remo…
polatengin Mar 2, 2026
06d0ec3
refactor completion item documentation to enhance readability by reor…
polatengin Mar 4, 2026
41f504d
refactor completion item documentation to improve readability by remo…
polatengin Mar 4, 2026
abbe7bc
refactor completion documentation methods to improve readability and …
polatengin Mar 4, 2026
7a48b0b
refactor AVM module completion to include module status in documentation
polatengin Mar 4, 2026
f2bd793
Enhance AVM module completion by adding mock MS Graph extensions and …
polatengin May 18, 2026
e772b97
Add unit test for filtered completions using catalog description when…
polatengin May 18, 2026
a080b55
Refactor using directives for consistency in AvmModuleDisplayNameProv…
polatengin May 18, 2026
56eb438
Skip completions for proposed modules in AVM module reference provider
polatengin Jun 11, 2026
4bafba3
Add tests to filter AVM module completions based on module status
polatengin Jun 11, 2026
f52740d
Update AVM module CSV URIs to use the latest links for improved accuracy
polatengin Jun 11, 2026
e132b26
Enhance AVM module display name tests to verify requested CSV URIs
polatengin Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/Bicep.Core.IntegrationTests/ExamplesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using Bicep.Core.Configuration;
using Bicep.Core.Diagnostics;
using Bicep.Core.Emit;
using Bicep.Core.Extensions;
Expand All @@ -20,6 +21,10 @@ namespace Bicep.Core.IntegrationTests
[TestClass]
public class ExamplesTests
{
private const string MsGraphSampleExtensionVersion = "0.1.8-preview";
private const string MsGraphBetaRepository = "bicep/extensions/microsoftgraph/beta";
private const string MsGraphV1Repository = "bicep/extensions/microsoftgraph/v1.0";

private static ServiceBuilder Services => new ServiceBuilder().WithDisabledAnalyzersConfiguration();

[NotNull]
Expand All @@ -33,7 +38,13 @@ public static async Task RunExampleTest(TestContext testContext, EmbeddedFile em
var bicepFile = baselineFolder.EntryFile;
var jsonFile = baselineFolder.GetFileOrEnsureCheckedIn(Path.ChangeExtension(embeddedBicep.FileName, jsonFileExtension));

var compiler = Services.WithFeatureOverrides(features).Build().GetCompiler();
var services = Services.WithFeatureOverrides(features);
if (RequiresMockMsGraphExtensions(embeddedBicep))
{
services = await AddMockMsGraphExtensions(services);
}

var compiler = services.Build().GetCompiler();
var compilation = await compiler.CreateCompilation(bicepFile.OutputFileUri.ToIOUri());
var model = compilation.GetEntrypointSemanticModel();

Expand Down Expand Up @@ -107,6 +118,29 @@ public void ExampleData_should_return_a_number_of_records()
private static IEnumerable<object[]> GetAllExampleData()
=> ExampleData.GetAllExampleData().Select(x => new object[] { x.BicepFile });

private static bool RequiresMockMsGraphExtensions(EmbeddedFile embeddedBicep)
=> embeddedBicep.StreamPath.StartsWith("Files/user_submitted/extensibility/microsoftGraph/", StringComparison.Ordinal);

private static async Task<ServiceBuilder> AddMockMsGraphExtensions(ServiceBuilder services)
{
services = services.WithContainerRegistryClientFactory(RegistryHelper.CreateMockRegistryClient(
new RegistryHelper.RepoDescriptor(LanguageConstants.BicepPublicMcrRegistry, MsGraphBetaRepository, [MsGraphSampleExtensionVersion]),
new RegistryHelper.RepoDescriptor(LanguageConstants.BicepPublicMcrRegistry, MsGraphV1Repository, [MsGraphSampleExtensionVersion])));

var serviceProvider = services.Build();
await RegistryHelper.PublishExtensionToRegistryAsync(
serviceProvider,
$"br:{LanguageConstants.BicepPublicMcrRegistry}/{MsGraphBetaRepository}:{MsGraphSampleExtensionVersion}",
ExtensionResourceTypeHelper.GetMockMsGraphTypesTgz("MicrosoftGraphBeta", MsGraphSampleExtensionVersion, "beta"));

await RegistryHelper.PublishExtensionToRegistryAsync(
serviceProvider,
$"br:{LanguageConstants.BicepPublicMcrRegistry}/{MsGraphV1Repository}:{MsGraphSampleExtensionVersion}",
ExtensionResourceTypeHelper.GetMockMsGraphTypesTgz("MicrosoftGraph", MsGraphSampleExtensionVersion, "v1.0"));

return services;
}

private static bool IsPermittedMissingTypeDiagnostic(IDiagnostic diagnostic)
{
if (diagnostic.Code != "BCP081")
Expand Down
106 changes: 106 additions & 0 deletions src/Bicep.Core.UnitTests/Utils/ExtensionResourceTypeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,112 @@ public static BinaryData GetMockDesiredStateConfigurationTypesTgz()
("types.json", StreamHelper.GetString(stream => TypeSerializer.Serialize(stream, factory.GetTypes()))));
}

public static BinaryData GetMockMsGraphTypesTgz(string extensionName, string extensionVersion, string apiVersion)
{
var factory = new TypeFactory([]);

var stringType = factory.Create(() => new StringType());
var booleanType = factory.Create(() => new BooleanType());
var anyType = factory.Create(() => new AnyType());
var stringArrayType = factory.Create(() => new ArrayType(factory.GetReference(stringType)));
var anyArrayType = factory.Create(() => new ArrayType(factory.GetReference(anyType)));

var stringTypeRef = factory.GetReference(stringType);
var booleanTypeRef = factory.GetReference(booleanType);
var anyTypeRef = factory.GetReference(anyType);
var stringArrayTypeRef = factory.GetReference(stringArrayType);
var anyArrayTypeRef = factory.GetReference(anyArrayType);

ObjectTypeProperty Property(ITypeReference typeRef, ObjectTypePropertyFlags flags = ObjectTypePropertyFlags.None) => new(typeRef, flags, null);

ResourceType Resource(string name, Dictionary<string, ObjectTypeProperty> properties)
{
var bodyType = factory.Create(() => new ObjectType(name, properties, null));

return factory.Create(() => new ResourceType(
name,
factory.GetReference(bodyType),
null,
writableScopes_in: ScopeType.All,
readableScopes_in: ScopeType.All));
}

Dictionary<string, ObjectTypeProperty> ApplicationProperties() => new()
{
["uniqueName"] = Property(stringTypeRef, ObjectTypePropertyFlags.Required),
["displayName"] = Property(stringTypeRef),
["appId"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly),
["id"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly),
["appRoles"] = Property(anyArrayTypeRef),
["api"] = Property(anyTypeRef),
};

ResourceType[] resourceTypes = apiVersion switch
{
"beta" =>
[
Resource("Microsoft.Graph/applications@beta", ApplicationProperties()),
Resource("Microsoft.Graph/servicePrincipals@beta", new()
{
["appId"] = Property(stringTypeRef, ObjectTypePropertyFlags.Required),
["id"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly),
}),
Resource("Microsoft.Graph/oauth2PermissionGrants@beta", new()
{
["clientId"] = Property(stringTypeRef),
["consentType"] = Property(stringTypeRef),
["resourceId"] = Property(stringTypeRef),
["scope"] = Property(stringTypeRef),
["id"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly),
}),
Resource("Microsoft.Graph/appRoleAssignedTo@beta", new()
{
["appRoleId"] = Property(stringTypeRef),
["principalId"] = Property(stringTypeRef),
["resourceId"] = Property(stringTypeRef),
["id"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly),
}),
Resource("Microsoft.Graph/groups@beta", new()
{
["uniqueName"] = Property(stringTypeRef, ObjectTypePropertyFlags.Required),
["displayName"] = Property(stringTypeRef),
["mailEnabled"] = Property(booleanTypeRef),
["mailNickname"] = Property(stringTypeRef),
["securityEnabled"] = Property(booleanTypeRef),
["groupTypes"] = Property(stringArrayTypeRef),
["owners"] = Property(stringArrayTypeRef),
["id"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly),
}),
],
"v1.0" =>
[
Resource("Microsoft.Graph/applications@v1.0", ApplicationProperties()),
Resource("Microsoft.Graph/applications/federatedIdentityCredentials@v1.0", new()
{
["name"] = Property(stringTypeRef, ObjectTypePropertyFlags.Required),
["audiences"] = Property(stringArrayTypeRef),
["description"] = Property(stringTypeRef),
["issuer"] = Property(stringTypeRef),
["subject"] = Property(stringTypeRef),
["id"] = Property(stringTypeRef, ObjectTypePropertyFlags.ReadOnly),
}),
],
_ => throw new ArgumentException($"Unsupported Microsoft Graph API version '{apiVersion}'.", nameof(apiVersion)),
};

var settings = new TypeSettings(name: extensionName, version: extensionVersion, isSingleton: false, configurationType: null!);
var index = new TypeIndex(
resourceTypes.ToDictionary(x => x.Name, x => new CrossFileTypeReference("types.json", factory.GetIndex(x))),
new Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<CrossFileTypeReference>>>(),
[],
settings,
null);

return GetTypesTgzBytesFromFiles(
("index.json", StreamHelper.GetString(stream => TypeSerializer.SerializeIndex(stream, index))),
("types.json", StreamHelper.GetString(stream => TypeSerializer.Serialize(stream, factory.GetTypes()))));
}

public static BinaryData GetTypesTgzBytesFromFiles(params (string filePath, string contents)[] files)
{
var stream = new MemoryStream();
Expand Down
16 changes: 8 additions & 8 deletions src/Bicep.LangServer.IntegrationTests/CompletionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4459,16 +4459,16 @@ public async Task Public_module_version_completions(string inputWithCursors, Bic
first.Label.Should().Be("1.0.2");
first.SortText.Should().Be("0000");
first.Kind.Should().Be(CompletionItemKind.Snippet);
first.Detail.Should().Be("d1");
first.Documentation!.MarkupContent!.Value.Should().Be("[View Documentation](contoso.com/help1)");
first.Detail.Should().BeNull();
first.Documentation!.MarkupContent!.Value.Should().Be("### d1 \n**Version:** 1.0.2 \n**Full module path:** app/dapr-containerapp \n**Description:** d1 \n[View Documentation](contoso.com/help1)");
},
second =>
{
second.Label.Should().Be("1.0.1");
second.SortText.Should().Be("0001");
second.Kind.Should().Be(CompletionItemKind.Snippet);
second.Detail.Should().BeNull();
second.Documentation.Should().BeNull();
second.Documentation!.MarkupContent!.Value.Should().Be("**Version:** 1.0.1 \n**Full module path:** app/dapr-containerapp \n**Description:** N/A \n**Documentation:** N/A");
}
);
}
Expand Down Expand Up @@ -4532,16 +4532,16 @@ public async Task Private_module_version_completions(string inputWithCursors, Bi
first.Label.Should().Be("v101");
first.SortText.Should().Be("0000");
first.Kind.Should().Be(CompletionItemKind.Snippet);
first.Detail.Should().Be("d101");
first.Documentation!.MarkupContent!.Value.Should().Be("[View Documentation](contoso.com/help/d101.html)");
first.Detail.Should().BeNull();
first.Documentation!.MarkupContent!.Value.Should().Be("### d101 \n**Version:** v101 \n**Full module path:** app/private-app \n**Description:** d101 \n[View Documentation](contoso.com/help/d101.html)");
},
second =>
{
second.Label.Should().Be("v100");
second.SortText.Should().Be("0001");
second.Kind.Should().Be(CompletionItemKind.Snippet);
second.Detail.Should().Be("d100");
second.Documentation!.MarkupContent!.Value.Should().Be("[View Documentation](contoso.com/help/d100.html)");
second.Detail.Should().BeNull();
second.Documentation!.MarkupContent!.Value.Should().Be("### d100 \n**Version:** v100 \n**Full module path:** app/private-app \n**Description:** d100 \n[View Documentation](contoso.com/help/d100.html)");
}
);
}
Expand All @@ -4565,7 +4565,7 @@ public async Task Public_registry_module_completions_support_prefix_matching(str
settingsProvider.Setup(x => x.GetSetting(LangServerConstants.GetAllAzureContainerRegistriesForCompletionsSetting)).Returns(false);

var publicModuleMetadataProvider = RegistryCatalogMocks.MockPublicMetadataProvider([
("bicep/abc/foo/bar", "d1", "contoso.com/help1", []),
("bicep/abc/foo/bar", "d1", "contoso.com/help1", []),
("bicep/abc/food/bar", "d2", "contoso.com/help2", []),
("bicep/abc/bar/bar", "d3", "contoso.com/help3", []),
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Concurrent;
using System.Net.Http;
using System.Reflection;
using Bicep.LanguageServer.Completions;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Bicep.LangServer.UnitTests.Completions
{
[TestClass]
public class AvmModuleDisplayNameProviderTests
{
[TestMethod]
public async Task StartCache_WhenCsvFilesAreAvailable_LoadsDisplayNamesAndStatuses()
{
const string csvContent = """
ModuleName,ModuleDisplayName,ModuleStatus
bicep/avm/ptn/ai-platform/baseline,AI platform baseline,Available
bicep/avm/res/compute/virtual-machine,Virtual machine,Deprecated
""";

var client = new TestAvmModuleCsvIndexHttpClient((_, _) => Task.FromResult(csvContent));
var provider = new AvmModuleDisplayNameProvider(client);

provider.StartCache();
await WaitForLoadTaskAsync(provider);

provider.TryGetModuleDisplayName("bicep/avm/ptn/ai-platform/baseline", out var patternDisplayName).Should().BeTrue();
patternDisplayName.Should().Be("AI platform baseline");

provider.TryGetModuleStatus("bicep/avm/ptn/ai-platform/baseline", out var patternModuleStatus).Should().BeTrue();
patternModuleStatus.Should().Be("Available");

provider.TryGetModuleDisplayName("avm/res/compute/virtual-machine", out var resourceDisplayName).Should().BeTrue();
resourceDisplayName.Should().Be("Virtual machine");

provider.TryGetModuleStatus("avm/res/compute/virtual-machine", out var resourceModuleStatus).Should().BeTrue();
resourceModuleStatus.Should().Be("Deprecated");

client.RequestCount.Should().Be(3);
client.RequestedUris.Select(uri => uri.ToString()).Should().BeEquivalentTo([
"https://aka.ms/avm/index/bicep/utl/csv",
"https://aka.ms/avm/index/bicep/ptn/csv",
"https://aka.ms/avm/index/bicep/res/csv",
]);
}

[TestMethod]
public async Task StartCache_WhenCsvFetchFails_CompletesAndUsesEmptyLookup()
{
var client = new TestAvmModuleCsvIndexHttpClient((_, _) =>
Task.FromException<string>(new HttpRequestException("CSV unavailable")));
var provider = new AvmModuleDisplayNameProvider(client);

var startCache = provider.StartCache;
startCache.Should().NotThrow();

await WaitForLoadTaskAsync(provider);

provider.TryGetModuleDisplayName("bicep/avm/ptn/ai-platform/baseline", out var displayName).Should().BeFalse();
displayName.Should().BeNull();

provider.TryGetModuleStatus("bicep/avm/ptn/ai-platform/baseline", out var moduleStatus).Should().BeFalse();
moduleStatus.Should().BeNull();

client.RequestCount.Should().Be(3);
}

private static async Task WaitForLoadTaskAsync(AvmModuleDisplayNameProvider provider)
{
var loadTask = GetLoadTask(provider);
var completedTask = await Task.WhenAny(loadTask, Task.Delay(TimeSpan.FromSeconds(5)));

completedTask.Should().Be(loadTask);
await loadTask;
}

private static Task GetLoadTask(AvmModuleDisplayNameProvider provider)
{
var loadTaskField = typeof(AvmModuleDisplayNameProvider).GetField("loadTask", BindingFlags.NonPublic | BindingFlags.Instance);
loadTaskField.Should().NotBeNull();

var loadTask = loadTaskField?.GetValue(provider) as Task;
loadTask.Should().NotBeNull();

return loadTask ?? Task.CompletedTask;
}

private sealed class TestAvmModuleCsvIndexHttpClient : IAvmModuleCsvIndexHttpClient
{
private readonly Func<Uri, CancellationToken, Task<string>> getCsvAsync;
private readonly ConcurrentQueue<Uri> requestedUris = new();
private int requestCount;

public TestAvmModuleCsvIndexHttpClient(Func<Uri, CancellationToken, Task<string>> getCsvAsync)
{
this.getCsvAsync = getCsvAsync;
}

public int RequestCount => Volatile.Read(ref requestCount);

public IEnumerable<Uri> RequestedUris => requestedUris;

public async Task<string> GetCsvAsync(Uri csvUri, CancellationToken cancellationToken)
{
requestedUris.Enqueue(csvUri);

try
{
return await getCsvAsync(csvUri, cancellationToken);
}
finally
{
Interlocked.Increment(ref requestCount);
}
}
}
}
}
Loading
Loading