From 90205703ea16ceed2aeda2d92e3d0e251e41671d Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 15 Oct 2026 16:31:42 -0400 Subject: [PATCH 1/5] Add LRO metadata Spector coverage Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 74764156-f3ea-445f-9ebd-67821f33bb10 --- .../add-lro-metadata-spector-2026-10-15.md | 7 + packages/azure-http-specs/spec-summary.md | 36 ++++ .../lro-metadata/main.tsp | 115 +++++++++++ .../lro-metadata/mockapi.ts | 191 ++++++++++++++++++ 4 files changed, 349 insertions(+) create mode 100644 .chronus/changes/add-lro-metadata-spector-2026-10-15.md create mode 100644 packages/azure-http-specs/specs/azure/client-generator-core/lro-metadata/main.tsp create mode 100644 packages/azure-http-specs/specs/azure/client-generator-core/lro-metadata/mockapi.ts diff --git a/.chronus/changes/add-lro-metadata-spector-2026-10-15.md b/.chronus/changes/add-lro-metadata-spector-2026-10-15.md new file mode 100644 index 0000000000..2bf687a369 --- /dev/null +++ b/.chronus/changes/add-lro-metadata-spector-2026-10-15.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@azure-tools/azure-http-specs" +--- + +Add Spector coverage for LRO operation ID extraction and typed polling metadata. diff --git a/packages/azure-http-specs/spec-summary.md b/packages/azure-http-specs/spec-summary.md index f3560ed8e1..be6ee1c49d 100644 --- a/packages/azure-http-specs/spec-summary.md +++ b/packages/azure-http-specs/spec-summary.md @@ -1268,6 +1268,42 @@ Expected response body: } ``` +### Azure_ClientGenerator_Core_LroMetadata_analyze + +- Endpoint: `post /azure/client-generator-core/lro-metadata/analyzers/{analyzerId}:analyze` + +Verifies that `@lroOperationId("lastPathSegment")` exposes the server-issued operation ID from +the status-monitor URI without changing the polling protocol. + +The generated LRO method should expose `analysis-123`, derived from the final path segment of +`Operation-Location`. + +Expected calls: + +- POST `/analyzers/document-1:analyze` returns 202 with + `Operation-Location: {endpoint}/analyzerResults/analysis-123` +- GET `/analyzerResults/analysis-123` returns a running status +- GET `/analyzerResults/analysis-123` returns a succeeded status with the analysis result + +### Azure_ClientGenerator_Core_LroMetadata_translate + +- Endpoint: `post /azure/client-generator-core/lro-metadata/documents/{documentId}:translate` + +Verifies that property-level `@lroPollingMetadata` exposes a status-monitor property with its +exact SDK type and serialization metadata. + +The generated LRO method should expose optional `int32` polling metadata named `percentComplete`, +serialized on the wire as `percent_complete`. + +Expected calls: + +- POST `/documents/document-1:translate` returns 202 with + `Operation-Location: {endpoint}/documentTranslationJobs/translation-123` +- GET `/documentTranslationJobs/translation-123` returns a running status with + `percent_complete: 50` +- GET `/documentTranslationJobs/translation-123` returns a succeeded status with + `percent_complete: 100` and the translation result + ### Azure_ClientGenerator_Core_NextLinkVerb_listItems - Endpoint: `post /azure/client-generator-core/next-link-verb/items` diff --git a/packages/azure-http-specs/specs/azure/client-generator-core/lro-metadata/main.tsp b/packages/azure-http-specs/specs/azure/client-generator-core/lro-metadata/main.tsp new file mode 100644 index 0000000000..a97527f7cd --- /dev/null +++ b/packages/azure-http-specs/specs/azure/client-generator-core/lro-metadata/main.tsp @@ -0,0 +1,115 @@ +import "@azure-tools/typespec-azure-core"; +import "@azure-tools/typespec-client-generator-core"; +import "@typespec/rest"; +import "@typespec/spector"; +import "@typespec/versioning"; + +using Azure.ClientGenerator.Core; +using Azure.Core; +using Azure.Core.Traits; +using Spector; +using TypeSpec.Http; +using TypeSpec.Rest; +using TypeSpec.Versioning; + +@doc("Tests client metadata exposed for Azure long-running operations.") +@scenarioService( + "/azure/client-generator-core/lro-metadata", + { + versioned: Versions, + } +) +@global.Azure.ClientGenerator.Core.clientNamespace("azure.clientgenerator.core.lrometadata", "java") +@global.Azure.ClientGenerator.Core.clientNamespace( + "specs.azure.clientgenerator.core.lrometadata", + "python" +) +namespace _Specs_.Azure.ClientGenerator.Core.LroMetadata; + +enum Versions { + v2022_12_01_preview: "2022-12-01-preview", +} + +alias ResourceOperations = global.Azure.Core.ResourceOperations; + +@resource("analyzerResults") +model AnalyzerResultStatus { + @key + @visibility(Lifecycle.Read) + id: string; + + status: global.Azure.Core.Foundations.OperationState; + result?: AnalyzerResult; +} + +model AnalyzerResult { + summary: string; +} + +op getAnalyzerResultStatus is ResourceOperations.ResourceRead; + +@scenario +@scenarioDoc(""" + Verifies that `@lroOperationId("lastPathSegment")` exposes the server-issued operation ID from + the status-monitor URI without changing the polling protocol. + + The generated LRO method should expose `analysis-123`, derived from the final path segment of + `Operation-Location`. + + Expected calls: + - POST `/analyzers/document-1:analyze` returns 202 with + `Operation-Location: {endpoint}/analyzerResults/analysis-123` + - GET `/analyzerResults/analysis-123` returns a running status + - GET `/analyzerResults/analysis-123` returns a succeeded status with the analysis result + """) +@route("/analyzers/{analyzerId}:analyze") +@post +@pollingOperation(getAnalyzerResultStatus) +@lroOperationId("lastPathSegment") +op analyze(@path analyzerId: string): global.Azure.Core.Foundations.AcceptedResponse & + global.Azure.Core.Foundations.LongRunningStatusLocation; + +@resource("documentTranslationJobs") +model DocumentTranslationStatus { + @key + @visibility(Lifecycle.Read) + id: string; + + status: global.Azure.Core.Foundations.OperationState; + + @encodedName("application/json", "percent_complete") + @lroPollingMetadata + percentComplete?: int32; + + result?: TranslationResult; +} + +model TranslationResult { + translatedDocumentUrl: url; +} + +op getDocumentTranslationStatus is ResourceOperations.ResourceRead; + +@scenario +@scenarioDoc(""" + Verifies that property-level `@lroPollingMetadata` exposes a status-monitor property with its + exact SDK type and serialization metadata. + + The generated LRO method should expose optional `int32` polling metadata named `percentComplete`, + serialized on the wire as `percent_complete`. + + Expected calls: + - POST `/documents/document-1:translate` returns 202 with + `Operation-Location: {endpoint}/documentTranslationJobs/translation-123` + - GET `/documentTranslationJobs/translation-123` returns a running status with + `percent_complete: 50` + - GET `/documentTranslationJobs/translation-123` returns a succeeded status with + `percent_complete: 100` and the translation result + """) +@route("/documents/{documentId}:translate") +@post +@pollingOperation(getDocumentTranslationStatus) +op translate(@path documentId: string): global.Azure.Core.Foundations.AcceptedResponse & + global.Azure.Core.Foundations.LongRunningStatusLocation; diff --git a/packages/azure-http-specs/specs/azure/client-generator-core/lro-metadata/mockapi.ts b/packages/azure-http-specs/specs/azure/client-generator-core/lro-metadata/mockapi.ts new file mode 100644 index 0000000000..e4bdd46e7e --- /dev/null +++ b/packages/azure-http-specs/specs/azure/client-generator-core/lro-metadata/mockapi.ts @@ -0,0 +1,191 @@ +import { + dyn, + dynItem, + json, + MockRequest, + passOnSuccess, + type ScenarioMockApi, +} from "@typespec/spec-api"; + +export const Scenarios: Record = {}; + +let analyzePollCount = 0; +let translatePollCount = 0; + +Scenarios.Azure_ClientGenerator_Core_LroMetadata_analyze = passOnSuccess([ + { + uri: "/azure/client-generator-core/lro-metadata/analyzers/document-1:analyze", + method: "post", + request: { + query: { + "api-version": "2022-12-01-preview", + }, + }, + response: { + status: 202, + headers: { + "operation-location": dyn`${dynItem("baseUrl")}/azure/client-generator-core/lro-metadata/analyzerResults/analysis-123`, + }, + }, + handler: (req: MockRequest) => { + req.expect.containsQueryParam("api-version", "2022-12-01-preview"); + analyzePollCount = 0; + return { + status: 202, + headers: { + "operation-location": `${req.baseUrl}/azure/client-generator-core/lro-metadata/analyzerResults/analysis-123`, + }, + }; + }, + kind: "MockApiDefinition", + }, + { + uri: "/azure/client-generator-core/lro-metadata/analyzerResults/analysis-123", + method: "get", + request: { + query: { + "api-version": "2022-12-01-preview", + }, + }, + response: { + status: 200, + body: json({ + id: "analysis-123", + status: "Running", + }), + }, + handler: analyzeLroHandler, + kind: "MockApiDefinition", + }, + { + uri: "/azure/client-generator-core/lro-metadata/analyzerResults/analysis-123", + method: "get", + request: { + query: { + "api-version": "2022-12-01-preview", + }, + }, + response: { + status: 200, + body: json({ + id: "analysis-123", + status: "Succeeded", + result: { + summary: "The document contains three sections.", + }, + }), + }, + handler: analyzeLroHandler, + kind: "MockApiDefinition", + }, +]); + +Scenarios.Azure_ClientGenerator_Core_LroMetadata_translate = passOnSuccess([ + { + uri: "/azure/client-generator-core/lro-metadata/documents/document-1:translate", + method: "post", + request: { + query: { + "api-version": "2022-12-01-preview", + }, + }, + response: { + status: 202, + headers: { + "operation-location": dyn`${dynItem("baseUrl")}/azure/client-generator-core/lro-metadata/documentTranslationJobs/translation-123`, + }, + }, + handler: (req: MockRequest) => { + req.expect.containsQueryParam("api-version", "2022-12-01-preview"); + translatePollCount = 0; + return { + status: 202, + headers: { + "operation-location": `${req.baseUrl}/azure/client-generator-core/lro-metadata/documentTranslationJobs/translation-123`, + }, + }; + }, + kind: "MockApiDefinition", + }, + { + uri: "/azure/client-generator-core/lro-metadata/documentTranslationJobs/translation-123", + method: "get", + request: { + query: { + "api-version": "2022-12-01-preview", + }, + }, + response: { + status: 200, + body: json({ + id: "translation-123", + status: "Running", + percent_complete: 50, + }), + }, + handler: translateLroHandler, + kind: "MockApiDefinition", + }, + { + uri: "/azure/client-generator-core/lro-metadata/documentTranslationJobs/translation-123", + method: "get", + request: { + query: { + "api-version": "2022-12-01-preview", + }, + }, + response: { + status: 200, + body: json({ + id: "translation-123", + status: "Succeeded", + percent_complete: 100, + result: { + translatedDocumentUrl: "https://example.org/documents/document-1.fr", + }, + }), + }, + handler: translateLroHandler, + kind: "MockApiDefinition", + }, +]); + +function analyzeLroHandler(req: MockRequest) { + req.expect.containsQueryParam("api-version", "2022-12-01-preview"); + const response = + analyzePollCount > 0 + ? { + id: "analysis-123", + status: "Succeeded", + result: { + summary: "The document contains three sections.", + }, + } + : { + id: "analysis-123", + status: "Running", + }; + analyzePollCount += 1; + return { status: 200, body: json(response) }; +} + +function translateLroHandler(req: MockRequest) { + req.expect.containsQueryParam("api-version", "2022-12-01-preview"); + const response = + translatePollCount > 0 + ? { + id: "translation-123", + status: "Succeeded", + percent_complete: 100, + result: { + translatedDocumentUrl: "https://example.org/documents/document-1.fr", + }, + } + : { + id: "translation-123", + status: "Running", + percent_complete: 50, + }; + translatePollCount += 1; + return { status: 200, body: json(response) }; +} From 9cb822edb030e8a207a374bf035b47a35cb4f6a5 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 16 Oct 2026 16:27:02 -0400 Subject: [PATCH 2/5] Reorganize LRO metadata Spector scenarios Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d18612ec-bd77-4fdd-86d0-467051688ac6 --- .../add-lro-metadata-spector-2026-10-15.md | 2 +- packages/azure-http-specs/spec-summary.md | 8 +++---- .../{lro-metadata => lro/metadata}/main.tsp | 19 ++++++++------- .../{lro-metadata => lro/metadata}/mockapi.ts | 24 +++++++++---------- 4 files changed, 28 insertions(+), 25 deletions(-) rename packages/azure-http-specs/specs/azure/client-generator-core/{lro-metadata => lro/metadata}/main.tsp (92%) rename packages/azure-http-specs/specs/azure/client-generator-core/{lro-metadata => lro/metadata}/mockapi.ts (83%) diff --git a/.chronus/changes/add-lro-metadata-spector-2026-10-15.md b/.chronus/changes/add-lro-metadata-spector-2026-10-15.md index 2bf687a369..d3a5d9832a 100644 --- a/.chronus/changes/add-lro-metadata-spector-2026-10-15.md +++ b/.chronus/changes/add-lro-metadata-spector-2026-10-15.md @@ -1,5 +1,5 @@ --- -changeKind: internal +changeKind: feature packages: - "@azure-tools/azure-http-specs" --- diff --git a/packages/azure-http-specs/spec-summary.md b/packages/azure-http-specs/spec-summary.md index be6ee1c49d..98f35a3f3d 100644 --- a/packages/azure-http-specs/spec-summary.md +++ b/packages/azure-http-specs/spec-summary.md @@ -1268,9 +1268,9 @@ Expected response body: } ``` -### Azure_ClientGenerator_Core_LroMetadata_analyze +### Azure_ClientGenerator_Core_Lro_Metadata_analyze -- Endpoint: `post /azure/client-generator-core/lro-metadata/analyzers/{analyzerId}:analyze` +- Endpoint: `post /azure/client-generator-core/lro/metadata/analyzers/{analyzerId}:analyze` Verifies that `@lroOperationId("lastPathSegment")` exposes the server-issued operation ID from the status-monitor URI without changing the polling protocol. @@ -1285,9 +1285,9 @@ Expected calls: - GET `/analyzerResults/analysis-123` returns a running status - GET `/analyzerResults/analysis-123` returns a succeeded status with the analysis result -### Azure_ClientGenerator_Core_LroMetadata_translate +### Azure_ClientGenerator_Core_Lro_Metadata_translate -- Endpoint: `post /azure/client-generator-core/lro-metadata/documents/{documentId}:translate` +- Endpoint: `post /azure/client-generator-core/lro/metadata/documents/{documentId}:translate` Verifies that property-level `@lroPollingMetadata` exposes a status-monitor property with its exact SDK type and serialization metadata. diff --git a/packages/azure-http-specs/specs/azure/client-generator-core/lro-metadata/main.tsp b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp similarity index 92% rename from packages/azure-http-specs/specs/azure/client-generator-core/lro-metadata/main.tsp rename to packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp index a97527f7cd..82b8e4387a 100644 --- a/packages/azure-http-specs/specs/azure/client-generator-core/lro-metadata/main.tsp +++ b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp @@ -14,17 +14,20 @@ using TypeSpec.Versioning; @doc("Tests client metadata exposed for Azure long-running operations.") @scenarioService( - "/azure/client-generator-core/lro-metadata", + "/azure/client-generator-core/lro/metadata", { versioned: Versions, } ) -@global.Azure.ClientGenerator.Core.clientNamespace("azure.clientgenerator.core.lrometadata", "java") @global.Azure.ClientGenerator.Core.clientNamespace( - "specs.azure.clientgenerator.core.lrometadata", + "azure.clientgenerator.core.lro.metadata", + "java" +) +@global.Azure.ClientGenerator.Core.clientNamespace( + "specs.azure.clientgenerator.core.lro.metadata", "python" ) -namespace _Specs_.Azure.ClientGenerator.Core.LroMetadata; +namespace _Specs_.Azure.ClientGenerator.Core.Lro.Metadata; enum Versions { v2022_12_01_preview: "2022-12-01-preview", @@ -54,10 +57,10 @@ op getAnalyzerResultStatus is ResourceOperations.ResourceRead = {}; let analyzePollCount = 0; let translatePollCount = 0; -Scenarios.Azure_ClientGenerator_Core_LroMetadata_analyze = passOnSuccess([ +Scenarios.Azure_ClientGenerator_Core_Lro_Metadata_analyze = passOnSuccess([ { - uri: "/azure/client-generator-core/lro-metadata/analyzers/document-1:analyze", + uri: "/azure/client-generator-core/lro/metadata/analyzers/document-1:analyze", method: "post", request: { query: { @@ -24,7 +24,7 @@ Scenarios.Azure_ClientGenerator_Core_LroMetadata_analyze = passOnSuccess([ response: { status: 202, headers: { - "operation-location": dyn`${dynItem("baseUrl")}/azure/client-generator-core/lro-metadata/analyzerResults/analysis-123`, + "operation-location": dyn`${dynItem("baseUrl")}/azure/client-generator-core/lro/metadata/analyzerResults/analysis-123`, }, }, handler: (req: MockRequest) => { @@ -33,14 +33,14 @@ Scenarios.Azure_ClientGenerator_Core_LroMetadata_analyze = passOnSuccess([ return { status: 202, headers: { - "operation-location": `${req.baseUrl}/azure/client-generator-core/lro-metadata/analyzerResults/analysis-123`, + "operation-location": `${req.baseUrl}/azure/client-generator-core/lro/metadata/analyzerResults/analysis-123`, }, }; }, kind: "MockApiDefinition", }, { - uri: "/azure/client-generator-core/lro-metadata/analyzerResults/analysis-123", + uri: "/azure/client-generator-core/lro/metadata/analyzerResults/analysis-123", method: "get", request: { query: { @@ -58,7 +58,7 @@ Scenarios.Azure_ClientGenerator_Core_LroMetadata_analyze = passOnSuccess([ kind: "MockApiDefinition", }, { - uri: "/azure/client-generator-core/lro-metadata/analyzerResults/analysis-123", + uri: "/azure/client-generator-core/lro/metadata/analyzerResults/analysis-123", method: "get", request: { query: { @@ -80,9 +80,9 @@ Scenarios.Azure_ClientGenerator_Core_LroMetadata_analyze = passOnSuccess([ }, ]); -Scenarios.Azure_ClientGenerator_Core_LroMetadata_translate = passOnSuccess([ +Scenarios.Azure_ClientGenerator_Core_Lro_Metadata_translate = passOnSuccess([ { - uri: "/azure/client-generator-core/lro-metadata/documents/document-1:translate", + uri: "/azure/client-generator-core/lro/metadata/documents/document-1:translate", method: "post", request: { query: { @@ -92,7 +92,7 @@ Scenarios.Azure_ClientGenerator_Core_LroMetadata_translate = passOnSuccess([ response: { status: 202, headers: { - "operation-location": dyn`${dynItem("baseUrl")}/azure/client-generator-core/lro-metadata/documentTranslationJobs/translation-123`, + "operation-location": dyn`${dynItem("baseUrl")}/azure/client-generator-core/lro/metadata/documentTranslationJobs/translation-123`, }, }, handler: (req: MockRequest) => { @@ -101,14 +101,14 @@ Scenarios.Azure_ClientGenerator_Core_LroMetadata_translate = passOnSuccess([ return { status: 202, headers: { - "operation-location": `${req.baseUrl}/azure/client-generator-core/lro-metadata/documentTranslationJobs/translation-123`, + "operation-location": `${req.baseUrl}/azure/client-generator-core/lro/metadata/documentTranslationJobs/translation-123`, }, }; }, kind: "MockApiDefinition", }, { - uri: "/azure/client-generator-core/lro-metadata/documentTranslationJobs/translation-123", + uri: "/azure/client-generator-core/lro/metadata/documentTranslationJobs/translation-123", method: "get", request: { query: { @@ -127,7 +127,7 @@ Scenarios.Azure_ClientGenerator_Core_LroMetadata_translate = passOnSuccess([ kind: "MockApiDefinition", }, { - uri: "/azure/client-generator-core/lro-metadata/documentTranslationJobs/translation-123", + uri: "/azure/client-generator-core/lro/metadata/documentTranslationJobs/translation-123", method: "get", request: { query: { From 885347874c1184414980a17312f7bc273258b6ff Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 19 Oct 2026 15:13:12 -0400 Subject: [PATCH 3/5] Add API version to LRO Spector operations Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d18612ec-bd77-4fdd-86d0-467051688ac6 --- .../azure/client-generator-core/lro/metadata/main.tsp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp index 82b8e4387a..5d279b4757 100644 --- a/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp +++ b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp @@ -71,7 +71,10 @@ op getAnalyzerResultStatus is ResourceOperations.ResourceRead; @resource("documentTranslationJobs") @@ -114,5 +117,8 @@ op getDocumentTranslationStatus is ResourceOperations.ResourceRead; From a86c3945dabb38494c30f64d435dbcc462a626d6 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 19 Oct 2026 15:26:35 -0400 Subject: [PATCH 4/5] Refine LRO metadata Spector scenarios Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d18612ec-bd77-4fdd-86d0-467051688ac6 --- .../add-lro-metadata-spector-2026-10-15.md | 2 +- packages/azure-http-specs/spec-summary.md | 40 ++++--- .../lro/metadata/main.tsp | 101 +++++++----------- .../lro/metadata/mockapi.ts | 99 ++++++++++------- 4 files changed, 122 insertions(+), 120 deletions(-) diff --git a/.chronus/changes/add-lro-metadata-spector-2026-10-15.md b/.chronus/changes/add-lro-metadata-spector-2026-10-15.md index d3a5d9832a..2bce5441fe 100644 --- a/.chronus/changes/add-lro-metadata-spector-2026-10-15.md +++ b/.chronus/changes/add-lro-metadata-spector-2026-10-15.md @@ -4,4 +4,4 @@ packages: - "@azure-tools/azure-http-specs" --- -Add Spector coverage for LRO operation ID extraction and typed polling metadata. +Add service-shaped Spector contracts for LRO operation ID extraction and nested typed polling metadata. diff --git a/packages/azure-http-specs/spec-summary.md b/packages/azure-http-specs/spec-summary.md index 98f35a3f3d..fa060c970c 100644 --- a/packages/azure-http-specs/spec-summary.md +++ b/packages/azure-http-specs/spec-summary.md @@ -1270,39 +1270,37 @@ Expected response body: ### Azure_ClientGenerator_Core_Lro_Metadata_analyze -- Endpoint: `post /azure/client-generator-core/lro/metadata/analyzers/{analyzerId}:analyze` +- Endpoint: `post /azure/client-generator-core/lro/metadata/documentModels/{modelId}:analyze` -Verifies that `@lroOperationId("lastPathSegment")` exposes the server-issued operation ID from -the status-monitor URI without changing the polling protocol. +Declares a Document Intelligence-shaped flow for emitter tests of +`@lroOperationId("lastPathSegment")`. -The generated LRO method should expose `analysis-123`, derived from the final path segment of -`Operation-Location`. +The service returns the complete opaque status-monitor URI, including its API-version query. +Polling must continue with that URI. Emitter-specific client tests can separately assert that +the server-issued ID `analysis-123` is exposed from the final non-empty path segment. Expected calls: -- POST `/analyzers/document-1:analyze` returns 202 with - `Operation-Location: {endpoint}/analyzerResults/analysis-123` -- GET `/analyzerResults/analysis-123` returns a running status -- GET `/analyzerResults/analysis-123` returns a succeeded status with the analysis result +- POST `/documentModels/document-1:analyze` returns 202 with + `Operation-Location: {endpoint}/documentModels/document-1/analyzeResults/analysis-123?api-version=2022-12-01-preview` +- GET that exact status-monitor URI returns a running status +- GET that exact status-monitor URI returns a succeeded status with the analysis result -### Azure_ClientGenerator_Core_Lro_Metadata_translate +### Azure_ClientGenerator_Core_Lro_Metadata_analyzeWithUsage -- Endpoint: `post /azure/client-generator-core/lro/metadata/documents/{documentId}:translate` +- Endpoint: `post /azure/client-generator-core/lro/metadata/contentAnalyzers/{analyzerId}:analyze` -Verifies that property-level `@lroPollingMetadata` exposes a status-monitor property with its -exact SDK type and serialization metadata. +Declares a Content Understanding-shaped flow for emitter tests of property-level +`@lroPollingMetadata`. -The generated LRO method should expose optional `int32` polling metadata named `percentComplete`, -serialized on the wire as `percent_complete`. +The optional `usage` value is a typed nested model with optional numeric fields and a token +dictionary. Emitter-specific client tests can assert the generated public type and runtime value. Expected calls: -- POST `/documents/document-1:translate` returns 202 with - `Operation-Location: {endpoint}/documentTranslationJobs/translation-123` -- GET `/documentTranslationJobs/translation-123` returns a running status with - `percent_complete: 50` -- GET `/documentTranslationJobs/translation-123` returns a succeeded status with - `percent_complete: 100` and the translation result +- POST `/contentAnalyzers/analyzer-1:analyze` returns 202 with a status-monitor URI +- GET the status-monitor URI returns a running status with nested `usage` +- GET the status-monitor URI returns a succeeded status with nested `usage` and the result ### Azure_ClientGenerator_Core_NextLinkVerb_listItems diff --git a/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp index 5d279b4757..e3e9a6492a 100644 --- a/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp +++ b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp @@ -1,18 +1,15 @@ import "@azure-tools/typespec-azure-core"; import "@azure-tools/typespec-client-generator-core"; -import "@typespec/rest"; import "@typespec/spector"; import "@typespec/versioning"; using Azure.ClientGenerator.Core; using Azure.Core; -using Azure.Core.Traits; using Spector; using TypeSpec.Http; -using TypeSpec.Rest; using TypeSpec.Versioning; -@doc("Tests client metadata exposed for Azure long-running operations.") +@doc("Defines service-shaped HTTP flows for testing client LRO metadata.") @scenarioService( "/azure/client-generator-core/lro/metadata", { @@ -33,92 +30,76 @@ enum Versions { v2022_12_01_preview: "2022-12-01-preview", } -alias ResourceOperations = global.Azure.Core.ResourceOperations; - -@resource("analyzerResults") -model AnalyzerResultStatus { - @key - @visibility(Lifecycle.Read) +model DocumentAnalysisStatus { id: string; - status: global.Azure.Core.Foundations.OperationState; - result?: AnalyzerResult; + result?: DocumentAnalysisResult; } -model AnalyzerResult { +model DocumentAnalysisResult { summary: string; } -op getAnalyzerResultStatus is ResourceOperations.ResourceRead; - @scenario @scenarioDoc(""" - Verifies that `@lroOperationId("lastPathSegment")` exposes the server-issued operation ID from - the status-monitor URI without changing the polling protocol. - - The generated LRO method should expose `analysis-123`, derived from the final path segment of - `Operation-Location`. - + Declares a Document Intelligence-shaped flow for emitter tests of + `@lroOperationId("lastPathSegment")`. + + The service returns the complete opaque status-monitor URI, including its API-version query. + Polling must continue with that URI. Emitter-specific client tests can separately assert that + the server-issued ID `analysis-123` is exposed from the final non-empty path segment. + Expected calls: - - POST `/analyzers/document-1:analyze` returns 202 with - `Operation-Location: {endpoint}/analyzerResults/analysis-123` - - GET `/analyzerResults/analysis-123` returns a running status - - GET `/analyzerResults/analysis-123` returns a succeeded status with the analysis result + - POST `/documentModels/document-1:analyze` returns 202 with + `Operation-Location: {endpoint}/documentModels/document-1/analyzeResults/analysis-123?api-version=2022-12-01-preview` + - GET that exact status-monitor URI returns a running status + - GET that exact status-monitor URI returns a succeeded status with the analysis result """) -@route("/analyzers/{analyzerId}:analyze") +@route("/documentModels/{modelId}:analyze") @post -@pollingOperation(getAnalyzerResultStatus) @lroOperationId("lastPathSegment") op analyze( - @path analyzerId: string, + @path modelId: string, ...global.Azure.Core.Foundations.ApiVersionParameter, ): global.Azure.Core.Foundations.AcceptedResponse & - global.Azure.Core.Foundations.LongRunningStatusLocation; + global.Azure.Core.Foundations.LongRunningStatusLocation; -@resource("documentTranslationJobs") -model DocumentTranslationStatus { - @key - @visibility(Lifecycle.Read) - id: string; +model UsageDetails { + documentPagesBasic?: int32; + tokens?: Record; +} +model ContentAnalysisStatus { + id: string; status: global.Azure.Core.Foundations.OperationState; - @encodedName("application/json", "percent_complete") @lroPollingMetadata - percentComplete?: int32; + usage?: UsageDetails; - result?: TranslationResult; + result?: ContentAnalysisResult; } -model TranslationResult { - translatedDocumentUrl: url; +model ContentAnalysisResult { + contents: string; } -op getDocumentTranslationStatus is ResourceOperations.ResourceRead; - @scenario @scenarioDoc(""" - Verifies that property-level `@lroPollingMetadata` exposes a status-monitor property with its - exact SDK type and serialization metadata. - - The generated LRO method should expose optional `int32` polling metadata named `percentComplete`, - serialized on the wire as `percent_complete`. - + Declares a Content Understanding-shaped flow for emitter tests of property-level + `@lroPollingMetadata`. + + The optional `usage` value is a typed nested model with optional numeric fields and a token + dictionary. Emitter-specific client tests can assert the generated public type and runtime value. + Expected calls: - - POST `/documents/document-1:translate` returns 202 with - `Operation-Location: {endpoint}/documentTranslationJobs/translation-123` - - GET `/documentTranslationJobs/translation-123` returns a running status with - `percent_complete: 50` - - GET `/documentTranslationJobs/translation-123` returns a succeeded status with - `percent_complete: 100` and the translation result + - POST `/contentAnalyzers/analyzer-1:analyze` returns 202 with a status-monitor URI + - GET the status-monitor URI returns a running status with nested `usage` + - GET the status-monitor URI returns a succeeded status with nested `usage` and the result """) -@route("/documents/{documentId}:translate") +@route("/contentAnalyzers/{analyzerId}:analyze") @post -@pollingOperation(getDocumentTranslationStatus) -op translate( - @path documentId: string, +op analyzeWithUsage( + @path analyzerId: string, ...global.Azure.Core.Foundations.ApiVersionParameter, ): global.Azure.Core.Foundations.AcceptedResponse & - global.Azure.Core.Foundations.LongRunningStatusLocation; + global.Azure.Core.Foundations.LongRunningStatusLocation; diff --git a/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/mockapi.ts b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/mockapi.ts index 9c7213cd62..ffd4a6765b 100644 --- a/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/mockapi.ts +++ b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/mockapi.ts @@ -9,42 +9,43 @@ import { export const Scenarios: Record = {}; +const apiVersion = "2022-12-01-preview"; let analyzePollCount = 0; -let translatePollCount = 0; +let analyzeWithUsagePollCount = 0; Scenarios.Azure_ClientGenerator_Core_Lro_Metadata_analyze = passOnSuccess([ { - uri: "/azure/client-generator-core/lro/metadata/analyzers/document-1:analyze", + uri: "/azure/client-generator-core/lro/metadata/documentModels/document-1:analyze", method: "post", request: { query: { - "api-version": "2022-12-01-preview", + "api-version": apiVersion, }, }, response: { status: 202, headers: { - "operation-location": dyn`${dynItem("baseUrl")}/azure/client-generator-core/lro/metadata/analyzerResults/analysis-123`, + "operation-location": dyn`${dynItem("baseUrl")}/azure/client-generator-core/lro/metadata/documentModels/document-1/analyzeResults/analysis-123?api-version=2022-12-01-preview`, }, }, handler: (req: MockRequest) => { - req.expect.containsQueryParam("api-version", "2022-12-01-preview"); + req.expect.containsQueryParam("api-version", apiVersion); analyzePollCount = 0; return { status: 202, headers: { - "operation-location": `${req.baseUrl}/azure/client-generator-core/lro/metadata/analyzerResults/analysis-123`, + "operation-location": `${req.baseUrl}/azure/client-generator-core/lro/metadata/documentModels/document-1/analyzeResults/analysis-123?api-version=${apiVersion}`, }, }; }, kind: "MockApiDefinition", }, { - uri: "/azure/client-generator-core/lro/metadata/analyzerResults/analysis-123", + uri: "/azure/client-generator-core/lro/metadata/documentModels/document-1/analyzeResults/analysis-123", method: "get", request: { query: { - "api-version": "2022-12-01-preview", + "api-version": apiVersion, }, }, response: { @@ -58,11 +59,11 @@ Scenarios.Azure_ClientGenerator_Core_Lro_Metadata_analyze = passOnSuccess([ kind: "MockApiDefinition", }, { - uri: "/azure/client-generator-core/lro/metadata/analyzerResults/analysis-123", + uri: "/azure/client-generator-core/lro/metadata/documentModels/document-1/analyzeResults/analysis-123", method: "get", request: { query: { - "api-version": "2022-12-01-preview", + "api-version": apiVersion, }, }, response: { @@ -80,78 +81,89 @@ Scenarios.Azure_ClientGenerator_Core_Lro_Metadata_analyze = passOnSuccess([ }, ]); -Scenarios.Azure_ClientGenerator_Core_Lro_Metadata_translate = passOnSuccess([ +Scenarios.Azure_ClientGenerator_Core_Lro_Metadata_analyzeWithUsage = passOnSuccess([ { - uri: "/azure/client-generator-core/lro/metadata/documents/document-1:translate", + uri: "/azure/client-generator-core/lro/metadata/contentAnalyzers/analyzer-1:analyze", method: "post", request: { query: { - "api-version": "2022-12-01-preview", + "api-version": apiVersion, }, }, response: { status: 202, headers: { - "operation-location": dyn`${dynItem("baseUrl")}/azure/client-generator-core/lro/metadata/documentTranslationJobs/translation-123`, + "operation-location": dyn`${dynItem("baseUrl")}/azure/client-generator-core/lro/metadata/analyzerResults/usage-123?api-version=2022-12-01-preview`, }, }, handler: (req: MockRequest) => { - req.expect.containsQueryParam("api-version", "2022-12-01-preview"); - translatePollCount = 0; + req.expect.containsQueryParam("api-version", apiVersion); + analyzeWithUsagePollCount = 0; return { status: 202, headers: { - "operation-location": `${req.baseUrl}/azure/client-generator-core/lro/metadata/documentTranslationJobs/translation-123`, + "operation-location": `${req.baseUrl}/azure/client-generator-core/lro/metadata/analyzerResults/usage-123?api-version=${apiVersion}`, }, }; }, kind: "MockApiDefinition", }, { - uri: "/azure/client-generator-core/lro/metadata/documentTranslationJobs/translation-123", + uri: "/azure/client-generator-core/lro/metadata/analyzerResults/usage-123", method: "get", request: { query: { - "api-version": "2022-12-01-preview", + "api-version": apiVersion, }, }, response: { status: 200, body: json({ - id: "translation-123", + id: "usage-123", status: "Running", - percent_complete: 50, + usage: { + documentPagesBasic: 1, + tokens: { + input: 10, + }, + }, }), }, - handler: translateLroHandler, + handler: analyzeWithUsageLroHandler, kind: "MockApiDefinition", }, { - uri: "/azure/client-generator-core/lro/metadata/documentTranslationJobs/translation-123", + uri: "/azure/client-generator-core/lro/metadata/analyzerResults/usage-123", method: "get", request: { query: { - "api-version": "2022-12-01-preview", + "api-version": apiVersion, }, }, response: { status: 200, body: json({ - id: "translation-123", + id: "usage-123", status: "Succeeded", - percent_complete: 100, + usage: { + documentPagesBasic: 1, + tokens: { + input: 10, + output: 4, + }, + }, result: { - translatedDocumentUrl: "https://example.org/documents/document-1.fr", + contents: "The document contains three sections.", }, }), }, - handler: translateLroHandler, + handler: analyzeWithUsageLroHandler, kind: "MockApiDefinition", }, ]); function analyzeLroHandler(req: MockRequest) { - req.expect.containsQueryParam("api-version", "2022-12-01-preview"); + req.expect.containsQueryParam("api-version", apiVersion); const response = analyzePollCount > 0 ? { @@ -169,23 +181,34 @@ function analyzeLroHandler(req: MockRequest) { return { status: 200, body: json(response) }; } -function translateLroHandler(req: MockRequest) { - req.expect.containsQueryParam("api-version", "2022-12-01-preview"); +function analyzeWithUsageLroHandler(req: MockRequest) { + req.expect.containsQueryParam("api-version", apiVersion); const response = - translatePollCount > 0 + analyzeWithUsagePollCount > 0 ? { - id: "translation-123", + id: "usage-123", status: "Succeeded", - percent_complete: 100, + usage: { + documentPagesBasic: 1, + tokens: { + input: 10, + output: 4, + }, + }, result: { - translatedDocumentUrl: "https://example.org/documents/document-1.fr", + contents: "The document contains three sections.", }, } : { - id: "translation-123", + id: "usage-123", status: "Running", - percent_complete: 50, + usage: { + documentPagesBasic: 1, + tokens: { + input: 10, + }, + }, }; - translatePollCount += 1; + analyzeWithUsagePollCount += 1; return { status: 200, body: json(response) }; } From 637d079a41a7398178a1de27f54744e9e29421f6 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 19 Oct 2026 16:30:24 -0400 Subject: [PATCH 5/5] Update LRO metadata Spector contracts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5066e9e2-6a77-46f5-9ab4-b1472cdd5bc8 --- .../add-lro-metadata-spector-2026-10-15.md | 2 +- packages/azure-http-specs/spec-summary.md | 8 ++-- .../lro/metadata/main.tsp | 40 ++++++++++++++----- .../lro/metadata/mockapi.ts | 2 +- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/.chronus/changes/add-lro-metadata-spector-2026-10-15.md b/.chronus/changes/add-lro-metadata-spector-2026-10-15.md index 2bce5441fe..3e56b5208c 100644 --- a/.chronus/changes/add-lro-metadata-spector-2026-10-15.md +++ b/.chronus/changes/add-lro-metadata-spector-2026-10-15.md @@ -4,4 +4,4 @@ packages: - "@azure-tools/azure-http-specs" --- -Add service-shaped Spector contracts for LRO operation ID extraction and nested typed polling metadata. +Add service-shaped Azure HTTP scenarios for LRO operation ID extraction and nested typed status metadata. diff --git a/packages/azure-http-specs/spec-summary.md b/packages/azure-http-specs/spec-summary.md index fa060c970c..a1436cb54d 100644 --- a/packages/azure-http-specs/spec-summary.md +++ b/packages/azure-http-specs/spec-summary.md @@ -1273,7 +1273,7 @@ Expected response body: - Endpoint: `post /azure/client-generator-core/lro/metadata/documentModels/{modelId}:analyze` Declares a Document Intelligence-shaped flow for emitter tests of -`@lroOperationId("lastPathSegment")`. +`@lroOperationId` on the status-monitor operation ID path parameter. The service returns the complete opaque status-monitor URI, including its API-version query. Polling must continue with that URI. Emitter-specific client tests can separately assert that @@ -1288,17 +1288,17 @@ Expected calls: ### Azure_ClientGenerator_Core_Lro_Metadata_analyzeWithUsage -- Endpoint: `post /azure/client-generator-core/lro/metadata/contentAnalyzers/{analyzerId}:analyze` +- Endpoint: `post /azure/client-generator-core/lro/metadata/analyzers/{analyzerId}:analyze` Declares a Content Understanding-shaped flow for emitter tests of property-level -`@lroPollingMetadata`. +`@lroStatusMetadata`. The optional `usage` value is a typed nested model with optional numeric fields and a token dictionary. Emitter-specific client tests can assert the generated public type and runtime value. Expected calls: -- POST `/contentAnalyzers/analyzer-1:analyze` returns 202 with a status-monitor URI +- POST `/analyzers/analyzer-1:analyze` returns 202 with a status-monitor URI - GET the status-monitor URI returns a running status with nested `usage` - GET the status-monitor URI returns a succeeded status with nested `usage` and the result diff --git a/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp index e3e9a6492a..49b4483afc 100644 --- a/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp +++ b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp @@ -40,15 +40,27 @@ model DocumentAnalysisResult { summary: string; } +@route("/documentModels/{modelId}/analyzeResults/{resultId}") +@get +op getDocumentAnalysisStatus( + @path modelId: string, + + @lroOperationId + @path + resultId: string, + + ...global.Azure.Core.Foundations.ApiVersionParameter, +): DocumentAnalysisStatus; + @scenario @scenarioDoc(""" Declares a Document Intelligence-shaped flow for emitter tests of - `@lroOperationId("lastPathSegment")`. - + `@lroOperationId` on the status-monitor operation ID path parameter. + The service returns the complete opaque status-monitor URI, including its API-version query. Polling must continue with that URI. Emitter-specific client tests can separately assert that the server-issued ID `analysis-123` is exposed from the final non-empty path segment. - + Expected calls: - POST `/documentModels/document-1:analyze` returns 202 with `Operation-Location: {endpoint}/documentModels/document-1/analyzeResults/analysis-123?api-version=2022-12-01-preview` @@ -57,7 +69,7 @@ model DocumentAnalysisResult { """) @route("/documentModels/{modelId}:analyze") @post -@lroOperationId("lastPathSegment") +@pollingOperation(getDocumentAnalysisStatus) op analyze( @path modelId: string, ...global.Azure.Core.Foundations.ApiVersionParameter, @@ -73,7 +85,7 @@ model ContentAnalysisStatus { id: string; status: global.Azure.Core.Foundations.OperationState; - @lroPollingMetadata + @lroStatusMetadata usage?: UsageDetails; result?: ContentAnalysisResult; @@ -83,21 +95,29 @@ model ContentAnalysisResult { contents: string; } +@route("/analyzerResults/{operationId}") +@get +op getContentAnalysisStatus( + @path operationId: string, + ...global.Azure.Core.Foundations.ApiVersionParameter, +): ContentAnalysisStatus; + @scenario @scenarioDoc(""" Declares a Content Understanding-shaped flow for emitter tests of property-level - `@lroPollingMetadata`. - + `@lroStatusMetadata`. + The optional `usage` value is a typed nested model with optional numeric fields and a token dictionary. Emitter-specific client tests can assert the generated public type and runtime value. - + Expected calls: - - POST `/contentAnalyzers/analyzer-1:analyze` returns 202 with a status-monitor URI + - POST `/analyzers/analyzer-1:analyze` returns 202 with a status-monitor URI - GET the status-monitor URI returns a running status with nested `usage` - GET the status-monitor URI returns a succeeded status with nested `usage` and the result """) -@route("/contentAnalyzers/{analyzerId}:analyze") +@route("/analyzers/{analyzerId}:analyze") @post +@pollingOperation(getContentAnalysisStatus) op analyzeWithUsage( @path analyzerId: string, ...global.Azure.Core.Foundations.ApiVersionParameter, diff --git a/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/mockapi.ts b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/mockapi.ts index ffd4a6765b..a43b4aaefa 100644 --- a/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/mockapi.ts +++ b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/mockapi.ts @@ -83,7 +83,7 @@ Scenarios.Azure_ClientGenerator_Core_Lro_Metadata_analyze = passOnSuccess([ Scenarios.Azure_ClientGenerator_Core_Lro_Metadata_analyzeWithUsage = passOnSuccess([ { - uri: "/azure/client-generator-core/lro/metadata/contentAnalyzers/analyzer-1:analyze", + uri: "/azure/client-generator-core/lro/metadata/analyzers/analyzer-1:analyze", method: "post", request: { query: {