Discovers and implements gaps in Spector test coverage for the Azure C# HTTP client emitter. Use when asked to find missing Spector scenarios, add Spector test coverage, or implement a specific Spector spec for the Azure C# emitter. Can also compare coverage between the Azure dashboard and the Standard (TypeSpec core) dashboard.
72
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
This skill discovers which Spector scenarios the Azure C# emitter (@azure-tools/typespec-csharp) does not yet cover, then implements the missing test(s). Spector scenarios are defined in two packages:
@typespec/http-specs — general HTTP client scenarios@azure-tools/azure-http-specs — Azure-specific scenarios (core, LRO, paging, resource-manager, etc.)Two coverage dashboards exist:
| Dashboard | URL | Scope |
|---|---|---|
| Standard | https://typespec.io/can-i-use/http/ | Standard HTTP specs only |
| Azure | https://azure.github.io/typespec-azure/can-i-use/http/ | Standard + Azure specs |
Tip: Comparing the two dashboards reveals Standard scenarios where the Azure emitter lags behind (or ahead of) the core TypeSpec C# emitter in
microsoft/typespec.
Note:
{PKG}refers to<repo-root>/eng/packages/http-client-csharpthroughout this document.
You may receive one of:
packages/http-specs/specs/... or packages/azure-http-specs/specs/....http/encode/duration, http/azure/core/lro/standard, http/type/union/discriminated.{PKG}/generator/TestProjects/Spector.Tests/Http/.{PKG}/generator/TestProjects/Spector.Tests/TestProjects.Spector.Tests.csproj if a new project reference is needed.Generate.ps1).Test-Spector.ps1 -filter "<spec-path>"Before starting, ensure the build environment is ready.
{PKG}):
cd {PKG}
npm cinpm run build⚠️ Do NOT run
pnpm installorpnpm buildat the repo root — only the http-client-csharp package build is needed.
The Azure C# emitter tests specs from two sources:
@typespec/http-specs): Located at {PKG}/node_modules/@typespec/http-specs/specs/@azure-tools/azure-http-specs): Located at {PKG}/node_modules/@azure-tools/azure-http-specs/specs/The file {PKG}/eng/scripts/Spector-Helper.psm1 defines which specs are included/excluded.
Failing/excluded specs are defined in the $failingSpecs array. Always check that file for the current list — do not hardcode specs here.
Current exclusions fall into three categories:
streaming/jsonl, response/status-code-range (namespace conflict with Azure.Response), type/fileazure/client-generator-core/alternate-type, azure/client-generator-core/deserialize-empty-string-as-nullazure/resource-manager/* specs (common-properties, non-resource, operation-templates, resources, large-header, method-subscription-id, multi-service variants)Compare available specs against existing tests. Run this from {PKG}:
Import-Module "{PKG}/eng/scripts/Spector-Helper.psm1" -DisableNameChecking -Force
# Get all valid specs
$specs = Get-Sorted-Specs | ForEach-Object { Get-SubPath $_ }
# Get all test files
$testFiles = Get-ChildItem -Path "{PKG}/generator/TestProjects/Spector.Tests/Http" -Recurse -Filter "*Tests.cs" |
ForEach-Object { $_.FullName }
# For each spec, check if a corresponding test directory/file exists
foreach ($spec in $specs) {
$specParts = $spec -replace '/', '\' -split '\\'
$testPath = "{PKG}/generator/TestProjects/Spector.Tests/Http"
# ... check if test exists
}The two dashboards report coverage for different scopes:
@typespec/http-specs scenarios only, reported by the base @typespec/http-client-csharp emitter in the microsoft/typespec repo.@typespec/http-specs and @azure-tools/azure-http-specs scenarios, reported by the @azure-tools/typespec-csharp emitter in this repo.To find gaps between the two dashboards:
azure/*) only appear on the Azure dashboard.Common causes of Standard coverage gaps in Azure:
microsoft/typespec added a test that hasn't been ported here yet.$failingSpecs in Spector-Helper.psm1).Response vs Azure.Response).Standard spec test directories (under Spector.Tests/Http/):
Authentication/ (ApiKey, Http/Custom, OAuth2, Union)
Client/ (ClientNamespace, Naming, Overload, Structure/*)
Encode/ (Array, Bytes, DateTime, Duration, Numeric)
Parameters/ (Basic, BodyOptionality, CollectionFormat, Path, Query, Spread)
Payload/ (ContentNegotiation, JsonMergePatch, MediaType, MultiPart, Pageable, Xml)
Resiliency/ (SrvDriven/V1, SrvDriven/V2)
Routes/
Serialization/ (EncodedName/Json)
Server/ (Endpoint/NotDefined, Path/Multiple, Path/Single, Versions/*)
Service/
SpecialHeaders/ (ConditionalRequest, Repeatability)
SpecialWords/
Versioning/ (Added, MadeOptional, Removed, RenamedFrom, ReturnTypeChangedFrom, TypeChangedFrom)
_Type/ (Array, Dictionary, Enum/*, Model/*, Property/*, Scalar, Union)Azure spec test directories (under Spector.Tests/Http/Azure/):
ClientGeneratorCore/ (Access, ApiVersion/*, ClientInitialization/*, ClientLocation/*, HierarchyBuilding, Override, Usage)
Core/ (Basic, Lro/Rpc, Lro/Standard, Model, Page, Scalar, Traits)
Encode/ (Duration)
Example/ (Basic)
Payload/ (Pageable)
SpecialHeaders/ (ClientRequestId)
Versioning/ (PreviewVersion/V1, PreviewVersion/V2)The naive approach of matching spec paths to test directory names can produce false positives because:
type/ → _Type/, array → _Array, enum → _Enum)content-negotiation → ContentNegotiation)UnionTests.cs covers type/union but NOT type/union/discriminated)To find real gaps, verify each candidate by checking whether a test directory exists for the spec's exact path, including sub-paths. A test file at a parent level does NOT cover child specs.
Important: The gap list evolves over time. Always re-run the comparison to get current gaps. All committed Spector libraries are stubbed —
[SpectorTest]will auto-skip tests unless you regenerate with-Stubbed $falseor useTest-Spector.ps1.
All Spector libraries committed to the repository are stubbed. Generate.ps1 defaults to $Stubbed = $true, which causes the emitter to use AzureStubGenerator instead of AzureClientGenerator. Stubbed clients use expression-bodied constructors (=>) instead of block bodies ({ }), and the [SpectorTest] attribute automatically skips tests for stubbed clients.
To generate unstubbed code (for local testing), pass -Stubbed $false:
pwsh eng/scripts/Generate.ps1 -filter "<spec-path>" -Stubbed $falseHowever, the recommended way to test is via Test-Spector.ps1 (see Step 7), which handles the unstubbed regeneration, test execution, and directory restoration automatically.
Spec files live in:
{PKG}/node_modules/@typespec/http-specs/specs/{PKG}/node_modules/@azure-tools/azure-http-specs/specs/If node_modules is not installed, specs can also be found in the upstream repos:
Each spec contains:
main.tsp — the TypeSpec definition with @scenario and @scenarioDoc decoratorsclient.tsp (optional) — client-level customizations; takes priority over main.tsp during generationtspconfig.yaml (optional in the Spector test project) — C#-specific generation optionsRead the @scenarioDoc decorators to understand:
Use Generate.ps1 with a filter to generate only the specific spec:
cd {PKG}
# Generate unstubbed (for local testing/development)
pwsh eng/scripts/Generate.ps1 -filter "<spec-path>" -Stubbed $false
# Generate stubbed (default, matches what is committed to repo)
pwsh eng/scripts/Generate.ps1 -filter "<spec-path>"Examples:
# Standard spec (unstubbed for testing)
pwsh eng/scripts/Generate.ps1 -filter "http/encode/duration" -Stubbed $false
# Azure spec
pwsh eng/scripts/Generate.ps1 -filter "http/azure/core/lro" -Stubbed $false
# Versioning spec (generates v1 + v2 automatically)
pwsh eng/scripts/Generate.ps1 -filter "http/versioning/added" -Stubbed $falseThe generated code lands in {PKG}/generator/TestProjects/Spector/<spec-path>/src/Generated/.
# Check that client code was generated
Get-ChildItem "{PKG}/generator/TestProjects/Spector/<spec-path>/src/Generated/" -Filter "*Client.cs"Note: All committed Spector libraries are stubbed. The
[SpectorTest]attribute automatically skips tests for stubbed clients. UseTest-Spector.ps1(Step 7) to regenerate unstubbed, run tests, and restore automatically.
Browse the generated code to understand:
*Client.cs — the entry point(s)Get*Client() methodsGetAsync(), PutAsync(body), SendAsync()Models/ — request/response shapesnew XClient(Uri endpoint, XClientOptions options) or new XClient(Uri endpoint, KeyCredential credential, XClientOptions options)Pay attention to:
ClientResult, ClientResult<T>, AsyncPageable<T>)RequestContent usage, Azure.Core types, internal vs public visibilityThe test directory structure mirrors the spec path with these transformations:
http/ → Http/content-negotiation → ContentNegotiation)type/ → _Type/ (leading underscore because Type is a C# keyword)array → _Array (same reason)enum → _Enum (same reason)azure/ → Azure/ (Azure specs get their own top-level test directory)Namespace pattern: TestProjects.Spector.Tests.Http.<PascalCasePath>
Example mappings:
| Spec path | Test directory | Namespace |
|---|---|---|
http/encode/duration | Http/Encode/Duration/ | TestProjects.Spector.Tests.Http.Encode.Duration |
http/azure/core/basic | Http/Azure/Core/Basic/ | TestProjects.Spector.Tests.Http.Azure.Core.Basic |
http/azure/client-generator-core/access | Http/Azure/ClientGeneratorCore/Access/ | TestProjects.Spector.Tests.Http.Azure.ClientGeneratorCore.Access |
http/type/union/discriminated | Http/_Type/Union/Discriminated/ | TestProjects.Spector.Tests.Http._Type.Union.Discriminated |
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Threading.Tasks;
using <GeneratedNamespace>;
using NUnit.Framework;
namespace TestProjects.Spector.Tests.Http.<Category>.<SubCategory>
{
public class <Name>Tests : SpectorTestBase
{
[SpectorTest]
public Task <ScenarioName>() => Test(async (host) =>
{
var response = await new <Client>(host, null).<Method>Async(<args>);
Assert.AreEqual(<expectedStatusCode>, response.GetRawResponse().Status);
});
}
}Simple void operation (204 response):
[SpectorTest]
public Task SimpleOp() => Test(async (host) =>
{
var response = await new MyClient(host, null).DoThingAsync();
Assert.AreEqual(204, response.GetRawResponse().Status);
});GET with typed response:
[SpectorTest]
public Task GetValue() => Test(async (host) =>
{
var response = await new MyClient(host, null).GetValueAsync();
Assert.AreEqual("expected", response.Value);
});Azure RequestContent pattern (common in Azure specs):
[SpectorTest]
public Task SendAction() => Test(async (host) =>
{
var value = new { stringProperty = "text", intProperty = 42 };
var response = await new MyClient(host, null)
.ActionAsync("query", "header", RequestContent.Create(value));
Assert.AreEqual(200, response.Status);
});Round-trip (GET then PUT):
[SpectorTest]
public Task RoundTrip() => Test(async (host) =>
{
var client = new MyClient(host, null);
var getResult = await client.GetAsync();
var response = await client.PutAsync(getResult.Value);
Assert.AreEqual(204, response.GetRawResponse().Status);
});Azure access/visibility test (using reflection for internal members):
[SpectorTest]
public Task InternalOp() => Test(async (host) =>
{
var client = new MyClient(host, null);
var internalClient = GetProperty(client, "InternalOpClient");
var response = await InvokeMethodAsync(internalClient!, "InternalAsync");
Assert.AreEqual(204, ((ClientResult)response!).GetRawResponse().Status);
});Pagination:
[SpectorTest]
public Task ListItems() => Test(async (host) =>
{
var items = new MyClient(host, null).GetItemsAsync();
int count = 0;
await foreach (var item in items)
{
count++;
}
Assert.Greater(count, 0);
});Error assertion:
[SpectorTest]
public Task InvalidKey() => Test((host) =>
{
var exception = Assert.ThrowsAsync<ClientResultException>(
() => new MyClient(host, new ApiKeyCredential("invalid"), null).CallAsync());
Assert.AreEqual(403, exception!.Status);
return Task.CompletedTask;
});using System;
using System.ClientModel; // ClientResult, ClientResultException, ApiKeyCredential
using System.IO; // For file/stream scenarios
using System.Text.Json.Nodes; // For parsing raw JSON responses
using Azure.Core; // RequestContent (Azure specs)
using NUnit.Framework; // Test frameworkIf a new Spector test project directory was created (new spec), a project reference may be needed in TestProjects.Spector.Tests.csproj:
<ProjectReference Include="..\Spector\http\<spec-path>\src\<ProjectName>.csproj" />Check existing references to match the pattern. The project name typically matches the package-name from tspconfig.yaml or is derived from the namespace.
Only add a project reference if one doesn't already exist for the spec.
The Test-Spector.ps1 script is the recommended way to test Spector specs. It automatically:
cd {PKG}
# Test a specific spec
pwsh eng/scripts/Test-Spector.ps1 -filter "<spec-path>"Examples:
# Standard spec
pwsh eng/scripts/Test-Spector.ps1 -filter "http/encode/duration"
# Azure spec
pwsh eng/scripts/Test-Spector.ps1 -filter "http/azure/core/basic"pwsh eng/scripts/Get-Spector-Coverage.ps1This regenerates ALL specs, runs the full test suite, and produces a coverage file at {PKG}/generator/artifacts/coverage/tsp-spector-coverage-azure.json.
If you need more control, you can manually generate unstubbed, build, and run tests:
cd {PKG}
# Generate unstubbed
pwsh eng/scripts/Generate.ps1 -filter "<spec-path>" -Stubbed $false
# Build
dotnet build generator
# Run only your new tests
dotnet test generator/TestProjects/Spector.Tests/TestProjects.Spector.Tests.csproj `
--filter "FullyQualifiedName~TestProjects.Spector.Tests.Http.<YourNamespace>"
# Restore the directory to stubbed state when done
git clean -xfd generator/TestProjects/Spector/<spec-path>
git restore generator/TestProjects/Spector/<spec-path>Versioning specs generate two clients (v1 and v2). Tests go in separate subdirectories:
Http/Versioning/<Name>/V1/<Name>V1Tests.cs
Http/Versioning/<Name>/V2/<Name>V2Tests.csGeneration is handled automatically by Generate.ps1 when the path contains versioning.
Similar to versioning — generates v1 and v2 clients from old.tsp and main.tsp.
Some specs have a tspconfig.yaml in the Spector test project that overrides the package-name. Check {PKG}/generator/TestProjects/Spector/<spec-path>/tspconfig.yaml before importing the generated namespace.
All azure/resource-manager/* specs are excluded from the data-plane generator and delegated to the management generator at eng/packages/http-client-csharp-mgmt. These are not expected to have tests in {PKG}.
Understanding how test results reach the dashboards:
dotnet test runs against the Spector mock server (via @typespec/spector CLI)tsp-spector-coverage-azure.json to {PKG}/generator/artifacts/coverage/archetype-typespec-emitter.yml) uploads coverage via:
npx tsp-spector upload-coverage \
--coverageFile tsp-spector-coverage-azure.json \
--generatorName @azure-typespec/<SpectorName> \
--storageAccountName typespec \
--containerName coverages \
--generatorMode azureSpector-Helper.psm1 to remove items from the failing list unless you're sure the generator now supports them.Generate.ps1 defaults to $Stubbed = $true. Use Test-Spector.ps1 to temporarily regenerate unstubbed and run tests.Test-Spector.ps1 regenerates them unstubbed..cs), .csproj changes, and tspconfig.yaml if needed.<Feature>Tests.cs in the matching directory.[SpectorTest] attribute (not [Test]) for all Spector tests — it enables auto-skip for stubbed implementations.SpectorTestBase.[SpectorTest] attribute uses Roslyn to detect stubbed implementations by checking constructor syntax.SpectorTestBase class provides reflection helpers (InvokeMethodAsync, GetProperty, InvokeMethod) for testing internal/private members — this is especially useful for Azure specs that test access modifiers.0afb185
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.