Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/add-lro-metadata-spector-2026-10-15.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/azure-http-specs"
---

Add service-shaped Azure HTTP scenarios for LRO operation ID extraction and nested typed status metadata.
34 changes: 34 additions & 0 deletions packages/azure-http-specs/spec-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,40 @@ Expected response body:
}
```

### Azure_ClientGenerator_Core_Lro_Metadata_analyze

- Endpoint: `post /azure/client-generator-core/lro/metadata/documentModels/{modelId}:analyze`

Declares a Document Intelligence-shaped flow for emitter tests of
`@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`
- 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_analyzeWithUsage

- Endpoint: `post /azure/client-generator-core/lro/metadata/analyzers/{analyzerId}:analyze`

Declares a Content Understanding-shaped flow for emitter tests of property-level
`@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 `/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

### Azure_ClientGenerator_Core_NextLinkVerb_listItems

- Endpoint: `post /azure/client-generator-core/next-link-verb/items`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import "@azure-tools/typespec-azure-core";
import "@azure-tools/typespec-client-generator-core";
import "@typespec/spector";
import "@typespec/versioning";

using Azure.ClientGenerator.Core;
using Azure.Core;
using Spector;
using TypeSpec.Http;
using TypeSpec.Versioning;

@doc("Defines service-shaped HTTP flows for testing client LRO metadata.")
@scenarioService(
"/azure/client-generator-core/lro/metadata",
{
versioned: Versions,
}
)
@global.Azure.ClientGenerator.Core.clientNamespace(
"azure.clientgenerator.core.lro.metadata",
"java"
)
@global.Azure.ClientGenerator.Core.clientNamespace(
"specs.azure.clientgenerator.core.lro.metadata",
"python"
)
namespace _Specs_.Azure.ClientGenerator.Core.Lro.Metadata;

enum Versions {
v2022_12_01_preview: "2022-12-01-preview",
}

model DocumentAnalysisStatus {
id: string;
status: global.Azure.Core.Foundations.OperationState;
result?: DocumentAnalysisResult;
}

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` 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`
- 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("/documentModels/{modelId}:analyze")
@post
@pollingOperation(getDocumentAnalysisStatus)
op analyze(
@path modelId: string,
...global.Azure.Core.Foundations.ApiVersionParameter,
): global.Azure.Core.Foundations.AcceptedResponse &
global.Azure.Core.Foundations.LongRunningStatusLocation<DocumentAnalysisStatus>;

model UsageDetails {
documentPagesBasic?: int32;
tokens?: Record<int32>;
}

model ContentAnalysisStatus {
id: string;
status: global.Azure.Core.Foundations.OperationState;

@lroStatusMetadata
usage?: UsageDetails;

result?: ContentAnalysisResult;
}

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
`@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 `/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("/analyzers/{analyzerId}:analyze")
@post
@pollingOperation(getContentAnalysisStatus)
op analyzeWithUsage(
@path analyzerId: string,
...global.Azure.Core.Foundations.ApiVersionParameter,
): global.Azure.Core.Foundations.AcceptedResponse &
global.Azure.Core.Foundations.LongRunningStatusLocation<ContentAnalysisStatus>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import {
dyn,
dynItem,
json,
MockRequest,
passOnSuccess,
type ScenarioMockApi,
} from "@typespec/spec-api";

export const Scenarios: Record<string, ScenarioMockApi> = {};

const apiVersion = "2022-12-01-preview";
let analyzePollCount = 0;
let analyzeWithUsagePollCount = 0;

Scenarios.Azure_ClientGenerator_Core_Lro_Metadata_analyze = passOnSuccess([
{
uri: "/azure/client-generator-core/lro/metadata/documentModels/document-1:analyze",
method: "post",
request: {
query: {
"api-version": apiVersion,
},
},
response: {
status: 202,
headers: {
"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", apiVersion);
analyzePollCount = 0;
return {
status: 202,
headers: {
"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/documentModels/document-1/analyzeResults/analysis-123",
method: "get",
request: {
query: {
"api-version": apiVersion,
},
},
response: {
status: 200,
body: json({
id: "analysis-123",

@weidongxu-microsoft Weidong Xu (weidongxu-microsoft) Oct 20, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

body should not have this id: "analysis-123" (ContentAnalysisStatus model too)

The problem of Document Intelligence is that they do not follow the guideline and did not have this id.
If they had this id, @operationId is not needed at all.
They just want to extract the equivalent of this id from operation-location header (to call another getAnalyzeResultPdf/getAnalyzeResultFigure API).

That is the reason they added/customized the getResultId() in Java that parse the value of operation-location header and return the last segment.

AKA, this is not used for polling related thing. It is only about "extract" the non-exist id from operation-location header.

status: "Running",
}),
},
handler: analyzeLroHandler,
kind: "MockApiDefinition",
},
{
uri: "/azure/client-generator-core/lro/metadata/documentModels/document-1/analyzeResults/analysis-123",
method: "get",
request: {
query: {
"api-version": apiVersion,
},
},
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_Lro_Metadata_analyzeWithUsage = passOnSuccess([
{
uri: "/azure/client-generator-core/lro/metadata/analyzers/analyzer-1:analyze",
method: "post",
request: {
query: {
"api-version": apiVersion,
},
},
response: {
status: 202,
headers: {
"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", apiVersion);
analyzeWithUsagePollCount = 0;
return {
status: 202,
headers: {
"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/analyzerResults/usage-123",
method: "get",
request: {
query: {
"api-version": apiVersion,
},
},
response: {
status: 200,
body: json({
id: "usage-123",
status: "Running",
usage: {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would we want a test without usage being returned?

documentPagesBasic: 1,
tokens: {
input: 10,
},
},
}),
},
handler: analyzeWithUsageLroHandler,
kind: "MockApiDefinition",
},
{
uri: "/azure/client-generator-core/lro/metadata/analyzerResults/usage-123",
method: "get",
request: {
query: {
"api-version": apiVersion,
},
},
response: {
status: 200,
body: json({
id: "usage-123",
status: "Succeeded",
usage: {
documentPagesBasic: 1,
tokens: {
input: 10,
output: 4,
},
},
result: {
contents: "The document contains three sections.",
},
}),
},
handler: analyzeWithUsageLroHandler,
kind: "MockApiDefinition",
},
]);

function analyzeLroHandler(req: MockRequest) {
req.expect.containsQueryParam("api-version", apiVersion);
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 analyzeWithUsageLroHandler(req: MockRequest) {
req.expect.containsQueryParam("api-version", apiVersion);
const response =
analyzeWithUsagePollCount > 0
? {
id: "usage-123",
status: "Succeeded",
usage: {
documentPagesBasic: 1,
tokens: {
input: 10,
output: 4,
},
},
result: {
contents: "The document contains three sections.",
},
}
: {
id: "usage-123",
status: "Running",
usage: {
documentPagesBasic: 1,
tokens: {
input: 10,
},
},
};
analyzeWithUsagePollCount += 1;
return { status: 200, body: json(response) };
}
Loading