From 9e33d69afa4d9360014a59dd24a4d386701dc700 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 07:38:47 +0000 Subject: [PATCH 1/7] Initial plan From 4ed8a25c7d84be29a3d12fd1abd530d996507241 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 08:18:33 +0000 Subject: [PATCH 2/7] Add documentation for resolveArmResources API investigation and limitations Co-authored-by: live1206 <5196139+live1206@users.noreply.github.com> --- .../emitter/src/resource-detection.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/eng/packages/http-client-csharp-mgmt/emitter/src/resource-detection.ts b/eng/packages/http-client-csharp-mgmt/emitter/src/resource-detection.ts index be700258250f..5231f9448306 100644 --- a/eng/packages/http-client-csharp-mgmt/emitter/src/resource-detection.ts +++ b/eng/packages/http-client-csharp-mgmt/emitter/src/resource-detection.ts @@ -1,6 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +// NOTE: This resource detection implementation uses decorator-based parsing to identify +// ARM resource operations. A future enhancement is to replace this with the +// resolveArmResources API from @azure-tools/typespec-azure-resource-manager. +// See: https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/src/resource.ts +// +// Current limitation: The resolveArmResources API does not properly handle multiple +// singleton resources (e.g., when both Employee and CurrentEmployee are defined with +// different @singleton keys, only one is returned). This needs to be fixed in the +// TypeSpec ARM library before the replacement can be completed. + import { CodeModel, CSharpEmitterContext, From 73e7465264ca229c48665fc53bd59612ecb42cfa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 03:17:52 +0000 Subject: [PATCH 3/7] Add test to demonstrate resolveArmResources API bug with multiple singletons Co-authored-by: live1206 <5196139+live1206@users.noreply.github.com> --- .../test/resolve-arm-resources-bug.test.ts | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-bug.test.ts diff --git a/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-bug.test.ts b/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-bug.test.ts new file mode 100644 index 000000000000..0dc77716112a --- /dev/null +++ b/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-bug.test.ts @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +/** + * This test file demonstrates a bug in the resolveArmResources API from + * @azure-tools/typespec-azure-resource-manager. + * + * Bug Description: + * When multiple singleton resources are defined with different @singleton keys + * (e.g., Employee with default key and CurrentEmployee with "current" key), + * the resolveArmResources API does not properly distinguish between them. + * All resolved resources point to the same model type instead of their respective types. + * + * This test is marked with .skip because it demonstrates a bug in an external library + * that needs to be fixed before the test will pass. + */ + +import { beforeEach, describe, it } from "vitest"; +import { + createEmitterTestHost, + typeSpecCompile +} from "./test-util.js"; +import { TestHost } from "@typespec/compiler/testing"; +import { resolveArmResources } from "@azure-tools/typespec-azure-resource-manager"; +import { strictEqual, ok } from "assert"; + +/** + * Shared TypeSpec schema for multiple singleton resources test. + * Defines two singleton resources with different @singleton keys. + */ +const multipleSingletonsSchema = ` +/** Employee properties */ +model EmployeeProperties { + /** Age of employee */ + age?: int32; +} + +/** An Employee singleton resource with default key */ +@singleton +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +/** A CurrentEmployee singleton resource with "current" key */ +@singleton("current") +model CurrentEmployee is TrackedResource { + ...ResourceNameParameter; +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface Employees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; +} + +@armResourceOperations +interface CurrentEmployees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; +} +`; + +describe("resolveArmResources API Bug Demonstration", () => { + let runner: TestHost; + beforeEach(async () => { + runner = await createEmitterTestHost(); + }); + + /** + * This test demonstrates that resolveArmResources does not correctly return + * multiple singleton resources with different @singleton keys. + * + * Expected behavior: The API should return both Employee and CurrentEmployee + * as separate resources with their respective model types. + * + * Actual behavior: All resolved resources have type.name === "Employee", + * meaning CurrentEmployee is never properly identified. + */ + it.skip("should return distinct resources for multiple singletons with different keys", async () => { + const program = await typeSpecCompile(multipleSingletonsSchema, runner); + + // Use resolveArmResources API to get all resolved ARM resources + const provider = resolveArmResources(program); + const resolvedResources = provider.resources ?? []; + + // Check that we have resources returned + ok(resolvedResources.length > 0, "Should have at least one resolved resource"); + + // Find resources by their model type name + const employeeResources = resolvedResources.filter( + (r) => r.type.name === "Employee" + ); + const currentEmployeeResources = resolvedResources.filter( + (r) => r.type.name === "CurrentEmployee" + ); + + // BUG: This assertion fails because resolveArmResources returns all resources + // with type.name === "Employee" instead of distinguishing CurrentEmployee + ok( + currentEmployeeResources.length > 0, + "Should have at least one CurrentEmployee resource - BUG: resolveArmResources does not return CurrentEmployee" + ); + + // Verify that Employee resources are returned correctly + ok( + employeeResources.length > 0, + "Should have at least one Employee resource" + ); + + // Verify that the total count is correct (should be 2 distinct resources for CRUD operations) + // Note: The API may return multiple entries per resource for different operation paths + const uniqueResourceTypes = new Set(resolvedResources.map((r) => r.type.name)); + strictEqual( + uniqueResourceTypes.size, + 2, + "Should have exactly 2 unique resource types (Employee and CurrentEmployee)" + ); + }); + + /** + * This test shows the current (buggy) behavior of resolveArmResources + * where all resources point to the same model type. + */ + it("demonstrates current buggy behavior - all resources have same type", async () => { + const program = await typeSpecCompile(multipleSingletonsSchema, runner); + + // Use resolveArmResources API to get all resolved ARM resources + const provider = resolveArmResources(program); + const resolvedResources = provider.resources ?? []; + + // BUG: All resolved resources have type.name === "Employee" + // CurrentEmployee is never returned despite being defined + const allResourceTypeNames = resolvedResources.map((r) => r.type.name); + const uniqueResourceTypes = new Set(allResourceTypeNames); + + // This assertion passes but demonstrates the bug: + // We expect 2 unique types but only get 1 + strictEqual( + uniqueResourceTypes.size, + 1, + "BUG: resolveArmResources only returns Employee type, missing CurrentEmployee" + ); + + // All resources point to Employee + ok( + allResourceTypeNames.every((name) => name === "Employee"), + "BUG: All resolved resources have type Employee" + ); + + // CurrentEmployee is never returned + const hasCurrentEmployee = allResourceTypeNames.some( + (name) => name === "CurrentEmployee" + ); + strictEqual( + hasCurrentEmployee, + false, + "BUG: CurrentEmployee is never returned by resolveArmResources" + ); + }); +}); From 39b4461722f3098a1fbccd75215e2d022efa48ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 06:26:04 +0000 Subject: [PATCH 4/7] Add comprehensive test file to validate resolveArmResources API coverage Co-authored-by: live1206 <5196139+live1206@users.noreply.github.com> --- .../resolve-arm-resources-validation.test.ts | 1050 +++++++++++++++++ 1 file changed, 1050 insertions(+) create mode 100644 eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts diff --git a/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts b/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts new file mode 100644 index 000000000000..be0930a32ebe --- /dev/null +++ b/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts @@ -0,0 +1,1050 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +/** + * This test file verifies if the resolveArmResources API from + * @azure-tools/typespec-azure-resource-manager can provide the same information + * that is currently extracted using the decorator-based resource detection logic + * in resource-detection.ts. + * + * Each test case corresponds to a case in resource-detection.test.ts and verifies + * whether resolveArmResources API can return the expected resource information. + */ + +import { beforeEach, describe, it } from "vitest"; +import { createEmitterTestHost, typeSpecCompile } from "./test-util.js"; +import { TestHost } from "@typespec/compiler/testing"; +import { + resolveArmResources, + getSingletonResourceKey, + ResolvedResource +} from "@azure-tools/typespec-azure-resource-manager"; +import { strictEqual, ok } from "assert"; + +describe("resolveArmResources API Validation", () => { + let runner: TestHost; + beforeEach(async () => { + runner = await createEmitterTestHost(); + }); + + /** + * Helper to get resource type string from ResourceType object + */ + function getResourceTypeString(resource: ResolvedResource): string { + return `${resource.resourceType.provider}/${resource.resourceType.types.join("/")}`; + } + + /** + * Helper to determine resource scope from path + */ + function getResourceScope( + resource: ResolvedResource + ): "ResourceGroup" | "Subscription" | "Tenant" | "ManagementGroup" | "Extension" { + const path = resource.resourceInstancePath; + if (path.startsWith("/{resourceUri}") || path.startsWith("/{scope}")) { + return "Extension"; + } else if ( + path.startsWith( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/" + ) + ) { + return "ResourceGroup"; + } else if (path.startsWith("/subscriptions/{subscriptionId}/")) { + return "Subscription"; + } else if ( + path.startsWith( + "/providers/Microsoft.Management/managementGroups/{managementGroupId}/" + ) + ) { + return "ManagementGroup"; + } + return "Tenant"; + } + + it("resource group resource - basic validation", async () => { + const program = await typeSpecCompile( + ` +/** An Employee parent resource */ +model EmployeeParent is TrackedResource { + ...ResourceNameParameter; +} + +/** Employee parent properties */ +model EmployeeParentProperties { + /** Age of employee */ + age?: int32; +} + +/** An Employee resource */ +@parentResource(EmployeeParent) +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +/** Employee properties */ +model EmployeeProperties { + /** Age of employee */ + age?: int32; + + /** City of employee */ + city?: string; + + /** Profile of employee */ + @encode("base64url") + profile?: bytes; + + /** The status of the last operation. */ + @visibility(Lifecycle.Read) + provisioningState?: ProvisioningState; +} + +/** The provisioning state of a resource. */ +@lroStatus +union ProvisioningState { + string, + + /** The resource create request has been accepted */ + Accepted: "Accepted", + + /** The resource is being provisioned */ + Provisioning: "Provisioning", + + /** The resource is updating */ + Updating: "Updating", + + /** Resource has been created. */ + Succeeded: "Succeeded", + + /** Resource creation failed. */ + Failed: "Failed", + + /** Resource creation was canceled. */ + Canceled: "Canceled", + + /** The resource is being deleted */ + Deleting: "Deleting", +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface EmployeesParent { + get is ArmResourceRead; +} + +@armResourceOperations +interface Employees1 { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmCustomPatchSync< + Employee, + Azure.ResourceManager.Foundations.ResourceUpdateModel + >; +} + +@armResourceOperations +interface Employees2 { + delete is ArmResourceDeleteWithoutOkAsync; + listByResourceGroup is ArmResourceListByParent; + listBySubscription is ArmListBySubscription; +} +`, + runner + ); + + const provider = resolveArmResources(program); + const resolvedResources = provider.resources ?? []; + + // Find Employee and EmployeeParent resources + const employeeResource = resolvedResources.find( + (r) => r.type.name === "Employee" + ); + const employeeParentResource = resolvedResources.find( + (r) => r.type.name === "EmployeeParent" + ); + + ok(employeeResource, "Should find Employee resource"); + ok(employeeParentResource, "Should find EmployeeParent resource"); + + // Verify Employee resource properties + strictEqual( + employeeResource.resourceInstancePath, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/employeeParents/{employeeParentName}/employees/{employeeName}", + "Employee resourceInstancePath should match" + ); + strictEqual( + getResourceTypeString(employeeResource), + "Microsoft.ContosoProviderHub/employeeParents/employees", + "Employee resource type should match" + ); + strictEqual( + getResourceScope(employeeResource), + "ResourceGroup", + "Employee should be ResourceGroup scoped" + ); + + // Verify parent relationship + ok(employeeResource.parent, "Employee should have a parent"); + strictEqual( + employeeResource.parent.type.name, + "EmployeeParent", + "Employee's parent should be EmployeeParent" + ); + strictEqual( + employeeResource.parent.resourceInstancePath, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/employeeParents/{employeeParentName}", + "Parent resourceInstancePath should match" + ); + + // Verify EmployeeParent resource properties + strictEqual( + employeeParentResource.resourceInstancePath, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/employeeParents/{employeeParentName}", + "EmployeeParent resourceInstancePath should match" + ); + strictEqual( + getResourceTypeString(employeeParentResource), + "Microsoft.ContosoProviderHub/employeeParents", + "EmployeeParent resource type should match" + ); + + // Verify operations are present (lifecycle operations are arrays) + ok( + employeeResource.operations.lifecycle.read && + employeeResource.operations.lifecycle.read.length > 0, + "Employee should have read operation" + ); + ok( + employeeResource.operations.lifecycle.createOrUpdate && + employeeResource.operations.lifecycle.createOrUpdate.length > 0, + "Employee should have createOrUpdate operation" + ); + ok( + employeeResource.operations.lifecycle.update && + employeeResource.operations.lifecycle.update.length > 0, + "Employee should have update operation" + ); + ok( + employeeResource.operations.lifecycle.delete && + employeeResource.operations.lifecycle.delete.length > 0, + "Employee should have delete operation" + ); + ok( + employeeResource.operations.lists.length > 0, + "Employee should have list operations" + ); + }); + + /** + * This test validates the KNOWN BUG in resolveArmResources API with multiple singletons. + * + * BUG DESCRIPTION: + * When multiple singleton resources are defined with different @singleton keys + * (e.g., Employee with default key and CurrentEmployee with "current" key), + * the resolveArmResources API does not properly distinguish between them. + * All resolved resources point to the same model type (Employee) instead of + * their respective types. + * + * This test intentionally asserts the CURRENT BUGGY BEHAVIOR to document it. + * Once the bug is fixed in @azure-tools/typespec-azure-resource-manager, + * these assertions should be updated to verify correct behavior. + */ + it("singleton resource - demonstrates bug with multiple singletons", async () => { + const program = await typeSpecCompile( + ` +/** An Employee singleton resource */ +@singleton +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +@singleton("current") +model CurrentEmployee is TrackedResource { + ...ResourceNameParameter; +} + +/** Employee properties */ +model EmployeeProperties { + /** Age of employee */ + age?: int32; + + /** City of employee */ + city?: string; + + /** Profile of employee */ + @encode("base64url") + profile?: bytes; + + /** The status of the last operation. */ + @visibility(Lifecycle.Read) + provisioningState?: ProvisioningState; +} + +/** The provisioning state of a resource. */ +@lroStatus +union ProvisioningState { + string, + + /** The resource create request has been accepted */ + Accepted: "Accepted", + + /** The resource is being provisioned */ + Provisioning: "Provisioning", + + /** The resource is updating */ + Updating: "Updating", + + /** Resource has been created. */ + Succeeded: "Succeeded", + + /** Resource creation failed. */ + Failed: "Failed", + + /** Resource creation was canceled. */ + Canceled: "Canceled", + + /** The resource is being deleted */ + Deleting: "Deleting", +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface Employees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmCustomPatchSync< + Employee, + Azure.ResourceManager.Foundations.ResourceUpdateModel + >; +} + +@armResourceOperations +interface CurrentEmployees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmCustomPatchSync< + CurrentEmployee, + Azure.ResourceManager.Foundations.ResourceUpdateModel + >; +} +`, + runner + ); + + const provider = resolveArmResources(program); + const resolvedResources = provider.resources ?? []; + + // Get unique resource types + const resourceTypeNames = resolvedResources.map((r) => r.type.name); + const uniqueTypes = new Set(resourceTypeNames); + + // KNOWN BUG: resolveArmResources does not properly distinguish multiple singletons + // Expected behavior (when bug is fixed): Should return both "Employee" and "CurrentEmployee" + // Current buggy behavior: Returns only "Employee" for all resources + // + // When the bug is fixed, update these assertions to: + // strictEqual(uniqueTypes.size, 2, "Should have 2 unique resource types"); + // ok(resourceTypeNames.includes("CurrentEmployee"), "Should include CurrentEmployee"); + strictEqual( + uniqueTypes.size, + 1, + "KNOWN BUG: Only 1 unique resource type returned instead of 2" + ); + ok( + !resourceTypeNames.includes("CurrentEmployee"), + "KNOWN BUG: CurrentEmployee is NOT returned by resolveArmResources" + ); + + // Find Employee resource and verify singleton key can be obtained via getSingletonResourceKey + const employeeResource = resolvedResources.find( + (r) => r.type.name === "Employee" + ); + ok(employeeResource, "Should find Employee resource"); + + // getSingletonResourceKey works correctly when called directly on the model + const employeeSingletonKey = getSingletonResourceKey( + program, + employeeResource.type + ); + strictEqual( + employeeSingletonKey, + "default", + "Employee singleton key should be 'default'" + ); + }); + + it("resource with grand parent under a resource group", async () => { + const program = await typeSpecCompile( + ` +/** A Company grandparent resource */ +model Company is TrackedResource { + ...ResourceNameParameter; +} + +/** Company properties */ +model CompanyProperties { + /** Name of company */ + name?: string; +} + +/** A Department parent resource */ +@parentResource(Company) +model Department is TrackedResource { + ...ResourceNameParameter; +} + +/** Department properties */ +model DepartmentProperties { + /** Name of department */ + name?: string; +} + +/** An Employee resource with grandparent */ +@parentResource(Department) +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +/** Employee properties */ +model EmployeeProperties { + /** Age of employee */ + age?: int32; + + /** City of employee */ + city?: string; + + /** Profile of employee */ + @encode("base64url") + profile?: bytes; +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface Companies { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; +} + +@armResourceOperations +interface Departments { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; +} + +@armResourceOperations +interface Employees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmCustomPatchSync< + Employee, + Azure.ResourceManager.Foundations.ResourceUpdateModel + >; + delete is ArmResourceDeleteWithoutOkAsync; + listByResourceGroup is ArmResourceListByParent; +} +`, + runner + ); + + const provider = resolveArmResources(program); + const resolvedResources = provider.resources ?? []; + + // Find resources + const companyResource = resolvedResources.find( + (r) => r.type.name === "Company" + ); + const departmentResource = resolvedResources.find( + (r) => r.type.name === "Department" + ); + const employeeResource = resolvedResources.find( + (r) => r.type.name === "Employee" + ); + + ok(companyResource, "Should find Company resource"); + ok(departmentResource, "Should find Department resource"); + ok(employeeResource, "Should find Employee resource"); + + // Verify Employee resource + strictEqual( + employeeResource.resourceInstancePath, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/companies/{companyName}/departments/{departmentName}/employees/{employeeName}", + "Employee resourceInstancePath should match" + ); + strictEqual( + getResourceTypeString(employeeResource), + "Microsoft.ContosoProviderHub/companies/departments/employees", + "Employee resource type should match" + ); + strictEqual( + getResourceScope(employeeResource), + "ResourceGroup", + "Employee should be ResourceGroup scoped" + ); + + // Verify parent chain + ok(employeeResource.parent, "Employee should have a parent"); + strictEqual( + employeeResource.parent.type.name, + "Department", + "Employee's parent should be Department" + ); + ok(employeeResource.parent.parent, "Department should have a parent"); + strictEqual( + employeeResource.parent.parent.type.name, + "Company", + "Department's parent should be Company" + ); + + // Verify Department resource + strictEqual( + departmentResource.resourceInstancePath, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/companies/{companyName}/departments/{departmentName}", + "Department resourceInstancePath should match" + ); + strictEqual( + getResourceTypeString(departmentResource), + "Microsoft.ContosoProviderHub/companies/departments", + "Department resource type should match" + ); + + // Verify Company resource + strictEqual( + companyResource.resourceInstancePath, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/companies/{companyName}", + "Company resourceInstancePath should match" + ); + strictEqual( + getResourceTypeString(companyResource), + "Microsoft.ContosoProviderHub/companies", + "Company resource type should match" + ); + strictEqual(companyResource.parent, undefined, "Company should have no parent"); + }); + + it("resource with grand parent under a subscription", async () => { + const program = await typeSpecCompile( + ` +/** A Company grandparent resource */ +@subscriptionResource +model Company is TrackedResource { + ...ResourceNameParameter; +} + +/** Company properties */ +model CompanyProperties { + /** Name of company */ + name?: string; +} + +/** A Department parent resource */ +@subscriptionResource +@parentResource(Company) +model Department is TrackedResource { + ...ResourceNameParameter; +} + +/** Department properties */ +model DepartmentProperties { + /** Name of department */ + name?: string; +} + +/** An Employee resource with grandparent */ +@subscriptionResource +@parentResource(Department) +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +/** Employee properties */ +model EmployeeProperties { + /** Age of employee */ + age?: int32; + + /** City of employee */ + city?: string; + + /** Profile of employee */ + @encode("base64url") + profile?: bytes; +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface Companies { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; +} + +@armResourceOperations +interface Departments { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; +} + +@armResourceOperations +interface Employees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmCustomPatchSync< + Employee, + Azure.ResourceManager.Foundations.ResourceUpdateModel + >; + delete is ArmResourceDeleteWithoutOkAsync; + listByResourceGroup is ArmResourceListByParent; +} +`, + runner + ); + + const provider = resolveArmResources(program); + const resolvedResources = provider.resources ?? []; + + // Find resources + const companyResource = resolvedResources.find( + (r) => r.type.name === "Company" + ); + const departmentResource = resolvedResources.find( + (r) => r.type.name === "Department" + ); + const employeeResource = resolvedResources.find( + (r) => r.type.name === "Employee" + ); + + ok(companyResource, "Should find Company resource"); + ok(departmentResource, "Should find Department resource"); + ok(employeeResource, "Should find Employee resource"); + + // Verify Employee resource + strictEqual( + employeeResource.resourceInstancePath, + "/subscriptions/{subscriptionId}/providers/Microsoft.ContosoProviderHub/companies/{companyName}/departments/{departmentName}/employees/{employeeName}", + "Employee resourceInstancePath should match" + ); + strictEqual( + getResourceScope(employeeResource), + "Subscription", + "Employee should be Subscription scoped" + ); + + // Verify Department resource + strictEqual( + departmentResource.resourceInstancePath, + "/subscriptions/{subscriptionId}/providers/Microsoft.ContosoProviderHub/companies/{companyName}/departments/{departmentName}", + "Department resourceInstancePath should match" + ); + strictEqual( + getResourceScope(departmentResource), + "Subscription", + "Department should be Subscription scoped" + ); + + // Verify Company resource + strictEqual( + companyResource.resourceInstancePath, + "/subscriptions/{subscriptionId}/providers/Microsoft.ContosoProviderHub/companies/{companyName}", + "Company resourceInstancePath should match" + ); + strictEqual( + getResourceScope(companyResource), + "Subscription", + "Company should be Subscription scoped" + ); + }); + + it("resource with grand parent under a tenant", async () => { + const program = await typeSpecCompile( + ` +/** A Company grandparent resource */ +@tenantResource +model Company is TrackedResource { + ...ResourceNameParameter; +} + +/** Company properties */ +model CompanyProperties { + /** Name of company */ + name?: string; +} + +/** A Department parent resource */ +@tenantResource +@parentResource(Company) +model Department is TrackedResource { + ...ResourceNameParameter; +} + +/** Department properties */ +model DepartmentProperties { + /** Name of department */ + name?: string; +} + +/** An Employee resource with grandparent */ +@tenantResource +@parentResource(Department) +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +/** Employee properties */ +model EmployeeProperties { + /** Age of employee */ + age?: int32; + + /** City of employee */ + city?: string; + + /** Profile of employee */ + @encode("base64url") + profile?: bytes; +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface Companies { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; +} + +@armResourceOperations +interface Departments { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; +} + +@armResourceOperations +interface Employees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmCustomPatchSync< + Employee, + Azure.ResourceManager.Foundations.ResourceUpdateModel + >; + delete is ArmResourceDeleteWithoutOkAsync; + listByResourceGroup is ArmResourceListByParent; +} +`, + runner + ); + + const provider = resolveArmResources(program); + const resolvedResources = provider.resources ?? []; + + // Find resources + const companyResource = resolvedResources.find( + (r) => r.type.name === "Company" + ); + const departmentResource = resolvedResources.find( + (r) => r.type.name === "Department" + ); + const employeeResource = resolvedResources.find( + (r) => r.type.name === "Employee" + ); + + ok(companyResource, "Should find Company resource"); + ok(departmentResource, "Should find Department resource"); + ok(employeeResource, "Should find Employee resource"); + + // Verify Employee resource + strictEqual( + employeeResource.resourceInstancePath, + "/providers/Microsoft.ContosoProviderHub/companies/{companyName}/departments/{departmentName}/employees/{employeeName}", + "Employee resourceInstancePath should match" + ); + strictEqual( + getResourceScope(employeeResource), + "Tenant", + "Employee should be Tenant scoped" + ); + + // Verify Department resource + strictEqual( + departmentResource.resourceInstancePath, + "/providers/Microsoft.ContosoProviderHub/companies/{companyName}/departments/{departmentName}", + "Department resourceInstancePath should match" + ); + strictEqual( + getResourceScope(departmentResource), + "Tenant", + "Department should be Tenant scoped" + ); + + // Verify Company resource + strictEqual( + companyResource.resourceInstancePath, + "/providers/Microsoft.ContosoProviderHub/companies/{companyName}", + "Company resourceInstancePath should match" + ); + strictEqual( + getResourceScope(companyResource), + "Tenant", + "Company should be Tenant scoped" + ); + }); + + it("resource scope determined from Get method (SubscriptionLocationResource parent)", async () => { + const program = await typeSpecCompile( + ` +@parentResource(SubscriptionLocationResource) +model Employee is ProxyResource { + ...ResourceNameParameter; +} + +model EmployeeProperties { + age?: int32; +} + +union EmployeeType { + string, +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface Employees { + get is ArmResourceRead; +} +`, + runner + ); + + const provider = resolveArmResources(program); + const resolvedResources = provider.resources ?? []; + + // Find Employee resource + const employeeResource = resolvedResources.find( + (r) => r.type.name === "Employee" + ); + ok(employeeResource, "Should find Employee resource"); + + // Verify scope is Subscription based on path + strictEqual( + getResourceScope(employeeResource), + "Subscription", + "Employee should be Subscription scoped based on path" + ); + + // Verify operations (lifecycle operations are arrays) + ok( + employeeResource.operations.lifecycle.read && + employeeResource.operations.lifecycle.read.length > 0, + "Employee should have read operation" + ); + }); + + it("parent-child resource with list operation only", async () => { + const program = await typeSpecCompile( + ` +/** An Employee parent resource */ +model EmployeeParent is TrackedResource { + ...ResourceNameParameter; +} + +/** Employee parent properties */ +model EmployeeParentProperties { + /** Name of parent */ + name?: string; +} + +/** An Employee resource */ +@parentResource(EmployeeParent) +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +/** Employee properties */ +model EmployeeProperties { + /** Age of employee */ + age?: int32; + + /** City of employee */ + city?: string; +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface EmployeeParents { + get is ArmResourceRead; +} + +@armResourceOperations +interface Employees { + listByParent is ArmResourceListByParent; +} +`, + runner + ); + + const provider = resolveArmResources(program); + const resolvedResources = provider.resources ?? []; + + // Find resources + const employeeParentResource = resolvedResources.find( + (r) => r.type.name === "EmployeeParent" + ); + const employeeResource = resolvedResources.find( + (r) => r.type.name === "Employee" + ); + + ok(employeeParentResource, "Should find EmployeeParent resource"); + ok(employeeResource, "Should find Employee resource"); + + // Employee has only list operation, no CRUD (arrays will be undefined or empty) + ok( + !employeeResource.operations.lifecycle.read || + employeeResource.operations.lifecycle.read.length === 0, + "Employee should NOT have read operation" + ); + ok( + !employeeResource.operations.lifecycle.createOrUpdate || + employeeResource.operations.lifecycle.createOrUpdate.length === 0, + "Employee should NOT have createOrUpdate operation" + ); + ok( + !employeeResource.operations.lifecycle.delete || + employeeResource.operations.lifecycle.delete.length === 0, + "Employee should NOT have delete operation" + ); + ok( + employeeResource.operations.lists.length > 0, + "Employee should have list operations" + ); + + // EmployeeParent has read operation + ok( + employeeParentResource.operations.lifecycle.read && + employeeParentResource.operations.lifecycle.read.length > 0, + "EmployeeParent should have read operation" + ); + }); + + it("resource scope as ManagementGroup", async () => { + const program = await typeSpecCompile( + ` +/** An Employee resource */ +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +/** Employee properties */ +model EmployeeProperties { + /** Age of employee */ + age?: int32; + + /** City of employee */ + city?: string; +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface Employees { + get is Extension.Read< + Extension.ManagementGroup<"managementGroupId">, + Employee + >; +} +`, + runner + ); + + const provider = resolveArmResources(program); + const resolvedResources = provider.resources ?? []; + + // Find Employee resource + const employeeResource = resolvedResources.find( + (r) => r.type.name === "Employee" + ); + ok(employeeResource, "Should find Employee resource"); + + // Verify ManagementGroup scope from path + strictEqual( + getResourceScope(employeeResource), + "ManagementGroup", + "Employee should be ManagementGroup scoped" + ); + }); + + it("API returns operation details for lifecycle operations", async () => { + const program = await typeSpecCompile( + ` +/** An Employee resource */ +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +/** Employee properties */ +model EmployeeProperties { + /** Age of employee */ + age?: int32; +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface Employees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmCustomPatchSync< + Employee, + Azure.ResourceManager.Foundations.ResourceUpdateModel + >; + delete is ArmResourceDeleteWithoutOkAsync; + listByResourceGroup is ArmResourceListByParent; +} +`, + runner + ); + + const provider = resolveArmResources(program); + const resolvedResources = provider.resources ?? []; + + const employeeResource = resolvedResources.find( + (r) => r.type.name === "Employee" + ); + ok(employeeResource, "Should find Employee resource"); + + // Verify all lifecycle operations are present (they are arrays) + ok( + employeeResource.operations.lifecycle.read && + employeeResource.operations.lifecycle.read.length > 0, + "Should have read operation" + ); + ok( + employeeResource.operations.lifecycle.createOrUpdate && + employeeResource.operations.lifecycle.createOrUpdate.length > 0, + "Should have createOrUpdate operation" + ); + ok( + employeeResource.operations.lifecycle.update && + employeeResource.operations.lifecycle.update.length > 0, + "Should have update operation" + ); + ok( + employeeResource.operations.lifecycle.delete && + employeeResource.operations.lifecycle.delete.length > 0, + "Should have delete operation" + ); + + // Verify list operations + ok( + employeeResource.operations.lists.length > 0, + "Should have list operations" + ); + + // Verify operation path information (lifecycle operations are arrays) + const readOps = employeeResource.operations.lifecycle.read; + ok(readOps && readOps.length > 0, "Read operations should exist"); + strictEqual( + readOps[0].path, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/employees/{employeeName}", + "Read operation path should match" + ); + }); +}); From 4def0069841b1e6c5f9def895fb1af83ea05083a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 07:12:36 +0000 Subject: [PATCH 5/7] Document resolveArmResources API limitations with version info Co-authored-by: live1206 <5196139+live1206@users.noreply.github.com> --- .../resolve-arm-resources-validation.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts b/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts index be0930a32ebe..50ceaf5606bb 100644 --- a/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts +++ b/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts @@ -9,6 +9,28 @@ * * Each test case corresponds to a case in resource-detection.test.ts and verifies * whether resolveArmResources API can return the expected resource information. + * + * KNOWN BUGS IN resolveArmResources API (as of @azure-tools/typespec-azure-resource-manager v0.62.0): + * + * 1. Multiple singleton resources with different @singleton keys are not properly distinguished. + * When both Employee and CurrentEmployee are defined with different singleton keys, + * the API returns all resources pointing to the same Employee type only. + * See: "singleton resource - demonstrates bug with multiple singletons" test case + * + * 2. Singleton child resources return incorrect resourceInstancePath. + * For @singleton("current") @parentResource(Bar) BarSettingsResource, + * the API returns the parent path /bars/{barName} instead of /bars/{barName}/settings/current. + * + * 3. Duplicate resources are returned for the same model. + * The API can return 2-3 resolved resources for the same TypeSpec model. + * + * 4. Parent information is often undefined even when @parentResource is specified. + * + * 5. Resource type for child resources uses the parent's type instead of the full type path. + * + * These bugs prevent using resolveArmResources as a direct replacement for the + * decorator-based resource detection logic in resource-detection.ts. + * Once these bugs are fixed in the TypeSpec ARM library, the replacement can be completed. */ import { beforeEach, describe, it } from "vitest"; From 28ae6a091ce242c3e782ddb16099675778fbdb8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 07:45:05 +0000 Subject: [PATCH 6/7] Add tests demonstrating resolveArmResources API bugs 2-5 with existing TypeSpec reference Co-authored-by: live1206 <5196139+live1206@users.noreply.github.com> --- .../resolve-arm-resources-validation.test.ts | 191 +++++++++++++++++- 1 file changed, 189 insertions(+), 2 deletions(-) diff --git a/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts b/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts index 50ceaf5606bb..f4fe47f41ff9 100644 --- a/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts +++ b/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts @@ -16,17 +16,26 @@ * When both Employee and CurrentEmployee are defined with different singleton keys, * the API returns all resources pointing to the same Employee type only. * See: "singleton resource - demonstrates bug with multiple singletons" test case + * See also: resolve-arm-resources-bug.test.ts for dedicated tests * * 2. Singleton child resources return incorrect resourceInstancePath. - * For @singleton("current") @parentResource(Bar) BarSettingsResource, + * For @singleton("current") @parentResource(Bar) BarSettings, * the API returns the parent path /bars/{barName} instead of /bars/{barName}/settings/current. + * Existing TypeSpec: generator/TestProjects/Local/Mgmt-TypeSpec/bar.tsp (BarSettingsResource) + * See: "singleton child resource - demonstrates path bug" test case below * * 3. Duplicate resources are returned for the same model. - * The API can return 2-3 resolved resources for the same TypeSpec model. + * The API can return 2-4 resolved resources for the same TypeSpec model. + * Existing TypeSpec: generator/TestProjects/Local/Mgmt-TypeSpec/bar.tsp (BarSettingsResource) + * See: "singleton child resource - demonstrates duplicate resources bug" test case below * * 4. Parent information is often undefined even when @parentResource is specified. + * Existing TypeSpec: generator/TestProjects/Local/Mgmt-TypeSpec/bar.tsp (BarSettingsResource) + * See: "singleton child resource - demonstrates parent undefined bug" test case below * * 5. Resource type for child resources uses the parent's type instead of the full type path. + * Existing TypeSpec: generator/TestProjects/Local/Mgmt-TypeSpec/bar.tsp (BarSettingsResource) + * See: "singleton child resource - demonstrates resource type bug" test case below * * These bugs prevent using resolveArmResources as a direct replacement for the * decorator-based resource detection logic in resource-detection.ts. @@ -1069,4 +1078,182 @@ interface Employees { "Read operation path should match" ); }); + + /** + * TypeSpec for testing singleton child resource bugs. + * This matches the structure in generator/TestProjects/Local/Mgmt-TypeSpec/bar.tsp + * where BarSettingsResource is a @singleton("current") @parentResource(Bar) resource. + */ + const singletonChildResourceSchema = ` +/** A parent resource */ +model Bar is TrackedResource { + ...ResourceNameParameter; +} + +model BarProperties { + name?: string; +} + +/** A singleton child resource with "current" key - similar to BarSettingsResource in bar.tsp */ +@singleton("current") +@parentResource(Bar) +model BarSettings is ProxyResource { + ...ResourceNameParameter; +} + +model BarSettingsProperties { + enabled?: boolean; +} + +interface Operations extends Azure.ResourceManager.Operations {} + +@armResourceOperations +interface Bars { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; +} + +@armResourceOperations +interface BarSettingsOps { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; +} +`; + + /** + * BUG 2: Singleton child resources return incorrect resourceInstancePath. + * + * Expected: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/bars/{barName}/settings/current + * Actual: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/bars/{barName} + * + * This bug affects BarSettingsResource in generator/TestProjects/Local/Mgmt-TypeSpec/bar.tsp + */ + it("singleton child resource - demonstrates path bug (BUG 2)", async () => { + const program = await typeSpecCompile(singletonChildResourceSchema, runner); + + const provider = resolveArmResources(program); + const resources = provider.resources ?? []; + + // Find BarSettings resource + const barSettingsResources = resources.filter( + (r) => r.type.name === "BarSettings" + ); + + ok(barSettingsResources.length > 0, "Should find BarSettings resource"); + + const barSettings = barSettingsResources[0]; + + // KNOWN BUG: The path should include /settings/current but it only shows parent path + // Expected (when bug is fixed): + // strictEqual( + // barSettings.resourceInstancePath, + // "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/bars/{barName}/settings/current", + // "BarSettings path should include /settings/current" + // ); + + // Current buggy behavior: path is same as parent's path + strictEqual( + barSettings.resourceInstancePath, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/bars/{barName}", + "KNOWN BUG: BarSettings path is parent path instead of /bars/{barName}/settings/current" + ); + }); + + /** + * BUG 3: Duplicate resources are returned for the same model. + * + * Expected: 2 unique resources (Bar and BarSettings) + * Actual: 4 resources (2 Bar and 2 BarSettings) + * + * This bug affects all resources, including BarSettingsResource in bar.tsp + */ + it("singleton child resource - demonstrates duplicate resources bug (BUG 3)", async () => { + const program = await typeSpecCompile(singletonChildResourceSchema, runner); + + const provider = resolveArmResources(program); + const resources = provider.resources ?? []; + + // Count resources by type + const barCount = resources.filter((r) => r.type.name === "Bar").length; + const barSettingsCount = resources.filter( + (r) => r.type.name === "BarSettings" + ).length; + + // KNOWN BUG: Multiple duplicate resources are returned + // Expected (when bug is fixed): + // strictEqual(barCount, 1, "Should have exactly 1 Bar resource"); + // strictEqual(barSettingsCount, 1, "Should have exactly 1 BarSettings resource"); + + // Current buggy behavior: duplicates are returned + ok(barCount > 1, "KNOWN BUG: More than 1 Bar resource returned"); + ok(barSettingsCount > 1, "KNOWN BUG: More than 1 BarSettings resource returned"); + ok(resources.length > 2, "KNOWN BUG: Total resources should be > 2 due to duplicates"); + }); + + /** + * BUG 4: Parent information is undefined even when @parentResource is specified. + * + * Expected: BarSettings.parent.type.name === "Bar" + * Actual: BarSettings.parent === undefined + * + * This bug affects BarSettingsResource in generator/TestProjects/Local/Mgmt-TypeSpec/bar.tsp + */ + it("singleton child resource - demonstrates parent undefined bug (BUG 4)", async () => { + const program = await typeSpecCompile(singletonChildResourceSchema, runner); + + const provider = resolveArmResources(program); + const resources = provider.resources ?? []; + + // Find BarSettings resource + const barSettings = resources.find((r) => r.type.name === "BarSettings"); + ok(barSettings, "Should find BarSettings resource"); + + // KNOWN BUG: Parent is undefined even though @parentResource(Bar) is specified + // Expected (when bug is fixed): + // ok(barSettings.parent, "BarSettings should have a parent"); + // strictEqual(barSettings.parent.type.name, "Bar", "Parent should be Bar"); + + // Current buggy behavior: parent is undefined + strictEqual( + barSettings.parent, + undefined, + "KNOWN BUG: BarSettings.parent is undefined despite @parentResource(Bar)" + ); + }); + + /** + * BUG 5: Resource type for child resources uses parent's type instead of full path. + * + * Expected: Microsoft.ContosoProviderHub/bars/settings + * Actual: Microsoft.ContosoProviderHub/bars + * + * This bug affects BarSettingsResource in generator/TestProjects/Local/Mgmt-TypeSpec/bar.tsp + */ + it("singleton child resource - demonstrates resource type bug (BUG 5)", async () => { + const program = await typeSpecCompile(singletonChildResourceSchema, runner); + + const provider = resolveArmResources(program); + const resources = provider.resources ?? []; + + // Find BarSettings resource + const barSettings = resources.find((r) => r.type.name === "BarSettings"); + ok(barSettings, "Should find BarSettings resource"); + + const resourceTypeString = `${barSettings.resourceType.provider}/${barSettings.resourceType.types.join("/")}`; + + // KNOWN BUG: Resource type should include child segment but only shows parent type + // Expected (when bug is fixed): + // strictEqual( + // resourceTypeString, + // "Microsoft.ContosoProviderHub/bars/settings", + // "BarSettings resource type should be bars/settings" + // ); + + // Current buggy behavior: uses parent's resource type + strictEqual( + resourceTypeString, + "Microsoft.ContosoProviderHub/bars", + "KNOWN BUG: BarSettings uses parent's resource type instead of bars/settings" + ); + }); }); From 6a123ff29e5c28513d864bf973198a13c1445d4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 08:56:33 +0000 Subject: [PATCH 7/7] Merge resolve-arm-resources-bug.test.ts into validation test file Co-authored-by: live1206 <5196139+live1206@users.noreply.github.com> --- .../test/resolve-arm-resources-bug.test.ts | 162 --------------- .../resolve-arm-resources-validation.test.ts | 187 ++++++++++-------- 2 files changed, 102 insertions(+), 247 deletions(-) delete mode 100644 eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-bug.test.ts diff --git a/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-bug.test.ts b/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-bug.test.ts deleted file mode 100644 index 0dc77716112a..000000000000 --- a/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-bug.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -/** - * This test file demonstrates a bug in the resolveArmResources API from - * @azure-tools/typespec-azure-resource-manager. - * - * Bug Description: - * When multiple singleton resources are defined with different @singleton keys - * (e.g., Employee with default key and CurrentEmployee with "current" key), - * the resolveArmResources API does not properly distinguish between them. - * All resolved resources point to the same model type instead of their respective types. - * - * This test is marked with .skip because it demonstrates a bug in an external library - * that needs to be fixed before the test will pass. - */ - -import { beforeEach, describe, it } from "vitest"; -import { - createEmitterTestHost, - typeSpecCompile -} from "./test-util.js"; -import { TestHost } from "@typespec/compiler/testing"; -import { resolveArmResources } from "@azure-tools/typespec-azure-resource-manager"; -import { strictEqual, ok } from "assert"; - -/** - * Shared TypeSpec schema for multiple singleton resources test. - * Defines two singleton resources with different @singleton keys. - */ -const multipleSingletonsSchema = ` -/** Employee properties */ -model EmployeeProperties { - /** Age of employee */ - age?: int32; -} - -/** An Employee singleton resource with default key */ -@singleton -model Employee is TrackedResource { - ...ResourceNameParameter; -} - -/** A CurrentEmployee singleton resource with "current" key */ -@singleton("current") -model CurrentEmployee is TrackedResource { - ...ResourceNameParameter; -} - -interface Operations extends Azure.ResourceManager.Operations {} - -@armResourceOperations -interface Employees { - get is ArmResourceRead; - createOrUpdate is ArmResourceCreateOrReplaceAsync; -} - -@armResourceOperations -interface CurrentEmployees { - get is ArmResourceRead; - createOrUpdate is ArmResourceCreateOrReplaceAsync; -} -`; - -describe("resolveArmResources API Bug Demonstration", () => { - let runner: TestHost; - beforeEach(async () => { - runner = await createEmitterTestHost(); - }); - - /** - * This test demonstrates that resolveArmResources does not correctly return - * multiple singleton resources with different @singleton keys. - * - * Expected behavior: The API should return both Employee and CurrentEmployee - * as separate resources with their respective model types. - * - * Actual behavior: All resolved resources have type.name === "Employee", - * meaning CurrentEmployee is never properly identified. - */ - it.skip("should return distinct resources for multiple singletons with different keys", async () => { - const program = await typeSpecCompile(multipleSingletonsSchema, runner); - - // Use resolveArmResources API to get all resolved ARM resources - const provider = resolveArmResources(program); - const resolvedResources = provider.resources ?? []; - - // Check that we have resources returned - ok(resolvedResources.length > 0, "Should have at least one resolved resource"); - - // Find resources by their model type name - const employeeResources = resolvedResources.filter( - (r) => r.type.name === "Employee" - ); - const currentEmployeeResources = resolvedResources.filter( - (r) => r.type.name === "CurrentEmployee" - ); - - // BUG: This assertion fails because resolveArmResources returns all resources - // with type.name === "Employee" instead of distinguishing CurrentEmployee - ok( - currentEmployeeResources.length > 0, - "Should have at least one CurrentEmployee resource - BUG: resolveArmResources does not return CurrentEmployee" - ); - - // Verify that Employee resources are returned correctly - ok( - employeeResources.length > 0, - "Should have at least one Employee resource" - ); - - // Verify that the total count is correct (should be 2 distinct resources for CRUD operations) - // Note: The API may return multiple entries per resource for different operation paths - const uniqueResourceTypes = new Set(resolvedResources.map((r) => r.type.name)); - strictEqual( - uniqueResourceTypes.size, - 2, - "Should have exactly 2 unique resource types (Employee and CurrentEmployee)" - ); - }); - - /** - * This test shows the current (buggy) behavior of resolveArmResources - * where all resources point to the same model type. - */ - it("demonstrates current buggy behavior - all resources have same type", async () => { - const program = await typeSpecCompile(multipleSingletonsSchema, runner); - - // Use resolveArmResources API to get all resolved ARM resources - const provider = resolveArmResources(program); - const resolvedResources = provider.resources ?? []; - - // BUG: All resolved resources have type.name === "Employee" - // CurrentEmployee is never returned despite being defined - const allResourceTypeNames = resolvedResources.map((r) => r.type.name); - const uniqueResourceTypes = new Set(allResourceTypeNames); - - // This assertion passes but demonstrates the bug: - // We expect 2 unique types but only get 1 - strictEqual( - uniqueResourceTypes.size, - 1, - "BUG: resolveArmResources only returns Employee type, missing CurrentEmployee" - ); - - // All resources point to Employee - ok( - allResourceTypeNames.every((name) => name === "Employee"), - "BUG: All resolved resources have type Employee" - ); - - // CurrentEmployee is never returned - const hasCurrentEmployee = allResourceTypeNames.some( - (name) => name === "CurrentEmployee" - ); - strictEqual( - hasCurrentEmployee, - false, - "BUG: CurrentEmployee is never returned by resolveArmResources" - ); - }); -}); diff --git a/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts b/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts index f4fe47f41ff9..aef24deae12e 100644 --- a/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts +++ b/eng/packages/http-client-csharp-mgmt/emitter/test/resolve-arm-resources-validation.test.ts @@ -267,124 +267,141 @@ interface Employees2 { }); /** - * This test validates the KNOWN BUG in resolveArmResources API with multiple singletons. - * - * BUG DESCRIPTION: - * When multiple singleton resources are defined with different @singleton keys - * (e.g., Employee with default key and CurrentEmployee with "current" key), - * the resolveArmResources API does not properly distinguish between them. - * All resolved resources point to the same model type (Employee) instead of - * their respective types. - * - * This test intentionally asserts the CURRENT BUGGY BEHAVIOR to document it. - * Once the bug is fixed in @azure-tools/typespec-azure-resource-manager, - * these assertions should be updated to verify correct behavior. + * Shared TypeSpec schema for multiple singleton resources test. + * Defines two singleton resources with different @singleton keys. */ - it("singleton resource - demonstrates bug with multiple singletons", async () => { - const program = await typeSpecCompile( - ` -/** An Employee singleton resource */ + const multipleSingletonsSchema = ` +/** Employee properties */ +model EmployeeProperties { + /** Age of employee */ + age?: int32; +} + +/** An Employee singleton resource with default key */ @singleton model Employee is TrackedResource { ...ResourceNameParameter; } +/** A CurrentEmployee singleton resource with "current" key */ @singleton("current") model CurrentEmployee is TrackedResource { ...ResourceNameParameter; } -/** Employee properties */ -model EmployeeProperties { - /** Age of employee */ - age?: int32; - - /** City of employee */ - city?: string; - - /** Profile of employee */ - @encode("base64url") - profile?: bytes; - - /** The status of the last operation. */ - @visibility(Lifecycle.Read) - provisioningState?: ProvisioningState; -} - -/** The provisioning state of a resource. */ -@lroStatus -union ProvisioningState { - string, - - /** The resource create request has been accepted */ - Accepted: "Accepted", - - /** The resource is being provisioned */ - Provisioning: "Provisioning", - - /** The resource is updating */ - Updating: "Updating", - - /** Resource has been created. */ - Succeeded: "Succeeded", - - /** Resource creation failed. */ - Failed: "Failed", - - /** Resource creation was canceled. */ - Canceled: "Canceled", - - /** The resource is being deleted */ - Deleting: "Deleting", -} - interface Operations extends Azure.ResourceManager.Operations {} @armResourceOperations interface Employees { get is ArmResourceRead; createOrUpdate is ArmResourceCreateOrReplaceAsync; - update is ArmCustomPatchSync< - Employee, - Azure.ResourceManager.Foundations.ResourceUpdateModel - >; } @armResourceOperations interface CurrentEmployees { get is ArmResourceRead; createOrUpdate is ArmResourceCreateOrReplaceAsync; - update is ArmCustomPatchSync< - CurrentEmployee, - Azure.ResourceManager.Foundations.ResourceUpdateModel - >; } -`, - runner +`; + + /** + * BUG 1: Multiple singleton resources with different @singleton keys are not properly distinguished. + * + * This test demonstrates that resolveArmResources does not correctly return + * multiple singleton resources with different @singleton keys. + * + * Expected behavior: The API should return both Employee and CurrentEmployee + * as separate resources with their respective model types. + * + * Actual behavior: All resolved resources have type.name === "Employee", + * meaning CurrentEmployee is never properly identified. + * + * This test is marked with .skip because it demonstrates a bug in an external library + * that needs to be fixed before the test will pass. + */ + it.skip("singleton resource - should return distinct resources for multiple singletons (BUG 1 - expected behavior)", async () => { + const program = await typeSpecCompile(multipleSingletonsSchema, runner); + + // Use resolveArmResources API to get all resolved ARM resources + const provider = resolveArmResources(program); + const resolvedResources = provider.resources ?? []; + + // Check that we have resources returned + ok(resolvedResources.length > 0, "Should have at least one resolved resource"); + + // Find resources by their model type name + const employeeResources = resolvedResources.filter( + (r) => r.type.name === "Employee" + ); + const currentEmployeeResources = resolvedResources.filter( + (r) => r.type.name === "CurrentEmployee" ); + // BUG: This assertion fails because resolveArmResources returns all resources + // with type.name === "Employee" instead of distinguishing CurrentEmployee + ok( + currentEmployeeResources.length > 0, + "Should have at least one CurrentEmployee resource - BUG: resolveArmResources does not return CurrentEmployee" + ); + + // Verify that Employee resources are returned correctly + ok( + employeeResources.length > 0, + "Should have at least one Employee resource" + ); + + // Verify that the total count is correct (should be 2 distinct resources for CRUD operations) + // Note: The API may return multiple entries per resource for different operation paths + const uniqueResourceTypes = new Set(resolvedResources.map((r) => r.type.name)); + strictEqual( + uniqueResourceTypes.size, + 2, + "Should have exactly 2 unique resource types (Employee and CurrentEmployee)" + ); + }); + + /** + * BUG 1: Multiple singleton resources with different @singleton keys are not properly distinguished. + * + * This test validates the KNOWN BUG in resolveArmResources API with multiple singletons. + * It intentionally asserts the CURRENT BUGGY BEHAVIOR to document it. + * Once the bug is fixed in @azure-tools/typespec-azure-resource-manager, + * these assertions should be updated to verify correct behavior. + */ + it("singleton resource - demonstrates bug with multiple singletons (BUG 1 - current behavior)", async () => { + const program = await typeSpecCompile(multipleSingletonsSchema, runner); + + // Use resolveArmResources API to get all resolved ARM resources const provider = resolveArmResources(program); const resolvedResources = provider.resources ?? []; - // Get unique resource types - const resourceTypeNames = resolvedResources.map((r) => r.type.name); - const uniqueTypes = new Set(resourceTypeNames); - - // KNOWN BUG: resolveArmResources does not properly distinguish multiple singletons - // Expected behavior (when bug is fixed): Should return both "Employee" and "CurrentEmployee" - // Current buggy behavior: Returns only "Employee" for all resources - // - // When the bug is fixed, update these assertions to: - // strictEqual(uniqueTypes.size, 2, "Should have 2 unique resource types"); - // ok(resourceTypeNames.includes("CurrentEmployee"), "Should include CurrentEmployee"); + // BUG: All resolved resources have type.name === "Employee" + // CurrentEmployee is never returned despite being defined + const allResourceTypeNames = resolvedResources.map((r) => r.type.name); + const uniqueResourceTypes = new Set(allResourceTypeNames); + + // This assertion passes but demonstrates the bug: + // We expect 2 unique types but only get 1 strictEqual( - uniqueTypes.size, + uniqueResourceTypes.size, 1, - "KNOWN BUG: Only 1 unique resource type returned instead of 2" + "KNOWN BUG: resolveArmResources only returns Employee type, missing CurrentEmployee" ); + + // All resources point to Employee ok( - !resourceTypeNames.includes("CurrentEmployee"), - "KNOWN BUG: CurrentEmployee is NOT returned by resolveArmResources" + allResourceTypeNames.every((name) => name === "Employee"), + "KNOWN BUG: All resolved resources have type Employee" + ); + + // CurrentEmployee is never returned + const hasCurrentEmployee = allResourceTypeNames.some( + (name) => name === "CurrentEmployee" + ); + strictEqual( + hasCurrentEmployee, + false, + "KNOWN BUG: CurrentEmployee is never returned by resolveArmResources" ); // Find Employee resource and verify singleton key can be obtained via getSingletonResourceKey